From d47d3f7d91c63ce09ef22673c047ec4a9afcf296 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 16:37:17 -0400 Subject: [PATCH 01/57] fix: terminate external strategy process trees --- README.md | 4 +- lib/dune | 3 + lib/process_tree_stubs.c | 15 +++ lib/strategy_process.ml | 204 +++++++++++++++++++++++++++++---- test/cli.t | 34 ++++++ test/dune | 14 ++- test/fake_strategy.py | 40 ++++++- test/test_strategy_protocol.ml | 65 +++++++++++ 8 files changed, 354 insertions(+), 25 deletions(-) create mode 100644 lib/process_tree_stubs.c diff --git a/README.md b/README.md index a86543f..ac08bdc 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,9 @@ own the child's standard input and output; strategy diagnostics belong on standa one request is outstanding. Initialization must return `ready`, each event must return `intents`, and shutdown must return `stopped`. Wrong versions or sequences, unknown or malformed fields, oversized responses, EOF, timeout, extra output, and nonzero exit all fail the replay. The journal -and transcript retain partial artifacts for diagnosis. +and transcript retain partial artifacts for diagnosis. The strategy runs in a dedicated process +group. Failure and cancellation send `SIGTERM` to the complete group, allow one second for graceful +exit, then send `SIGKILL` and allow five seconds to reap the process tree. Discover the executable version and machine-readable compatibility surface: diff --git a/lib/dune b/lib/dune index 2db4e27..ecba7b6 100644 --- a/lib/dune +++ b/lib/dune @@ -1,4 +1,7 @@ (library (name trading_engine) (public_name trading_engine) + (foreign_stubs + (language c) + (names process_tree_stubs)) (libraries ptime yojson zarith fmt logs unix eio eio.unix)) diff --git a/lib/process_tree_stubs.c b/lib/process_tree_stubs.c new file mode 100644 index 0000000..d1ae03a --- /dev/null +++ b/lib/process_tree_stubs.c @@ -0,0 +1,15 @@ +#include + +#if defined(__linux__) +#include +#endif + +CAMLprim value trading_engine_enable_child_subreaper(value unit) +{ + (void)unit; +#if defined(__linux__) + return Val_int(prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0)); +#else + return Val_int(0); +#endif +} diff --git a/lib/strategy_process.ml b/lib/strategy_process.ml index c5054c9..b1af72c 100644 --- a/lib/strategy_process.ml +++ b/lib/strategy_process.ml @@ -2,13 +2,27 @@ type t = { input : Eio.Flow.sink_ty Eio.Resource.t; close_input : unit -> unit; output : Eio.Buf_read.t; - await_process : unit -> Eio.Process.exit_status; + child : child; clock : float Eio.Time.clock_ty Eio.Resource.t; transcript : Strategy_transcript.t; timeout : float; mutable next_sequence : int64; } +and child = { + process : Eio_unix.Process.ty Eio.Resource.t; + pgid : int; + clock : float Eio.Time.clock_ty Eio.Resource.t; + mutable status : Eio.Process.exit_status option; +} + +external enable_child_subreaper : unit -> int + = "trading_engine_enable_child_subreaper" + +let graceful_termination_timeout = 1.0 +let forced_reap_timeout = 5.0 +let process_poll_interval = 0.01 + let ( let* ) result function_ = match result with Ok value -> function_ value | Error _ as error -> error @@ -29,6 +43,113 @@ let exception_message stage exception_ = stage ^ ": strategy response exceeds the maximum message size" | _ -> stage ^ ": " ^ Printexc.to_string exception_ +let await_child (child : child) = + match child.status with + | Some status -> status + | None -> + let status = Eio.Process.await child.process in + child.status <- Some status; + status + +let await_child_for (child : child) timeout = + match child.status with + | Some _ as status -> status + | None -> ( + try + match + Eio.Time.with_timeout child.clock timeout (fun () -> + Ok (await_child child)) + with + | Ok status -> Some status + | Error `Timeout -> None + with exception_ -> + raise + (Failure + (exception_message "waiting for external strategy" exception_))) + +let process_group_exists pgid = + try + Unix.kill (-pgid) 0; + true + with + | Unix.Unix_error (Unix.ESRCH, _, _) -> false + | Unix.Unix_error (Unix.EPERM, _, _) -> true + +let signal_process_group pgid signal = + try + Unix.kill (-pgid) signal; + Ok () + with + | Unix.Unix_error (Unix.ESRCH, _, _) -> Ok () + | Unix.Unix_error (code, operation, target) -> + Error + (Printf.sprintf + "could not signal external strategy process group: %s(%s): %s" + operation target (Unix.error_message code)) + +let rec reap_descendants pgid = + try + match Unix.waitpid [ Unix.WNOHANG ] (-pgid) with + | 0, _ -> () + | _, _ -> reap_descendants pgid + with Unix.Unix_error (Unix.ECHILD, _, _) -> () + +let rec wait_for_process_group (child : child) deadline = + reap_descendants child.pgid; + if not (process_group_exists child.pgid) then true + else + let remaining = deadline -. Eio.Time.now child.clock in + if Float.compare remaining 0.0 <= 0 then false + else ( + Eio.Time.sleep child.clock (Float.min process_poll_interval remaining); + wait_for_process_group child deadline) + +let terminate_process_group (child : child) = + Eio.Cancel.protect (fun () -> + let graceful_deadline = + Eio.Time.now child.clock +. graceful_termination_timeout + in + let* () = signal_process_group child.pgid Sys.sigterm in + let direct_status = + match child.status with + | Some _ as status -> status + | None -> + let remaining = graceful_deadline -. Eio.Time.now child.clock in + if Float.compare remaining 0.0 <= 0 then None + else await_child_for child remaining + in + let group_stopped = + match direct_status with + | None -> false + | Some _ -> wait_for_process_group child graceful_deadline + in + if group_stopped then Ok () + else + let forced_deadline = Eio.Time.now child.clock +. forced_reap_timeout in + let* () = signal_process_group child.pgid Sys.sigkill in + let direct_status = + match direct_status with + | Some _ as status -> status + | None -> await_child_for child forced_reap_timeout + in + match direct_status with + | None -> + Error "external strategy did not exit after forced termination" + | Some _ -> + if wait_for_process_group child forced_deadline then Ok () + else + Error + "external strategy descendants remained after forced \ + termination") + +let append_cleanup_error result child = + match terminate_process_group child with + | Ok () -> result + | Error cleanup -> ( + match result with + | Ok _ -> Error cleanup + | Error message -> Error (message ^ "; " ^ cleanup)) + let exchange session ~stage ~expected_sequence request = let* () = Strategy_transcript.append session.transcript @@ -103,7 +224,7 @@ let await_exit session = try match Eio.Time.with_timeout session.clock session.timeout (fun () -> - Ok (session.await_process ())) + Ok (await_child session.child)) with | Ok status -> Ok status | Error `Timeout -> Error "external strategy did not exit after shutdown" @@ -113,7 +234,21 @@ let await_exit session = let* status = status in match status with | `Exited 0 -> ( - match Eio.Buf_read.peek_char session.output with + let* () = terminate_process_group session.child in + let trailing_output = + try + match + Eio.Time.with_timeout session.clock session.timeout (fun () -> + Ok (Eio.Buf_read.peek_char session.output)) + with + | Ok value -> Ok value + | Error `Timeout -> + Error "external strategy stdout did not close after exit" + with exception_ -> + Error (exception_message "reading final strategy output" exception_) + in + let* trailing_output = trailing_output in + match trailing_output with | None -> Ok () | Some _ -> Error "external strategy wrote data after its stopped response") @@ -143,19 +278,31 @@ let with_session ~env ~command ~timeout ~transcript_path let result = Eio.Switch.run ~name:"external-strategy" @@ fun switch -> let process_manager = Eio.Stdenv.process_mgr env in - let child_stdout, strategy_stdout = - Eio.Process.pipe ~sw:switch process_manager - in - let strategy_stdin, child_stdin = - Eio.Process.pipe ~sw:switch process_manager + if enable_child_subreaper () <> 0 then + failwith "could not enable external strategy child reaping"; + let child_stdout, strategy_stdout = Eio_unix.pipe switch in + let strategy_stdin, child_stdin = Eio_unix.pipe switch in + let fds = + [ + (0, Eio_unix.Resource.fd strategy_stdin, `Blocking); + (1, Eio_unix.Resource.fd strategy_stdout, `Blocking); + (2, Eio_unix.Resource.fd (Eio.Stdenv.stderr env), `Blocking); + ] in let process = - Eio.Process.spawn ~sw:switch process_manager - ~stdin:strategy_stdin ~stdout:strategy_stdout - ~stderr:(Eio.Stdenv.stderr env) ~executable command + Eio_unix.Process.spawn_unix ~sw:switch process_manager ~pgid:0 + ~fds ~executable command in Eio.Flow.close strategy_stdin; Eio.Flow.close strategy_stdout; + let child = + { + process; + pgid = Eio.Process.pid process; + clock = Eio.Stdenv.clock env; + status = None; + } + in let close_input () = Eio.Flow.close child_stdin in let session = { @@ -165,18 +312,28 @@ let with_session ~env ~command ~timeout ~transcript_path Eio.Buf_read.of_flow ~max_size:(Strategy_protocol.max_message_bytes + 1) child_stdout; - await_process = (fun () -> Eio.Process.await process); + child; clock = Eio.Stdenv.clock env; transcript; timeout; next_sequence = 1L; } in - let* identity = initialize session initialization in - let* value = use session in - let* () = shutdown session in - let* () = await_exit session in - Ok (value, identity) + try + let result = + let* identity = initialize session initialization in + let* value = use session in + let* () = shutdown session in + let* () = await_exit session in + Ok (value, identity) + in + match result with + | Ok _ -> result + | Error _ -> append_cleanup_error result child + with exception_ -> + let backtrace = Printexc.get_raw_backtrace () in + ignore (terminate_process_group child); + Printexc.raise_with_backtrace exception_ backtrace in match result with | Error _ as error -> fail error @@ -184,7 +341,12 @@ let with_session ~env ~command ~timeout ~transcript_path match Strategy_transcript.commit transcript with | Ok () -> Ok value | Error _ as error -> error) - with exception_ -> - fail - (Error - (exception_message "external strategy process" exception_)))) + with + | Eio.Cancel.Cancelled _ as exception_ -> + Strategy_transcript.close_preserving_partial transcript; + raise exception_ + | exception_ -> + fail + (Error + (exception_message "external strategy process" exception_)) + )) diff --git a/test/cli.t b/test/cli.t index e2ae7df..5d41f44 100644 --- a/test/cli.t +++ b/test/cli.t @@ -107,6 +107,40 @@ $ check_strategy_failure unknown-field "unknown or missing fields" unknown-field: rejected + $ check_process_tree_failure () { + > mode="$1" + > expected="$2" + > directory="process-tree-$mode" + > mkdir "$directory" + > pid_path="$directory/grandchild.pid" + > output=$(../bin/main.exe --input ../contracts/strategy/v3/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-arg "$pid_path" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 0.2 2>&1) + > status=$? + > test "$status" -eq 123 || return 1 + > case "$output" in *"$expected"*) ;; *) return 1 ;; esac + > test -s "$pid_path" || return 1 + > pid=$(cat "$pid_path") + > python3 - "$pid" <<'PY' || return 1 + > import os + > import sys + > import time + > pid = int(sys.argv[1]) + > deadline = time.monotonic() + 2 + > while True: + > try: + > os.kill(pid, 0) + > except ProcessLookupError: + > break + > if time.monotonic() >= deadline: + > raise SystemExit("grandchild process survived cleanup") + > time.sleep(0.01) + > PY + > echo "$mode: process tree reaped" + > } + $ check_process_tree_failure spawn-grandchild "timed out" + spawn-grandchild: process tree reaped + $ check_process_tree_failure grandchild-malformed "invalid strategy response JSON" + grandchild-malformed: process tree reaped + $ mkdir external-stream $ ../bin/main.exe --input-format jsonl --input ../contracts/strategy/v3/fixtures/external.scenario.jsonl --journal external-stream/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-stream/run.strategy.jsonl --strategy-timeout 5 run=external-demo audits=10 orders=1 active=0 filled=1 rejected=0 diff --git a/test/dune b/test/dune index 987fb76..1dd3057 100644 --- a/test/dune +++ b/test/dune @@ -16,8 +16,18 @@ ../contracts/v3/fixtures/demo.scenario.jsonl ../contracts/v3/journal.schema.json ../contracts/v3/scenario-stream.schema.json - ../contracts/v3/scenario.schema.json) - (libraries trading_engine ptime yojson alcotest qcheck-core qcheck-alcotest)) + ../contracts/v3/scenario.schema.json + fake_strategy.py) + (libraries + trading_engine + ptime + yojson + alcotest + qcheck-core + qcheck-alcotest + unix + eio + eio_main)) (cram (deps diff --git a/test/fake_strategy.py b/test/fake_strategy.py index b5f1703..b230dbb 100755 --- a/test/fake_strategy.py +++ b/test/fake_strategy.py @@ -4,6 +4,9 @@ from __future__ import annotations import json +import os +import signal +import subprocess import sys import time @@ -11,6 +14,39 @@ MODE = sys.argv[1] if len(sys.argv) > 1 else "success" +if MODE in { + "spawn-grandchild", + "spawn-grandchild-success", + "grandchild-malformed", +}: + signal.signal(signal.SIGTERM, signal.SIG_IGN) + grandchild_pid_path = sys.argv[2] + grandchild = """ +import os +import signal +import sys +import time + +signal.signal(signal.SIGTERM, signal.SIG_IGN) +with open(sys.argv[1], "w", encoding="ascii") as channel: + channel.write(str(os.getpid())) + channel.flush() +time.sleep(60) +""" + subprocess.Popen( + [sys.executable, "-c", grandchild, grandchild_pid_path], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + close_fds=True, + ) + deadline = time.monotonic() + 1 + while not os.path.exists(grandchild_pid_path): + if time.monotonic() >= deadline: + raise RuntimeError("grandchild did not publish its PID") + time.sleep(0.01) + + def response(request: dict[str, object]) -> dict[str, object]: sequence = request["strategy_sequence"] message_type = request["message_type"] @@ -98,7 +134,9 @@ def response(request: dict[str, object]) -> dict[str, object]: raise SystemExit(0) if MODE == "stall": time.sleep(60) - if MODE == "malformed": + if MODE == "spawn-grandchild": + time.sleep(60) + if MODE in {"malformed", "grandchild-malformed"}: print("{", flush=True) continue if MODE == "oversized": diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index 4fa05c4..dc02bee 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -259,6 +259,67 @@ let transcript_records_direction_and_sequence () = | `String value -> value | _ -> Alcotest.fail "expected transcript direction") +let absent_temp_path suffix = + let path = Filename.temp_file "trading-engine-process" suffix in + Sys.remove path; + path + +let remove_if_exists path = if Sys.file_exists path then Sys.remove path + +let grandchild_pid pid_path = + In_channel.with_open_text pid_path (fun channel -> + In_channel.input_all channel |> String.trim |> int_of_string) + +let check_process_gone pid = + match Unix.kill pid 0 with + | () -> Alcotest.fail "grandchild process survived session cleanup" + | exception Unix.Unix_error (Unix.ESRCH, _, _) -> () + +let with_process_tree_paths test = + let pid_path = absent_temp_path ".pid" in + let transcript_path = absent_temp_path ".jsonl" in + Fun.protect + ~finally:(fun () -> + remove_if_exists pid_path; + remove_if_exists transcript_path; + remove_if_exists (transcript_path ^ ".partial")) + (fun () -> test pid_path transcript_path) + +let callback_exception_reaps_process_tree () = + with_process_tree_paths @@ fun pid_path transcript_path -> + let result = + Eio_main.run @@ fun env -> + T.Strategy_process.with_session ~env + ~command:[ "./fake_strategy.py"; "spawn-grandchild-success"; pid_path ] + ~timeout:1.0 ~transcript_path ~initialization:(initialization ()) + (fun _ -> raise Exit) + in + let message = error result in + Alcotest.(check bool) + "callback exception reported" true + (String.ends_with ~suffix:"Stdlib.Exit" message); + grandchild_pid pid_path |> check_process_gone + +let cancellation_reaps_process_tree () = + with_process_tree_paths @@ fun pid_path transcript_path -> + let timed_out = + Eio_main.run @@ fun env -> + try + ignore + (Eio.Time.with_timeout_exn (Eio.Stdenv.clock env) 0.2 (fun () -> + T.Strategy_process.with_session ~env + ~command: + [ "./fake_strategy.py"; "spawn-grandchild-success"; pid_path ] + ~timeout:1.0 ~transcript_path ~initialization:(initialization ()) + (fun _ -> + Eio.Time.sleep (Eio.Stdenv.clock env) 60.0; + Ok ()))); + false + with Eio.Time.Timeout -> true + in + Alcotest.(check bool) "session cancellation timed out" true timed_out; + grandchild_pid pid_path |> check_process_gone + let tests = [ Alcotest.test_case "initialize message is complete" `Quick @@ -271,4 +332,8 @@ let tests = responses_are_strict_and_typed; Alcotest.test_case "transcript records direction" `Quick transcript_records_direction_and_sequence; + Alcotest.test_case "callback exception reaps process tree" `Slow + callback_exception_reaps_process_tree; + Alcotest.test_case "cancellation reaps process tree" `Slow + cancellation_reaps_process_tree; ] From 45501702f00c21d4c27e8ffa6b85e80c702e82d5 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 16:53:00 -0400 Subject: [PATCH 02/57] refactor: introduce structured engine diagnostics --- README.md | 6 +- bin/main.ml | 91 ++++++++++++++--- docs/architecture.md | 7 +- docs/diagnostics.md | 46 +++++++++ lib/contract.ml | 1 + lib/diagnostic.ml | 182 +++++++++++++++++++++++++++++++++ lib/diagnostic.mli | 94 +++++++++++++++++ lib/external_replay.ml | 68 +++++++++--- lib/external_replay.mli | 4 +- lib/journal.ml | 73 ++++++++++--- lib/journal.mli | 6 +- lib/replay.ml | 61 +++++++++-- lib/replay.mli | 6 +- lib/scenario.ml | 58 +++++++++-- lib/scenario.mli | 14 +-- lib/scenario_stream.ml | 156 +++++++++++++++++++--------- lib/scenario_stream.mli | 16 +-- lib/sha256.ml | 6 +- lib/sha256.mli | 2 +- lib/strategy_process.ml | 136 +++++++++++++++++------- lib/strategy_process.mli | 6 +- lib/strategy_protocol.ml | 43 +++++++- lib/strategy_protocol.mli | 6 +- lib/strategy_transcript.ml | 64 +++++++++--- lib/strategy_transcript.mli | 6 +- test/cli.t | 12 ++- test/dune | 1 + test/test_diagnostic.ml | 71 +++++++++++++ test/test_engine.ml | 1 + test/test_scenario.ml | 39 +++++-- test/test_strategy_protocol.ml | 15 +-- test/test_support.ml | 5 +- 32 files changed, 1083 insertions(+), 219 deletions(-) create mode 100644 docs/diagnostics.md create mode 100644 lib/diagnostic.ml create mode 100644 lib/diagnostic.mli create mode 100644 test/test_diagnostic.ml diff --git a/README.md b/README.md index ac08bdc..12d18fb 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,10 @@ opam exec -- dune exec trading-engine -- --capabilities Clients must confirm that both `scenario_contract_versions` and `journal_contract_versions` contain the scenario's `contract_version` before starting a replay. External clients must also -require their version in `strategy_protocol_versions`. +require their version in `strategy_protocol_versions`. Runtime failures use the structured +diagnostic contract advertised by `diagnostic_versions`. Human diagnostics remain the default. +Use `--diagnostic-format json` to receive one JSON diagnostic on standard error with a stable code, +phase, typed context, and sanitized underlying cause. The final and `.partial` journal paths must not already exist. Batch JSON hashes the same complete document it parses. JSON Lines input is hashed and validated in a bounded-memory pass before the @@ -178,6 +181,7 @@ production recovery log. ## Architecture and contracts - [Architecture](docs/architecture.md) +- [Diagnostic contract](docs/diagnostics.md) - [Scenario contract](docs/scenario.md) - [Current contract v3 and conformance fixtures](contracts/v3/README.md) - [Frozen contract v2](contracts/v2/README.md) diff --git a/bin/main.ml b/bin/main.ml index fdbc957..296b043 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -1,5 +1,17 @@ open Cmdliner +type diagnostic_format = Human | Json + +let cli_error message = + Trading_engine.Diagnostic.make + ~code:Trading_engine.Diagnostic.Cli_invalid_arguments + ~phase:Trading_engine.Diagnostic.Cli message + +let input_error ~message exception_ = + Trading_engine.Diagnostic.of_exception + ~code:Trading_engine.Diagnostic.Input_io + ~phase:Trading_engine.Diagnostic.Input ~message exception_ + let count predicate values = List.fold_left (fun total value -> total + Bool.to_int (predicate value)) @@ -138,7 +150,11 @@ let run_external_stream environment input journal strategy = let execute_json environment input journal validate_only strategy = let document = try Ok (In_channel.with_open_bin input In_channel.input_all) - with Sys_error message -> Error ("could not read scenario: " ^ message) + with Sys_error message as exception_ -> + Error + (input_error + ~message:("could not read scenario: " ^ message) + exception_) in match document with | Error _ as error -> error @@ -149,7 +165,9 @@ let execute_json environment input journal validate_only strategy = | Ok scenario -> ( if validate_only then match journal with - | Some _ -> Error "--journal cannot be used with --validate-only" + | Some _ -> + Error + (cli_error "--journal cannot be used with --validate-only") | None -> ( match Trading_engine.Replay.run ~scenario_sha256 scenario with | Error message -> Error message @@ -166,7 +184,9 @@ let execute_json environment input journal validate_only strategy = else match journal with | None -> - Error "--journal is required unless --validate-only is set" + Error + (cli_error + "--journal is required unless --validate-only is set") | Some path -> ( match strategy with | None -> run_replay scenario_sha256 scenario path @@ -177,7 +197,8 @@ let execute_json environment input journal validate_only strategy = let execute_jsonl environment input journal validate_only strategy = if validate_only then match journal with - | Some _ -> Error "--journal cannot be used with --validate-only" + | Some _ -> + Error (cli_error "--journal cannot be used with --validate-only") | None -> ( match Trading_engine.Replay.run_stream input with | Error message -> Error message @@ -190,7 +211,8 @@ let execute_jsonl environment input journal validate_only strategy = Ok ()) else match journal with - | None -> Error "--journal is required unless --validate-only is set" + | None -> + Error (cli_error "--journal is required unless --validate-only is set") | Some path -> ( match strategy with | None -> run_stream input path @@ -207,14 +229,17 @@ let external_strategy executable arguments timeout transcript = | None, None, [], None -> Ok None | None, _, _, _ -> Error - "--strategy-arg, --strategy-timeout, and --strategy-transcript require \ - --strategy-executable" + (cli_error + "--strategy-arg, --strategy-timeout, and --strategy-transcript \ + require --strategy-executable") | Some _, None, _, _ -> - Error "--strategy-transcript is required with --strategy-executable" + Error + (cli_error + "--strategy-transcript is required with --strategy-executable") | Some executable, Some transcript, arguments, timeout -> let timeout = Option.value timeout ~default:30.0 in if (not (Float.is_finite timeout)) || Float.compare timeout 0.0 <= 0 then - Error "--strategy-timeout must be finite and positive" + Error (cli_error "--strategy-timeout must be finite and positive") else Ok (Some { command = executable :: arguments; timeout; transcript }) let execute environment input journal validate_only capabilities input_format @@ -235,10 +260,12 @@ let execute environment input journal validate_only capabilities input_format Ok () | _ -> Error - "--capabilities cannot be combined with replay or strategy options" + (cli_error + "--capabilities cannot be combined with replay or strategy options") else match input with - | None -> Error "--input is required unless --capabilities is set" + | None -> + Error (cli_error "--input is required unless --capabilities is set") | Some path -> ( match external_strategy strategy_executable strategy_arguments @@ -247,7 +274,8 @@ let execute environment input journal validate_only capabilities input_format | Error _ as error -> error | Ok (Some _) when validate_only -> Error - "external strategy options cannot be used with --validate-only" + (cli_error + "external strategy options cannot be used with --validate-only") | Ok strategy -> execute_scenario environment path journal validate_only strategy input_format) @@ -277,6 +305,12 @@ let capabilities = let doc = "Print machine-readable engine capabilities as JSON and exit." in Arg.(value & flag & info [ "capabilities" ] ~doc) +let diagnostic_format = + let formats = Arg.enum [ ("human", Human); ("json", Json) ] in + let doc = "Render runtime diagnostics as $(docv) (default: human)." in + Arg.( + value & opt formats Human & info [ "diagnostic-format" ] ~docv:"FORMAT" ~doc) + let strategy_executable = let doc = "Launch $(docv) as the external strategy process without using a shell." @@ -329,12 +363,39 @@ let command environment = (Cmd.info "trading-engine" ~version:Trading_engine.Contract.engine_version ~doc ~man) Term.( - const (execute environment) + const + (fun + input + journal + validate_only + capabilities + input_format + strategy_executable + strategy_arguments + strategy_timeout + strategy_transcript + diagnostic_format + -> + ( diagnostic_format, + execute environment input journal validate_only capabilities + input_format strategy_executable strategy_arguments + strategy_timeout strategy_transcript )) $ input $ journal $ validate_only $ capabilities $ input_format $ strategy_executable $ strategy_argument $ strategy_timeout - $ strategy_transcript) + $ strategy_transcript $ diagnostic_format) let () = Fmt_tty.setup_std_outputs (); Eio_main.run @@ fun environment -> - exit (Cmd.eval_result (command environment)) + match Cmd.eval_value' (command environment) with + | `Exit code -> exit code + | `Ok (_, Ok ()) -> exit Cmd.Exit.ok + | `Ok (format, Error diagnostic) -> + let rendered = + match format with + | Human -> + "trading-engine: " ^ Trading_engine.Diagnostic.to_human diagnostic + | Json -> Trading_engine.Diagnostic.to_json diagnostic + in + Fmt.epr "%s@." rendered; + exit Cmd.Exit.some_error diff --git a/docs/architecture.md b/docs/architecture.md index 2e92b15..adc36f4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,7 +19,12 @@ journal files, and the runtime shell. | `Engine` | Sequencing, portfolio reconciliation, and pure suspend/resume orchestration | | `Scenario`, `Scenario_stream`, `Replay` | Strict batch and bounded-memory scripted runners | | `Strategy_protocol`, `Strategy_process`, `External_replay` | Versioned child supervision and external runners | -| `Sha256`, `Codec`, `Journal`, `Strategy_transcript` | Input identity, stable audit JSON, and file publication | +| `Sha256`, `Codec`, `Diagnostic`, `Journal`, `Strategy_transcript` | Input identity, stable diagnostics and audit JSON, and file publication | + +Boundary failures use the versioned [diagnostic contract](diagnostics.md). Pure domain constructors +and reducer internals keep plain errors inside the deterministic boundary; replay adapters attach +stable codes, phases, source locations, event causality, and sanitized exception details before +returning an error to callers. ## Reducer phases diff --git a/docs/diagnostics.md b/docs/diagnostics.md new file mode 100644 index 0000000..dd94ca3 --- /dev/null +++ b/docs/diagnostics.md @@ -0,0 +1,46 @@ +# Diagnostics + +Process and file boundaries return diagnostic contract version `1`. The CLI prints the concise +`message` by default. Pass `--diagnostic-format json` to write one machine-readable diagnostic to +standard error. The process exits with status 123 for either format. + +Every JSON diagnostic contains: + +| Field | Type | Meaning | +| --- | --- | --- | +| `diagnostic_version` | string | Diagnostic contract version | +| `code` | string | Stable machine classification | +| `phase` | string | `cli`, `input`, `validation`, `replay`, `reducer`, `strategy`, or `artifact` | +| `message` | string | Concise human description; clients must not parse it | +| `context` | object | Known location and causality fields | +| `cause` | object or null | Sanitized underlying exception | + +Context fields are omitted when unknown. `json_path`, `event_id`, and `order_id` are strings; +`line` is a JSON integer; `sequence` is a canonical int64 string; and `causation_ids` is an ordered +array of event ID strings. A cause contains `kind` and `message`, plus `operation` and `target` for +Unix errors. Diagnostics retain no input record, strategy message, or unrelated payload data. + +Version 1 defines these codes: + +| Code | Meaning | +| --- | --- | +| `cli.invalid_arguments` | Runtime option combination is invalid | +| `input.io` | Input open, read, or hash operation failed | +| `scenario.invalid_json` | Scenario or strategy JSON syntax is invalid | +| `scenario.invalid` | Batch scenario validation failed | +| `scenario.unsupported_contract` | Scenario contract version is unsupported | +| `scenario_stream.invalid` | Stream envelope, ordering, or payload validation failed | +| `scenario_stream.changed` | Stream bytes changed between validation and replay | +| `replay.failed` | Replay orchestration invariant failed | +| `reducer.failed` | Pure engine processing rejected the requested transition | +| `strategy.invalid_configuration` | Strategy command or timeout is invalid | +| `strategy.protocol` | Strategy exchange violated the protocol | +| `strategy.timeout` | Strategy exchange or shutdown exceeded its deadline | +| `strategy.process` | Strategy spawn, signaling, supervision, or process I/O failed | +| `strategy.exit` | Strategy exited with an unsuccessful status | +| `artifact.exists` | A final or partial artifact path already exists | +| `artifact.io` | Artifact creation, append, close, or publication failed | +| `artifact.state` | Artifact writer lifecycle operation is invalid | + +Adding codes or optional context fields does not change the diagnostic version. Removing a code, +changing a field type, or changing a code's meaning requires a new version. diff --git a/lib/contract.ml b/lib/contract.ml index 05de043..9e34397 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -13,6 +13,7 @@ let capabilities_to_yojson () = ("journal_formats", strings [ "jsonl" ]); ("execution_models", strings Execution_model.supported); ("strategy_protocol_versions", strings [ strategy_protocol_version ]); + ("diagnostic_versions", strings [ Diagnostic.version ]); ] let capabilities_to_string () = diff --git a/lib/diagnostic.ml b/lib/diagnostic.ml new file mode 100644 index 0000000..9289165 --- /dev/null +++ b/lib/diagnostic.ml @@ -0,0 +1,182 @@ +let version = "1" + +type code = + | Cli_invalid_arguments + | Input_io + | Scenario_invalid_json + | Scenario_invalid + | Scenario_unsupported_contract + | Scenario_stream_invalid + | Scenario_stream_changed + | Replay_failed + | Reducer_failed + | Strategy_invalid_configuration + | Strategy_protocol + | Strategy_timeout + | Strategy_process + | Strategy_exit + | Artifact_exists + | Artifact_io + | Artifact_state + +type phase = Cli | Input | Validation | Replay | Reducer | Strategy | Artifact + +type cause = { + kind : string; + message : string; + operation : string option; + target : string option; +} + +type context = { + json_path : string option; + line : int option; + sequence : int64 option; + event_id : string option; + order_id : string option; + causation_ids : string list; +} + +type t = { + code : code; + phase : phase; + message : string; + context : context; + cause : cause option; +} + +let code_to_string = function + | Cli_invalid_arguments -> "cli.invalid_arguments" + | Input_io -> "input.io" + | Scenario_invalid_json -> "scenario.invalid_json" + | Scenario_invalid -> "scenario.invalid" + | Scenario_unsupported_contract -> "scenario.unsupported_contract" + | Scenario_stream_invalid -> "scenario_stream.invalid" + | Scenario_stream_changed -> "scenario_stream.changed" + | Replay_failed -> "replay.failed" + | Reducer_failed -> "reducer.failed" + | Strategy_invalid_configuration -> "strategy.invalid_configuration" + | Strategy_protocol -> "strategy.protocol" + | Strategy_timeout -> "strategy.timeout" + | Strategy_process -> "strategy.process" + | Strategy_exit -> "strategy.exit" + | Artifact_exists -> "artifact.exists" + | Artifact_io -> "artifact.io" + | Artifact_state -> "artifact.state" + +let phase_to_string = function + | Cli -> "cli" + | Input -> "input" + | Validation -> "validation" + | Replay -> "replay" + | Reducer -> "reducer" + | Strategy -> "strategy" + | Artifact -> "artifact" + +let cause_of_exception = function + | Unix.Unix_error (code, operation, target) -> + { + kind = "unix_error"; + message = Unix.error_message code; + operation = Some operation; + target = Some target; + } + | Sys_error message -> + { kind = "system_error"; message; operation = None; target = None } + | exception_ -> + { + kind = "exception"; + message = Printexc.to_string exception_; + operation = None; + target = None; + } + +let context ?json_path ?line ?sequence ?event_id ?order_id ?(causation_ids = []) + () = + { json_path; line; sequence; event_id; order_id; causation_ids } + +let make ?json_path ?line ?sequence ?event_id ?order_id ?causation_ids ?cause + ~code ~phase message = + { + code; + phase; + message; + context = + context ?json_path ?line ?sequence ?event_id ?order_id ?causation_ids (); + cause; + } + +let of_exception ?json_path ?line ?sequence ~code ~phase ~message exception_ = + make ?json_path ?line ?sequence + ~cause:(cause_of_exception exception_) + ~code ~phase message + +let annotate ?json_path ?line ?sequence ?event_id ?order_id ?causation_ids + diagnostic = + let choose supplied existing = + match existing with Some _ -> existing | None -> supplied + in + let context = diagnostic.context in + { + diagnostic with + context = + { + json_path = choose json_path context.json_path; + line = choose line context.line; + sequence = choose sequence context.sequence; + event_id = choose event_id context.event_id; + order_id = choose order_id context.order_id; + causation_ids = + Option.value causation_ids ~default:context.causation_ids; + }; + } + +let combine primary secondary = + { + primary with + message = primary.message ^ "; " ^ secondary.message; + cause = + (match primary.cause with + | Some _ as cause -> cause + | None -> secondary.cause); + } + +let optional name value encode = + match value with None -> [] | Some value -> [ (name, encode value) ] + +let context_to_yojson context = + `Assoc + (optional "json_path" context.json_path (fun value -> `String value) + @ optional "line" context.line (fun value -> `Int value) + @ optional "sequence" context.sequence (fun value -> + `String (Int64.to_string value)) + @ optional "event_id" context.event_id (fun value -> `String value) + @ optional "order_id" context.order_id (fun value -> `String value) + @ + if context.causation_ids = [] then [] + else + [ + ( "causation_ids", + `List (List.map (fun value -> `String value) context.causation_ids) ); + ]) + +let cause_to_yojson cause = + `Assoc + ([ ("kind", `String cause.kind); ("message", `String cause.message) ] + @ optional "operation" cause.operation (fun value -> `String value) + @ optional "target" cause.target (fun value -> `String value)) + +let to_yojson diagnostic = + `Assoc + [ + ("diagnostic_version", `String version); + ("code", `String (code_to_string diagnostic.code)); + ("phase", `String (phase_to_string diagnostic.phase)); + ("message", `String diagnostic.message); + ("context", context_to_yojson diagnostic.context); + ("cause", Option.fold ~none:`Null ~some:cause_to_yojson diagnostic.cause); + ] + +let to_json diagnostic = to_yojson diagnostic |> Yojson.Safe.to_string +let to_human diagnostic = diagnostic.message +let pp formatter diagnostic = Fmt.string formatter (to_human diagnostic) diff --git a/lib/diagnostic.mli b/lib/diagnostic.mli new file mode 100644 index 0000000..a1371f1 --- /dev/null +++ b/lib/diagnostic.mli @@ -0,0 +1,94 @@ +(** Stable structured errors for process and file boundaries. *) + +val version : string + +type code = + | Cli_invalid_arguments + | Input_io + | Scenario_invalid_json + | Scenario_invalid + | Scenario_unsupported_contract + | Scenario_stream_invalid + | Scenario_stream_changed + | Replay_failed + | Reducer_failed + | Strategy_invalid_configuration + | Strategy_protocol + | Strategy_timeout + | Strategy_process + | Strategy_exit + | Artifact_exists + | Artifact_io + | Artifact_state + +type phase = Cli | Input | Validation | Replay | Reducer | Strategy | Artifact + +type cause = private { + kind : string; + message : string; + operation : string option; + target : string option; +} + +type context = private { + json_path : string option; + line : int option; + sequence : int64 option; + event_id : string option; + order_id : string option; + causation_ids : string list; +} + +type t = private { + code : code; + phase : phase; + message : string; + context : context; + cause : cause option; +} + +val cause_of_exception : exn -> cause + +val make : + ?json_path:string -> + ?line:int -> + ?sequence:int64 -> + ?event_id:string -> + ?order_id:string -> + ?causation_ids:string list -> + ?cause:cause -> + code:code -> + phase:phase -> + string -> + t + +val of_exception : + ?json_path:string -> + ?line:int -> + ?sequence:int64 -> + code:code -> + phase:phase -> + message:string -> + exn -> + t + +val annotate : + ?json_path:string -> + ?line:int -> + ?sequence:int64 -> + ?event_id:string -> + ?order_id:string -> + ?causation_ids:string list -> + t -> + t + +val combine : t -> t -> t +(** [combine primary secondary] preserves the primary identity and context while + adding the secondary message and an underlying cause when needed. *) + +val code_to_string : code -> string +val phase_to_string : phase -> string +val to_yojson : t -> Yojson.Safe.t +val to_json : t -> string +val to_human : t -> string +val pp : t Fmt.t diff --git a/lib/external_replay.ml b/lib/external_replay.ml index 86850d6..feb2596 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -31,6 +31,17 @@ type stream_state = { audit_count : int64; } +let reducer ?sequence message = + Diagnostic.make ?sequence ~code:Diagnostic.Reducer_failed + ~phase:Diagnostic.Reducer message + +let replay ?sequence message = + Diagnostic.make ?sequence ~code:Diagnostic.Replay_failed + ~phase:Diagnostic.Replay message + +let reducer_result ?sequence result = + Result.map_error (reducer ?sequence) result + let ( let* ) result function_ = match result with Ok value -> function_ value | Error _ as error -> error @@ -69,8 +80,9 @@ let create_runner ~run_id ~scenario_sha256 ~risk ~execution_model ~execution ~max_internal_events ~initial_cash = let* config = Engine.config ~risk ~execution_model ~execution ~max_internal_events + |> reducer_result in - Runner.create ~run_id ~scenario_sha256 ~config ~initial_cash + Runner.create ~run_id ~scenario_sha256 ~config ~initial_cash |> reducer_result let append_events journal events = match journal with @@ -85,22 +97,26 @@ let append_events journal events = let add_audit_count count events = let added = Int64.of_int (List.length events) in if Int64.compare count (Int64.sub Int64.max_int added) > 0 then - Error "audit event count is exhausted" + Error (replay "audit event count is exhausted") else Ok (Int64.add count added) let rec drive respond progress = match Runner.strategy_request progress with | Some (context, event) -> let* intents = respond context event in - let* progress = Runner.resume progress intents in + let* progress = Runner.resume progress intents |> reducer_result in drive respond progress | None -> ( match Runner.slice_result progress with | Some result -> Ok result - | None -> Error "interactive engine reached an invalid progress state") + | None -> + Error (replay "interactive engine reached an invalid progress state")) let process_slice respond runner market_slice = - let* progress = Runner.process_slice runner market_slice in + let* progress = + Runner.process_slice runner market_slice + |> reducer_result ~sequence:market_slice.Market_slice.slice_sequence + in drive respond progress let close_journal = function @@ -110,7 +126,8 @@ let close_journal = function let run ~env ~scenario_sha256 ~journal_path ~transcript_path ~strategy_command ~strategy_timeout (scenario : Scenario.t) = if scenario.schedule <> [] then - Error "external strategy replay requires an empty scenario schedule" + Error + (replay "external strategy replay requires an empty scenario schedule") else let* initial = create_runner ~run_id:scenario.run_id ~scenario_sha256 ~risk:scenario.risk @@ -135,7 +152,9 @@ let run ~env ~scenario_sha256 ~journal_path ~transcript_path ~strategy_command let* state, audits_rev = List.fold_left step (Ok (initial, [])) scenario.slices in - let* state, valuation, completion_events = Runner.complete state in + let* state, valuation, completion_events = + Runner.complete state |> reducer_result + in let* () = append_events journal_ref completion_events in Ok ( state, @@ -172,7 +191,9 @@ let validate_stream_pass ~scenario_sha256 channel = Ok (runner, initialization_of_header ~scenario_sha256 header, 0L)) ~step:(fun (runner, initialization, slice_count) item -> if item.Scenario.intents <> [] then - Error "external strategy replay requires empty streamed intents" + Error + (replay ~sequence:item.market_slice.slice_sequence + "external strategy replay requires empty streamed intents") else let* runner, _ = process_slice (fun _ _ -> Ok []) runner item.market_slice @@ -180,9 +201,9 @@ let validate_stream_pass ~scenario_sha256 channel = Ok (runner, initialization, Int64.succ slice_count)) ~finish:(fun (runner, initialization, counted_slices) ~slice_count -> if not (Int64.equal counted_slices slice_count) then - Error "scenario stream slice count changed during validation" + Error (replay "scenario stream slice count changed during validation") else - let* _, _, _ = Runner.complete runner in + let* _, _, _ = Runner.complete runner |> reducer_result in Ok { initialization; slice_count }) let replay_stream_pass ~scenario_sha256 ~journal ~session channel = @@ -198,7 +219,9 @@ let replay_stream_pass ~scenario_sha256 ~journal ~session channel = Ok { runner; journal = Some journal; audit_count = 0L }) ~step:(fun state item -> if item.Scenario.intents <> [] then - Error "external strategy replay requires empty streamed intents" + Error + (replay ~sequence:item.market_slice.slice_sequence + "external strategy replay requires empty streamed intents") else let* runner, events = process_slice @@ -209,7 +232,9 @@ let replay_stream_pass ~scenario_sha256 ~journal ~session channel = let* audit_count = add_audit_count state.audit_count events in Ok { state with runner; audit_count }) ~finish:(fun state ~slice_count -> - let* runner, valuation, events = Runner.complete state.runner in + let* runner, valuation, events = + Runner.complete state.runner |> reducer_result + in let* () = append_events state.journal events in let* audit_count = add_audit_count state.audit_count events in Ok (runner, valuation, audit_count, slice_count)) @@ -229,7 +254,10 @@ let run_stream ~env ~journal_path ~transcript_path ~strategy_command seek_in channel 0; let validated_sha256 = Sha256.digest_channel channel in if not (String.equal scenario_sha256 validated_sha256) then - Error "scenario stream changed during validation" + Error + (Diagnostic.make ~code:Diagnostic.Scenario_stream_changed + ~phase:Diagnostic.Input + "scenario stream changed during validation") else let* journal = Journal.create journal_path in journal_ref := Some journal; @@ -244,7 +272,10 @@ let run_stream ~env ~journal_path ~transcript_path ~strategy_command seek_in channel 0; let replayed_sha256 = Sha256.digest_channel channel in if not (String.equal scenario_sha256 replayed_sha256) then - Error "scenario stream changed during replay" + Error + (Diagnostic.make ~code:Diagnostic.Scenario_stream_changed + ~phase:Diagnostic.Input + "scenario stream changed during replay") else Ok (runner, valuation, audit_count, slice_count)) in match session_result with @@ -266,5 +297,10 @@ let run_stream ~env ~journal_path ~transcript_path ~strategy_command slice_count; strategy; })) - with Sys_error message -> - fail (Error ("could not read scenario stream: " ^ message)) + with Sys_error message as exception_ -> + fail + (Error + (Diagnostic.of_exception ~code:Diagnostic.Input_io + ~phase:Diagnostic.Input + ~message:("could not read scenario stream: " ^ message) + exception_)) diff --git a/lib/external_replay.mli b/lib/external_replay.mli index 13a96e3..3fbb1df 100644 --- a/lib/external_replay.mli +++ b/lib/external_replay.mli @@ -28,7 +28,7 @@ val run : strategy_command:string list -> strategy_timeout:float -> Scenario.t -> - (result, string) Stdlib.result + (result, Diagnostic.t) Stdlib.result val run_stream : env:Eio_unix.Stdenv.base -> @@ -37,4 +37,4 @@ val run_stream : strategy_command:string list -> strategy_timeout:float -> string -> - (streamed_result, string) Stdlib.result + (streamed_result, Diagnostic.t) Stdlib.result diff --git a/lib/journal.ml b/lib/journal.ml index e7a9e84..9db53d6 100644 --- a/lib/journal.ml +++ b/lib/journal.ml @@ -5,12 +5,34 @@ type t = { mutable closed : bool; } +let diagnostic ?event_id ?order_id ?causation_ids ~code message = + Diagnostic.make ?event_id ?order_id ?causation_ids ~code + ~phase:Diagnostic.Artifact message + +let audit_context event = + let event_id = Id.Event.to_string event.Audit.event_id in + let causation_ids = List.map Id.Event.to_string event.causation_ids in + let order_id = + match event.event with + | Audit.Order_accepted order | Order_rejected order -> + Some (Id.Order.to_string order.Order.id) + | Order_cancelled { order; _ } -> Some (Id.Order.to_string order.id) + | Fill_applied fill -> Some (Id.Order.to_string fill.Fill.order_id) + | Margin_limited { order_id; _ } -> Some (Id.Order.to_string order_id) + | _ -> None + in + (event_id, order_id, causation_ids) + let create final_path = let partial_path = final_path ^ ".partial" in if Sys.file_exists final_path then - Error ("journal already exists: " ^ final_path) + Error + (diagnostic ~code:Diagnostic.Artifact_exists + ("journal already exists: " ^ final_path)) else if Sys.file_exists partial_path then - Error ("partial journal already exists: " ^ partial_path) + Error + (diagnostic ~code:Diagnostic.Artifact_exists + ("partial journal already exists: " ^ partial_path)) else try let channel = @@ -19,18 +41,34 @@ let create final_path = 0o600 partial_path in Ok { final_path; partial_path; channel; closed = false } - with Sys_error message -> Error ("could not create journal: " ^ message) + with Sys_error message as exception_ -> + Error + (Diagnostic.of_exception ~code:Diagnostic.Artifact_io + ~phase:Diagnostic.Artifact + ~message:("could not create journal: " ^ message) + exception_) let append journal event = - if journal.closed then Error "cannot append to a closed journal" + let event_id, order_id, causation_ids = audit_context event in + if journal.closed then + Error + (diagnostic ~event_id ?order_id ~causation_ids + ~code:Diagnostic.Artifact_state "cannot append to a closed journal") else try output_string journal.channel (Codec.audit_to_string event); output_char journal.channel '\n'; flush journal.channel; Ok () - with Sys_error message -> - Error ("could not append journal " ^ journal.partial_path ^ ": " ^ message) + with Sys_error message as exception_ -> + Error + (Diagnostic.of_exception ~code:Diagnostic.Artifact_io + ~phase:Diagnostic.Artifact + ~message: + ("could not append journal " ^ journal.partial_path ^ ": " + ^ message) + exception_ + |> Diagnostic.annotate ~event_id ?order_id ~causation_ids) let close_preserving_partial journal = if not journal.closed then ( @@ -38,7 +76,10 @@ let close_preserving_partial journal = close_out_noerr journal.channel) let commit journal = - if journal.closed then Error "cannot commit a closed journal" + if journal.closed then + Error + (diagnostic ~code:Diagnostic.Artifact_state + "cannot commit a closed journal") else try flush journal.channel; @@ -48,11 +89,19 @@ let commit journal = Unix.unlink journal.partial_path; Ok () with - | Sys_error message -> + | Sys_error message as exception_ -> close_preserving_partial journal; - Error ("could not finalize journal: " ^ message) - | Unix.Unix_error (code, operation, target) -> + Error + (Diagnostic.of_exception ~code:Diagnostic.Artifact_io + ~phase:Diagnostic.Artifact + ~message:("could not finalize journal: " ^ message) + exception_) + | Unix.Unix_error (code, operation, target) as exception_ -> close_preserving_partial journal; Error - (Printf.sprintf "could not finalize journal: %s(%s): %s" operation - target (Unix.error_message code)) + (Diagnostic.of_exception ~code:Diagnostic.Artifact_io + ~phase:Diagnostic.Artifact + ~message: + (Printf.sprintf "could not finalize journal: %s(%s): %s" + operation target (Unix.error_message code)) + exception_) diff --git a/lib/journal.mli b/lib/journal.mli index 29b58c2..0a5619b 100644 --- a/lib/journal.mli +++ b/lib/journal.mli @@ -2,7 +2,7 @@ type t -val create : string -> (t, string) result -val append : t -> Audit.t -> (unit, string) result +val create : string -> (t, Diagnostic.t) result +val append : t -> Audit.t -> (unit, Diagnostic.t) result val close_preserving_partial : t -> unit -val commit : t -> (unit, string) result +val commit : t -> (unit, Diagnostic.t) result diff --git a/lib/replay.ml b/lib/replay.ml index dc3b63a..39dfc46 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -28,6 +28,17 @@ type stream_state = { schedule_count : int64; } +let reducer ?sequence message = + Diagnostic.make ?sequence ~code:Diagnostic.Reducer_failed + ~phase:Diagnostic.Reducer message + +let replay ?sequence message = + Diagnostic.make ?sequence ~code:Diagnostic.Replay_failed + ~phase:Diagnostic.Replay message + +let reducer_result ?sequence result = + Result.map_error (reducer ?sequence) result + let append_events journal events = match journal with | None -> Ok () @@ -42,7 +53,7 @@ let append_events journal events = let add_audit_count count events = let added = Int64.of_int (List.length events) in if Int64.compare count (Int64.sub Int64.max_int added) > 0 then - Error "audit event count is exhausted" + Error (replay "audit event count is exhausted") else Ok (Int64.add count added) let run ~scenario_sha256 ?journal_path scenario = @@ -66,7 +77,9 @@ let run ~scenario_sha256 ?journal_path scenario = | Ok () -> result | Error _ as error -> error) in - match Scripted_strategy.create scenario.Scenario.schedule with + match + Scripted_strategy.create scenario.Scenario.schedule |> reducer_result + with | Error _ as error -> fail error | Ok strategy_state -> ( match @@ -74,12 +87,14 @@ let run ~scenario_sha256 ?journal_path scenario = ~execution_model:scenario.execution_model ~execution:scenario.execution ~max_internal_events:scenario.max_internal_events + |> reducer_result with | Error _ as error -> fail error | Ok config -> ( match Runner.create ~run_id:scenario.run_id ~scenario_sha256 ~config ~initial_cash:scenario.initial_cash ~strategy_state + |> reducer_result with | Error _ as error -> fail error | Ok initial -> ( @@ -87,7 +102,12 @@ let run ~scenario_sha256 ?journal_path scenario = match result with | Error _ as error -> error | Ok (state, audits_rev) -> ( - match Runner.process_slice state market_slice with + match + Runner.process_slice state market_slice + |> reducer_result + ~sequence: + market_slice.Market_slice.slice_sequence + with | Error _ as error -> error | Ok (state, events) -> ( match append_events journal events with @@ -100,7 +120,7 @@ let run ~scenario_sha256 ?journal_path scenario = with | Error _ as error -> fail error | Ok (state, audits_rev) -> ( - match Runner.complete state with + match Runner.complete state |> reducer_result with | Error _ as error -> fail error | Ok (state, valuation, completion_events) -> ( match append_events journal completion_events with @@ -121,7 +141,7 @@ let run ~scenario_sha256 ?journal_path scenario = let run_stream_pass ~scenario_sha256 ~journal channel = Scenario_stream.fold_channel channel ~init:(fun header -> - match Scripted_strategy.create [] with + match Scripted_strategy.create [] |> reducer_result with | Error _ as error -> error | Ok strategy_state -> ( match @@ -129,12 +149,14 @@ let run_stream_pass ~scenario_sha256 ~journal channel = ~execution_model:header.execution_model ~execution:header.execution ~max_internal_events:header.max_internal_events + |> reducer_result with | Error _ as error -> error | Ok config -> ( match Runner.create ~run_id:header.run_id ~scenario_sha256 ~config ~initial_cash:header.initial_cash ~strategy_state + |> reducer_result with | Error _ as error -> error | Ok runner -> @@ -151,11 +173,15 @@ let run_stream_pass ~scenario_sha256 ~journal channel = match Scripted_strategy.create [ (item.Scenario.market_slice.slice_sequence, item.intents) ] + |> reducer_result ~sequence:item.market_slice.slice_sequence with | Error _ as error -> error | Ok strategy_state -> ( let runner = Runner.with_strategy_state state.runner strategy_state in - match Runner.process_slice runner item.market_slice with + match + Runner.process_slice runner item.market_slice + |> reducer_result ~sequence:item.market_slice.slice_sequence + with | Error _ as error -> error | Ok (runner, events) -> ( match append_events state.journal events with @@ -169,7 +195,7 @@ let run_stream_pass ~scenario_sha256 ~journal channel = |> Result.map (fun audit_count -> { state with runner; audit_count; schedule_count })))) ~finish:(fun state ~slice_count -> - match Runner.complete state.runner with + match Runner.complete state.runner |> reducer_result with | Error _ as error -> error | Ok (runner, valuation, events) -> ( match append_events state.journal events with @@ -205,7 +231,10 @@ let run_stream ?journal_path path = seek_in channel 0; let validated_sha256 = Sha256.digest_channel channel in if not (String.equal scenario_sha256 validated_sha256) then - Error "scenario stream changed during validation" + Error + (Diagnostic.make ~code:Diagnostic.Scenario_stream_changed + ~phase:Diagnostic.Input + "scenario stream changed during validation") else match journal_path with | None -> Ok validated @@ -225,10 +254,20 @@ let run_stream ?journal_path path = let replayed_sha256 = Sha256.digest_channel channel in if not (String.equal scenario_sha256 replayed_sha256) then - fail (Error "scenario stream changed during replay") + fail + (Error + (Diagnostic.make + ~code:Diagnostic.Scenario_stream_changed + ~phase:Diagnostic.Input + "scenario stream changed during replay")) else match Journal.commit created with | Error _ as error -> error | Ok () -> Ok replayed))))) - with Sys_error message -> - fail (Error ("could not read scenario stream: " ^ message)) + with Sys_error message as exception_ -> + fail + (Error + (Diagnostic.of_exception ~code:Diagnostic.Input_io + ~phase:Diagnostic.Input + ~message:("could not read scenario stream: " ^ message) + exception_)) diff --git a/lib/replay.mli b/lib/replay.mli index 3ee4523..8a6447a 100644 --- a/lib/replay.mli +++ b/lib/replay.mli @@ -24,7 +24,9 @@ val run : scenario_sha256:string -> ?journal_path:string -> Scenario.t -> - (result, string) Stdlib.result + (result, Diagnostic.t) Stdlib.result val run_stream : - ?journal_path:string -> string -> (streamed_result, string) Stdlib.result + ?journal_path:string -> + string -> + (streamed_result, Diagnostic.t) Stdlib.result diff --git a/lib/scenario.ml b/lib/scenario.ml index 2d1bdf4..2855d6e 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -368,7 +368,11 @@ let parse_intent json = | None -> Error "intent is missing type") | _ -> Error "intent must be a JSON object" -let intent_of_yojson = parse_intent +let intent_of_yojson json = + parse_intent json + |> Result.map_error (fun message -> + Diagnostic.make ~code:Diagnostic.Scenario_invalid + ~phase:Diagnostic.Validation ~json_path:"$" message) let parse_schedule_item json = let* fields = @@ -763,7 +767,7 @@ let validate_schedule risk catalog schedule slices = in validate None schedule -let of_yojson json = +let of_yojson_result json = let* fields = object_fields ~name:"scenario" ~expected: @@ -865,15 +869,39 @@ let of_yojson json = slices; } +let of_yojson json = + let code, json_path = + match json with + | `Assoc fields -> ( + match List.assoc_opt "contract_version" fields with + | Some (`String supplied) + when not (String.equal supplied Contract.version) -> + (Diagnostic.Scenario_unsupported_contract, "$.contract_version") + | _ -> (Diagnostic.Scenario_invalid, "$")) + | _ -> (Diagnostic.Scenario_invalid, "$") + in + of_yojson_result json + |> Result.map_error (fun message -> + Diagnostic.make ~code ~phase:Diagnostic.Validation ~json_path message) + let of_string document = try Yojson.Safe.from_string document |> of_yojson - with Yojson.Json_error message -> Error ("invalid scenario JSON: " ^ message) + with Yojson.Json_error message as exception_ -> + Error + (Diagnostic.of_exception ~code:Diagnostic.Scenario_invalid_json + ~phase:Diagnostic.Input ~json_path:"$" + ~message:("invalid scenario JSON: " ^ message) + exception_) let read_file path = try In_channel.with_open_bin path In_channel.input_all |> of_string - with Sys_error message -> Error ("could not read scenario: " ^ message) + with Sys_error message as exception_ -> + Error + (Diagnostic.of_exception ~code:Diagnostic.Input_io ~phase:Diagnostic.Input + ~message:("could not read scenario: " ^ message) + exception_) -let stream_header_of_yojson ~contract_version json = +let stream_header_of_yojson_result ~contract_version json = let* fields = object_fields ~name:"scenario stream header payload" ~expected: @@ -894,7 +922,7 @@ let stream_header_of_yojson ~contract_version json = ((("contract_version", `String contract_version) :: fields) @ [ ("schedule", `List []); ("slices", `List []) ]) in - let* scenario = of_yojson scenario_json in + let* scenario = of_yojson_result scenario_json in Ok { contract_version = scenario.contract_version; @@ -909,7 +937,7 @@ let stream_header_of_yojson ~contract_version json = max_internal_events = scenario.max_internal_events; } -let stream_item_of_yojson header ~previous json = +let stream_item_of_yojson_result header ~previous json = let* fields = object_fields ~name:"scenario stream slice payload" ~expected:[ "market_slice"; "intents" ] @@ -975,3 +1003,19 @@ let stream_item_of_yojson header ~previous json = | None | Some _ -> Ok () in Ok { market_slice; intents; action_ids } + +let stream_header_of_yojson ~contract_version json = + let code, json_path = + if String.equal contract_version Contract.version then + (Diagnostic.Scenario_stream_invalid, "$.payload") + else (Diagnostic.Scenario_unsupported_contract, "$.contract_version") + in + stream_header_of_yojson_result ~contract_version json + |> Result.map_error (fun message -> + Diagnostic.make ~code ~phase:Diagnostic.Validation ~json_path message) + +let stream_item_of_yojson header ~previous json = + stream_item_of_yojson_result header ~previous json + |> Result.map_error (fun message -> + Diagnostic.make ~code:Diagnostic.Scenario_stream_invalid + ~phase:Diagnostic.Validation ~json_path:"$.payload" message) diff --git a/lib/scenario.mli b/lib/scenario.mli index de777df..95df2db 100644 --- a/lib/scenario.mli +++ b/lib/scenario.mli @@ -34,16 +34,18 @@ type stream_item = private { action_ids : Id.Corporate_action.Set.t; } -val of_yojson : Yojson.Safe.t -> (t, string) result -val of_string : string -> (t, string) result -val read_file : string -> (t, string) result -val intent_of_yojson : Yojson.Safe.t -> (Strategy.intent, string) result +val of_yojson : Yojson.Safe.t -> (t, Diagnostic.t) result +val of_string : string -> (t, Diagnostic.t) result +val read_file : string -> (t, Diagnostic.t) result +val intent_of_yojson : Yojson.Safe.t -> (Strategy.intent, Diagnostic.t) result val stream_header_of_yojson : - contract_version:string -> Yojson.Safe.t -> (stream_header, string) result + contract_version:string -> + Yojson.Safe.t -> + (stream_header, Diagnostic.t) result val stream_item_of_yojson : stream_header -> previous:stream_item option -> Yojson.Safe.t -> - (stream_item, string) result + (stream_item, Diagnostic.t) result diff --git a/lib/scenario_stream.ml b/lib/scenario_stream.ml index 2870634..8045aa2 100644 --- a/lib/scenario_stream.ml +++ b/lib/scenario_stream.ml @@ -1,13 +1,18 @@ let ( let* ) result function_ = match result with Ok value -> function_ value | Error _ as error -> error +let invalid ?json_path ?line ?sequence message = + Diagnostic.make ?json_path ?line ?sequence + ~code:Diagnostic.Scenario_stream_invalid ~phase:Diagnostic.Validation + message + let object_fields ~name ~expected = function | `Assoc fields -> let names = List.map fst fields in let actual = List.sort_uniq String.compare names in let expected = List.sort_uniq String.compare expected in if List.length names <> List.length actual then - Error (name ^ " has duplicate JSON fields") + Error (invalid (name ^ " has duplicate JSON fields")) else if actual = expected then Ok fields else let missing = @@ -17,19 +22,20 @@ let object_fields ~name ~expected = function List.filter (fun key -> not (List.mem key expected)) actual in Error - (Printf.sprintf "%s fields differ: missing=[%s], extra=[%s]" name - (String.concat "," missing) - (String.concat "," extra)) - | _ -> Error (name ^ " must be a JSON object") + (invalid + (Printf.sprintf "%s fields differ: missing=[%s], extra=[%s]" name + (String.concat "," missing) + (String.concat "," extra))) + | _ -> Error (invalid (name ^ " must be a JSON object")) let field fields name = match List.assoc_opt name fields with | Some value -> Ok value - | None -> Error ("missing JSON field: " ^ name) + | None -> Error (invalid ("missing JSON field: " ^ name)) let string ~name = function | `String value -> Ok value - | _ -> Error (name ^ " must be a string") + | _ -> Error (invalid (name ^ " must be a string")) let int64_string ~name ~positive value = let* value = string ~name value in @@ -41,51 +47,73 @@ let int64_string ~name ~positive value = Ok parsed | _ -> let requirement = if positive then "a positive" else "a nonnegative" in - Error (name ^ " must be " ^ requirement ^ " canonical int64 string") + Error + (invalid (name ^ " must be " ^ requirement ^ " canonical int64 string")) let parse_json ~line_number line = if String.equal line "" then - Error "scenario stream must not contain blank records" + Error + (invalid ~line:line_number ~json_path:"$" + "scenario stream must not contain blank records") else try Ok (Yojson.Safe.from_string line) - with Yojson.Json_error message -> + with Yojson.Json_error message as exception_ -> Error - (Printf.sprintf "invalid scenario stream JSON at line %d: %s" - line_number message) + (Diagnostic.of_exception ~code:Diagnostic.Scenario_invalid_json + ~phase:Diagnostic.Input ~line:line_number ~json_path:"$" + ~message: + (Printf.sprintf "invalid scenario stream JSON at line %d: %s" + line_number message) + exception_) type envelope = { record_type : string; payload : Yojson.Safe.t } let parse_envelope ~line_number ~expected_sequence line = - let* json = parse_json ~line_number line in - let name = Printf.sprintf "scenario stream record %d" line_number in - let* fields = - object_fields ~name - ~expected: - [ "contract_version"; "scenario_sequence"; "record_type"; "payload" ] - json - in - let* contract_json = field fields "contract_version" in - let* contract_version = string ~name:"contract_version" contract_json in - if not (String.equal contract_version Contract.version) then - Error - (Printf.sprintf "unsupported scenario contract_version %S (expected %S)" - contract_version Contract.version) - else - let* sequence_json = field fields "scenario_sequence" in - let* scenario_sequence = - int64_string ~name:"scenario_sequence" ~positive:true sequence_json + let result = + let* json = parse_json ~line_number line in + let name = Printf.sprintf "scenario stream record %d" line_number in + let* fields = + object_fields ~name + ~expected: + [ "contract_version"; "scenario_sequence"; "record_type"; "payload" ] + json in - if not (Int64.equal scenario_sequence expected_sequence) then - Error "scenario_sequence must be contiguous and start at one" + let* contract_json = field fields "contract_version" in + let* contract_version = string ~name:"contract_version" contract_json in + if not (String.equal contract_version Contract.version) then + Error + (Diagnostic.make ~code:Diagnostic.Scenario_unsupported_contract + ~phase:Diagnostic.Validation ~line:line_number + ~sequence:expected_sequence ~json_path:"$.contract_version" + (Printf.sprintf + "unsupported scenario contract_version %S (expected %S)" + contract_version Contract.version)) else - let* type_json = field fields "record_type" in - let* record_type = string ~name:"record_type" type_json in - let* payload = field fields "payload" in - Ok { record_type; payload } + let* sequence_json = field fields "scenario_sequence" in + let* scenario_sequence = + int64_string ~name:"scenario_sequence" ~positive:true sequence_json + in + if not (Int64.equal scenario_sequence expected_sequence) then + Error + (invalid ~line:line_number ~sequence:scenario_sequence + ~json_path:"$.scenario_sequence" + "scenario_sequence must be contiguous and start at one") + else + let* type_json = field fields "record_type" in + let* record_type = string ~name:"record_type" type_json in + let* payload = field fields "payload" in + Ok { record_type; payload } + in + Result.map_error + (Diagnostic.annotate ~line:line_number ~sequence:expected_sequence + ~json_path:"$") + result let successor sequence = if Int64.equal sequence Int64.max_int then - Error "scenario_sequence is exhausted" + Error + (invalid ~sequence ~json_path:"$.scenario_sequence" + "scenario_sequence is exhausted") else Ok (Int64.succ sequence) let footer_count payload = @@ -99,23 +127,33 @@ let footer_count payload = let fold_channel channel ~init ~step ~finish = let line_number = ref 1 in match In_channel.input_line channel with - | None -> Error "scenario stream must start with scenario_header" + | None -> + Error + (invalid ~line:1 ~sequence:1L + "scenario stream must start with scenario_header") | Some line -> let* envelope = parse_envelope ~line_number:1 ~expected_sequence:1L line in if not (String.equal envelope.record_type "scenario_header") then - Error "scenario_header must be the first scenario stream record" + Error + (invalid ~line:1 ~sequence:1L ~json_path:"$.record_type" + "scenario_header must be the first scenario stream record") else let* header = Scenario.stream_header_of_yojson ~contract_version:Contract.version envelope.payload + |> Result.map_error + (Diagnostic.annotate ~line:1 ~sequence:1L ~json_path:"$.payload") in let* state = init header in let rec loop state previous slice_count expected_sequence = incr line_number; match In_channel.input_line channel with - | None -> Error "scenario_end must terminate the scenario stream" + | None -> + Error + (invalid ~line:!line_number ~sequence:expected_sequence + "scenario_end must terminate the scenario stream") | Some line -> let* envelope = parse_envelope ~line_number:!line_number ~expected_sequence line @@ -124,31 +162,46 @@ let fold_channel channel ~init ~step ~finish = let* item = Scenario.stream_item_of_yojson header ~previous envelope.payload + |> Result.map_error + (Diagnostic.annotate ~line:!line_number + ~sequence:expected_sequence ~json_path:"$.payload") in let* state = step state item in let* expected_sequence = successor expected_sequence in if Int64.equal slice_count Int64.max_int then - Error "scenario slice count is exhausted" + Error + (invalid ~line:!line_number ~sequence:expected_sequence + "scenario slice count is exhausted") else loop state (Some item) (Int64.succ slice_count) expected_sequence else if String.equal envelope.record_type "scenario_end" then - let* declared_count = footer_count envelope.payload in + let* declared_count = + footer_count envelope.payload + |> Result.map_error + (Diagnostic.annotate ~line:!line_number + ~sequence:expected_sequence ~json_path:"$.payload") + in if not (Int64.equal declared_count slice_count) then Error - "scenario_end slice_count differs from streamed market \ - slices" + (invalid ~line:!line_number ~sequence:expected_sequence + ~json_path:"$.payload.slice_count" + "scenario_end slice_count differs from streamed market \ + slices") else match In_channel.input_line channel with | Some _ -> Error - "scenario_end must be the terminal scenario stream \ - record" + (invalid ~line:(!line_number + 1) + "scenario_end must be the terminal scenario stream \ + record") | None -> finish state ~slice_count else Error - ("unsupported scenario stream record_type: " - ^ envelope.record_type) + (invalid ~line:!line_number ~sequence:expected_sequence + ~json_path:"$.record_type" + ("unsupported scenario stream record_type: " + ^ envelope.record_type)) in let* expected_sequence = successor 1L in loop state None 0L expected_sequence @@ -157,5 +210,8 @@ let fold_file path ~init ~step ~finish = try In_channel.with_open_bin path (fun channel -> fold_channel channel ~init ~step ~finish) - with Sys_error message -> - Error ("could not read scenario stream: " ^ message) + with Sys_error message as exception_ -> + Error + (Diagnostic.of_exception ~code:Diagnostic.Input_io ~phase:Diagnostic.Input + ~message:("could not read scenario stream: " ^ message) + exception_) diff --git a/lib/scenario_stream.mli b/lib/scenario_stream.mli index edd4c8d..7cf0f88 100644 --- a/lib/scenario_stream.mli +++ b/lib/scenario_stream.mli @@ -2,14 +2,14 @@ val fold_channel : in_channel -> - init:(Scenario.stream_header -> ('state, string) result) -> - step:('state -> Scenario.stream_item -> ('state, string) result) -> - finish:('state -> slice_count:int64 -> ('result, string) result) -> - ('result, string) result + init:(Scenario.stream_header -> ('state, Diagnostic.t) result) -> + step:('state -> Scenario.stream_item -> ('state, Diagnostic.t) result) -> + finish:('state -> slice_count:int64 -> ('result, Diagnostic.t) result) -> + ('result, Diagnostic.t) result val fold_file : string -> - init:(Scenario.stream_header -> ('state, string) result) -> - step:('state -> Scenario.stream_item -> ('state, string) result) -> - finish:('state -> slice_count:int64 -> ('result, string) result) -> - ('result, string) result + init:(Scenario.stream_header -> ('state, Diagnostic.t) result) -> + step:('state -> Scenario.stream_item -> ('state, Diagnostic.t) result) -> + finish:('state -> slice_count:int64 -> ('result, Diagnostic.t) result) -> + ('result, Diagnostic.t) result diff --git a/lib/sha256.ml b/lib/sha256.ml index f602463..1b7d5ac 100644 --- a/lib/sha256.ml +++ b/lib/sha256.ml @@ -247,4 +247,8 @@ let digest_file path = try In_channel.with_open_bin path (fun channel -> digest_channel channel |> Result.ok) - with Sys_error message -> Error ("could not hash scenario: " ^ message) + with Sys_error message as exception_ -> + Error + (Diagnostic.of_exception ~code:Diagnostic.Input_io ~phase:Diagnostic.Input + ~message:("could not hash scenario: " ^ message) + exception_) diff --git a/lib/sha256.mli b/lib/sha256.mli index 9a9da58..a31ebbc 100644 --- a/lib/sha256.mli +++ b/lib/sha256.mli @@ -2,4 +2,4 @@ val digest_string : string -> string val digest_channel : in_channel -> string -val digest_file : string -> (string, string) result +val digest_file : string -> (string, Diagnostic.t) result diff --git a/lib/strategy_process.ml b/lib/strategy_process.ml index b1af72c..ff0b848 100644 --- a/lib/strategy_process.ml +++ b/lib/strategy_process.ml @@ -28,20 +28,35 @@ let ( let* ) result function_ = let valid_timeout value = Float.is_finite value && Float.compare value 0.0 > 0 +let diagnostic ?sequence ~code message = + Diagnostic.make ?sequence ~code ~phase:Diagnostic.Strategy message + let next_sequence session = if Int64.equal session.next_sequence Int64.max_int then - Error "strategy protocol sequence is exhausted" + Error + (diagnostic ~sequence:session.next_sequence + ~code:Diagnostic.Strategy_protocol + "strategy protocol sequence is exhausted") else let current = session.next_sequence in session.next_sequence <- Int64.succ current; Ok current -let exception_message stage exception_ = - match exception_ with - | End_of_file -> stage ^ ": external strategy closed stdout" - | Eio.Buf_read.Buffer_limit_exceeded -> - stage ^ ": strategy response exceeds the maximum message size" - | _ -> stage ^ ": " ^ Printexc.to_string exception_ +let exception_diagnostic ?sequence stage exception_ = + let code, message = + match exception_ with + | End_of_file -> + ( Diagnostic.Strategy_protocol, + stage ^ ": external strategy closed stdout" ) + | Eio.Buf_read.Buffer_limit_exceeded -> + ( Diagnostic.Strategy_protocol, + stage ^ ": strategy response exceeds the maximum message size" ) + | _ -> + ( Diagnostic.Strategy_process, + stage ^ ": " ^ Printexc.to_string exception_ ) + in + Diagnostic.of_exception ?sequence ~code ~phase:Diagnostic.Strategy ~message + exception_ let await_child (child : child) = match child.status with @@ -65,7 +80,9 @@ let await_child_for (child : child) timeout = with exception_ -> raise (Failure - (exception_message "waiting for external strategy" exception_))) + (Diagnostic.to_human + (exception_diagnostic "waiting for external strategy" exception_))) + ) let process_group_exists pgid = try @@ -81,11 +98,15 @@ let signal_process_group pgid signal = Ok () with | Unix.Unix_error (Unix.ESRCH, _, _) -> Ok () - | Unix.Unix_error (code, operation, target) -> + | Unix.Unix_error (code, operation, target) as exception_ -> Error - (Printf.sprintf - "could not signal external strategy process group: %s(%s): %s" - operation target (Unix.error_message code)) + (Diagnostic.of_exception ~code:Diagnostic.Strategy_process + ~phase:Diagnostic.Strategy + ~message: + (Printf.sprintf + "could not signal external strategy process group: %s(%s): %s" + operation target (Unix.error_message code)) + exception_) let rec reap_descendants pgid = try @@ -134,13 +155,16 @@ let terminate_process_group (child : child) = in match direct_status with | None -> - Error "external strategy did not exit after forced termination" + Error + (diagnostic ~code:Diagnostic.Strategy_process + "external strategy did not exit after forced termination") | Some _ -> if wait_for_process_group child forced_deadline then Ok () else Error - "external strategy descendants remained after forced \ - termination") + (diagnostic ~code:Diagnostic.Strategy_process + "external strategy descendants remained after forced \ + termination")) let append_cleanup_error result child = match terminate_process_group child with @@ -148,7 +172,7 @@ let append_cleanup_error result child = | Error cleanup -> ( match result with | Ok _ -> Error cleanup - | Error message -> Error (message ^ "; " ^ cleanup)) + | Error original -> Error (Diagnostic.combine original cleanup)) let exchange session ~stage ~expected_sequence request = let* () = @@ -164,12 +188,19 @@ let exchange session ~stage ~expected_sequence request = Ok (Eio.Buf_read.line session.output)) with | Ok response -> Ok response - | Error `Timeout -> Error (stage ^ ": external strategy timed out") - with exception_ -> Error (exception_message stage exception_) + | Error `Timeout -> + Error + (diagnostic ~sequence:expected_sequence + ~code:Diagnostic.Strategy_timeout + (stage ^ ": external strategy timed out")) + with exception_ -> + Error (exception_diagnostic ~sequence:expected_sequence stage exception_) in let* response = response in let* response, response_json = Strategy_protocol.response_of_string ~expected_sequence response + |> Result.map_error + (Diagnostic.annotate ~sequence:expected_sequence ~json_path:"$") in let* () = Strategy_transcript.append session.transcript @@ -190,9 +221,13 @@ let initialize session initialization = match response with | Strategy_protocol.Ready identity -> Ok identity | Failed message -> - Error ("external strategy initialization failed: " ^ message) + Error + (diagnostic ~sequence ~code:Diagnostic.Strategy_protocol + ("external strategy initialization failed: " ^ message)) | Intents _ | Stopped -> - Error "external strategy returned the wrong initialization response" + Error + (diagnostic ~sequence ~code:Diagnostic.Strategy_protocol + "external strategy returned the wrong initialization response") let on_event session context event = let* sequence = next_sequence session in @@ -202,9 +237,14 @@ let on_event session context event = in match response with | Strategy_protocol.Intents intents -> Ok intents - | Failed message -> Error ("external strategy failed: " ^ message) + | Failed message -> + Error + (diagnostic ~sequence ~code:Diagnostic.Strategy_protocol + ("external strategy failed: " ^ message)) | Ready _ | Stopped -> - Error "external strategy returned the wrong event response" + Error + (diagnostic ~sequence ~code:Diagnostic.Strategy_protocol + "external strategy returned the wrong event response") let shutdown session = let* sequence = next_sequence session in @@ -214,9 +254,14 @@ let shutdown session = in match response with | Strategy_protocol.Stopped -> Ok () - | Failed message -> Error ("external strategy shutdown failed: " ^ message) + | Failed message -> + Error + (diagnostic ~sequence ~code:Diagnostic.Strategy_protocol + ("external strategy shutdown failed: " ^ message)) | Ready _ | Intents _ -> - Error "external strategy returned the wrong shutdown response" + Error + (diagnostic ~sequence ~code:Diagnostic.Strategy_protocol + "external strategy returned the wrong shutdown response") let await_exit session = session.close_input (); @@ -227,9 +272,12 @@ let await_exit session = Ok (await_child session.child)) with | Ok status -> Ok status - | Error `Timeout -> Error "external strategy did not exit after shutdown" + | Error `Timeout -> + Error + (diagnostic ~code:Diagnostic.Strategy_timeout + "external strategy did not exit after shutdown") with exception_ -> - Error (exception_message "waiting for external strategy" exception_) + Error (exception_diagnostic "waiting for external strategy" exception_) in let* status = status in match status with @@ -243,29 +291,45 @@ let await_exit session = with | Ok value -> Ok value | Error `Timeout -> - Error "external strategy stdout did not close after exit" + Error + (diagnostic ~code:Diagnostic.Strategy_timeout + "external strategy stdout did not close after exit") with exception_ -> - Error (exception_message "reading final strategy output" exception_) + Error + (exception_diagnostic "reading final strategy output" exception_) in let* trailing_output = trailing_output in match trailing_output with | None -> Ok () | Some _ -> - Error "external strategy wrote data after its stopped response") + Error + (diagnostic ~code:Diagnostic.Strategy_protocol + "external strategy wrote data after its stopped response")) | `Exited code -> - Error (Printf.sprintf "external strategy exited with code %d" code) + Error + (diagnostic ~code:Diagnostic.Strategy_exit + (Printf.sprintf "external strategy exited with code %d" code)) | `Signaled signal -> - Error (Printf.sprintf "external strategy was killed by signal %d" signal) + Error + (diagnostic ~code:Diagnostic.Strategy_exit + (Printf.sprintf "external strategy was killed by signal %d" signal)) let with_session ~env ~command ~timeout ~transcript_path ~(initialization : Strategy_protocol.initialization) use = if not (valid_timeout timeout) then - Error "strategy response timeout must be finite and positive" + Error + (diagnostic ~code:Diagnostic.Strategy_invalid_configuration + "strategy response timeout must be finite and positive") else match command with - | [] -> Error "external strategy command must not be empty" + | [] -> + Error + (diagnostic ~code:Diagnostic.Strategy_invalid_configuration + "external strategy command must not be empty") | executable :: _ when String.length executable = 0 -> - Error "external strategy executable must not be empty" + Error + (diagnostic ~code:Diagnostic.Strategy_invalid_configuration + "external strategy executable must not be empty") | executable :: _ -> ( match Strategy_transcript.create transcript_path with | Error _ as error -> error @@ -348,5 +412,5 @@ let with_session ~env ~command ~timeout ~transcript_path | exception_ -> fail (Error - (exception_message "external strategy process" exception_)) - )) + (exception_diagnostic "external strategy process" + exception_)))) diff --git a/lib/strategy_process.mli b/lib/strategy_process.mli index 29ef2c0..893c72b 100644 --- a/lib/strategy_process.mli +++ b/lib/strategy_process.mli @@ -8,11 +8,11 @@ val with_session : timeout:float -> transcript_path:string -> initialization:Strategy_protocol.initialization -> - (t -> ('a, string) result) -> - ('a * Strategy_protocol.identity, string) result + (t -> ('a, Diagnostic.t) result) -> + ('a * Strategy_protocol.identity, Diagnostic.t) result val on_event : t -> Strategy.context -> Strategy.event -> - (Strategy.intent list, string) result + (Strategy.intent list, Diagnostic.t) result diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index 6bade90..78164d5 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -269,7 +269,10 @@ let parse_intents_payload json = List.fold_left (fun result value -> let* intents = result in - let* intent = Scenario.intent_of_yojson value in + let* intent = + Scenario.intent_of_yojson value + |> Result.map_error Diagnostic.to_human + in Ok (intent :: intents)) (Ok []) values |> Result.map (fun values -> Intents (List.rev values)) @@ -279,7 +282,7 @@ let parse_stopped_payload json = let* _ = object_fields ~name:"strategy stopped payload" ~expected:[] json in Ok Stopped -let response_of_yojson ~expected_sequence json = +let response_of_yojson_result ~expected_sequence json = let* fields = object_fields ~name:"strategy response" ~expected: @@ -322,16 +325,46 @@ let response_of_yojson ~expected_sequence json = |> Result.map (fun message -> Failed message) | value -> Error ("unsupported strategy response type: " ^ value) +let response_of_yojson ~expected_sequence json = + let json_path = + match json with + | `Assoc fields -> ( + match List.assoc_opt "strategy_protocol_version" fields with + | Some (`String supplied) when not (String.equal supplied version) -> + "$.strategy_protocol_version" + | _ -> ( + match List.assoc_opt "strategy_sequence" fields with + | Some (`String supplied) + when not + (String.equal supplied (Int64.to_string expected_sequence)) + -> + "$.strategy_sequence" + | _ -> "$")) + | _ -> "$" + in + response_of_yojson_result ~expected_sequence json + |> Result.map_error (fun message -> + Diagnostic.make ~code:Diagnostic.Strategy_protocol + ~phase:Diagnostic.Strategy ~sequence:expected_sequence ~json_path + message) + let response_of_string ~expected_sequence document = if String.length document > max_message_bytes then - Error "strategy response exceeds the maximum message size" + Error + (Diagnostic.make ~code:Diagnostic.Strategy_protocol + ~phase:Diagnostic.Strategy ~sequence:expected_sequence + "strategy response exceeds the maximum message size") else try let json = Yojson.Safe.from_string document in response_of_yojson ~expected_sequence json |> Result.map (fun response -> (response, json)) - with Yojson.Json_error message -> - Error ("invalid strategy response JSON: " ^ message) + with Yojson.Json_error message as exception_ -> + Error + (Diagnostic.of_exception ~code:Diagnostic.Strategy_protocol + ~phase:Diagnostic.Strategy ~sequence:expected_sequence ~json_path:"$" + ~message:("invalid strategy response JSON: " ^ message) + exception_) let direction_to_string = function | Engine_to_strategy -> "engine_to_strategy" diff --git a/lib/strategy_protocol.mli b/lib/strategy_protocol.mli index 1f323cf..0bca4a4 100644 --- a/lib/strategy_protocol.mli +++ b/lib/strategy_protocol.mli @@ -34,10 +34,12 @@ val event_message : val shutdown_message : sequence:int64 -> Yojson.Safe.t val response_of_yojson : - expected_sequence:int64 -> Yojson.Safe.t -> (response, string) result + expected_sequence:int64 -> Yojson.Safe.t -> (response, Diagnostic.t) result val response_of_string : - expected_sequence:int64 -> string -> (response * Yojson.Safe.t, string) result + expected_sequence:int64 -> + string -> + (response * Yojson.Safe.t, Diagnostic.t) result val transcript_record : transcript_sequence:int64 -> diff --git a/lib/strategy_transcript.ml b/lib/strategy_transcript.ml index 1ac60f4..e510fad 100644 --- a/lib/strategy_transcript.ml +++ b/lib/strategy_transcript.ml @@ -6,12 +6,19 @@ type t = { mutable closed : bool; } +let diagnostic ?sequence ~code message = + Diagnostic.make ?sequence ~code ~phase:Diagnostic.Artifact message + let create final_path = let partial_path = final_path ^ ".partial" in if Sys.file_exists final_path then - Error ("strategy transcript already exists: " ^ final_path) + Error + (diagnostic ~code:Diagnostic.Artifact_exists + ("strategy transcript already exists: " ^ final_path)) else if Sys.file_exists partial_path then - Error ("partial strategy transcript already exists: " ^ partial_path) + Error + (diagnostic ~code:Diagnostic.Artifact_exists + ("partial strategy transcript already exists: " ^ partial_path)) else try let channel = @@ -27,14 +34,24 @@ let create final_path = next_sequence = 1L; closed = false; } - with Sys_error message -> - Error ("could not create strategy transcript: " ^ message) + with Sys_error message as exception_ -> + Error + (Diagnostic.of_exception ~code:Diagnostic.Artifact_io + ~phase:Diagnostic.Artifact + ~message:("could not create strategy transcript: " ^ message) + exception_) let append transcript ~direction message = if transcript.closed then - Error "cannot append to a closed strategy transcript" + Error + (diagnostic ~sequence:transcript.next_sequence + ~code:Diagnostic.Artifact_state + "cannot append to a closed strategy transcript") else if Int64.equal transcript.next_sequence Int64.max_int then - Error "strategy transcript sequence is exhausted" + Error + (diagnostic ~sequence:transcript.next_sequence + ~code:Diagnostic.Artifact_state + "strategy transcript sequence is exhausted") else try let record = @@ -47,10 +64,14 @@ let append transcript ~direction message = flush transcript.channel; transcript.next_sequence <- Int64.succ transcript.next_sequence; Ok () - with Sys_error message -> + with Sys_error message as exception_ -> Error - ("could not append strategy transcript " ^ transcript.partial_path - ^ ": " ^ message) + (Diagnostic.of_exception ~sequence:transcript.next_sequence + ~code:Diagnostic.Artifact_io ~phase:Diagnostic.Artifact + ~message: + ("could not append strategy transcript " ^ transcript.partial_path + ^ ": " ^ message) + exception_) let close_preserving_partial transcript = if not transcript.closed then ( @@ -58,7 +79,11 @@ let close_preserving_partial transcript = close_out_noerr transcript.channel) let commit transcript = - if transcript.closed then Error "cannot commit a closed strategy transcript" + if transcript.closed then + Error + (diagnostic ~sequence:transcript.next_sequence + ~code:Diagnostic.Artifact_state + "cannot commit a closed strategy transcript") else try flush transcript.channel; @@ -68,11 +93,20 @@ let commit transcript = Unix.unlink transcript.partial_path; Ok () with - | Sys_error message -> + | Sys_error message as exception_ -> close_preserving_partial transcript; - Error ("could not finalize strategy transcript: " ^ message) - | Unix.Unix_error (code, operation, target) -> + Error + (Diagnostic.of_exception ~code:Diagnostic.Artifact_io + ~phase:Diagnostic.Artifact + ~message:("could not finalize strategy transcript: " ^ message) + exception_) + | Unix.Unix_error (code, operation, target) as exception_ -> close_preserving_partial transcript; Error - (Printf.sprintf "could not finalize strategy transcript: %s(%s): %s" - operation target (Unix.error_message code)) + (Diagnostic.of_exception ~code:Diagnostic.Artifact_io + ~phase:Diagnostic.Artifact + ~message: + (Printf.sprintf + "could not finalize strategy transcript: %s(%s): %s" operation + target (Unix.error_message code)) + exception_) diff --git a/lib/strategy_transcript.mli b/lib/strategy_transcript.mli index 9b6b5c4..5f7f4e3 100644 --- a/lib/strategy_transcript.mli +++ b/lib/strategy_transcript.mli @@ -2,13 +2,13 @@ type t -val create : string -> (t, string) result +val create : string -> (t, Diagnostic.t) result val append : t -> direction:Strategy_protocol.direction -> Yojson.Safe.t -> - (unit, string) result + (unit, Diagnostic.t) result val close_preserving_partial : t -> unit -val commit : t -> (unit, string) result +val commit : t -> (unit, Diagnostic.t) result diff --git a/test/cli.t b/test/cli.t index 5d41f44..7bd077a 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":["3"],"journal_contract_versions":["3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"strategy_protocol_versions":["3"]} + {"engine_version":"1.0.0","scenario_contract_versions":["3"],"journal_contract_versions":["3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"strategy_protocol_versions":["3"],"diagnostic_versions":["1"]} $ ../bin/main.exe --validate-only --input ../contracts/v3/fixtures/demo.scenario.json valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=3e19fa66bc6425bb8ed7a89b338080a831dd39ea778c3c7f9e8ce1d3370fbee0 @@ -22,6 +22,16 @@ trading-engine: scenario_end must terminate the scenario stream [123] + $ diagnostic=$(../bin/main.exe --diagnostic-format json --validate-only --input-format jsonl --input truncated.scenario.jsonl 2>&1); status=$?; test "$status" -eq 123; python3 - "$diagnostic" <<'PY' + > import json + > import sys + > diagnostic = json.loads(sys.argv[1]) + > print(diagnostic["diagnostic_version"], diagnostic["code"], diagnostic["phase"]) + > print(diagnostic["context"]["line"], diagnostic["context"]["sequence"], diagnostic["cause"]) + > PY + 1 scenario_stream.invalid validation + 6 6 None + $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v3/fixtures/demo.scenario.json > invalid-tick.json $ ../bin/main.exe --validate-only --input invalid-tick.json trading-engine: market prices and volumes must align with instrument increments diff --git a/test/dune b/test/dune index 1dd3057..0d77f6e 100644 --- a/test/dune +++ b/test/dune @@ -2,6 +2,7 @@ (name test_engine) (modules test_support + test_diagnostic test_domain test_accounting test_execution diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml new file mode 100644 index 0000000..2380034 --- /dev/null +++ b/test/test_diagnostic.ml @@ -0,0 +1,71 @@ +module T = Trading_engine + +let field name = function + | `Assoc fields -> List.assoc name fields + | _ -> Alcotest.fail "expected JSON object" + +let renders_stable_machine_context () = + let diagnostic = + T.Diagnostic.make ~code:T.Diagnostic.Scenario_stream_invalid + ~phase:T.Diagnostic.Validation ~json_path:"$.payload.market_slice" ~line:7 + ~sequence:9L ~event_id:"event-9" ~order_id:"order-2" + ~causation_ids:[ "event-7"; "event-8" ] "invalid market slice" + in + let json = T.Diagnostic.to_yojson diagnostic in + Alcotest.(check string) + "diagnostic version" "1" + (match field "diagnostic_version" json with + | `String value -> value + | _ -> Alcotest.fail "expected version"); + Alcotest.(check string) + "stable code" "scenario_stream.invalid" + (match field "code" json with + | `String value -> value + | _ -> Alcotest.fail "expected code"); + let context = field "context" json in + Alcotest.(check string) + "JSON path" "$.payload.market_slice" + (match field "json_path" context with + | `String value -> value + | _ -> Alcotest.fail "expected JSON path"); + Alcotest.(check int) + "line" 7 + (match field "line" context with + | `Int value -> value + | _ -> Alcotest.fail "expected line"); + Alcotest.(check string) + "sequence" "9" + (match field "sequence" context with + | `String value -> value + | _ -> Alcotest.fail "expected sequence") + +let preserves_sanitized_exception () = + let diagnostic = + T.Diagnostic.of_exception ~code:T.Diagnostic.Artifact_io + ~phase:T.Diagnostic.Artifact ~message:"could not publish artifact" + (Unix.Unix_error (Unix.EACCES, "link", "/tmp/output")) + in + let cause = T.Diagnostic.to_yojson diagnostic |> field "cause" in + Alcotest.(check string) + "cause kind" "unix_error" + (match field "kind" cause with + | `String value -> value + | _ -> Alcotest.fail "expected cause kind"); + Alcotest.(check string) + "operation" "link" + (match field "operation" cause with + | `String value -> value + | _ -> Alcotest.fail "expected operation"); + Alcotest.(check string) + "target" "/tmp/output" + (match field "target" cause with + | `String value -> value + | _ -> Alcotest.fail "expected target") + +let tests = + [ + Alcotest.test_case "renders stable machine context" `Quick + renders_stable_machine_context; + Alcotest.test_case "preserves sanitized exception" `Quick + preserves_sanitized_exception; + ] diff --git a/test/test_engine.ml b/test/test_engine.ml index 3e837aa..769f324 100644 --- a/test/test_engine.ml +++ b/test/test_engine.ml @@ -1,6 +1,7 @@ let () = Alcotest.run "trading-engine" [ + ("diagnostic", Test_diagnostic.tests); ("domain", Test_domain.tests); ("accounting", Test_accounting.tests); ("execution", Test_execution.tests); diff --git a/test/test_scenario.ml b/test/test_scenario.ml index a8c6f08..80d0a75 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -170,16 +170,23 @@ let contract_version_is_required_and_supported () = else (name, value)) fields) in + let unsupported_diagnostic = T.Scenario.of_yojson unsupported |> error in Alcotest.(check string) "unsupported version diagnosed" "unsupported scenario contract_version \"2\" (expected \"3\")" - (T.Scenario.of_yojson unsupported |> error) + (T.Diagnostic.to_human unsupported_diagnostic); + Alcotest.(check string) + "unsupported version code" "scenario.unsupported_contract" + (T.Diagnostic.code_to_string unsupported_diagnostic.code); + Alcotest.(check (option string)) + "contract path" (Some "$.contract_version") + unsupported_diagnostic.context.json_path let duplicate_fields_are_rejected () = let changed = map_root (fun fields -> ("initial_cash", `String "0") :: fields) in - let message = T.Scenario.of_yojson changed |> error in + let message = T.Scenario.of_yojson changed |> diagnostic_message in Alcotest.(check bool) "duplicate field diagnosed" true (String.starts_with ~prefix:"scenario has duplicate JSON fields" message) @@ -264,7 +271,7 @@ let market_slice_timeline_is_non_overlapping () = Alcotest.(check string) label "market slice start must not precede previous end" (scenario_with_second_slice_start start_at - |> T.Scenario.of_yojson |> error)) + |> T.Scenario.of_yojson |> diagnostic_message)) [ ("backward start rejected", "2026-01-01T14:30:00Z"); ("overlapping start rejected", "2026-01-02T20:00:00Z"); @@ -291,7 +298,7 @@ let invalid_schedule_sequences_are_rejected () = let missing = update_first_schedule (change_field "after_slice_sequence" (`String "999")) in - let message = T.Scenario.of_yojson missing |> error in + let message = T.Scenario.of_yojson missing |> diagnostic_message in Alcotest.(check string) "missing sequence diagnosed" "scheduled intents refer to missing market slice sequence 999" message; @@ -312,7 +319,7 @@ let invalid_schedule_sequences_are_rejected () = in Alcotest.(check string) "duplicate schedule rejected" "schedule sequences must increase" - (T.Scenario.of_yojson duplicate |> error) + (T.Scenario.of_yojson duplicate |> diagnostic_message) let duplicate_and_incomplete_slice_bars_are_rejected () = let duplicate = @@ -408,7 +415,7 @@ let execution_model_is_required_and_supported () = in Alcotest.(check string) "unsupported model diagnosed" "unsupported execution model \"future_model\"" - (T.Scenario.of_yojson unsupported |> error) + (T.Scenario.of_yojson unsupported |> diagnostic_message) let deterministic_replay () = let scenario = demo () in @@ -639,7 +646,8 @@ let streamed_contract_requires_ordered_terminal_records () = Alcotest.(check string) "truncation diagnosed" "scenario_end must terminate the scenario stream" - (T.Replay.run_stream ~journal_path:journal path |> error); + (T.Replay.run_stream ~journal_path:journal path + |> diagnostic_message); Alcotest.(check bool) "invalid stream has no journal" false (Sys.file_exists journal); Alcotest.(check bool) @@ -656,10 +664,21 @@ let streamed_contract_requires_ordered_terminal_records () = records in with_stream skipped (fun path -> + let diagnostic = T.Replay.run_stream path |> error in Alcotest.(check string) "sequence gap diagnosed" "scenario_sequence must be contiguous and start at one" - (T.Replay.run_stream path |> error)) + (T.Diagnostic.to_human diagnostic); + Alcotest.(check string) + "stream diagnostic code" "scenario_stream.invalid" + (T.Diagnostic.code_to_string diagnostic.code); + Alcotest.(check (option int)) + "record line" (Some 3) diagnostic.context.line; + Alcotest.(check (option int64)) + "observed sequence" (Some 9L) diagnostic.context.sequence; + Alcotest.(check (option string)) + "sequence path" (Some "$.scenario_sequence") + diagnostic.context.json_path) let stream_with_second_slice_start start_at = stream_records () @@ -683,7 +702,7 @@ let streamed_market_slice_timeline_is_non_overlapping () = with_stream (stream_with_second_slice_start start_at) (fun path -> Alcotest.(check string) label "market slice start must not precede previous end" - (T.Replay.run_stream path |> error))) + (T.Replay.run_stream path |> diagnostic_message))) [ ("backward start rejected", "2026-01-01T14:30:00Z"); ("overlapping start rejected", "2026-01-02T20:00:00Z"); @@ -711,7 +730,7 @@ let streamed_intents_are_causal_before_execution () = "lookahead intent rejected" "scheduled order intent after slice 1 is received after the next \ executable market slice starts" - (T.Replay.run_stream path |> error)) + (T.Replay.run_stream path |> diagnostic_message)) let large_stream_replay_does_not_retain_audit_history () = let slice_count = 10_000 in diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index dc02bee..76075da 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -183,7 +183,8 @@ let responses_are_strict_and_typed () = Alcotest.(check string) "metric value" "0.5" value | _ -> Alcotest.fail "expected metric intent"); let wrong_sequence = - T.Strategy_protocol.response_of_yojson ~expected_sequence:4L ready |> error + T.Strategy_protocol.response_of_yojson ~expected_sequence:4L ready + |> diagnostic_message in Alcotest.(check bool) "wrong sequence rejected" true @@ -202,7 +203,7 @@ let responses_are_strict_and_typed () = "duplicate field rejected" "strategy response must not contain duplicate fields" (T.Strategy_protocol.response_of_yojson ~expected_sequence:3L duplicate - |> error); + |> diagnostic_message); let wrong_version = `Assoc [ @@ -215,7 +216,7 @@ let responses_are_strict_and_typed () = Alcotest.(check string) "wrong version rejected" "unsupported strategy protocol version: 1" (T.Strategy_protocol.response_of_yojson ~expected_sequence:3L wrong_version - |> error); + |> diagnostic_message); let unknown_field = `Assoc [ @@ -229,18 +230,18 @@ let responses_are_strict_and_typed () = Alcotest.(check string) "unknown field rejected" "strategy response has unknown or missing fields" (T.Strategy_protocol.response_of_yojson ~expected_sequence:3L unknown_field - |> error); + |> diagnostic_message); Alcotest.(check bool) "malformed JSON rejected" true (T.Strategy_protocol.response_of_string ~expected_sequence:3L "{" - |> error + |> diagnostic_message |> String.starts_with ~prefix:"invalid strategy response JSON:"); Alcotest.(check string) "oversized response rejected" "strategy response exceeds the maximum message size" (T.Strategy_protocol.response_of_string ~expected_sequence:3L (String.make (T.Strategy_protocol.max_message_bytes + 1) 'x') - |> error) + |> diagnostic_message) let transcript_records_direction_and_sequence () = let message = T.Strategy_protocol.shutdown_message ~sequence:9L in @@ -294,7 +295,7 @@ let callback_exception_reaps_process_tree () = ~timeout:1.0 ~transcript_path ~initialization:(initialization ()) (fun _ -> raise Exit) in - let message = error result in + let message = diagnostic_message result in Alcotest.(check bool) "callback exception reported" true (String.ends_with ~suffix:"Stdlib.Exit" message); diff --git a/test/test_support.ml b/test/test_support.ml index a22e677..9f50b67 100644 --- a/test/test_support.ml +++ b/test/test_support.ml @@ -1,11 +1,14 @@ module T = Trading_engine -let ok = function Ok value -> value | Error message -> Alcotest.fail message +let ok = function + | Ok value -> value + | Error _ -> Alcotest.fail "unexpected error" let error = function | Error message -> message | Ok _ -> Alcotest.fail "expected an error" +let diagnostic_message result = error result |> T.Diagnostic.to_human let price value = T.Scalar.Price.of_decimal_string value |> ok let quantity value = T.Scalar.Quantity.of_decimal_string value |> ok let weight value = T.Scalar.Weight.of_decimal_string value |> ok From 033e5daf8483307af6219455f9b6de6b92b08730 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 16:57:26 -0400 Subject: [PATCH 03/57] fix: preserve capability schema compatibility --- README.md | 8 ++++---- lib/contract.ml | 1 - test/cli.t | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 12d18fb..a9d1495 100644 --- a/README.md +++ b/README.md @@ -127,10 +127,10 @@ opam exec -- dune exec trading-engine -- --capabilities Clients must confirm that both `scenario_contract_versions` and `journal_contract_versions` contain the scenario's `contract_version` before starting a replay. External clients must also -require their version in `strategy_protocol_versions`. Runtime failures use the structured -diagnostic contract advertised by `diagnostic_versions`. Human diagnostics remain the default. -Use `--diagnostic-format json` to receive one JSON diagnostic on standard error with a stable code, -phase, typed context, and sanitized underlying cause. +require their version in `strategy_protocol_versions`. Runtime failures can use the structured +diagnostic contract identified by each diagnostic's `diagnostic_version`. Human diagnostics remain +the default. Use `--diagnostic-format json` to receive one JSON diagnostic on standard error with a +stable code, phase, typed context, and sanitized underlying cause. The final and `.partial` journal paths must not already exist. Batch JSON hashes the same complete document it parses. JSON Lines input is hashed and validated in a bounded-memory pass before the diff --git a/lib/contract.ml b/lib/contract.ml index 9e34397..05de043 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -13,7 +13,6 @@ let capabilities_to_yojson () = ("journal_formats", strings [ "jsonl" ]); ("execution_models", strings Execution_model.supported); ("strategy_protocol_versions", strings [ strategy_protocol_version ]); - ("diagnostic_versions", strings [ Diagnostic.version ]); ] let capabilities_to_string () = diff --git a/test/cli.t b/test/cli.t index 7bd077a..629f0e5 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":["3"],"journal_contract_versions":["3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"strategy_protocol_versions":["3"],"diagnostic_versions":["1"]} + {"engine_version":"1.0.0","scenario_contract_versions":["3"],"journal_contract_versions":["3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"strategy_protocol_versions":["3"]} $ ../bin/main.exe --validate-only --input ../contracts/v3/fixtures/demo.scenario.json valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=3e19fa66bc6425bb8ed7a89b338080a831dd39ea778c3c7f9e8ce1d3370fbee0 From 165df8acf4074b7fb4ecd96e91a3d6e3d4c934c9 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 17:04:12 -0400 Subject: [PATCH 04/57] fix: reserve working-order exposure by side --- docs/execution-model.md | 14 +++++ lib/risk.ml | 113 ++++++++++++++++++++++++++++------------ test/test_accounting.ml | 85 ++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 34 deletions(-) diff --git a/docs/execution-model.md b/docs/execution-model.md index 95cb4c0..b2aac63 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -42,6 +42,20 @@ positions and submits at most one market order per instrument. Each order is cap market remainder is IOC, but the desired target is retried after a later slice until reached or superseded. +## Working-order risk + +Pre-trade risk reserves each active order's unfilled quantity by side. For every instrument, it +values both the position after all reserved buys and the position after all reserved sells, then +uses the larger absolute endpoint for portfolio exposure. Opposing orders therefore cannot hide +risk by netting before either execution path is known. + +A new order is rejected while an active opposite-side order exists for the same instrument. This +self-cross rule applies to direct and target-generated orders. Same-side orders may coexist, and +their remaining quantities share the applicable long or short position limit. Risk-reducing orders +remain permitted when an actual position is already outside a limit, but one order may not cross +that position through zero. Fill-time position, exposure, leverage, and margin checks remain the +final defense against price and account changes after acceptance. + ## Market and limit prices A market order executes at the open of its first eligible slice. diff --git a/lib/risk.ml b/lib/risk.ml index 2fe7d42..4d0a309 100644 --- a/lib/risk.ml +++ b/lib/risk.ml @@ -193,58 +193,104 @@ let check_alignment instrument request = if Scalar.Price.is_multiple price ~tick:instrument.tick_size then Ok () else Error "limit price is not aligned to the instrument tick size" -let signed_order_quantity request = +type reservations = { buys : Scalar.Quantity.t; sells : Scalar.Quantity.t } + +let empty_reservations = + { buys = Scalar.Quantity.zero; sells = Scalar.Quantity.zero } + +let reservations_for_instrument ~oms instrument_id = + Oms.active_for_instrument oms instrument_id + |> List.fold_left + (fun result order -> + let* reservations = result in + let remaining = Order.remaining_quantity order in + match order.Order.request.side with + | Order.Buy -> + let* buys = Scalar.Quantity.add reservations.buys remaining in + Ok { reservations with buys } + | Order.Sell -> + let* sells = Scalar.Quantity.add reservations.sells remaining in + Ok { reservations with sells }) + (Ok empty_reservations) + +let add_request reservations request = match request.Order.side with - | Order.Buy -> Ok request.quantity - | Order.Sell -> Scalar.Quantity.negate request.quantity + | Order.Buy -> + let* buys = Scalar.Quantity.add reservations.buys request.quantity in + Ok { reservations with buys } + | Order.Sell -> + let* sells = Scalar.Quantity.add reservations.sells request.quantity in + Ok { reservations with sells } -let working_position ~account ~oms instrument_id = +let directional_positions ~account instrument_id reservations = let current = Account.position_quantity account instrument_id in - let active = Oms.active_for_instrument oms instrument_id in - List.fold_left - (fun result order -> - let* quantity = result in - let remaining = Order.remaining_quantity order in - let* delta = - match order.Order.request.side with - | Order.Buy -> Ok remaining - | Order.Sell -> Scalar.Quantity.negate remaining - in - Scalar.Quantity.add quantity delta) - (Ok current) active + let* buy_position = Scalar.Quantity.add current reservations.buys in + let* sell_position = Scalar.Quantity.subtract current reservations.sells in + Ok (buy_position, sell_position) + +let position_for_side side (buy_position, sell_position) = + match side with Order.Buy -> buy_position | Order.Sell -> sell_position + +let worst_directional_position positions = + let buy_position, sell_position = positions in + let* buy_absolute = Scalar.Quantity.absolute buy_position in + let* sell_absolute = Scalar.Quantity.absolute sell_position in + if Scalar.Quantity.compare buy_absolute sell_absolute >= 0 then + Ok buy_position + else Ok sell_position + +let check_self_cross ~oms request = + let active = Oms.active_for_instrument oms request.Order.instrument_id in + if + List.exists + (fun order -> order.Order.request.side <> request.Order.side) + active + then Error "order would self-cross an active opposite-side order" + else Ok () let projected_position ~account ~oms request = - let* pending = working_position ~account ~oms request.Order.instrument_id in - let* projected = - let* delta = signed_order_quantity request in - Scalar.Quantity.add pending delta + let* reservations = + reservations_for_instrument ~oms request.Order.instrument_id in + let* pending_positions = + directional_positions ~account request.instrument_id reservations + in + let pending = position_for_side request.side pending_positions in + let* projected_reservations = add_request reservations request in + let* projected_positions = + directional_positions ~account request.instrument_id projected_reservations + in + let projected = position_for_side request.side projected_positions in if Scalar.Quantity.is_positive pending && Scalar.Quantity.is_negative projected || Scalar.Quantity.is_negative pending && Scalar.Quantity.is_positive projected then Error "one order must not cross a position through zero" - else Ok projected + else Ok (pending, projected) -let projected_valuation_quantities state ~account ~oms request projected = +let projected_valuation_quantities state ~account ~oms request = Id.Instrument.Map.bindings state.instruments |> List.fold_left (fun result (instrument_id, _) -> let* values = result in - if Id.Instrument.equal instrument_id request.Order.instrument_id then - Ok ((instrument_id, projected) :: values) - else - let* quantity = working_position ~account ~oms instrument_id in - Ok ((instrument_id, quantity) :: values)) + let* reservations = reservations_for_instrument ~oms instrument_id in + let* reservations = + if Id.Instrument.equal instrument_id request.Order.instrument_id then + add_request reservations request + else Ok reservations + in + let* positions = + directional_positions ~account instrument_id reservations + in + let* quantity = worst_directional_position positions in + Ok ((instrument_id, quantity) :: values)) (Ok []) |> Result.map List.rev let projected_gross_exposure state ~account ~oms ~marks ~fx_rates request = - let* projected = projected_position ~account ~oms request in - let* () = check_position state projected in let* quantities = - projected_valuation_quantities state ~account ~oms request projected + projected_valuation_quantities state ~account ~oms request in let mark_map = List.fold_left @@ -293,15 +339,14 @@ let check state ~account ~oms ~marks ~fx_rates request = | None -> Error "order refers to an unknown instrument" | Some instrument -> let* () = check_alignment instrument request in - let* pending = - working_position ~account ~oms request.Order.instrument_id - in - let* projected = projected_position ~account ~oms request in + let* () = check_self_cross ~oms request in + let* pending, projected = projected_position ~account ~oms request in let* pending_absolute = Scalar.Quantity.absolute pending in let* projected_absolute = Scalar.Quantity.absolute projected in if Scalar.Quantity.compare projected_absolute pending_absolute <= 0 then Ok () else + let* () = check_position state projected in let* before = Account.value account ~instruments:(instruments state) ~marks ~fx_rates diff --git a/test/test_accounting.ml b/test/test_accounting.ml index 2a3d948..39e93de 100644 --- a/test/test_accounting.ml +++ b/test/test_accounting.ml @@ -202,6 +202,85 @@ let risk_reserves_working_sells () = "second sell oversubscribes holdings" true (Result.is_error (risk_check risk ~account ~oms second)) +let add_working_order oms ~id ~accepted_sequence request = + T.Oms.accept oms ~id:(order_id id) + ~created_event_id:(event_id (id ^ "-event")) + ~accepted_sequence + ~created_at:(timestamp "2026-01-02T21:00:02Z") + ~eligible_after_slice_sequence:1L request + |> ok |> fst + +let risk_rejects_self_crossing_orders () = + let account = test_account () in + let configured = risk () in + let buy = request ~side:T.Order.Buy ~quantity_value:"4" () in + let buy_oms = + add_working_order T.Oms.empty ~id:"buy" ~accepted_sequence:1L buy + in + let sell = request ~side:T.Order.Sell ~quantity_value:"1" () in + Alcotest.(check string) + "sell against working buy" + "order would self-cross an active opposite-side order" + (risk_check configured ~account ~oms:buy_oms sell |> error); + let sell_oms = + add_working_order T.Oms.empty ~id:"sell" ~accepted_sequence:1L sell + in + Alcotest.(check string) + "buy against working sell" + "order would self-cross an active opposite-side order" + (risk_check configured ~account ~oms:sell_oms buy |> error) + +let risk_reserves_partial_order_remainders () = + let account = test_account () in + let configured = risk ~max_long:"5" () in + let oms, order = + oms_with_order (request ~side:T.Order.Buy ~quantity_value:"10" ()) + in + let partial = fill ~quantity_value:"6" order in + let oms = + match T.Oms.apply_fill oms partial with + | Ok (oms, T.Oms.Applied _) -> oms + | Ok (_, T.Oms.Duplicate) -> Alcotest.fail "expected an applied fill" + | Error message -> Alcotest.fail message + in + Alcotest.(check (result unit string)) + "one unit fits after partial fill" (Ok ()) + (risk_check configured ~account ~oms + (request ~side:T.Order.Buy ~quantity_value:"1" ())); + Alcotest.(check string) + "two units exceed the reserved long limit" + "position would exceed the maximum long position" + (risk_check configured ~account ~oms + (request ~side:T.Order.Buy ~quantity_value:"2" ()) + |> error) + +let risk_values_directional_reservations_without_netting () = + let primary = instrument () in + let hedge = instrument ~id:"hedge" ~symbol:"HEDGE" () in + let configured = risk ~instruments:[ primary; hedge ] ~max_gross:"1000" () in + let account = test_account () in + let primary_id = instrument_id "test-equity" in + let buy = + request ~instrument:primary_id ~side:T.Order.Buy ~quantity_value:"8" () + in + let sell = + request ~instrument:primary_id ~side:T.Order.Sell ~quantity_value:"8" () + in + let oms = + add_working_order T.Oms.empty ~id:"buy" ~accepted_sequence:1L buy + |> fun oms -> add_working_order oms ~id:"sell" ~accepted_sequence:2L sell + in + let result = + T.Risk.check configured ~account ~oms + ~marks:[ (primary_id, price "100"); (instrument_id "hedge", price "100") ] + ~fx_rates:[ ("USD", price "1") ] + (request ~instrument:(instrument_id "hedge") ~side:T.Order.Buy + ~quantity_value:"4" ()) + in + Alcotest.(check string) + "opposing reservations retain their directional exposure" + "portfolio would exceed maximum gross exposure" (result |> error) + let accounting_identity_property = let open QCheck2 in let generator = @@ -265,5 +344,11 @@ let tests = fills_settle_to_explicit_margin_cash; Alcotest.test_case "risk reserves working sells" `Quick risk_reserves_working_sells; + Alcotest.test_case "risk rejects self-crossing orders" `Quick + risk_rejects_self_crossing_orders; + Alcotest.test_case "risk reserves partial-order remainders" `Quick + risk_reserves_partial_order_remainders; + Alcotest.test_case "risk values directional reservations without netting" + `Quick risk_values_directional_reservations_without_netting; QCheck_alcotest.to_alcotest ~speed_level:`Quick accounting_identity_property; ] From a25c968e52f8aed94a5cc4c400f4e644c5edbc27 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 17:23:03 -0400 Subject: [PATCH 05/57] fix: distinguish fill clipping reasons --- README.md | 16 +- contracts/v4/README.md | 31 +++ contracts/v4/dune | 16 ++ contracts/v4/fixtures/demo.journal.jsonl | 20 ++ contracts/v4/fixtures/demo.scenario.json | 113 ++++++++ contracts/v4/fixtures/demo.scenario.jsonl | 6 + .../v4/fixtures/fill-clipped.journal.jsonl | 10 + .../v4/fixtures/fill-clipped.scenario.json | 79 ++++++ contracts/v4/journal.schema.json | 177 ++++++++++++ contracts/v4/scenario-stream.schema.json | 75 +++++ contracts/v4/scenario.schema.json | 262 ++++++++++++++++++ docs/execution-model.md | 7 +- docs/persistra.md | 10 +- docs/scenario.md | 23 +- lib/audit.ml | 14 +- lib/audit.mli | 9 + lib/codec.ml | 49 ++++ lib/contract.ml | 9 +- lib/contract.mli | 3 + lib/engine.ml | 161 +++++++---- lib/engine.mli | 1 + lib/external_replay.ml | 22 +- lib/journal.ml | 3 +- lib/replay.ml | 8 +- lib/risk.ml | 60 +++- lib/risk.mli | 14 +- lib/scenario.ml | 13 +- lib/scenario_stream.ml | 30 +- test/cli.t | 22 +- test/dune | 58 +++- test/test_checkpoint4.ml | 107 ++++++- test/test_reducer.ml | 94 ++++++- test/test_scenario.ml | 58 +++- test/test_support.ml | 9 +- test/validate_schemas.py | 29 +- 35 files changed, 1443 insertions(+), 175 deletions(-) create mode 100644 contracts/v4/README.md create mode 100644 contracts/v4/dune create mode 100644 contracts/v4/fixtures/demo.journal.jsonl create mode 100644 contracts/v4/fixtures/demo.scenario.json create mode 100644 contracts/v4/fixtures/demo.scenario.jsonl create mode 100644 contracts/v4/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v4/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v4/journal.schema.json create mode 100644 contracts/v4/scenario-stream.schema.json create mode 100644 contracts/v4/scenario.schema.json diff --git a/README.md b/README.md index a9d1495..4cde003 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ scenario slices and scheduled or external intents - Deterministic liquidation-first matching, then sell-before-buy and FIFO priority - Shared per-instrument volume participation, partial fills, and GTC limits - One-slice IOC market orders -- Risk-aware fractional-lot clipping with structured `margin_limited` records +- Risk-aware fractional-lot clipping with structured `fill_clipped` reasons and thresholds - Fixed and notional fees with explicit rounding - Explicit multi-currency cash ledgers and complete per-slice FX marks in a base currency - Split and cash-dividend processing before matching, including target and order adjustment @@ -75,7 +75,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v3/fixtures/demo.scenario.json \ + --input contracts/v4/fixtures/demo.scenario.json \ --validate-only ``` @@ -83,7 +83,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v3/fixtures/demo.scenario.json \ + --input contracts/v4/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -91,7 +91,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v3/fixtures/demo.scenario.jsonl \ + --input contracts/v4/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -183,12 +183,12 @@ production recovery log. - [Architecture](docs/architecture.md) - [Diagnostic contract](docs/diagnostics.md) - [Scenario contract](docs/scenario.md) -- [Current contract v3 and conformance fixtures](contracts/v3/README.md) +- [Current contract v4 and conformance fixtures](contracts/v4/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v3/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v3/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v3/journal.schema.json) +- [Scenario JSON Schema](contracts/v4/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v4/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v4/journal.schema.json) - [External 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) diff --git a/contracts/v4/README.md b/contracts/v4/README.md new file mode 100644 index 0000000..b86761d --- /dev/null +++ b/contracts/v4/README.md @@ -0,0 +1,31 @@ +# Trading Engine contract v4 + +This directory is the authoritative v4 process and file contract shared by Trading Engine and +its clients. Version 3 remains available under `contracts/v3` during the client transition. + +- `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 4 replaces the ambiguous `margin_limited` journal event with `fill_clipped`. Its versioned, +exhaustive reason identifies the limiting policy and a typed threshold alongside the proposed and +permitted quantities. Every v4 scenario, stream record, and journal record carries +`"contract_version": "4"`; consumers reject missing or unsupported versions before interpreting +the remainder of a document. The scenario shape is otherwise unchanged from v3. + +`fill_clipped.payload.reason.version` is `"1"`. Its exhaustive policy and threshold pairs are: + +| Policy | Threshold unit | Threshold value | +| --- | --- | --- | +| `max_order_quantity` | `quantity` | Configured maximum order quantity | +| `max_long_position` | `quantity` | Configured maximum long position | +| `max_short_position` | `quantity` | Configured maximum short position magnitude | +| `max_gross_exposure` | `money` | Configured maximum gross exposure | +| `max_leverage` | `ratio` | Configured maximum leverage | +| `initial_margin` | `basis_points` | Configured initial-margin basis points | + +The event also records `order_id`, `instrument_id`, `proposed_quantity`, `permitted_quantity`, and +the proposed fill `price`. A consumer must reject unknown reason versions, policies, threshold +units, and policy/unit combinations. diff --git a/contracts/v4/dune b/contracts/v4/dune new file mode 100644 index 0000000..6cd8a55 --- /dev/null +++ b/contracts/v4/dune @@ -0,0 +1,16 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v4/journal.schema.json) + (scenario-stream.schema.json as contracts/v4/scenario-stream.schema.json) + (scenario.schema.json as contracts/v4/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v4/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v4/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl as contracts/v4/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v4/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v4/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v4/fixtures/demo.journal.jsonl b/contracts/v4/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..503d7c5 --- /dev/null +++ b/contracts/v4/fixtures/demo.journal.jsonl @@ -0,0 +1,20 @@ +{"contract_version":"4","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":"991890e8c1cc839a0c321a6d30b2ba20a8b588d4135310f43a548fbc929e9fcf","execution_model":"completed_bar_v1"}} +{"contract_version":"4","engine_sequence":"2","event_id":"demo-event-000000000002","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":[]}} +{"contract_version":"4","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.615","reference_price":"104"}]}} +{"contract_version":"4","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} +{"contract_version":"4","engine_sequence":"5","event_id":"demo-event-000000000005","causation_ids":["demo-event-000000000002","demo-event-000000000003"],"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":"9.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","updated_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"4","engine_sequence":"6","event_id":"demo-event-000000000006","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":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"10000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"0","mark":"104","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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"10000","maintenance_excess":"10000","margin_call":false}}} +{"contract_version":"4","engine_sequence":"7","event_id":"demo-event-000000000007","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} +{"contract_version":"4","engine_sequence":"8","event_id":"demo-event-000000000008","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} +{"contract_version":"4","engine_sequence":"9","event_id":"demo-event-000000000009","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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":"9.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","updated_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"4","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000003","demo-event-000000000007"],"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":"3.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000010","updated_event_id":"demo-event-000000000010","created_sequence":"10","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"4","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"642","long_market_value":"642","short_market_value":"0","gross_exposure":"642","cost_basis":"618.868","realized_pnl":"0","unrealized_pnl":"23.132","equity":"10023.132","dividend_pnl":"0","execution_fees":"0.868","borrow_fees":"0","total_fees":"0.868","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"6","mark":"107","fx_rate":"1","market_value":"642","base_market_value":"642","cost_basis":"618.868","base_cost_basis":"618.868","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"23.132","base_unrealized_pnl":"23.132","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0.868","base_execution_fees":"0.868","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0.868","base_total_fees":"0.868"}],"margin":{"initial_requirement":"321","maintenance_requirement":"160.5","initial_excess":"9702.132","maintenance_excess":"9862.632","margin_call":false}}} +{"contract_version":"4","engine_sequence":"12","event_id":"demo-event-000000000012","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":[]}} +{"contract_version":"4","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000010","demo-event-000000000012"],"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":"3.615","price":"107","notional":"386.805","fee":"0.636805","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} +{"contract_version":"4","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000012"],"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":"4","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.115","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000015","updated_event_id":"demo-event-000000000015","created_sequence":"15","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"4","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000012"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"8993.690195","net_market_value":"1009.575","long_market_value":"1009.575","short_market_value":"0","gross_exposure":"1009.575","cost_basis":"1006.309805","realized_pnl":"0","unrealized_pnl":"3.265195","equity":"10003.265195","dividend_pnl":"0","execution_fees":"1.504805","borrow_fees":"0","total_fees":"1.504805","cash_balances":[{"currency":"USD","amount":"8993.690195","fx_rate":"1","base_value":"8993.690195"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"9.615","mark":"105","fx_rate":"1","market_value":"1009.575","base_market_value":"1009.575","cost_basis":"1006.309805","base_cost_basis":"1006.309805","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"3.265195","base_unrealized_pnl":"3.265195","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"1.504805","base_execution_fees":"1.504805","borrow_fees":"0","base_borrow_fees":"0","total_fees":"1.504805","base_total_fees":"1.504805"}],"margin":{"initial_requirement":"504.7875","maintenance_requirement":"252.39375","initial_excess":"9498.477695","maintenance_excess":"9750.871445","margin_call":false}}} +{"contract_version":"4","engine_sequence":"17","event_id":"demo-event-000000000017","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":[]}} +{"contract_version":"4","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"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.115","price":"105","notional":"747.075","fee":"0.997075","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} +{"contract_version":"4","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}}} +{"contract_version":"4","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"991890e8c1cc839a0c321a6d30b2ba20a8b588d4135310f43a548fbc929e9fcf","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v4/fixtures/demo.scenario.json b/contracts/v4/fixtures/demo.scenario.json new file mode 100644 index 0000000..c5174ec --- /dev/null +++ b/contracts/v4/fixtures/demo.scenario.json @@ -0,0 +1,113 @@ +{ + "contract_version": "4", + "metadata": { + "producer": "trading-engine-demo", + "purpose": "deterministic conformance fixture" + }, + "run_id": "demo", + "base_currency": "USD", + "initial_cash": [ + { "currency": "USD", "amount": "10000" } + ], + "instruments": [ + { + "instrument_id": "demo-equity-acme", + "symbol": "ACME", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "0.001" + } + ], + "risk": { + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_gross_exposure": "1000000", + "max_leverage": "2", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "short_borrow_bps": 100 + }, + "execution": { + "model": "completed_bar_v1", + "participation_bps": 5000, + "fixed_fee": "0.25", + "fee_bps": 10 + }, + "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": [] + }, + { + "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": "12" } + ], + "fx_rates": [{ "currency": "USD", "rate": "1" }], + "corporate_actions": [] + }, + { + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v4/fixtures/demo.scenario.jsonl b/contracts/v4/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..829d797 --- /dev/null +++ b/contracts/v4/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"4","payload":{"base_currency":"USD","execution":{"fee_bps":10,"fixed_fee":"0.25","model":"completed_bar_v1","participation_bps":5000},"initial_cash":[{"amount":"10000","currency":"USD"}],"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"0.001","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"max_internal_events":1000,"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"risk":{"initial_margin_bps":5000,"maintenance_margin_bps":2500,"max_gross_exposure":"1000000","max_leverage":"2","max_long_position":"1000","max_order_quantity":"1000","max_short_position":"1000","short_borrow_bps":100},"run_id":"demo"},"record_type":"scenario_header","scenario_sequence":"1"} +{"contract_version":"4","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"}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"4","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"4","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"}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"4","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"}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"4","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v4/fixtures/fill-clipped.journal.jsonl b/contracts/v4/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..67d9a50 --- /dev/null +++ b/contracts/v4/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,10 @@ +{"contract_version":"4","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":"6ae16145d56f1ff2b7594c04917fd026a18e8fea9beb688a8aaf4a84cc7dca2c","execution_model":"completed_bar_v1"}} +{"contract_version":"4","engine_sequence":"2","event_id":"fill-clipped-event-000000000002","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":[]}} +{"contract_version":"4","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":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000003","updated_event_id":"fill-clipped-event-000000000003","created_sequence":"3","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"4","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false}}} +{"contract_version":"4","engine_sequence":"5","event_id":"fill-clipped-event-000000000005","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":[]}} +{"contract_version":"4","engine_sequence":"6","event_id":"fill-clipped-event-000000000006","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"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":"5","price":"100"}} +{"contract_version":"4","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"fill-clipped-fill-000000000001","order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"5","price":"100","notional":"500","fee":"10","executed_at":"2026-02-03T14:30:00.000000Z","slice_sequence":"2"}} +{"contract_version":"4","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"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","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000003","updated_event_id":"fill-clipped-event-000000000003","created_sequence":"3","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"5","filled_notional":"500","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"4","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}}} +{"contract_version":"4","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000009"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"6ae16145d56f1ff2b7594c04917fd026a18e8fea9beb688a8aaf4a84cc7dca2c","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v4/fixtures/fill-clipped.scenario.json b/contracts/v4/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..c575c46 --- /dev/null +++ b/contracts/v4/fixtures/fill-clipped.scenario.json @@ -0,0 +1,79 @@ +{ + "contract_version": "4", + "metadata": { + "producer": "trading-engine", + "purpose": "fill clipping conformance fixture" + }, + "run_id": "fill-clipped", + "base_currency": "USD", + "initial_cash": [ + { "currency": "USD", "amount": "550" } + ], + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "risk": { + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "short_borrow_bps": 100 + }, + "execution": { + "model": "completed_bar_v1", + "participation_bps": 10000, + "fixed_fee": "10", + "fee_bps": 0 + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "market", + "limit_price": 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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v4/journal.schema.json b/contracts/v4/journal.schema.json new file mode 100644 index 0000000..c580f03 --- /dev/null +++ b/contracts/v4/journal.schema.json @@ -0,0 +1,177 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v4/journal.schema.json", + "title": "Trading Engine v4 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": "4" }, + "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", "market_slice_received", "target_portfolio_requested", "order_accepted", "order_rejected", "order_cancelled", "split_applied", "cash_dividend_applied", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] + }, + "payload": { "type": "object" } + }, + "allOf": [ + { "if": { "properties": { "event_type": { "const": "run_started" } } }, "then": { "properties": { "payload": { "$ref": "#/$defs/runStarted" } } } }, + { "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"] } } }, "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } + }, + "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" } } } + ] + }, + "marketSlice": { + "type": "object", "additionalProperties": false, + "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], + "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" } } + } + }, + "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", "limit_price", "origin", "created_event_id", "updated_event_id", "created_sequence", "created_at", "eligible_after_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"] }, "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, "origin": { "enum": ["direct", "target_rebalance", "margin_liquidation"] }, "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" }, "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", "margin_call"] } } + }, + "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" } } + }, + "orderAdjusted": { + "type": "object", "additionalProperties": false, "required": ["order", "action_id"], + "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } + }, + "fill": { + "type": "object", "additionalProperties": false, + "required": ["fill_id", "order_id", "instrument_id", "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", "slice_sequence"], + "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/unsignedDecimal" }, "executed_at": { "$ref": "#/$defs/timestamp" }, "slice_sequence": { "$ref": "#/$defs/sequence" } } + }, + "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 } } + }, + "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" } } } + ] + }, + "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" } } + }, + "cashAttribution": { + "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], + "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } + }, + "positionAttribution": { + "type": "object", "additionalProperties": false, + "required": ["instrument_id", "quote_currency", "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"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, "quote_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "base_execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_total_fees": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "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" } } + }, + "valuation": { + "type": "object", "additionalProperties": false, + "required": ["base_currency", "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", "total_fees", "cash_balances", "positions", "margin"], + "properties": { + "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "cash_balances": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashAttribution" } }, "positions": { "type": "array", "items": { "$ref": "#/$defs/positionAttribution" } }, "margin": { "$ref": "#/$defs/margin" } + } + }, + "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": { "const": "completed_bar_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/v4/scenario-stream.schema.json b/contracts/v4/scenario-stream.schema.json new file mode 100644 index 0000000..ff9e62c --- /dev/null +++ b/contracts/v4/scenario-stream.schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v4/scenario-stream.schema.json", + "title": "Trading Engine v4 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": "4" }, + "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": "4" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/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": "4" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/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_cash", "instruments", "risk", "execution", "max_internal_events"], + "properties": { + "metadata": { "type": "object" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/cashBalance" } }, + "instruments": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/instrument" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/execution" }, + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 4611686018427387903 } + } + }, + "slicePayload": { + "type": "object", + "additionalProperties": false, + "required": ["market_slice", "intents"], + "properties": { + "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v4/scenario.schema.json b/contracts/v4/scenario.schema.json new file mode 100644 index 0000000..577db41 --- /dev/null +++ b/contracts/v4/scenario.schema.json @@ -0,0 +1,262 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json", + "title": "Trading Engine v4 replay scenario", + "description": "Strict deterministic scenario contract for fractional quantities, explicit FX, corporate actions, signed positions, and margin risk.", + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "metadata", "run_id", "base_currency", "initial_cash", "instruments", "risk", "execution", "max_internal_events", "schedule", "slices"], + "properties": { + "contract_version": { "const": "4" }, + "metadata": { "type": "object" }, + "run_id": { "$ref": "#/$defs/identifier" }, + "base_currency": { "$ref": "#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/cashBalance" } + }, + "instruments": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/instrument" } + }, + "risk": { "$ref": "#/$defs/risk" }, + "execution": { "$ref": "#/$defs/execution" }, + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 4611686018427387903 }, + "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": { + "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/unsignedDecimal" } + } + }, + "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" } + } + }, + "risk": { + "type": "object", + "additionalProperties": false, + "required": ["max_order_quantity", "max_long_position", "max_short_position", "max_gross_exposure", "max_leverage", "initial_margin_bps", "maintenance_margin_bps", "short_borrow_bps"], + "properties": { + "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, + "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "execution": { + "type": "object", + "additionalProperties": false, + "required": ["model", "participation_bps", "fixed_fee", "fee_bps"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, + "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "scheduleItem": { + "type": "object", + "additionalProperties": false, + "required": ["after_slice_sequence", "intents"], + "properties": { + "after_slice_sequence": { "$ref": "#/$defs/sequence" }, + "intents": { "type": "array", "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", "limit_price"], + "properties": { + "type": { "const": "submit_order" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "order_kind": { "enum": ["market", "limit"] }, + "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] } + } + }, + "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"], + "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" } } + } + }, + "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" } + } + } + ] + } + } +} diff --git a/docs/execution-model.md b/docs/execution-model.md index b2aac63..4a5cb48 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -124,8 +124,11 @@ post-fill position is within the long/short cap and whose fill quantity is no gr maximum order quantity. When absolute exposure increases, the projected account must also satisfy maximum gross exposure, maximum leverage, and initial margin. Reductions in absolute exposure are permitted without a new -initial-margin test. A clipped proposal emits `margin_limited`; a zero permitted quantity produces -no fill. Only the applied quantity consumes shared slice capacity. +initial-margin test. A clipped proposal emits `fill_clipped`; a zero permitted quantity produces +no fill. The event records reason taxonomy version `1`, the limiting policy, its typed threshold, +and both the proposed and permitted quantities. Only the applied quantity consumes shared slice +capacity. Candidate arithmetic, accounting, mark, and FX failures abort replay instead of being +misreported as policy clipping. This bounded-fill policy preserves split-adjusted GTC limit orders: an oversized remainder may fill over multiple slices. Market orders remain IOC, so they fill at most one bounded quantity and diff --git a/docs/persistra.md b/docs/persistra.md index f5bacd3..d1746d7 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -50,10 +50,12 @@ Persistra should: Do not let the engine read Persistra's internal DuckDB tables. Their schema and connection lifecycle belong to Persistra. -Use the current v3 [scenario](../contracts/v3/scenario.schema.json) and -[journal](../contracts/v3/journal.schema.json) JSON Schemas and their adjacent conformance fixtures -for structural checks. The engine parser is authoritative for ordering, catalog coverage, -causality, tick, lot, risk, and accounting invariants that JSON Schema cannot express. +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 also advertises current contract v4 while retaining 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 v3](../contracts/strategy/v3/README.md). Persistra's host turns protocol diff --git a/docs/scenario.md b/docs/scenario.md index 13706de..9836204 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 v3 demo](../contracts/v3/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v3/scenario.schema.json) provides structural validation. +Use [the v4 demo](../contracts/v4/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v4/scenario.schema.json) provides structural validation. The engine parser also enforces cross-field and cross-record invariants. ```sh @@ -27,8 +27,8 @@ adjacent to their decision slice rather than stored in a future-looking global s replay, the reader checks each intent-bearing slice against the next slice's start time while retaining only those two records. -The [stream record JSON Schema](../contracts/v3/scenario-stream.schema.json) validates each line, -and [the v3 stream fixture](../contracts/v3/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v4/scenario-stream.schema.json) validates each line, +and [the v4 stream fixture](../contracts/v4/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. @@ -37,7 +37,7 @@ retains current account, order, target, and latest-bar state required by executi | Field | Meaning | |---|---| -| `contract_version` | Required string identifying this file contract; v3 is `"3"` | +| `contract_version` | Required string identifying this file contract; v4 is `"4"` | | `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 | @@ -72,7 +72,7 @@ exposure must satisfy every applicable limit; exposure-reducing orders remain ad Execution contains: -- `model`, the compiled execution module selected by contract name; v3 supports +- `model`, the compiled execution module selected by contract name; v4 supports `completed_bar_v1` - `participation_bps`, from 0 through 10,000 - `fixed_fee`, a nonnegative money string @@ -170,7 +170,7 @@ than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v3/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v4/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 @@ -180,10 +180,11 @@ The first record is `run_started` with `scenario_sha256` and the selected execut hashes the exact batch document or stream bytes it parses. `market_slice_received` contains the complete normalized slice. Portfolio requests record their basis, original weight when applicable, computed quantity, and sizing reference price. Orders use -`eligible_after_slice_sequence`; fills use `slice_sequence`. `margin_limited` records a proposed -fill and the greatest lot-aligned quantity permitted by maximum order quantity, position, -exposure, leverage, and initial margin policy. Each order snapshot retains both creation and -latest-update event IDs. +`eligible_after_slice_sequence`; fills use `slice_sequence`. `fill_clipped` records the proposed +fill and the greatest lot-aligned permitted quantity. Its reason taxonomy version `1` names one of +`max_order_quantity`, `max_long_position`, `max_short_position`, `max_gross_exposure`, +`max_leverage`, or `initial_margin` and carries a quantity, money, ratio, or basis-points threshold. +Each order snapshot retains both creation and latest-update event IDs. The journal also records split/dividend application, split-driven order adjustments, short borrow fees, margin calls, liquidation-origin orders, and restoration. Every valuation contains complete diff --git a/lib/audit.ml b/lib/audit.ml index eeb48ec..532b8e6 100644 --- a/lib/audit.ml +++ b/lib/audit.ml @@ -52,6 +52,14 @@ type event = permitted_quantity : Scalar.Quantity.t; price : Scalar.Price.t; } + | Fill_clipped of { + order_id : Id.Order.t; + instrument_id : Id.Instrument.t; + proposed_quantity : Scalar.Quantity.t; + permitted_quantity : Scalar.Quantity.t; + price : Scalar.Price.t; + limit : Risk.fill_limit; + } | Borrow_fee_applied of { instrument_id : Id.Instrument.t; quote_currency : string; @@ -88,9 +96,10 @@ let event_id ~run_id ~engine_sequence = Printf.sprintf "%s-event-%012Ld" (Id.Run.to_string run_id) engine_sequence |> Id.Event.of_string_exn -let create ~engine_sequence ~causation_ids ~run_id ~recorded_at event = +let create ~contract_version ~engine_sequence ~causation_ids ~run_id + ~recorded_at event = { - contract_version = Contract.version; + contract_version; engine_sequence; event_id = event_id ~run_id ~engine_sequence; causation_ids; @@ -121,6 +130,7 @@ let event_name = function | Order_adjusted _ -> "order_adjusted" | Fill_applied _ -> "fill_applied" | Margin_limited _ -> "margin_limited" + | Fill_clipped _ -> "fill_clipped" | Borrow_fee_applied _ -> "borrow_fee_applied" | Margin_call_triggered _ -> "margin_call" | Margin_restored _ -> "margin_restored" diff --git a/lib/audit.mli b/lib/audit.mli index a8b10ab..154ad0e 100644 --- a/lib/audit.mli +++ b/lib/audit.mli @@ -54,6 +54,14 @@ type event = permitted_quantity : Scalar.Quantity.t; price : Scalar.Price.t; } + | Fill_clipped of { + order_id : Id.Order.t; + instrument_id : Id.Instrument.t; + proposed_quantity : Scalar.Quantity.t; + permitted_quantity : Scalar.Quantity.t; + price : Scalar.Price.t; + limit : Risk.fill_limit; + } | Borrow_fee_applied of { instrument_id : Id.Instrument.t; quote_currency : string; @@ -89,6 +97,7 @@ type t = private { val event_id : run_id:Id.Run.t -> engine_sequence:int64 -> Id.Event.t val create : + contract_version:string -> engine_sequence:int64 -> causation_ids:Id.Event.t list -> run_id:Id.Run.t -> diff --git a/lib/codec.ml b/lib/codec.ml index 2525ce8..8be46ea 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -82,6 +82,30 @@ let instrument_id value = string (Id.Instrument.to_string value) let order_id value = string (Id.Order.to_string value) let fill_id value = string (Id.Fill.to_string value) +let fill_limit_to_yojson = function + | Risk.Maximum_order_quantity value -> + ( "max_order_quantity", + `Assoc [ ("unit", string "quantity"); ("value", quantity value) ] ) + | Risk.Maximum_long_position value -> + ( "max_long_position", + `Assoc [ ("unit", string "quantity"); ("value", quantity value) ] ) + | Risk.Maximum_short_position value -> + ( "max_short_position", + `Assoc [ ("unit", string "quantity"); ("value", quantity value) ] ) + | Risk.Maximum_gross_exposure value -> + ( "max_gross_exposure", + `Assoc [ ("unit", string "money"); ("value", money value) ] ) + | Risk.Maximum_leverage value -> + ( "max_leverage", + `Assoc + [ + ("unit", string "ratio"); + ("value", string (Scalar.Ratio.to_decimal_string value)); + ] ) + | Risk.Initial_margin value -> + ( "initial_margin", + `Assoc [ ("unit", string "basis_points"); ("value", `Int value) ] ) + let bar_to_yojson bar = `Assoc [ @@ -340,6 +364,31 @@ let payload_to_yojson = function ("permitted_quantity", quantity permitted_quantity); ("price", price fill_price); ] + | Audit.Fill_clipped + { + order_id = id; + instrument_id = instrument; + proposed_quantity; + permitted_quantity; + price = fill_price; + limit; + } -> + let limiting_policy, threshold = fill_limit_to_yojson limit in + `Assoc + [ + ( "reason", + `Assoc + [ + ("version", string "1"); + ("policy", string limiting_policy); + ("threshold", threshold); + ] ); + ("order_id", order_id id); + ("instrument_id", instrument_id instrument); + ("proposed_quantity", quantity proposed_quantity); + ("permitted_quantity", quantity permitted_quantity); + ("price", price fill_price); + ] | Audit.Borrow_fee_applied { instrument_id = instrument; diff --git a/lib/contract.ml b/lib/contract.ml index 05de043..6ac079b 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,4 +1,7 @@ -let version = "3" +let version = "4" +let previous_version = "3" +let supported_versions = [ version; previous_version ] +let is_supported version = List.mem version supported_versions let strategy_protocol_version = "3" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -7,8 +10,8 @@ let capabilities_to_yojson () = `Assoc [ ("engine_version", `String engine_version); - ("scenario_contract_versions", strings [ version ]); - ("journal_contract_versions", strings [ version ]); + ("scenario_contract_versions", strings supported_versions); + ("journal_contract_versions", strings supported_versions); ("scenario_formats", strings [ "json"; "jsonl" ]); ("journal_formats", strings [ "jsonl" ]); ("execution_models", strings Execution_model.supported); diff --git a/lib/contract.mli b/lib/contract.mli index a81e2e8..4565459 100644 --- a/lib/contract.mli +++ b/lib/contract.mli @@ -1,6 +1,9 @@ (** Version and capability identifiers for the process/file boundary. *) val version : string +val previous_version : string +val supported_versions : string list +val is_supported : string -> bool val strategy_protocol_version : string val engine_version : string val capabilities_to_yojson : unit -> Yojson.Safe.t diff --git a/lib/engine.ml b/lib/engine.ml index a63b303..9739914 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -1,14 +1,26 @@ type config = { + contract_version : string; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; max_internal_events : int; } -let config ~risk ~execution_model ~execution ~max_internal_events = - if max_internal_events <= 0 then +let config ~contract_version ~risk ~execution_model ~execution + ~max_internal_events = + if not (Contract.is_supported contract_version) then + Error "engine contract version is unsupported" + else if max_internal_events <= 0 then Error "maximum internal events must be positive" - else Ok { risk; execution_model; execution; max_internal_events } + else + Ok + { + contract_version; + risk; + execution_model; + execution; + max_internal_events; + } let valid_sha256 value = String.length value = 64 @@ -134,7 +146,8 @@ module Interactive = struct Audit.event_id ~run_id:reduction.state.run_id ~engine_sequence in let audit = - Audit.create ~engine_sequence + Audit.create ~contract_version:reduction.state.config.contract_version + ~engine_sequence ~causation_ids:(normalize_causes reduction.causation_ids) ~run_id:reduction.state.run_id ~recorded_at:reduction.now event in @@ -867,31 +880,36 @@ module Interactive = struct Account.position_quantity state.account instrument.Instrument.id in let candidate quantity = - let* fee = - fill_fee state.config.execution proposed.Execution.price quantity - in - let* fill = - Fill.create ~id:(fill_id state) ~order_id:order.Order.id - ~instrument_id:instrument.id ~quote_currency:instrument.quote_currency - ~side:order.request.side ~quantity ~price:proposed.price ~fee - ~executed_at:proposed.executed_at - ~slice_sequence:market_slice.Market_slice.slice_sequence - in - let* account = Account.apply_fill state.account fill in - let after_position = Account.position_quantity account instrument.id in - let* before_absolute = Scalar.Quantity.absolute before_position in - let* after_absolute = Scalar.Quantity.absolute after_position in - let* () = - if Scalar.Quantity.compare after_absolute before_absolute <= 0 then - Ok () - else Risk.check_position state.config.risk after_position - in - let* after = - Account.value account ~instruments ~marks - ~fx_rates:state.latest_fx_rates + let prepared = + let* fee = + fill_fee state.config.execution proposed.Execution.price quantity + in + let* fill = + Fill.create ~id:(fill_id state) ~order_id:order.Order.id + ~instrument_id:instrument.id + ~quote_currency:instrument.quote_currency ~side:order.request.side + ~quantity ~price:proposed.price ~fee + ~executed_at:proposed.executed_at + ~slice_sequence:market_slice.Market_slice.slice_sequence + in + let* account = Account.apply_fill state.account fill in + let after_position = Account.position_quantity account instrument.id in + let* after = + Account.value account ~instruments ~marks + ~fx_rates:state.latest_fx_rates + in + Ok (fee, after_position, after) in - let* () = Risk.check_post_fill state.config.risk ~before ~after in - Ok fee + match prepared with + | Error message -> Error (`Invalid message) + | Ok (fee, after_position, after) -> ( + match + Risk.check_post_fill state.config.risk ~before_position + ~after_position ~before ~after + with + | Ok () -> Ok fee + | Error (Risk.Limit limit) -> Error (`Limit limit) + | Error (Risk.Invalid message) -> Error (`Invalid message)) in let lot_value = Scalar.Quantity.to_micros instrument.lot_size in let quantity_limit = @@ -901,27 +919,47 @@ module Interactive = struct let requested_lots = Int64.div (Scalar.Quantity.to_micros quantity_limit) lot_value in - let allowed lots = - if Int64.equal lots 0L then true - else - let quantity = Scalar.Quantity.of_micros (Int64.mul lots lot_value) in - Result.is_ok (candidate quantity) - in let rec search low high = - if Int64.compare low high >= 0 then low + if Int64.compare low high >= 0 then Ok low else let difference = Int64.sub high low in let upper_half = Int64.add (Int64.div difference 2L) (Int64.rem difference 2L) in let middle = Int64.add low upper_half in - if allowed middle then search middle high - else search low (Int64.pred middle) + let quantity = Scalar.Quantity.of_micros (Int64.mul middle lot_value) in + match candidate quantity with + | Ok _ -> search middle high + | Error (`Limit _) -> search low (Int64.pred middle) + | Error (`Invalid message) -> Error message in - let lots = search 0L requested_lots in + let* lots = search 0L requested_lots in let quantity = Scalar.Quantity.of_micros (Int64.mul lots lot_value) in - if Scalar.Quantity.is_zero quantity then Ok (quantity, Scalar.Money.zero) - else candidate quantity |> Result.map (fun fee -> (quantity, fee)) + let clipped = Scalar.Quantity.compare quantity proposed.quantity < 0 in + let* limit = + if not clipped then Ok None + else if Int64.equal lots requested_lots then + Ok + (Some + (Risk.Maximum_order_quantity + (Risk.max_order_quantity state.config.risk))) + else + let next_lots = Int64.succ lots in + let next_quantity = + Scalar.Quantity.of_micros (Int64.mul next_lots lot_value) + in + match candidate next_quantity with + | Error (`Limit limit) -> Ok (Some limit) + | Error (`Invalid message) -> Error message + | Ok _ -> Error "fill clipping search produced a nonmaximal quantity" + in + if Scalar.Quantity.is_zero quantity then + Ok (quantity, Scalar.Money.zero, limit) + else + match candidate quantity with + | Ok fee -> Ok (quantity, fee, limit) + | Error (`Invalid message) -> Error message + | Error (`Limit _) -> Error "permitted fill violates its limiting policy" let apply_fill reduction market_slice proposed quantity fee = match Oms.find reduction.state.oms proposed.Execution.order_id with @@ -1003,24 +1041,37 @@ module Interactive = struct | Some value -> Ok value | None -> Error "execution order refers to an unknown instrument" in - let* permitted_quantity, fee = + let* permitted_quantity, fee, limit = permitted_fill reduction.state market_slice order proposed instrument in - let clipped = - Scalar.Quantity.compare permitted_quantity proposed.quantity < 0 - in let* reduction = - if clipped then - emit reduction - (Audit.Margin_limited - { - order_id = order.id; - instrument_id = order.request.instrument_id; - requested_quantity = proposed.quantity; - permitted_quantity; - price = proposed.price; - }) - else Ok reduction + match limit with + | None -> Ok reduction + | Some limit -> + if + String.equal reduction.state.config.contract_version + Contract.previous_version + then + emit reduction + (Audit.Margin_limited + { + order_id = order.id; + instrument_id = order.request.instrument_id; + requested_quantity = proposed.quantity; + permitted_quantity; + price = proposed.price; + }) + else + emit reduction + (Audit.Fill_clipped + { + order_id = order.id; + instrument_id = order.request.instrument_id; + proposed_quantity = proposed.quantity; + permitted_quantity; + price = proposed.price; + limit; + }) in if Scalar.Quantity.is_zero permitted_quantity then Ok (reduction, permitted_quantity) diff --git a/lib/engine.mli b/lib/engine.mli index f6482c2..21228a6 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -3,6 +3,7 @@ type config val config : + contract_version:string -> risk:Risk.t -> execution_model:Execution_model.t -> execution:Execution.t -> diff --git a/lib/external_replay.ml b/lib/external_replay.ml index feb2596..aa074d8 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -76,10 +76,11 @@ let initialization_of_header ~scenario_sha256 (header : Scenario.stream_header) execution = header.execution; } -let create_runner ~run_id ~scenario_sha256 ~risk ~execution_model ~execution - ~max_internal_events ~initial_cash = +let create_runner ~contract_version ~run_id ~scenario_sha256 ~risk + ~execution_model ~execution ~max_internal_events ~initial_cash = let* config = - Engine.config ~risk ~execution_model ~execution ~max_internal_events + Engine.config ~contract_version ~risk ~execution_model ~execution + ~max_internal_events |> reducer_result in Runner.create ~run_id ~scenario_sha256 ~config ~initial_cash |> reducer_result @@ -130,7 +131,8 @@ let run ~env ~scenario_sha256 ~journal_path ~transcript_path ~strategy_command (replay "external strategy replay requires an empty scenario schedule") else let* initial = - create_runner ~run_id:scenario.run_id ~scenario_sha256 ~risk:scenario.risk + create_runner ~contract_version:scenario.contract_version + ~run_id:scenario.run_id ~scenario_sha256 ~risk:scenario.risk ~execution_model:scenario.execution_model ~execution:scenario.execution ~max_internal_events:scenario.max_internal_events ~initial_cash:scenario.initial_cash @@ -182,9 +184,9 @@ let validate_stream_pass ~scenario_sha256 channel = Scenario_stream.fold_channel channel ~init:(fun header -> let* runner = - create_runner ~run_id:header.Scenario.run_id ~scenario_sha256 - ~risk:header.risk ~execution_model:header.execution_model - ~execution:header.execution + create_runner ~contract_version:header.contract_version + ~run_id:header.Scenario.run_id ~scenario_sha256 ~risk:header.risk + ~execution_model:header.execution_model ~execution:header.execution ~max_internal_events:header.max_internal_events ~initial_cash:header.initial_cash in @@ -210,9 +212,9 @@ let replay_stream_pass ~scenario_sha256 ~journal ~session channel = Scenario_stream.fold_channel channel ~init:(fun header -> let* runner = - create_runner ~run_id:header.Scenario.run_id ~scenario_sha256 - ~risk:header.risk ~execution_model:header.execution_model - ~execution:header.execution + create_runner ~contract_version:header.contract_version + ~run_id:header.Scenario.run_id ~scenario_sha256 ~risk:header.risk + ~execution_model:header.execution_model ~execution:header.execution ~max_internal_events:header.max_internal_events ~initial_cash:header.initial_cash in diff --git a/lib/journal.ml b/lib/journal.ml index 9db53d6..fac444e 100644 --- a/lib/journal.ml +++ b/lib/journal.ml @@ -18,7 +18,8 @@ let audit_context event = Some (Id.Order.to_string order.Order.id) | Order_cancelled { order; _ } -> Some (Id.Order.to_string order.id) | Fill_applied fill -> Some (Id.Order.to_string fill.Fill.order_id) - | Margin_limited { order_id; _ } -> Some (Id.Order.to_string order_id) + | Margin_limited { order_id; _ } | Fill_clipped { order_id; _ } -> + Some (Id.Order.to_string order_id) | _ -> None in (event_id, order_id, causation_ids) diff --git a/lib/replay.ml b/lib/replay.ml index 39dfc46..343e72b 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -83,8 +83,8 @@ let run ~scenario_sha256 ?journal_path scenario = | Error _ as error -> fail error | Ok strategy_state -> ( match - Engine.config ~risk:scenario.risk - ~execution_model:scenario.execution_model + Engine.config ~contract_version:scenario.contract_version + ~risk:scenario.risk ~execution_model:scenario.execution_model ~execution:scenario.execution ~max_internal_events:scenario.max_internal_events |> reducer_result @@ -145,8 +145,8 @@ let run_stream_pass ~scenario_sha256 ~journal channel = | Error _ as error -> error | Ok strategy_state -> ( match - Engine.config ~risk:header.Scenario.risk - ~execution_model:header.execution_model + Engine.config ~contract_version:header.contract_version + ~risk:header.Scenario.risk ~execution_model:header.execution_model ~execution:header.execution ~max_internal_events:header.max_internal_events |> reducer_result diff --git a/lib/risk.ml b/lib/risk.ml index 4d0a309..7cd9082 100644 --- a/lib/risk.ml +++ b/lib/risk.ml @@ -19,6 +19,16 @@ type margin_snapshot = { margin_call : bool; } +type fill_limit = + | Maximum_order_quantity of Scalar.Quantity.t + | Maximum_long_position of Scalar.Quantity.t + | Maximum_short_position of Scalar.Quantity.t + | Maximum_gross_exposure of Scalar.Money.t + | Maximum_leverage of Scalar.Ratio.t + | Initial_margin of int + +type fill_check_error = Limit of fill_limit | Invalid of string + let ( let* ) result function_ = match result with Ok value -> function_ value | Error _ as error -> error @@ -163,13 +173,49 @@ let check_initial state valuation = check_initial_values state ~equity:valuation.Account.equity ~gross_exposure:valuation.gross_exposure -let check_post_fill state ~before ~after = - if - Scalar.Money.compare after.Account.gross_exposure - before.Account.gross_exposure - <= 0 - then Ok () - else check_initial state after +let invalid result = Result.map_error (fun message -> Invalid message) result + +let check_fill_initial state ~equity ~gross_exposure = + if Scalar.Money.compare gross_exposure state.max_gross_exposure > 0 then + Error (Limit (Maximum_gross_exposure state.max_gross_exposure)) + else + let* leveraged_equity = + Scalar.Money.multiply_ratio equity state.max_leverage |> invalid + in + if Scalar.Money.compare gross_exposure leveraged_equity > 0 then + Error (Limit (Maximum_leverage state.max_leverage)) + else + let* initial_requirement = + Scalar.Money.bps_ceil gross_exposure ~bps:state.initial_margin_bps + |> invalid + in + let* initial_excess = + Scalar.Money.subtract equity initial_requirement |> invalid + in + if Scalar.Money.compare initial_excess Scalar.Money.zero < 0 then + Error (Limit (Initial_margin state.initial_margin_bps)) + else Ok () + +let check_post_fill state ~before_position ~after_position ~before ~after = + let* before_absolute = Scalar.Quantity.absolute before_position |> invalid in + let* after_absolute = Scalar.Quantity.absolute after_position |> invalid in + if Scalar.Quantity.compare after_absolute before_absolute <= 0 then Ok () + else if Scalar.Quantity.compare after_position state.max_long_position > 0 + then Error (Limit (Maximum_long_position state.max_long_position)) + else + let* minimum_short = + Scalar.Quantity.negate state.max_short_position |> invalid + in + if Scalar.Quantity.compare after_position minimum_short < 0 then + Error (Limit (Maximum_short_position state.max_short_position)) + else if + Scalar.Money.compare after.Account.gross_exposure + before.Account.gross_exposure + <= 0 + then Ok () + else + check_fill_initial state ~equity:after.equity + ~gross_exposure:after.gross_exposure let check_position state quantity = if Scalar.Quantity.compare quantity state.max_long_position > 0 then diff --git a/lib/risk.mli b/lib/risk.mli index 6f23d7e..d457492 100644 --- a/lib/risk.mli +++ b/lib/risk.mli @@ -10,6 +10,16 @@ type margin_snapshot = private { margin_call : bool; } +type fill_limit = + | Maximum_order_quantity of Scalar.Quantity.t + | Maximum_long_position of Scalar.Quantity.t + | Maximum_short_position of Scalar.Quantity.t + | Maximum_gross_exposure of Scalar.Money.t + | Maximum_leverage of Scalar.Ratio.t + | Initial_margin of int + +type fill_check_error = Limit of fill_limit | Invalid of string + val create : base_currency:string -> instruments:Instrument.t list -> @@ -40,9 +50,11 @@ val check_initial : t -> Account.valuation -> (unit, string) result val check_post_fill : t -> + before_position:Scalar.Quantity.t -> + after_position:Scalar.Quantity.t -> before:Account.valuation -> after:Account.valuation -> - (unit, string) result + (unit, fill_check_error) result val check : t -> diff --git a/lib/scenario.ml b/lib/scenario.ml index 2855d6e..13aeac8 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -788,10 +788,12 @@ let of_yojson_result json = in let* contract_json = field fields "contract_version" in let* contract_version = string ~name:"contract_version" contract_json in - if not (String.equal contract_version Contract.version) then + if not (Contract.is_supported contract_version) then Error - (Printf.sprintf "unsupported scenario contract_version %S (expected %S)" - contract_version Contract.version) + (Printf.sprintf + "unsupported scenario contract_version %S (expected one of %s)" + contract_version + (String.concat ", " Contract.supported_versions)) else let* metadata = field fields "metadata" in let* () = @@ -874,8 +876,7 @@ let of_yojson json = match json with | `Assoc fields -> ( match List.assoc_opt "contract_version" fields with - | Some (`String supplied) - when not (String.equal supplied Contract.version) -> + | Some (`String supplied) when not (Contract.is_supported supplied) -> (Diagnostic.Scenario_unsupported_contract, "$.contract_version") | _ -> (Diagnostic.Scenario_invalid, "$")) | _ -> (Diagnostic.Scenario_invalid, "$") @@ -1006,7 +1007,7 @@ let stream_item_of_yojson_result header ~previous json = let stream_header_of_yojson ~contract_version json = let code, json_path = - if String.equal contract_version Contract.version then + if Contract.is_supported contract_version then (Diagnostic.Scenario_stream_invalid, "$.payload") else (Diagnostic.Scenario_unsupported_contract, "$.contract_version") in diff --git a/lib/scenario_stream.ml b/lib/scenario_stream.ml index 8045aa2..7b3c728 100644 --- a/lib/scenario_stream.ml +++ b/lib/scenario_stream.ml @@ -66,7 +66,11 @@ let parse_json ~line_number line = line_number message) exception_) -type envelope = { record_type : string; payload : Yojson.Safe.t } +type envelope = { + contract_version : string; + record_type : string; + payload : Yojson.Safe.t; +} let parse_envelope ~line_number ~expected_sequence line = let result = @@ -80,14 +84,15 @@ let parse_envelope ~line_number ~expected_sequence line = in let* contract_json = field fields "contract_version" in let* contract_version = string ~name:"contract_version" contract_json in - if not (String.equal contract_version Contract.version) then + if not (Contract.is_supported contract_version) then Error (Diagnostic.make ~code:Diagnostic.Scenario_unsupported_contract ~phase:Diagnostic.Validation ~line:line_number ~sequence:expected_sequence ~json_path:"$.contract_version" (Printf.sprintf - "unsupported scenario contract_version %S (expected %S)" - contract_version Contract.version)) + "unsupported scenario contract_version %S (expected one of %s)" + contract_version + (String.concat ", " Contract.supported_versions))) else let* sequence_json = field fields "scenario_sequence" in let* scenario_sequence = @@ -102,7 +107,7 @@ let parse_envelope ~line_number ~expected_sequence line = let* type_json = field fields "record_type" in let* record_type = string ~name:"record_type" type_json in let* payload = field fields "payload" in - Ok { record_type; payload } + Ok { contract_version; record_type; payload } in Result.map_error (Diagnostic.annotate ~line:line_number ~sequence:expected_sequence @@ -141,8 +146,8 @@ let fold_channel channel ~init ~step ~finish = "scenario_header must be the first scenario stream record") else let* header = - Scenario.stream_header_of_yojson ~contract_version:Contract.version - envelope.payload + Scenario.stream_header_of_yojson + ~contract_version:envelope.contract_version envelope.payload |> Result.map_error (Diagnostic.annotate ~line:1 ~sequence:1L ~json_path:"$.payload") in @@ -158,7 +163,16 @@ let fold_channel channel ~init ~step ~finish = let* envelope = parse_envelope ~line_number:!line_number ~expected_sequence line in - if String.equal envelope.record_type "market_slice" then + if + not + (String.equal envelope.contract_version + header.contract_version) + then + Error + (invalid ~line:!line_number ~sequence:expected_sequence + ~json_path:"$.contract_version" + "scenario stream contract_version must remain constant") + else if String.equal envelope.record_type "market_slice" then let* item = Scenario.stream_item_of_yojson header ~previous envelope.payload diff --git a/test/cli.t b/test/cli.t index 629f0e5..84696dd 100644 --- a/test/cli.t +++ b/test/cli.t @@ -2,22 +2,22 @@ 1.0.0 $ ../bin/main.exe --capabilities - {"engine_version":"1.0.0","scenario_contract_versions":["3"],"journal_contract_versions":["3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"strategy_protocol_versions":["3"]} + {"engine_version":"1.0.0","scenario_contract_versions":["4","3"],"journal_contract_versions":["4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"strategy_protocol_versions":["3"]} - $ ../bin/main.exe --validate-only --input ../contracts/v3/fixtures/demo.scenario.json - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=3e19fa66bc6425bb8ed7a89b338080a831dd39ea778c3c7f9e8ce1d3370fbee0 + $ ../bin/main.exe --validate-only --input ../contracts/v4/fixtures/demo.scenario.json + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=991890e8c1cc839a0c321a6d30b2ba20a8b588d4135310f43a548fbc929e9fcf - $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v3/fixtures/demo.scenario.jsonl - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=0615019643edcd2b75c1307456d93bfba13990cb09bdc80ffc7fac98056020cd + $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v4/fixtures/demo.scenario.jsonl + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=6afe9bbda482265cfa24c35167150f02eea1a457aa5025143f3556b8046ae91b - $ ../bin/main.exe --input-format jsonl --input ../contracts/v3/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl + $ ../bin/main.exe --input-format jsonl --input ../contracts/v4/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl run=demo audits=20 orders=3 active=0 filled=2 rejected=0 cash=9739.76812 equity=10004.76812 gross=265 realized=1.419136 unrealized=3.348984 fees=2.50188 journal=streamed.journal.jsonl $ wc -l < streamed.journal.jsonl 20 - $ head -n 5 ../contracts/v3/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl + $ head -n 5 ../contracts/v4/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl $ ../bin/main.exe --validate-only --input-format jsonl --input truncated.scenario.jsonl trading-engine: scenario_end must terminate the scenario stream [123] @@ -32,22 +32,22 @@ 1 scenario_stream.invalid validation 6 6 None - $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v3/fixtures/demo.scenario.json > invalid-tick.json + $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v4/fixtures/demo.scenario.json > invalid-tick.json $ ../bin/main.exe --validate-only --input invalid-tick.json trading-engine: market prices and volumes must align with instrument increments [123] - $ ../bin/main.exe --input ../contracts/v3/fixtures/demo.scenario.json + $ ../bin/main.exe --input ../contracts/v4/fixtures/demo.scenario.json trading-engine: --journal is required unless --validate-only is set [123] - $ ../bin/main.exe --validate-only --input ../contracts/v3/fixtures/demo.scenario.json --journal validation.journal.jsonl + $ ../bin/main.exe --validate-only --input ../contracts/v4/fixtures/demo.scenario.json --journal validation.journal.jsonl trading-engine: --journal cannot be used with --validate-only [123] $ test ! -e validation.journal.jsonl - $ ../bin/main.exe --input ../contracts/v3/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 + $ ../bin/main.exe --input ../contracts/v4/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 trading-engine: --strategy-arg, --strategy-timeout, and --strategy-transcript require --strategy-executable [123] $ test ! -e ignored.journal.jsonl diff --git a/test/dune b/test/dune index 0d77f6e..3261a43 100644 --- a/test/dune +++ b/test/dune @@ -12,12 +12,16 @@ test_scenario test_engine) (deps + ../contracts/v4/fixtures/demo.journal.jsonl + ../contracts/v4/fixtures/demo.scenario.json + ../contracts/v4/fixtures/demo.scenario.jsonl + ../contracts/v4/fixtures/fill-clipped.journal.jsonl + ../contracts/v4/fixtures/fill-clipped.scenario.json + ../contracts/v4/journal.schema.json + ../contracts/v4/scenario-stream.schema.json + ../contracts/v4/scenario.schema.json ../contracts/v3/fixtures/demo.journal.jsonl ../contracts/v3/fixtures/demo.scenario.json - ../contracts/v3/fixtures/demo.scenario.jsonl - ../contracts/v3/journal.schema.json - ../contracts/v3/scenario-stream.schema.json - ../contracts/v3/scenario.schema.json fake_strategy.py) (libraries trading_engine @@ -37,8 +41,50 @@ ../contracts/strategy/v3/fixtures/external.scenario.json ../contracts/strategy/v3/fixtures/external.scenario.jsonl ../contracts/strategy/v3/fixtures/external.strategy.jsonl - ../contracts/v3/fixtures/demo.scenario.json - ../contracts/v3/fixtures/demo.scenario.jsonl)) + ../contracts/v4/fixtures/demo.scenario.json + ../contracts/v4/fixtures/demo.scenario.jsonl)) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v4/fixtures/demo.journal.jsonl + ../contracts/v4/fixtures/demo.scenario.json + ../contracts/v4/fixtures/demo.scenario.jsonl + ../contracts/v4/journal.schema.json + ../contracts/v4/scenario-stream.schema.json + ../contracts/v4/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v4/scenario.schema.json} + %{dep:../contracts/v4/scenario-stream.schema.json} + %{dep:../contracts/v4/journal.schema.json} + %{dep:../contracts/v4/fixtures/demo.scenario.json} + %{dep:../contracts/v4/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v4/fixtures/demo.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v4/fixtures/fill-clipped.journal.jsonl + ../contracts/v4/fixtures/fill-clipped.scenario.json + ../contracts/v4/fixtures/demo.scenario.jsonl + ../contracts/v4/journal.schema.json + ../contracts/v4/scenario-stream.schema.json + ../contracts/v4/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v4/scenario.schema.json} + %{dep:../contracts/v4/scenario-stream.schema.json} + %{dep:../contracts/v4/journal.schema.json} + %{dep:../contracts/v4/fixtures/fill-clipped.scenario.json} + %{dep:../contracts/v4/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v4/fixtures/fill-clipped.journal.jsonl}))) (rule (alias runtest) diff --git a/test/test_checkpoint4.ml b/test/test_checkpoint4.ml index 554562c..36c9900 100644 --- a/test/test_checkpoint4.ml +++ b/test/test_checkpoint4.ml @@ -220,17 +220,27 @@ let split_caps_adjusted_market_fill () = List.find_map (fun event -> match event.T.Audit.event with - | T.Audit.Margin_limited { requested_quantity; permitted_quantity; _ } - -> - Some (requested_quantity, permitted_quantity) + | T.Audit.Fill_clipped + { + proposed_quantity; + permitted_quantity; + limit = T.Risk.Maximum_order_quantity threshold; + _; + } -> + Some (proposed_quantity, permitted_quantity, threshold) | _ -> None) events |> Option.get in Alcotest.check quantity_testable "adjusted proposal" (quantity "20") - (fst limited); + (let proposed, _, _ = limited in + proposed); Alcotest.check quantity_testable "maximum permitted fill" (quantity "10") - (snd limited) + (let _, permitted, _ = limited in + permitted); + Alcotest.check quantity_testable "maximum order threshold" (quantity "10") + (let _, _, threshold = limited in + threshold) let split_caps_partially_filled_limit_remainder () = let strategy_state = @@ -285,17 +295,27 @@ let split_caps_partially_filled_limit_remainder () = List.find_map (fun event -> match event.T.Audit.event with - | T.Audit.Margin_limited { requested_quantity; permitted_quantity; _ } - -> - Some (requested_quantity, permitted_quantity) + | T.Audit.Fill_clipped + { + proposed_quantity; + permitted_quantity; + limit = T.Risk.Maximum_order_quantity threshold; + _; + } -> + Some (proposed_quantity, permitted_quantity, threshold) | _ -> None) events |> Option.get in Alcotest.check quantity_testable "adjusted partial proposal" (quantity "12") - (fst limited); + (let proposed, _, _ = limited in + proposed); Alcotest.check quantity_testable "bounded partial fill" (quantity "10") - (snd limited); + (let _, permitted, _ = limited in + permitted); + Alcotest.check quantity_testable "maximum order threshold" (quantity "10") + (let _, _, threshold = limited in + threshold); let state, _ = Runner.process_slice state (market_slice ~bars:[ adjusted_bar ] 4L) |> ok in @@ -365,7 +385,7 @@ let reverse_split_restores_order_below_maximum () = (List.exists (fun event -> match event.T.Audit.event with - | T.Audit.Margin_limited _ -> true + | T.Audit.Fill_clipped _ -> true | _ -> false) events) @@ -479,6 +499,69 @@ let risk_allows_reducing_an_out_of_limit_position () = (Result.is_error (risk_check configured_risk ~account ~oms:T.Oms.empty increase)) +let fill_clipping_reason_taxonomy_is_stable () = + let cases = + [ + ( T.Risk.Maximum_order_quantity (quantity "10"), + "max_order_quantity", + "quantity", + `String "10" ); + ( T.Risk.Maximum_long_position (quantity "20"), + "max_long_position", + "quantity", + `String "20" ); + ( T.Risk.Maximum_short_position (quantity "30"), + "max_short_position", + "quantity", + `String "30" ); + ( T.Risk.Maximum_gross_exposure (money "1000"), + "max_gross_exposure", + "money", + `String "1000" ); + ( T.Risk.Maximum_leverage (T.Scalar.Ratio.of_decimal_string "2" |> ok), + "max_leverage", + "ratio", + `String "2" ); + (T.Risk.Initial_margin 5000, "initial_margin", "basis_points", `Int 5000); + ] + in + List.iteri + (fun index (limit, expected_policy, expected_unit, expected_value) -> + let audit = + T.Audit.create ~contract_version:T.Contract.version + ~engine_sequence:(Int64.of_int (index + 1)) + ~causation_ids:[] ~run_id:(run_id "clip-taxonomy") + ~recorded_at:(timestamp "2026-01-02T21:00:02Z") + (T.Audit.Fill_clipped + { + order_id = order_id "clip-order"; + instrument_id = instrument_id "test-equity"; + proposed_quantity = quantity "10"; + permitted_quantity = quantity "5"; + price = price "100"; + limit; + }) + in + let open Yojson.Safe.Util in + let reason = + T.Codec.audit_to_yojson audit |> member "payload" |> member "reason" + in + Alcotest.(check string) + "reason version" "1" + (reason |> member "version" |> to_string); + Alcotest.(check string) + "limiting policy" expected_policy + (reason |> member "policy" |> to_string); + let threshold = reason |> member "threshold" in + Alcotest.(check string) + "threshold unit" expected_unit + (threshold |> member "unit" |> to_string); + Alcotest.(check string) + "threshold value" + (Yojson.Safe.to_string expected_value) + (threshold |> member "value" |> Yojson.Safe.to_string)) + cases + let engine_requires_complete_currency_ledgers () = let instruments = [ instrument (); euro_instrument () ] in let config = engine_config ~risk:(risk ~instruments ()) () in @@ -510,6 +593,8 @@ let tests = short_borrow_accrues_before_matching; Alcotest.test_case "risk allows reduction above position cap" `Quick risk_allows_reducing_an_out_of_limit_position; + Alcotest.test_case "fill clipping reason taxonomy is stable" `Quick + fill_clipping_reason_taxonomy_is_stable; Alcotest.test_case "engine requires complete currency ledgers" `Quick engine_requires_complete_currency_ledgers; ] diff --git a/test/test_reducer.ml b/test/test_reducer.ml index e084c1c..d42c430 100644 --- a/test/test_reducer.ml +++ b/test/test_reducer.ml @@ -2,10 +2,12 @@ open Test_support module T = Trading_engine module Runner = T.Engine.Make (T.Scripted_strategy) -let runner ?(initial_cash = "10000") ?(risk = risk ()) ?execution_model - ?(execution = execution ()) schedule = +let runner ?contract_version ?(initial_cash = "10000") ?(risk = risk ()) + ?execution_model ?(execution = execution ()) schedule = let strategy_state = T.Scripted_strategy.create schedule |> ok in - let config = engine_config ~risk ?execution_model ~execution () in + let config = + engine_config ?contract_version ~risk ?execution_model ~execution () + in Runner.create ~run_id:(run_id "test-run") ~scenario_sha256 ~config ~initial_cash:[ ("USD", money initial_cash) ] ~strategy_state @@ -184,7 +186,7 @@ let superseding_target_replaces_retry () = (event_names events))) | _ -> Alcotest.fail "expected one replacement order" -let margin_limit_clips_buy_to_lots () = +let fill_limit_clips_buy_to_lots () = let constrained = risk ~max_leverage:"1" () in let state = runner ~initial_cash:"550" ~risk:constrained @@ -210,18 +212,80 @@ let margin_limit_clips_buy_to_lots () = let limited = List.find (fun audit -> - String.equal (T.Audit.event_name audit.T.Audit.event) "margin_limited") + String.equal (T.Audit.event_name audit.T.Audit.event) "fill_clipped") events in match limited.event with - | T.Audit.Margin_limited - { requested_quantity; permitted_quantity; price = fill_price; _ } -> - Alcotest.check quantity_testable "ten requested" (quantity "10") - requested_quantity; + | T.Audit.Fill_clipped + { + proposed_quantity; + permitted_quantity; + price = fill_price; + limit = T.Risk.Maximum_leverage threshold; + _; + } -> + Alcotest.check quantity_testable "ten proposed" (quantity "10") + proposed_quantity; Alcotest.check quantity_testable "five permitted" (quantity "5") permitted_quantity; - Alcotest.check price_testable "actual price" (price "100") fill_price - | _ -> Alcotest.fail "expected margin limit audit" + Alcotest.check price_testable "actual price" (price "100") fill_price; + Alcotest.(check string) + "leverage threshold" "1" + (T.Scalar.Ratio.to_decimal_string threshold) + | _ -> Alcotest.fail "expected leverage clipping audit" + +let v3_replays_keep_the_legacy_clipping_record () = + let constrained = risk ~max_leverage:"1" () in + let state = + runner ~contract_version:T.Contract.previous_version ~initial_cash:"550" + ~risk:constrained + ~execution:(execution ~fixed_fee:"10" ()) + [ (1L, [ target "10" ]) ] + in + let decision_bar = + bar ~open_price:"50" ~high_price:"50" ~low_price:"50" ~close_price:"50" 1L + in + let state, _ = + Runner.process_slice state (market_slice ~bars:[ decision_bar ] 1L) |> ok + in + let _, events = + Runner.process_slice state + (market_slice ~bars:[ bar ~open_price:"100" ~close_price:"100" 2L ] 2L) + |> ok + in + let limited = + List.find + (fun audit -> + String.equal (T.Audit.event_name audit.T.Audit.event) "margin_limited") + events + in + Alcotest.(check string) + "legacy journal version" T.Contract.previous_version + limited.contract_version; + match limited.event with + | T.Audit.Margin_limited { requested_quantity; permitted_quantity; _ } -> + Alcotest.check quantity_testable "legacy requested" (quantity "10") + requested_quantity; + Alcotest.check quantity_testable "legacy permitted" (quantity "5") + permitted_quantity + | _ -> Alcotest.fail "expected legacy margin_limited audit" + +let invalid_fill_candidates_fail_instead_of_clipping () = + let constrained = risk ~max_leverage:"1" () in + let state = + runner ~initial_cash:"9223372036854.775807" ~risk:constrained + [ + ( 1L, + [ + T.Strategy.Submit_order + (request ~side:T.Order.Sell ~quantity_value:"1" ()); + ] ); + ] + in + let state, _ = Runner.process_slice state (market_slice 1L) |> ok in + Alcotest.(check string) + "account overflow is not a clipping policy" "int64 addition overflow" + (Runner.process_slice state (market_slice 2L) |> error) let sells_precede_buys_in_the_same_slice () = let a = instrument ~id:"asset-a" ~symbol:"A" () in @@ -673,8 +737,12 @@ let tests = bounded_target_orders_make_progress; Alcotest.test_case "superseding target replaces retry" `Quick superseding_target_replaces_retry; - Alcotest.test_case "margin limit clips buys" `Quick - margin_limit_clips_buy_to_lots; + Alcotest.test_case "fill clipping identifies leverage" `Quick + fill_limit_clips_buy_to_lots; + Alcotest.test_case "v3 keeps legacy clipping records" `Quick + v3_replays_keep_the_legacy_clipping_record; + Alcotest.test_case "invalid fill candidates fail" `Quick + invalid_fill_candidates_fail_instead_of_clipping; Alcotest.test_case "same-slice sells precede buys" `Quick sells_precede_buys_in_the_same_slice; Alcotest.test_case "external ordering validation" `Quick diff --git a/test/test_scenario.ml b/test/test_scenario.ml index 80d0a75..d59771d 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/v3/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v4/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/v3/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v4/fixtures/demo.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -109,9 +109,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v3/scenario.schema.json"; - check_schema "../contracts/v3/scenario-stream.schema.json"; - check_schema "../contracts/v3/journal.schema.json" + check_schema "../contracts/v4/scenario.schema.json"; + check_schema "../contracts/v4/scenario-stream.schema.json"; + check_schema "../contracts/v4/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -173,7 +173,7 @@ 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 \"3\")" + "unsupported scenario contract_version \"2\" (expected one of 4, 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -522,11 +522,51 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v3/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v4/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual +let v3_replay_matches_frozen_golden_file () = + let document = + In_channel.with_open_bin "../contracts/v3/fixtures/demo.scenario.json" + In_channel.input_all + in + let scenario = T.Scenario.of_string document |> ok in + let result = + T.Replay.run ~scenario_sha256:(T.Sha256.digest_string document) scenario + |> ok + in + let actual = + result.audits |> List.map T.Codec.audit_to_string |> String.concat "\n" + |> fun value -> value ^ "\n" + in + let expected = + In_channel.with_open_bin "../contracts/v3/fixtures/demo.journal.jsonl" + In_channel.input_all + in + Alcotest.(check string) "frozen v3 audit contract" expected actual + +let fill_clipping_fixture_reconciles () = + let document = + In_channel.with_open_bin + "../contracts/v4/fixtures/fill-clipped.scenario.json" In_channel.input_all + in + let scenario = T.Scenario.of_string document |> ok in + let result = + T.Replay.run ~scenario_sha256:(T.Sha256.digest_string document) scenario + |> ok + in + let actual = + result.audits |> List.map T.Codec.audit_to_string |> String.concat "\n" + |> fun value -> value ^ "\n" + in + let expected = + In_channel.with_open_bin + "../contracts/v4/fixtures/fill-clipped.journal.jsonl" In_channel.input_all + in + Alcotest.(check string) "fill clipping audit reconciliation" expected actual + let journal_is_created_exclusively () = let scenario = demo () in let existing = Filename.temp_file "trading-engine" ".jsonl" in @@ -779,6 +819,10 @@ let tests = replay_ends_with_completion_summary; Alcotest.test_case "replay matches golden file" `Quick replay_matches_golden_file; + Alcotest.test_case "v3 replay matches frozen golden file" `Quick + v3_replay_matches_frozen_golden_file; + Alcotest.test_case "fill clipping fixture reconciles" `Quick + fill_clipping_fixture_reconciles; Alcotest.test_case "exclusive journal creation" `Quick journal_is_created_exclusively; Alcotest.test_case "exclusive journal finalization" `Quick diff --git a/test/test_support.ml b/test/test_support.ml index 9f50b67..2000368 100644 --- a/test/test_support.ml +++ b/test/test_support.ml @@ -138,13 +138,16 @@ let risk ?(base_currency = "USD") ?(instruments = [ instrument () ]) ~initial_margin_bps ~maintenance_margin_bps ~short_borrow_bps |> ok -let engine_config ?(risk = risk ()) ?execution_model ?(execution = execution ()) - ?(max_internal_events = 1000) () = +let engine_config ?(contract_version = T.Contract.version) ?(risk = risk ()) + ?execution_model ?(execution = execution ()) ?(max_internal_events = 1000) + () = let execution_model = Option.value execution_model ~default:(T.Execution_model.find "completed_bar_v1" |> ok) in - T.Engine.config ~risk ~execution_model ~execution ~max_internal_events |> ok + T.Engine.config ~contract_version ~risk ~execution_model ~execution + ~max_internal_events + |> ok let risk_check risk ~account ~oms request = T.Risk.check risk ~account ~oms diff --git a/test/validate_schemas.py b/test/validate_schemas.py index f62ca3e..3569418 100644 --- a/test/validate_schemas.py +++ b/test/validate_schemas.py @@ -115,7 +115,7 @@ def main() -> None: unsupported_execution_model = copy.deepcopy(scenario) unsupported_execution_model["execution"]["model"] = "future_model" expect_invalid(scenario_validator, unsupported_execution_model) - if contract_version == "3": + if contract_version in {"3", "4"}: excessive_feedback_cap = copy.deepcopy(scenario) excessive_feedback_cap["max_internal_events"] = 4611686018427387904 expect_invalid(scenario_validator, excessive_feedback_cap) @@ -129,7 +129,7 @@ def main() -> None: malformed_stream_slice["payload"]["market_slice"]["unexpected"] = True expect_invalid(stream_validator, malformed_stream_slice) noncanonical = copy.deepcopy(scenario) - if contract_version == "3": + if contract_version in {"3", "4"}: noncanonical["initial_cash"][0]["amount"] = "10000.0" else: noncanonical["initial_cash"] = "10000.0" @@ -153,6 +153,31 @@ def main() -> None: json.loads(line) for line in journal_path.read_text(encoding="utf-8").splitlines() ] + if contract_version == "4": + fill_clipped = copy.deepcopy(first_journal_record) + fill_clipped["event_type"] = "fill_clipped" + fill_clipped["payload"] = { + "reason": { + "version": "1", + "policy": "max_leverage", + "threshold": {"unit": "ratio", "value": "2"}, + }, + "order_id": "fixture-order", + "instrument_id": "fixture-instrument", + "proposed_quantity": "10", + "permitted_quantity": "5", + "price": "100", + } + journal_validator.validate(fill_clipped) + mismatched_threshold = copy.deepcopy(fill_clipped) + mismatched_threshold["payload"]["reason"]["threshold"] = { + "unit": "money", + "value": "2", + } + expect_invalid(journal_validator, mismatched_threshold) + unknown_policy = copy.deepcopy(fill_clipped) + unknown_policy["payload"]["reason"]["policy"] = "future_policy" + expect_invalid(journal_validator, unknown_policy) order_record = next( record for record in journal_records if record["event_type"] == "order_accepted" ) From bc86d33b82e747c5792eb8aaa90512aedc39818f Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 17:45:11 -0400 Subject: [PATCH 06/57] ci: test Persistra develop --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be7945a..6540957 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: with: persist-credentials: false repository: fallblu/persistra - ref: ${{ vars.PERSISTRA_COMPAT_REF || 'main' }} + ref: ${{ vars.PERSISTRA_COMPAT_REF || 'develop' }} path: persistra - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: From 931e539c308c846edd0c6aa8338a6799403c643e Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 17:58:08 -0400 Subject: [PATCH 07/57] test: inject boundary failures --- docs/architecture.md | 5 + lib/boundary_effects.ml | 54 +++++++++ lib/boundary_effects.mli | 32 +++++ lib/journal.ml | 79 +++++++------ lib/journal.mli | 2 +- lib/strategy_process.ml | 37 ++++-- lib/strategy_process.mli | 1 + lib/strategy_transcript.ml | 85 ++++++++------ lib/strategy_transcript.mli | 2 +- test/dune | 1 + test/test_boundary_failures.ml | 208 +++++++++++++++++++++++++++++++++ test/test_engine.ml | 1 + 12 files changed, 429 insertions(+), 78 deletions(-) create mode 100644 lib/boundary_effects.ml create mode 100644 lib/boundary_effects.mli create mode 100644 test/test_boundary_failures.ml diff --git a/docs/architecture.md b/docs/architecture.md index adc36f4..f17e887 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -26,6 +26,11 @@ and reducer internals keep plain errors inside the deterministic boundary; repla stable codes, phases, source locations, event causality, and sanitized exception details before returning an error to callers. +Artifact writers and the process supervisor route their minimal operating-system operations through +one boundary dispatcher. Production executes those effects directly. Failure-path tests replace one +operation at a time, including partial writes, without introducing files, pipes, processes, or fault +state into the reducer. + ## Reducer phases For each synchronized market slice, the engine: diff --git a/lib/boundary_effects.ml b/lib/boundary_effects.ml new file mode 100644 index 0000000..e36ac86 --- /dev/null +++ b/lib/boundary_effects.ml @@ -0,0 +1,54 @@ +type stage = + | Artifact_create + | Artifact_write + | Artifact_flush + | Artifact_close + | Artifact_publish + | Artifact_cleanup + | Process_spawn + | Process_exchange + | Process_terminate + | Process_reap + +type operation = + | Create_artifact of string + | Write_artifact of { channel : out_channel; contents : string } + | Flush_artifact + | Close_artifact + | Publish_artifact of { partial_path : string; final_path : string } + | Cleanup_artifact of string + | Spawn_process + | Exchange_process + | Terminate_process + | Reap_process + +type t = { perform : 'a. operation -> (unit -> 'a) -> 'a } + +let direct = { perform = (fun _ run -> run ()) } + +let perform (type result) effects operation (run : unit -> result) = + effects.perform operation run + +let stage = function + | Create_artifact _ -> Artifact_create + | Write_artifact _ -> Artifact_write + | Flush_artifact -> Artifact_flush + | Close_artifact -> Artifact_close + | Publish_artifact _ -> Artifact_publish + | Cleanup_artifact _ -> Artifact_cleanup + | Spawn_process -> Process_spawn + | Exchange_process -> Process_exchange + | Terminate_process -> Process_terminate + | Reap_process -> Process_reap + +let stage_to_string = function + | Artifact_create -> "artifact create" + | Artifact_write -> "artifact write" + | Artifact_flush -> "artifact flush" + | Artifact_close -> "artifact close" + | Artifact_publish -> "artifact publish" + | Artifact_cleanup -> "artifact cleanup" + | Process_spawn -> "process spawn" + | Process_exchange -> "process exchange" + | Process_terminate -> "process terminate" + | Process_reap -> "process reap" diff --git a/lib/boundary_effects.mli b/lib/boundary_effects.mli new file mode 100644 index 0000000..e0ad565 --- /dev/null +++ b/lib/boundary_effects.mli @@ -0,0 +1,32 @@ +(** Minimal effect dispatcher for deterministic boundary failure injection. *) + +type stage = + | Artifact_create + | Artifact_write + | Artifact_flush + | Artifact_close + | Artifact_publish + | Artifact_cleanup + | Process_spawn + | Process_exchange + | Process_terminate + | Process_reap + +type operation = + | Create_artifact of string + | Write_artifact of { channel : out_channel; contents : string } + | Flush_artifact + | Close_artifact + | Publish_artifact of { partial_path : string; final_path : string } + | Cleanup_artifact of string + | Spawn_process + | Exchange_process + | Terminate_process + | Reap_process + +type t = { perform : 'a. operation -> (unit -> 'a) -> 'a } + +val direct : t +val perform : t -> operation -> (unit -> 'a) -> 'a +val stage : operation -> stage +val stage_to_string : stage -> string diff --git a/lib/journal.ml b/lib/journal.ml index fac444e..a8ff8d9 100644 --- a/lib/journal.ml +++ b/lib/journal.ml @@ -2,6 +2,7 @@ type t = { final_path : string; partial_path : string; channel : out_channel; + effects : Boundary_effects.t; mutable closed : bool; } @@ -24,7 +25,13 @@ let audit_context event = in (event_id, order_id, causation_ids) -let create final_path = +let exception_message = function + | Sys_error message -> message + | Unix.Unix_error (code, operation, target) -> + Printf.sprintf "%s(%s): %s" operation target (Unix.error_message code) + | exception_ -> Printexc.to_string exception_ + +let create ?(effects = Boundary_effects.direct) final_path = let partial_path = final_path ^ ".partial" in if Sys.file_exists final_path then Error @@ -37,16 +44,18 @@ let create final_path = else try let channel = - open_out_gen - [ Open_wronly; Open_creat; Open_excl; Open_binary ] - 0o600 partial_path + Boundary_effects.perform effects + (Boundary_effects.Create_artifact partial_path) (fun () -> + open_out_gen + [ Open_wronly; Open_creat; Open_excl; Open_binary ] + 0o600 partial_path) in - Ok { final_path; partial_path; channel; closed = false } - with Sys_error message as exception_ -> + Ok { final_path; partial_path; channel; effects; closed = false } + with exception_ -> Error (Diagnostic.of_exception ~code:Diagnostic.Artifact_io ~phase:Diagnostic.Artifact - ~message:("could not create journal: " ^ message) + ~message:("could not create journal: " ^ exception_message exception_) exception_) let append journal event = @@ -57,17 +66,20 @@ let append journal event = ~code:Diagnostic.Artifact_state "cannot append to a closed journal") else try - output_string journal.channel (Codec.audit_to_string event); - output_char journal.channel '\n'; - flush journal.channel; + let contents = Codec.audit_to_string event ^ "\n" in + Boundary_effects.perform journal.effects + (Boundary_effects.Write_artifact { channel = journal.channel; contents }) + (fun () -> output_string journal.channel contents); + Boundary_effects.perform journal.effects Boundary_effects.Flush_artifact + (fun () -> flush journal.channel); Ok () - with Sys_error message as exception_ -> + with exception_ -> Error (Diagnostic.of_exception ~code:Diagnostic.Artifact_io ~phase:Diagnostic.Artifact ~message: ("could not append journal " ^ journal.partial_path ^ ": " - ^ message) + ^ exception_message exception_) exception_ |> Diagnostic.annotate ~event_id ?order_id ~causation_ids) @@ -83,26 +95,27 @@ let commit journal = "cannot commit a closed journal") else try - flush journal.channel; - close_out journal.channel; + Boundary_effects.perform journal.effects Boundary_effects.Flush_artifact + (fun () -> flush journal.channel); + Boundary_effects.perform journal.effects Boundary_effects.Close_artifact + (fun () -> close_out journal.channel); journal.closed <- true; - Unix.link journal.partial_path journal.final_path; - Unix.unlink journal.partial_path; + Boundary_effects.perform journal.effects + (Boundary_effects.Publish_artifact + { + partial_path = journal.partial_path; + final_path = journal.final_path; + }) + (fun () -> Unix.link journal.partial_path journal.final_path); + Boundary_effects.perform journal.effects + (Boundary_effects.Cleanup_artifact journal.partial_path) (fun () -> + Unix.unlink journal.partial_path); Ok () - with - | Sys_error message as exception_ -> - close_preserving_partial journal; - Error - (Diagnostic.of_exception ~code:Diagnostic.Artifact_io - ~phase:Diagnostic.Artifact - ~message:("could not finalize journal: " ^ message) - exception_) - | Unix.Unix_error (code, operation, target) as exception_ -> - close_preserving_partial journal; - Error - (Diagnostic.of_exception ~code:Diagnostic.Artifact_io - ~phase:Diagnostic.Artifact - ~message: - (Printf.sprintf "could not finalize journal: %s(%s): %s" - operation target (Unix.error_message code)) - exception_) + with exception_ -> + close_preserving_partial journal; + Error + (Diagnostic.of_exception ~code:Diagnostic.Artifact_io + ~phase:Diagnostic.Artifact + ~message: + ("could not finalize journal: " ^ exception_message exception_) + exception_) diff --git a/lib/journal.mli b/lib/journal.mli index 0a5619b..60bbed9 100644 --- a/lib/journal.mli +++ b/lib/journal.mli @@ -2,7 +2,7 @@ type t -val create : string -> (t, Diagnostic.t) result +val create : ?effects:Boundary_effects.t -> string -> (t, Diagnostic.t) result val append : t -> Audit.t -> (unit, Diagnostic.t) result val close_preserving_partial : t -> unit val commit : t -> (unit, Diagnostic.t) result diff --git a/lib/strategy_process.ml b/lib/strategy_process.ml index ff0b848..c49a818 100644 --- a/lib/strategy_process.ml +++ b/lib/strategy_process.ml @@ -5,6 +5,7 @@ type t = { child : child; clock : float Eio.Time.clock_ty Eio.Resource.t; transcript : Strategy_transcript.t; + effects : Boundary_effects.t; timeout : float; mutable next_sequence : int64; } @@ -13,6 +14,7 @@ and child = { process : Eio_unix.Process.ty Eio.Resource.t; pgid : int; clock : float Eio.Time.clock_ty Eio.Resource.t; + effects : Boundary_effects.t; mutable status : Eio.Process.exit_status option; } @@ -62,7 +64,10 @@ let await_child (child : child) = match child.status with | Some status -> status | None -> - let status = Eio.Process.await child.process in + let status = + Boundary_effects.perform child.effects Boundary_effects.Reap_process + (fun () -> Eio.Process.await child.process) + in child.status <- Some status; status @@ -125,7 +130,7 @@ let rec wait_for_process_group (child : child) deadline = Eio.Time.sleep child.clock (Float.min process_poll_interval remaining); wait_for_process_group child deadline) -let terminate_process_group (child : child) = +let terminate_process_group_direct (child : child) = Eio.Cancel.protect (fun () -> let graceful_deadline = Eio.Time.now child.clock +. graceful_termination_timeout @@ -166,6 +171,13 @@ let terminate_process_group (child : child) = "external strategy descendants remained after forced \ termination")) +let terminate_process_group (child : child) = + try + Boundary_effects.perform child.effects Boundary_effects.Terminate_process + (fun () -> terminate_process_group_direct child) + with exception_ -> + Error (exception_diagnostic "terminating external strategy" exception_) + let append_cleanup_error result child = match terminate_process_group child with | Ok () -> result @@ -184,8 +196,11 @@ let exchange session ~stage ~expected_sequence request = try match Eio.Time.with_timeout session.clock session.timeout (fun () -> - Eio.Flow.copy_string request_line session.input; - Ok (Eio.Buf_read.line session.output)) + Ok + (Boundary_effects.perform session.effects + Boundary_effects.Exchange_process (fun () -> + Eio.Flow.copy_string request_line session.input; + Eio.Buf_read.line session.output))) with | Ok response -> Ok response | Error `Timeout -> @@ -314,8 +329,8 @@ let await_exit session = (diagnostic ~code:Diagnostic.Strategy_exit (Printf.sprintf "external strategy was killed by signal %d" signal)) -let with_session ~env ~command ~timeout ~transcript_path - ~(initialization : Strategy_protocol.initialization) use = +let with_session ?(effects = Boundary_effects.direct) ~env ~command ~timeout + ~transcript_path ~(initialization : Strategy_protocol.initialization) use = if not (valid_timeout timeout) then Error (diagnostic ~code:Diagnostic.Strategy_invalid_configuration @@ -331,7 +346,7 @@ let with_session ~env ~command ~timeout ~transcript_path (diagnostic ~code:Diagnostic.Strategy_invalid_configuration "external strategy executable must not be empty") | executable :: _ -> ( - match Strategy_transcript.create transcript_path with + match Strategy_transcript.create ~effects transcript_path with | Error _ as error -> error | Ok transcript -> ( let fail result = @@ -354,8 +369,10 @@ let with_session ~env ~command ~timeout ~transcript_path ] in let process = - Eio_unix.Process.spawn_unix ~sw:switch process_manager ~pgid:0 - ~fds ~executable command + Boundary_effects.perform effects + Boundary_effects.Spawn_process (fun () -> + Eio_unix.Process.spawn_unix ~sw:switch process_manager + ~pgid:0 ~fds ~executable command) in Eio.Flow.close strategy_stdin; Eio.Flow.close strategy_stdout; @@ -364,6 +381,7 @@ let with_session ~env ~command ~timeout ~transcript_path process; pgid = Eio.Process.pid process; clock = Eio.Stdenv.clock env; + effects; status = None; } in @@ -379,6 +397,7 @@ let with_session ~env ~command ~timeout ~transcript_path child; clock = Eio.Stdenv.clock env; transcript; + effects; timeout; next_sequence = 1L; } diff --git a/lib/strategy_process.mli b/lib/strategy_process.mli index 893c72b..9c17150 100644 --- a/lib/strategy_process.mli +++ b/lib/strategy_process.mli @@ -3,6 +3,7 @@ type t val with_session : + ?effects:Boundary_effects.t -> env:Eio_unix.Stdenv.base -> command:string list -> timeout:float -> diff --git a/lib/strategy_transcript.ml b/lib/strategy_transcript.ml index e510fad..09670e9 100644 --- a/lib/strategy_transcript.ml +++ b/lib/strategy_transcript.ml @@ -2,6 +2,7 @@ type t = { final_path : string; partial_path : string; channel : out_channel; + effects : Boundary_effects.t; mutable next_sequence : int64; mutable closed : bool; } @@ -9,7 +10,13 @@ type t = { let diagnostic ?sequence ~code message = Diagnostic.make ?sequence ~code ~phase:Diagnostic.Artifact message -let create final_path = +let exception_message = function + | Sys_error message -> message + | Unix.Unix_error (code, operation, target) -> + Printf.sprintf "%s(%s): %s" operation target (Unix.error_message code) + | exception_ -> Printexc.to_string exception_ + +let create ?(effects = Boundary_effects.direct) final_path = let partial_path = final_path ^ ".partial" in if Sys.file_exists final_path then Error @@ -22,23 +29,28 @@ let create final_path = else try let channel = - open_out_gen - [ Open_wronly; Open_creat; Open_excl; Open_binary ] - 0o600 partial_path + Boundary_effects.perform effects + (Boundary_effects.Create_artifact partial_path) (fun () -> + open_out_gen + [ Open_wronly; Open_creat; Open_excl; Open_binary ] + 0o600 partial_path) in Ok { final_path; partial_path; channel; + effects; next_sequence = 1L; closed = false; } - with Sys_error message as exception_ -> + with exception_ -> Error (Diagnostic.of_exception ~code:Diagnostic.Artifact_io ~phase:Diagnostic.Artifact - ~message:("could not create strategy transcript: " ^ message) + ~message: + ("could not create strategy transcript: " + ^ exception_message exception_) exception_) let append transcript ~direction message = @@ -58,19 +70,23 @@ let append transcript ~direction message = Strategy_protocol.transcript_record ~transcript_sequence:transcript.next_sequence ~direction ~message in - output_string transcript.channel - (Strategy_protocol.message_to_string record); - output_char transcript.channel '\n'; - flush transcript.channel; + let contents = Strategy_protocol.message_to_string record ^ "\n" in + Boundary_effects.perform transcript.effects + (Boundary_effects.Write_artifact + { channel = transcript.channel; contents }) + (fun () -> output_string transcript.channel contents); + Boundary_effects.perform transcript.effects + Boundary_effects.Flush_artifact (fun () -> flush transcript.channel); transcript.next_sequence <- Int64.succ transcript.next_sequence; Ok () - with Sys_error message as exception_ -> + with exception_ -> Error (Diagnostic.of_exception ~sequence:transcript.next_sequence ~code:Diagnostic.Artifact_io ~phase:Diagnostic.Artifact ~message: ("could not append strategy transcript " ^ transcript.partial_path - ^ ": " ^ message) + ^ ": " + ^ exception_message exception_) exception_) let close_preserving_partial transcript = @@ -86,27 +102,28 @@ let commit transcript = "cannot commit a closed strategy transcript") else try - flush transcript.channel; - close_out transcript.channel; + Boundary_effects.perform transcript.effects + Boundary_effects.Flush_artifact (fun () -> flush transcript.channel); + Boundary_effects.perform transcript.effects + Boundary_effects.Close_artifact (fun () -> close_out transcript.channel); transcript.closed <- true; - Unix.link transcript.partial_path transcript.final_path; - Unix.unlink transcript.partial_path; + Boundary_effects.perform transcript.effects + (Boundary_effects.Publish_artifact + { + partial_path = transcript.partial_path; + final_path = transcript.final_path; + }) + (fun () -> Unix.link transcript.partial_path transcript.final_path); + Boundary_effects.perform transcript.effects + (Boundary_effects.Cleanup_artifact transcript.partial_path) (fun () -> + Unix.unlink transcript.partial_path); Ok () - with - | Sys_error message as exception_ -> - close_preserving_partial transcript; - Error - (Diagnostic.of_exception ~code:Diagnostic.Artifact_io - ~phase:Diagnostic.Artifact - ~message:("could not finalize strategy transcript: " ^ message) - exception_) - | Unix.Unix_error (code, operation, target) as exception_ -> - close_preserving_partial transcript; - Error - (Diagnostic.of_exception ~code:Diagnostic.Artifact_io - ~phase:Diagnostic.Artifact - ~message: - (Printf.sprintf - "could not finalize strategy transcript: %s(%s): %s" operation - target (Unix.error_message code)) - exception_) + with exception_ -> + close_preserving_partial transcript; + Error + (Diagnostic.of_exception ~code:Diagnostic.Artifact_io + ~phase:Diagnostic.Artifact + ~message: + ("could not finalize strategy transcript: " + ^ exception_message exception_) + exception_) diff --git a/lib/strategy_transcript.mli b/lib/strategy_transcript.mli index 5f7f4e3..3739ae5 100644 --- a/lib/strategy_transcript.mli +++ b/lib/strategy_transcript.mli @@ -2,7 +2,7 @@ type t -val create : string -> (t, Diagnostic.t) result +val create : ?effects:Boundary_effects.t -> string -> (t, Diagnostic.t) result val append : t -> diff --git a/test/dune b/test/dune index 3261a43..e1c6207 100644 --- a/test/dune +++ b/test/dune @@ -9,6 +9,7 @@ test_reducer test_checkpoint4 test_strategy_protocol + test_boundary_failures test_scenario test_engine) (deps diff --git a/test/test_boundary_failures.ml b/test/test_boundary_failures.ml new file mode 100644 index 0000000..4973174 --- /dev/null +++ b/test/test_boundary_failures.ml @@ -0,0 +1,208 @@ +open Test_support +module T = Trading_engine + +exception Injected_failure of string + +let remove_if_exists path = if Sys.file_exists path then Sys.remove path + +let with_absent_path suffix test = + let path = Filename.temp_file "trading-engine-boundary" suffix in + Sys.remove path; + Fun.protect + ~finally:(fun () -> + remove_if_exists path; + remove_if_exists (path ^ ".partial")) + (fun () -> test path) + +let injected_effects target = + let triggered = ref false in + let perform : type result. + T.Boundary_effects.operation -> (unit -> result) -> result = + fun operation run -> + if (not !triggered) && T.Boundary_effects.stage operation = target then ( + triggered := true; + match operation with + | T.Boundary_effects.Write_artifact { channel; contents } -> + output_substring channel contents 0 + (Int.min 8 (String.length contents)); + raise (Injected_failure (T.Boundary_effects.stage_to_string target)) + | T.Boundary_effects.Publish_artifact { final_path; _ } -> + Out_channel.with_open_bin final_path (fun channel -> + output_string channel "rival\n"); + run () + | _ -> + raise (Injected_failure (T.Boundary_effects.stage_to_string target))) + else run () + in + ({ T.Boundary_effects.perform }, triggered) + +let audit = + T.Audit.create ~contract_version:T.Contract.version ~engine_sequence:1L + ~causation_ids:[] + ~run_id:(run_id "boundary-failure") + ~recorded_at:(timestamp "2026-01-02T21:00:02Z") + (T.Audit.Run_started + { scenario_sha256; execution_model = "completed_bar_v1" }) + +module type Artifact_writer = sig + type t + + val create : + effects:T.Boundary_effects.t -> string -> (t, T.Diagnostic.t) result + + val append : t -> (unit, T.Diagnostic.t) result + val commit : t -> (unit, T.Diagnostic.t) result + val close_preserving_partial : t -> unit +end + +module Journal_writer = struct + type t = T.Journal.t + + let create ~effects path = T.Journal.create ~effects path + let append journal = T.Journal.append journal audit + let commit = T.Journal.commit + let close_preserving_partial = T.Journal.close_preserving_partial +end + +module Transcript_writer = struct + type t = T.Strategy_transcript.t + + let create ~effects path = T.Strategy_transcript.create ~effects path + + let append transcript = + T.Strategy_transcript.append transcript + ~direction:T.Strategy_protocol.Engine_to_strategy + (T.Strategy_protocol.shutdown_message ~sequence:1L) + + let commit = T.Strategy_transcript.commit + let close_preserving_partial = T.Strategy_transcript.close_preserving_partial +end + +let artifact_stages = + T.Boundary_effects. + [ + Artifact_create; + Artifact_write; + Artifact_flush; + Artifact_close; + Artifact_publish; + Artifact_cleanup; + ] + +let exercise_artifact_failure (type writer) writer_name + (module Writer : Artifact_writer with type t = writer) stage = + with_absent_path ".jsonl" @@ fun final_path -> + let partial_path = final_path ^ ".partial" in + let effects, triggered = injected_effects stage in + let result = + match Writer.create ~effects final_path with + | Error _ as error -> error + | Ok writer -> + let result = + match stage with + | T.Boundary_effects.Artifact_write | Artifact_flush -> + Writer.append writer + | Artifact_close | Artifact_publish | Artifact_cleanup -> ( + match Writer.append writer with + | Error _ as error -> error + | Ok () -> Writer.commit writer) + | Artifact_create -> Alcotest.fail "create failure was not injected" + | Process_spawn | Process_exchange | Process_terminate | Process_reap + -> + Alcotest.fail "expected an artifact stage" + in + Writer.close_preserving_partial writer; + result + in + let diagnostic = error result in + Alcotest.(check bool) "fault triggered" true !triggered; + Alcotest.(check string) + "artifact diagnostic" "artifact.io" + (T.Diagnostic.code_to_string diagnostic.code); + let expected_final = + stage = T.Boundary_effects.Artifact_publish + || stage = T.Boundary_effects.Artifact_cleanup + in + let expected_partial = stage <> T.Boundary_effects.Artifact_create in + Alcotest.(check bool) + (writer_name ^ " final invariant") + expected_final + (Sys.file_exists final_path); + Alcotest.(check bool) + (writer_name ^ " partial invariant") + expected_partial + (Sys.file_exists partial_path); + if stage = T.Boundary_effects.Artifact_write then + Alcotest.(check int64) + "short write retained" 8L + (In_channel.with_open_bin partial_path In_channel.length); + if stage = T.Boundary_effects.Artifact_publish then + Alcotest.(check string) + "publication rival preserved" "rival\n" + (In_channel.with_open_bin final_path In_channel.input_all); + if stage = T.Boundary_effects.Artifact_cleanup then + Alcotest.(check string) + "published and partial bytes agree" + (In_channel.with_open_bin partial_path In_channel.input_all) + (In_channel.with_open_bin final_path In_channel.input_all) + +let artifact_cases writer_name writer = + List.map + (fun stage -> + Alcotest.test_case + (writer_name ^ " " ^ T.Boundary_effects.stage_to_string stage) + `Quick + (fun () -> exercise_artifact_failure writer_name writer stage)) + artifact_stages + +let initialization () = + let instrument = instrument () in + T.Strategy_protocol. + { + scenario_contract_version = T.Contract.version; + scenario_sha256; + metadata = `Assoc [ ("experiment", `String "boundary-failure") ]; + run_id = run_id "boundary-failure"; + base_currency = "USD"; + initial_cash = [ ("USD", money "10000") ]; + instruments = [ instrument ]; + risk = risk ~instruments:[ instrument ] (); + execution_model = T.Execution_model.find "completed_bar_v1" |> ok; + execution = execution (); + } + +let process_stages = + T.Boundary_effects. + [ Process_spawn; Process_exchange; Process_terminate; Process_reap ] + +let exercise_process_failure stage = + with_absent_path ".strategy.jsonl" @@ fun final_path -> + let effects, triggered = injected_effects stage in + let result = + Eio_main.run @@ fun env -> + T.Strategy_process.with_session ~effects ~env + ~command:[ "./fake_strategy.py"; "success" ] + ~timeout:1.0 ~transcript_path:final_path + ~initialization:(initialization ()) (fun _ -> Ok ()) + in + let diagnostic = error result in + Alcotest.(check bool) "fault triggered" true !triggered; + Alcotest.(check string) + "process diagnostic" "strategy.process" + (T.Diagnostic.code_to_string diagnostic.code); + Alcotest.(check bool) "final absent" false (Sys.file_exists final_path); + Alcotest.(check bool) + "partial retained" true + (Sys.file_exists (final_path ^ ".partial")) + +let process_cases = + List.map + (fun stage -> + Alcotest.test_case (T.Boundary_effects.stage_to_string stage) `Quick + (fun () -> exercise_process_failure stage)) + process_stages + +let tests = + artifact_cases "journal" (module Journal_writer) + @ artifact_cases "transcript" (module Transcript_writer) + @ process_cases diff --git a/test/test_engine.ml b/test/test_engine.ml index 769f324..685e469 100644 --- a/test/test_engine.ml +++ b/test/test_engine.ml @@ -8,5 +8,6 @@ let () = ("reducer", Test_reducer.tests); ("checkpoint4", Test_checkpoint4.tests); ("strategy-protocol", Test_strategy_protocol.tests); + ("boundary-failures", Test_boundary_failures.tests); ("scenario", Test_scenario.tests); ] From 972afcfb9cf0ae069f6fd22076ad03a3b32013d0 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 18:08:59 -0400 Subject: [PATCH 08/57] refactor: unify artifact writers --- docs/architecture.md | 11 +- lib/artifact_writer.ml | 213 ++++++++++++++++++++++++++++++++++++ lib/artifact_writer.mli | 15 +++ lib/journal.ml | 108 ++---------------- lib/journal.mli | 1 + lib/strategy_transcript.ml | 132 +++------------------- lib/strategy_transcript.mli | 1 + 7 files changed, 262 insertions(+), 219 deletions(-) create mode 100644 lib/artifact_writer.ml create mode 100644 lib/artifact_writer.mli diff --git a/docs/architecture.md b/docs/architecture.md index f17e887..f22cb01 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,17 +19,18 @@ journal files, and the runtime shell. | `Engine` | Sequencing, portfolio reconciliation, and pure suspend/resume orchestration | | `Scenario`, `Scenario_stream`, `Replay` | Strict batch and bounded-memory scripted runners | | `Strategy_protocol`, `Strategy_process`, `External_replay` | Versioned child supervision and external runners | -| `Sha256`, `Codec`, `Diagnostic`, `Journal`, `Strategy_transcript` | Input identity, stable diagnostics and audit JSON, and file publication | +| `Sha256`, `Codec`, `Diagnostic`, `Artifact_writer`, `Journal`, `Strategy_transcript` | Input identity, stable diagnostics and audit JSON, and file publication | Boundary failures use the versioned [diagnostic contract](diagnostics.md). Pure domain constructors and reducer internals keep plain errors inside the deterministic boundary; replay adapters attach stable codes, phases, source locations, event causality, and sanitized exception details before returning an error to callers. -Artifact writers and the process supervisor route their minimal operating-system operations through -one boundary dispatcher. Production executes those effects directly. Failure-path tests replace one -operation at a time, including partial writes, without introducing files, pipes, processes, or fault -state into the reducer. +The journal and strategy transcript share one typed-state artifact lifecycle for exclusive staging, +append, close, no-replace publication, and cleanup. Artifact writers and the process supervisor route +their minimal operating-system operations through one boundary dispatcher. Production executes +those effects directly. Failure-path tests replace one operation at a time, including partial +writes, without introducing files, pipes, processes, or fault state into the reducer. ## Reducer phases diff --git a/lib/artifact_writer.ml b/lib/artifact_writer.ml new file mode 100644 index 0000000..2dd9937 --- /dev/null +++ b/lib/artifact_writer.ml @@ -0,0 +1,213 @@ +type open_state +type closed_state +type published_state +type complete_state + +type 'state file = { + label : string; + final_path : string; + partial_path : string; + channel : out_channel; + effects : Boundary_effects.t; +} + +type state = + | Open of open_state file + | Closed of closed_state file + | Published of published_state file + | Complete of complete_state file + +type t = { mutable state : state } + +let state_label = function + | Open file -> file.label + | Closed file -> file.label + | Published file -> file.label + | Complete file -> file.label + +let diagnostic ~code message = + Diagnostic.make ~code ~phase:Diagnostic.Artifact message + +let exception_message = function + | Sys_error message -> message + | Unix.Unix_error (code, operation, target) -> + Printf.sprintf "%s(%s): %s" operation target (Unix.error_message code) + | exception_ -> Printexc.to_string exception_ + +let exception_diagnostic ~label action exception_ = + Diagnostic.of_exception ~code:Diagnostic.Artifact_io + ~phase:Diagnostic.Artifact + ~message: + (Printf.sprintf "could not %s %s: %s" action label + (exception_message exception_)) + exception_ + +let transition file = + { + label = file.label; + final_path = file.final_path; + partial_path = file.partial_path; + channel = file.channel; + effects = file.effects; + } + +let create ?(effects = Boundary_effects.direct) ~label final_path = + let partial_path = final_path ^ ".partial" in + if Sys.file_exists final_path then + Error + (diagnostic ~code:Diagnostic.Artifact_exists + (Printf.sprintf "%s already exists: %s" label final_path)) + else if Sys.file_exists partial_path then + Error + (diagnostic ~code:Diagnostic.Artifact_exists + (Printf.sprintf "partial %s already exists: %s" label partial_path)) + else + try + let channel = + Boundary_effects.perform effects + (Boundary_effects.Create_artifact partial_path) (fun () -> + open_out_gen + [ Open_wronly; Open_creat; Open_excl; Open_binary ] + 0o600 partial_path) + in + Ok { state = Open { label; final_path; partial_path; channel; effects } } + with exception_ -> Error (exception_diagnostic ~label "create" exception_) + +let append artifact contents = + match artifact.state with + | Closed _ | Published _ | Complete _ -> + Error + (diagnostic ~code:Diagnostic.Artifact_state + ("cannot append to a closed " ^ state_label artifact.state)) + | Open file -> ( + try + Boundary_effects.perform file.effects + (Boundary_effects.Write_artifact { channel = file.channel; contents }) + (fun () -> output_string file.channel contents); + Boundary_effects.perform file.effects Boundary_effects.Flush_artifact + (fun () -> flush file.channel); + Ok () + with exception_ -> + Error (exception_diagnostic ~label:file.label "append" exception_)) + +let close_file artifact file = + let closed = transition file in + try + Boundary_effects.perform file.effects Boundary_effects.Flush_artifact + (fun () -> flush file.channel); + Boundary_effects.perform file.effects Boundary_effects.Close_artifact + (fun () -> close_out file.channel); + artifact.state <- Closed closed; + Ok () + with exception_ -> + close_out_noerr file.channel; + artifact.state <- Closed closed; + Error (exception_diagnostic ~label:file.label "close" exception_) + +let close_preserving_partial artifact = + match artifact.state with + | Open file -> + close_out_noerr file.channel; + artifact.state <- Closed (transition file) + | Closed _ | Published _ | Complete _ -> () + +let publish artifact = + match artifact.state with + | Closed file -> ( + try + Boundary_effects.perform file.effects + (Boundary_effects.Publish_artifact + { partial_path = file.partial_path; final_path = file.final_path }) + (fun () -> Unix.link file.partial_path file.final_path); + artifact.state <- Published (transition file); + Ok () + with exception_ -> + Error (exception_diagnostic ~label:file.label "publish" exception_)) + | Open _ | Published _ | Complete _ -> + Error + (diagnostic ~code:Diagnostic.Artifact_state + ("cannot publish " ^ state_label artifact.state + ^ " from its current state")) + +let cleanup_partial artifact = + match artifact.state with + | Published file -> ( + try + Boundary_effects.perform file.effects + (Boundary_effects.Cleanup_artifact file.partial_path) (fun () -> + Unix.unlink file.partial_path); + artifact.state <- Complete (transition file); + Ok () + with exception_ -> + Error (exception_diagnostic ~label:file.label "clean up" exception_)) + | Open _ | Closed _ | Complete _ -> + Error + (diagnostic ~code:Diagnostic.Artifact_state + ("cannot clean up " ^ state_label artifact.state + ^ " from its current state")) + +let rollback artifact = + match artifact.state with + | Published file -> ( + try + Boundary_effects.perform file.effects + (Boundary_effects.Cleanup_artifact file.final_path) (fun () -> + Unix.unlink file.final_path); + artifact.state <- Closed (transition file); + Ok () + with exception_ -> + Error (exception_diagnostic ~label:file.label "roll back" exception_)) + | Open _ | Closed _ | Complete _ -> Ok () + +let combine original = function + | Ok () -> original + | Error cleanup -> Diagnostic.combine original cleanup + +let rollback_all artifacts original = + List.fold_left + (fun diagnostic artifact -> rollback artifact |> combine diagnostic) + original artifacts + +let rec close_all = function + | [] -> Ok () + | artifact :: rest -> ( + match artifact.state with + | Open file -> ( + match close_file artifact file with + | Ok () -> close_all rest + | Error diagnostic -> + List.iter close_preserving_partial rest; + Error diagnostic) + | Closed _ | Published _ | Complete _ -> + List.iter close_preserving_partial rest; + Error + (diagnostic ~code:Diagnostic.Artifact_state + (state_label artifact.state ^ " is not open for commit"))) + +let rec publish_all published = function + | [] -> Ok () + | artifact :: rest -> ( + match publish artifact with + | Ok () -> publish_all (artifact :: published) rest + | Error diagnostic -> Error (rollback_all published diagnostic)) + +let rec cleanup_all = function + | [] -> Ok () + | artifact :: rest -> ( + match cleanup_partial artifact with + | Ok () -> cleanup_all rest + | Error _ as error -> error) + +let commit artifacts = + match artifacts with + | [] -> + Error + (diagnostic ~code:Diagnostic.Artifact_state + "artifact commit requires at least one writer") + | _ -> ( + match close_all artifacts with + | Error _ as error -> error + | Ok () -> ( + match publish_all [] artifacts with + | Error _ as error -> error + | Ok () -> cleanup_all artifacts)) diff --git a/lib/artifact_writer.mli b/lib/artifact_writer.mli new file mode 100644 index 0000000..3207cf8 --- /dev/null +++ b/lib/artifact_writer.mli @@ -0,0 +1,15 @@ +(** Exclusive staged-file writer with a typed internal lifecycle. *) + +type t + +val create : + ?effects:Boundary_effects.t -> + label:string -> + string -> + (t, Diagnostic.t) result + +val append : t -> string -> (unit, Diagnostic.t) result +val close_preserving_partial : t -> unit + +val commit : t list -> (unit, Diagnostic.t) result +(** Close, publish, and clean up every staged writer as one operation. *) diff --git a/lib/journal.ml b/lib/journal.ml index a8ff8d9..714cf18 100644 --- a/lib/journal.ml +++ b/lib/journal.ml @@ -1,14 +1,4 @@ -type t = { - final_path : string; - partial_path : string; - channel : out_channel; - effects : Boundary_effects.t; - mutable closed : bool; -} - -let diagnostic ?event_id ?order_id ?causation_ids ~code message = - Diagnostic.make ?event_id ?order_id ?causation_ids ~code - ~phase:Diagnostic.Artifact message +type t = { artifact : Artifact_writer.t } let audit_context event = let event_id = Id.Event.to_string event.Audit.event_id in @@ -25,97 +15,17 @@ let audit_context event = in (event_id, order_id, causation_ids) -let exception_message = function - | Sys_error message -> message - | Unix.Unix_error (code, operation, target) -> - Printf.sprintf "%s(%s): %s" operation target (Unix.error_message code) - | exception_ -> Printexc.to_string exception_ - -let create ?(effects = Boundary_effects.direct) final_path = - let partial_path = final_path ^ ".partial" in - if Sys.file_exists final_path then - Error - (diagnostic ~code:Diagnostic.Artifact_exists - ("journal already exists: " ^ final_path)) - else if Sys.file_exists partial_path then - Error - (diagnostic ~code:Diagnostic.Artifact_exists - ("partial journal already exists: " ^ partial_path)) - else - try - let channel = - Boundary_effects.perform effects - (Boundary_effects.Create_artifact partial_path) (fun () -> - open_out_gen - [ Open_wronly; Open_creat; Open_excl; Open_binary ] - 0o600 partial_path) - in - Ok { final_path; partial_path; channel; effects; closed = false } - with exception_ -> - Error - (Diagnostic.of_exception ~code:Diagnostic.Artifact_io - ~phase:Diagnostic.Artifact - ~message:("could not create journal: " ^ exception_message exception_) - exception_) +let create ?effects final_path = + Artifact_writer.create ?effects ~label:"journal" final_path + |> Result.map (fun artifact -> { artifact }) let append journal event = let event_id, order_id, causation_ids = audit_context event in - if journal.closed then - Error - (diagnostic ~event_id ?order_id ~causation_ids - ~code:Diagnostic.Artifact_state "cannot append to a closed journal") - else - try - let contents = Codec.audit_to_string event ^ "\n" in - Boundary_effects.perform journal.effects - (Boundary_effects.Write_artifact { channel = journal.channel; contents }) - (fun () -> output_string journal.channel contents); - Boundary_effects.perform journal.effects Boundary_effects.Flush_artifact - (fun () -> flush journal.channel); - Ok () - with exception_ -> - Error - (Diagnostic.of_exception ~code:Diagnostic.Artifact_io - ~phase:Diagnostic.Artifact - ~message: - ("could not append journal " ^ journal.partial_path ^ ": " - ^ exception_message exception_) - exception_ - |> Diagnostic.annotate ~event_id ?order_id ~causation_ids) + Artifact_writer.append journal.artifact (Codec.audit_to_string event ^ "\n") + |> Result.map_error (Diagnostic.annotate ~event_id ?order_id ~causation_ids) let close_preserving_partial journal = - if not journal.closed then ( - journal.closed <- true; - close_out_noerr journal.channel) + Artifact_writer.close_preserving_partial journal.artifact -let commit journal = - if journal.closed then - Error - (diagnostic ~code:Diagnostic.Artifact_state - "cannot commit a closed journal") - else - try - Boundary_effects.perform journal.effects Boundary_effects.Flush_artifact - (fun () -> flush journal.channel); - Boundary_effects.perform journal.effects Boundary_effects.Close_artifact - (fun () -> close_out journal.channel); - journal.closed <- true; - Boundary_effects.perform journal.effects - (Boundary_effects.Publish_artifact - { - partial_path = journal.partial_path; - final_path = journal.final_path; - }) - (fun () -> Unix.link journal.partial_path journal.final_path); - Boundary_effects.perform journal.effects - (Boundary_effects.Cleanup_artifact journal.partial_path) (fun () -> - Unix.unlink journal.partial_path); - Ok () - with exception_ -> - close_preserving_partial journal; - Error - (Diagnostic.of_exception ~code:Diagnostic.Artifact_io - ~phase:Diagnostic.Artifact - ~message: - ("could not finalize journal: " ^ exception_message exception_) - exception_) +let commit journal = Artifact_writer.commit [ journal.artifact ] +let artifact journal = journal.artifact diff --git a/lib/journal.mli b/lib/journal.mli index 60bbed9..da22f03 100644 --- a/lib/journal.mli +++ b/lib/journal.mli @@ -6,3 +6,4 @@ val create : ?effects:Boundary_effects.t -> string -> (t, Diagnostic.t) result val append : t -> Audit.t -> (unit, Diagnostic.t) result val close_preserving_partial : t -> unit val commit : t -> (unit, Diagnostic.t) result +val artifact : t -> Artifact_writer.t diff --git a/lib/strategy_transcript.ml b/lib/strategy_transcript.ml index 09670e9..b46cbf4 100644 --- a/lib/strategy_transcript.ml +++ b/lib/strategy_transcript.ml @@ -1,129 +1,31 @@ -type t = { - final_path : string; - partial_path : string; - channel : out_channel; - effects : Boundary_effects.t; - mutable next_sequence : int64; - mutable closed : bool; -} +type t = { artifact : Artifact_writer.t; mutable next_sequence : int64 } let diagnostic ?sequence ~code message = Diagnostic.make ?sequence ~code ~phase:Diagnostic.Artifact message -let exception_message = function - | Sys_error message -> message - | Unix.Unix_error (code, operation, target) -> - Printf.sprintf "%s(%s): %s" operation target (Unix.error_message code) - | exception_ -> Printexc.to_string exception_ - -let create ?(effects = Boundary_effects.direct) final_path = - let partial_path = final_path ^ ".partial" in - if Sys.file_exists final_path then - Error - (diagnostic ~code:Diagnostic.Artifact_exists - ("strategy transcript already exists: " ^ final_path)) - else if Sys.file_exists partial_path then - Error - (diagnostic ~code:Diagnostic.Artifact_exists - ("partial strategy transcript already exists: " ^ partial_path)) - else - try - let channel = - Boundary_effects.perform effects - (Boundary_effects.Create_artifact partial_path) (fun () -> - open_out_gen - [ Open_wronly; Open_creat; Open_excl; Open_binary ] - 0o600 partial_path) - in - Ok - { - final_path; - partial_path; - channel; - effects; - next_sequence = 1L; - closed = false; - } - with exception_ -> - Error - (Diagnostic.of_exception ~code:Diagnostic.Artifact_io - ~phase:Diagnostic.Artifact - ~message: - ("could not create strategy transcript: " - ^ exception_message exception_) - exception_) +let create ?effects final_path = + Artifact_writer.create ?effects ~label:"strategy transcript" final_path + |> Result.map (fun artifact -> { artifact; next_sequence = 1L }) let append transcript ~direction message = - if transcript.closed then - Error - (diagnostic ~sequence:transcript.next_sequence - ~code:Diagnostic.Artifact_state - "cannot append to a closed strategy transcript") - else if Int64.equal transcript.next_sequence Int64.max_int then + if Int64.equal transcript.next_sequence Int64.max_int then Error (diagnostic ~sequence:transcript.next_sequence ~code:Diagnostic.Artifact_state "strategy transcript sequence is exhausted") else - try - let record = - Strategy_protocol.transcript_record - ~transcript_sequence:transcript.next_sequence ~direction ~message - in - let contents = Strategy_protocol.message_to_string record ^ "\n" in - Boundary_effects.perform transcript.effects - (Boundary_effects.Write_artifact - { channel = transcript.channel; contents }) - (fun () -> output_string transcript.channel contents); - Boundary_effects.perform transcript.effects - Boundary_effects.Flush_artifact (fun () -> flush transcript.channel); - transcript.next_sequence <- Int64.succ transcript.next_sequence; - Ok () - with exception_ -> - Error - (Diagnostic.of_exception ~sequence:transcript.next_sequence - ~code:Diagnostic.Artifact_io ~phase:Diagnostic.Artifact - ~message: - ("could not append strategy transcript " ^ transcript.partial_path - ^ ": " - ^ exception_message exception_) - exception_) + let record = + Strategy_protocol.transcript_record + ~transcript_sequence:transcript.next_sequence ~direction ~message + in + Artifact_writer.append transcript.artifact + (Strategy_protocol.message_to_string record ^ "\n") + |> Result.map (fun () -> + transcript.next_sequence <- Int64.succ transcript.next_sequence) + |> Result.map_error (Diagnostic.annotate ~sequence:transcript.next_sequence) let close_preserving_partial transcript = - if not transcript.closed then ( - transcript.closed <- true; - close_out_noerr transcript.channel) + Artifact_writer.close_preserving_partial transcript.artifact -let commit transcript = - if transcript.closed then - Error - (diagnostic ~sequence:transcript.next_sequence - ~code:Diagnostic.Artifact_state - "cannot commit a closed strategy transcript") - else - try - Boundary_effects.perform transcript.effects - Boundary_effects.Flush_artifact (fun () -> flush transcript.channel); - Boundary_effects.perform transcript.effects - Boundary_effects.Close_artifact (fun () -> close_out transcript.channel); - transcript.closed <- true; - Boundary_effects.perform transcript.effects - (Boundary_effects.Publish_artifact - { - partial_path = transcript.partial_path; - final_path = transcript.final_path; - }) - (fun () -> Unix.link transcript.partial_path transcript.final_path); - Boundary_effects.perform transcript.effects - (Boundary_effects.Cleanup_artifact transcript.partial_path) (fun () -> - Unix.unlink transcript.partial_path); - Ok () - with exception_ -> - close_preserving_partial transcript; - Error - (Diagnostic.of_exception ~code:Diagnostic.Artifact_io - ~phase:Diagnostic.Artifact - ~message: - ("could not finalize strategy transcript: " - ^ exception_message exception_) - exception_) +let commit transcript = Artifact_writer.commit [ transcript.artifact ] +let artifact transcript = transcript.artifact diff --git a/lib/strategy_transcript.mli b/lib/strategy_transcript.mli index 3739ae5..de95797 100644 --- a/lib/strategy_transcript.mli +++ b/lib/strategy_transcript.mli @@ -12,3 +12,4 @@ val append : val close_preserving_partial : t -> unit val commit : t -> (unit, Diagnostic.t) result +val artifact : t -> Artifact_writer.t From 6cd876eefdfefd3e40dab12d14e5811e7fdd4b58 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 18:12:43 -0400 Subject: [PATCH 09/57] fix: publish replay artifacts together --- README.md | 9 +- docs/architecture.md | 5 +- lib/artifact_writer.ml | 38 ++++++- lib/boundary_effects.ml | 4 + lib/boundary_effects.mli | 2 + lib/external_replay.ml | 55 +++++++--- lib/strategy_process.ml | 193 ++++++++++++++++++--------------- lib/strategy_process.mli | 10 ++ test/test_boundary_failures.ml | 67 +++++++++++- 9 files changed, 268 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 4cde003..2bc75ca 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,12 @@ own the child's standard input and output; strategy diagnostics belong on standa one request is outstanding. Initialization must return `ready`, each event must return `intents`, and shutdown must return `stopped`. Wrong versions or sequences, unknown or malformed fields, oversized responses, EOF, timeout, extra output, and nonzero exit all fail the replay. The journal -and transcript retain partial artifacts for diagnosis. The strategy runs in a dedicated process -group. Failure and cancellation send `SIGTERM` to the complete group, allow one second for graceful -exit, then send `SIGKILL` and allow five seconds to reap the process tree. +and transcript remain partial until both are complete. The engine then closes both, publishes the +complete set without replacement, and removes the partial names. A close or publication failure +rolls back final names created by the transaction. A cleanup failure leaves the complete final set +and restores every partial name for diagnosis. The strategy runs in a dedicated process group. +Failure and cancellation send `SIGTERM` to the complete group, allow one second for graceful exit, +then send `SIGKILL` and allow five seconds to reap the process tree. Discover the executable version and machine-readable compatibility surface: diff --git a/docs/architecture.md b/docs/architecture.md index f22cb01..5b971b4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -119,7 +119,10 @@ External replay requires an empty batch schedule or empty streamed intent batche supervisor launches an explicit argument vector, permits one request at a time, enforces a per-exchange timeout and 1 MiB response limit, then requires a clean child exit with no extra standard output. It records every accepted request and response in sequence. Failures preserve -the transcript and journal partials; success publishes both requested paths without replacement. +the transcript and journal partials. After both writers close, publication links every final path +without replacement before removing any partial path. A close or link failure rolls back final +links created by the transaction. A cleanup failure keeps the complete final set and restores any +partial names already removed. ## Invariants diff --git a/lib/artifact_writer.ml b/lib/artifact_writer.ml index 2dd9937..ffc37af 100644 --- a/lib/artifact_writer.ml +++ b/lib/artifact_writer.ml @@ -159,6 +159,24 @@ let rollback artifact = Error (exception_diagnostic ~label:file.label "roll back" exception_)) | Open _ | Closed _ | Complete _ -> Ok () +let restore_partial artifact = + match artifact.state with + | Complete file -> ( + try + if not (Sys.file_exists file.partial_path) then + Boundary_effects.perform file.effects + (Boundary_effects.Restore_artifact + { + final_path = file.final_path; + partial_path = file.partial_path; + }) + (fun () -> Unix.link file.final_path file.partial_path); + artifact.state <- Published (transition file); + Ok () + with exception_ -> + Error (exception_diagnostic ~label:file.label "restore" exception_)) + | Open _ | Closed _ | Published _ -> Ok () + let combine original = function | Ok () -> original | Error cleanup -> Diagnostic.combine original cleanup @@ -168,6 +186,11 @@ let rollback_all artifacts original = (fun diagnostic artifact -> rollback artifact |> combine diagnostic) original artifacts +let restore_all artifacts original = + List.fold_left + (fun diagnostic artifact -> restore_partial artifact |> combine diagnostic) + original artifacts + let rec close_all = function | [] -> Ok () | artifact :: rest -> ( @@ -191,12 +214,15 @@ let rec publish_all published = function | Ok () -> publish_all (artifact :: published) rest | Error diagnostic -> Error (rollback_all published diagnostic)) -let rec cleanup_all = function - | [] -> Ok () - | artifact :: rest -> ( - match cleanup_partial artifact with - | Ok () -> cleanup_all rest - | Error _ as error -> error) +let cleanup_all artifacts = + let rec loop = function + | [] -> Ok () + | artifact :: rest -> ( + match cleanup_partial artifact with + | Ok () -> loop rest + | Error diagnostic -> Error (restore_all artifacts diagnostic)) + in + loop artifacts let commit artifacts = match artifacts with diff --git a/lib/boundary_effects.ml b/lib/boundary_effects.ml index e36ac86..0cce29c 100644 --- a/lib/boundary_effects.ml +++ b/lib/boundary_effects.ml @@ -5,6 +5,7 @@ type stage = | Artifact_close | Artifact_publish | Artifact_cleanup + | Artifact_restore | Process_spawn | Process_exchange | Process_terminate @@ -17,6 +18,7 @@ type operation = | Close_artifact | Publish_artifact of { partial_path : string; final_path : string } | Cleanup_artifact of string + | Restore_artifact of { final_path : string; partial_path : string } | Spawn_process | Exchange_process | Terminate_process @@ -36,6 +38,7 @@ let stage = function | Close_artifact -> Artifact_close | Publish_artifact _ -> Artifact_publish | Cleanup_artifact _ -> Artifact_cleanup + | Restore_artifact _ -> Artifact_restore | Spawn_process -> Process_spawn | Exchange_process -> Process_exchange | Terminate_process -> Process_terminate @@ -48,6 +51,7 @@ let stage_to_string = function | Artifact_close -> "artifact close" | Artifact_publish -> "artifact publish" | Artifact_cleanup -> "artifact cleanup" + | Artifact_restore -> "artifact restore" | Process_spawn -> "process spawn" | Process_exchange -> "process exchange" | Process_terminate -> "process terminate" diff --git a/lib/boundary_effects.mli b/lib/boundary_effects.mli index e0ad565..04827c6 100644 --- a/lib/boundary_effects.mli +++ b/lib/boundary_effects.mli @@ -7,6 +7,7 @@ type stage = | Artifact_close | Artifact_publish | Artifact_cleanup + | Artifact_restore | Process_spawn | Process_exchange | Process_terminate @@ -19,6 +20,7 @@ type operation = | Close_artifact | Publish_artifact of { partial_path : string; final_path : string } | Cleanup_artifact of string + | Restore_artifact of { final_path : string; partial_path : string } | Spawn_process | Exchange_process | Terminate_process diff --git a/lib/external_replay.ml b/lib/external_replay.ml index aa074d8..e60bb7f 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -120,9 +120,28 @@ let process_slice respond runner market_slice = in drive respond progress -let close_journal = function - | None -> () - | Some journal -> Journal.close_preserving_partial journal +let create_artifacts ~journal_path ~transcript_path = + let* journal = Journal.create journal_path in + match Strategy_transcript.create transcript_path with + | Ok transcript -> Ok (journal, transcript) + | Error _ as error -> + Journal.close_preserving_partial journal; + error + +let close_artifacts (journal, transcript) = + Journal.close_preserving_partial journal; + Strategy_transcript.close_preserving_partial transcript + +let protect_artifacts artifacts run = + try run () + with exception_ -> + let backtrace = Printexc.get_raw_backtrace () in + close_artifacts artifacts; + Printexc.raise_with_backtrace exception_ backtrace + +let commit_artifacts (journal, transcript) = + Artifact_writer.commit + [ Journal.artifact journal; Strategy_transcript.artifact transcript ] let run ~env ~scenario_sha256 ~journal_path ~transcript_path ~strategy_command ~strategy_timeout (scenario : Scenario.t) = @@ -137,11 +156,14 @@ let run ~env ~scenario_sha256 ~journal_path ~transcript_path ~strategy_command ~max_internal_events:scenario.max_internal_events ~initial_cash:scenario.initial_cash in - let* journal = Journal.create journal_path in + let* journal, transcript = + create_artifacts ~journal_path ~transcript_path + in let journal_ref = Some journal in let session_result = - Strategy_process.with_session ~env ~command:strategy_command - ~timeout:strategy_timeout ~transcript_path + protect_artifacts (journal, transcript) @@ fun () -> + Strategy_process.with_staged_session ~env ~command:strategy_command + ~timeout:strategy_timeout ~transcript ~initialization:(initialization_of_scenario ~scenario_sha256 scenario) (fun session -> let respond = Strategy_process.on_event session in @@ -165,10 +187,10 @@ let run ~env ~scenario_sha256 ~journal_path ~transcript_path ~strategy_command in match session_result with | Error _ as error -> - close_journal journal_ref; + close_artifacts (journal, transcript); error | Ok ((state, valuation, audits), strategy) -> ( - match Journal.commit journal with + match commit_artifacts (journal, transcript) with | Error _ as error -> error | Ok () -> Ok @@ -243,9 +265,9 @@ let replay_stream_pass ~scenario_sha256 ~journal ~session channel = let run_stream ~env ~journal_path ~transcript_path ~strategy_command ~strategy_timeout path = - let journal_ref = ref None in + let artifacts_ref = ref None in let fail result = - close_journal !journal_ref; + Option.iter close_artifacts !artifacts_ref; result in try @@ -261,11 +283,14 @@ let run_stream ~env ~journal_path ~transcript_path ~strategy_command ~phase:Diagnostic.Input "scenario stream changed during validation") else - let* journal = Journal.create journal_path in - journal_ref := Some journal; + let* journal, transcript = + create_artifacts ~journal_path ~transcript_path + in + artifacts_ref := Some (journal, transcript); let session_result = - Strategy_process.with_session ~env ~command:strategy_command - ~timeout:strategy_timeout ~transcript_path + protect_artifacts (journal, transcript) @@ fun () -> + Strategy_process.with_staged_session ~env ~command:strategy_command + ~timeout:strategy_timeout ~transcript ~initialization:validated.initialization (fun session -> seek_in channel 0; let* runner, valuation, audit_count, slice_count = @@ -283,7 +308,7 @@ let run_stream ~env ~journal_path ~transcript_path ~strategy_command match session_result with | Error _ as error -> fail error | Ok ((runner, valuation, audit_count, slice_count), strategy) -> ( - match Journal.commit journal with + match commit_artifacts (journal, transcript) with | Error _ as error -> error | Ok () -> Ok diff --git a/lib/strategy_process.ml b/lib/strategy_process.ml index c49a818..7827fb9 100644 --- a/lib/strategy_process.ml +++ b/lib/strategy_process.ml @@ -329,8 +329,7 @@ let await_exit session = (diagnostic ~code:Diagnostic.Strategy_exit (Printf.sprintf "external strategy was killed by signal %d" signal)) -let with_session ?(effects = Boundary_effects.direct) ~env ~command ~timeout - ~transcript_path ~(initialization : Strategy_protocol.initialization) use = +let validate_configuration ~command ~timeout = if not (valid_timeout timeout) then Error (diagnostic ~code:Diagnostic.Strategy_invalid_configuration @@ -345,91 +344,107 @@ let with_session ?(effects = Boundary_effects.direct) ~env ~command ~timeout Error (diagnostic ~code:Diagnostic.Strategy_invalid_configuration "external strategy executable must not be empty") - | executable :: _ -> ( - match Strategy_transcript.create ~effects transcript_path with - | Error _ as error -> error - | Ok transcript -> ( - let fail result = - Strategy_transcript.close_preserving_partial transcript; - result - in - try - let result = - Eio.Switch.run ~name:"external-strategy" @@ fun switch -> - let process_manager = Eio.Stdenv.process_mgr env in - if enable_child_subreaper () <> 0 then - failwith "could not enable external strategy child reaping"; - let child_stdout, strategy_stdout = Eio_unix.pipe switch in - let strategy_stdin, child_stdin = Eio_unix.pipe switch in - let fds = - [ - (0, Eio_unix.Resource.fd strategy_stdin, `Blocking); - (1, Eio_unix.Resource.fd strategy_stdout, `Blocking); - (2, Eio_unix.Resource.fd (Eio.Stdenv.stderr env), `Blocking); - ] - in - let process = - Boundary_effects.perform effects - Boundary_effects.Spawn_process (fun () -> - Eio_unix.Process.spawn_unix ~sw:switch process_manager - ~pgid:0 ~fds ~executable command) - in - Eio.Flow.close strategy_stdin; - Eio.Flow.close strategy_stdout; - let child = - { - process; - pgid = Eio.Process.pid process; - clock = Eio.Stdenv.clock env; - effects; - status = None; - } - in - let close_input () = Eio.Flow.close child_stdin in - let session = - { - input = (child_stdin :> Eio.Flow.sink_ty Eio.Resource.t); - close_input; - output = - Eio.Buf_read.of_flow - ~max_size:(Strategy_protocol.max_message_bytes + 1) - child_stdout; - child; - clock = Eio.Stdenv.clock env; - transcript; - effects; - timeout; - next_sequence = 1L; - } - in - try - let result = - let* identity = initialize session initialization in - let* value = use session in - let* () = shutdown session in - let* () = await_exit session in - Ok (value, identity) - in - match result with - | Ok _ -> result - | Error _ -> append_cleanup_error result child - with exception_ -> - let backtrace = Printexc.get_raw_backtrace () in - ignore (terminate_process_group child); - Printexc.raise_with_backtrace exception_ backtrace - in - match result with - | Error _ as error -> fail error - | Ok value -> ( - match Strategy_transcript.commit transcript with - | Ok () -> Ok value - | Error _ as error -> error) + | executable :: _ -> Ok executable + +let run_session ~effects ~env ~command ~executable ~timeout ~transcript + ~(initialization : Strategy_protocol.initialization) use = + try + Eio.Switch.run ~name:"external-strategy" @@ fun switch -> + let process_manager = Eio.Stdenv.process_mgr env in + if enable_child_subreaper () <> 0 then + failwith "could not enable external strategy child reaping"; + let child_stdout, strategy_stdout = Eio_unix.pipe switch in + let strategy_stdin, child_stdin = Eio_unix.pipe switch in + let fds = + [ + (0, Eio_unix.Resource.fd strategy_stdin, `Blocking); + (1, Eio_unix.Resource.fd strategy_stdout, `Blocking); + (2, Eio_unix.Resource.fd (Eio.Stdenv.stderr env), `Blocking); + ] + in + let process = + Boundary_effects.perform effects Boundary_effects.Spawn_process (fun () -> + Eio_unix.Process.spawn_unix ~sw:switch process_manager ~pgid:0 ~fds + ~executable command) + in + Eio.Flow.close strategy_stdin; + Eio.Flow.close strategy_stdout; + let child = + { + process; + pgid = Eio.Process.pid process; + clock = Eio.Stdenv.clock env; + effects; + status = None; + } + in + let close_input () = Eio.Flow.close child_stdin in + let session = + { + input = (child_stdin :> Eio.Flow.sink_ty Eio.Resource.t); + close_input; + output = + Eio.Buf_read.of_flow + ~max_size:(Strategy_protocol.max_message_bytes + 1) + child_stdout; + child; + clock = Eio.Stdenv.clock env; + transcript; + effects; + timeout; + next_sequence = 1L; + } + in + try + let result = + let* identity = initialize session initialization in + let* value = use session in + let* () = shutdown session in + let* () = await_exit session in + Ok (value, identity) + in + match result with + | Ok _ -> result + | Error _ -> append_cleanup_error result child + with exception_ -> + let backtrace = Printexc.get_raw_backtrace () in + ignore (terminate_process_group child); + Printexc.raise_with_backtrace exception_ backtrace + with + | Eio.Cancel.Cancelled _ as exception_ -> raise exception_ + | exception_ -> + Error (exception_diagnostic "external strategy process" exception_) + +let with_staged_session ?(effects = Boundary_effects.direct) ~env ~command + ~timeout ~transcript ~initialization use = + match validate_configuration ~command ~timeout with + | Error _ as error -> error + | Ok executable -> + run_session ~effects ~env ~command ~executable ~timeout ~transcript + ~initialization use + +let with_session ?(effects = Boundary_effects.direct) ~env ~command ~timeout + ~transcript_path ~initialization use = + match validate_configuration ~command ~timeout with + | Error _ as error -> error + | Ok executable -> ( + match Strategy_transcript.create ~effects transcript_path with + | Error _ as error -> error + | Ok transcript -> ( + let fail result = + Strategy_transcript.close_preserving_partial transcript; + result + in + try + match + run_session ~effects ~env ~command ~executable ~timeout + ~transcript ~initialization use with - | Eio.Cancel.Cancelled _ as exception_ -> - Strategy_transcript.close_preserving_partial transcript; - raise exception_ - | exception_ -> - fail - (Error - (exception_diagnostic "external strategy process" - exception_)))) + | Error _ as error -> fail error + | Ok value -> ( + match Strategy_transcript.commit transcript with + | Ok () -> Ok value + | Error _ as error -> error) + with Eio.Cancel.Cancelled _ as exception_ -> + Strategy_transcript.close_preserving_partial transcript; + raise exception_)) diff --git a/lib/strategy_process.mli b/lib/strategy_process.mli index 9c17150..d236fc5 100644 --- a/lib/strategy_process.mli +++ b/lib/strategy_process.mli @@ -2,6 +2,16 @@ type t +val with_staged_session : + ?effects:Boundary_effects.t -> + env:Eio_unix.Stdenv.base -> + command:string list -> + timeout:float -> + transcript:Strategy_transcript.t -> + initialization:Strategy_protocol.initialization -> + (t -> ('a, Diagnostic.t) result) -> + ('a * Strategy_protocol.identity, Diagnostic.t) result + val with_session : ?effects:Boundary_effects.t -> env:Eio_unix.Stdenv.base -> diff --git a/test/test_boundary_failures.ml b/test/test_boundary_failures.ml index 4973174..9b5dd26 100644 --- a/test/test_boundary_failures.ml +++ b/test/test_boundary_failures.ml @@ -107,6 +107,7 @@ let exercise_artifact_failure (type writer) writer_name | Error _ as error -> error | Ok () -> Writer.commit writer) | Artifact_create -> Alcotest.fail "create failure was not injected" + | Artifact_restore -> Alcotest.fail "expected a lifecycle failure" | Process_spawn | Process_exchange | Process_terminate | Process_reap -> Alcotest.fail "expected an artifact stage" @@ -155,6 +156,70 @@ let artifact_cases writer_name writer = (fun () -> exercise_artifact_failure writer_name writer stage)) artifact_stages +let nth_failure target occurrence = + let seen = ref 0 in + let perform : type result. + T.Boundary_effects.operation -> (unit -> result) -> result = + fun operation run -> + if T.Boundary_effects.stage operation = target then ( + seen := !seen + 1; + if !seen = occurrence then + raise (Injected_failure (T.Boundary_effects.stage_to_string target))); + run () + in + ({ T.Boundary_effects.perform }, seen) + +let exercise_transaction_failure stage occurrence = + with_absent_path ".journal.jsonl" @@ fun journal_path -> + with_absent_path ".strategy.jsonl" @@ fun transcript_path -> + let effects, seen = nth_failure stage occurrence in + let journal = + T.Artifact_writer.create ~effects ~label:"journal" journal_path |> ok + in + let transcript = + T.Artifact_writer.create ~effects ~label:"strategy transcript" + transcript_path + |> ok + in + T.Artifact_writer.append journal "journal\n" |> ok; + T.Artifact_writer.append transcript "transcript\n" |> ok; + let diagnostic = T.Artifact_writer.commit [ journal; transcript ] |> error in + Alcotest.(check int) "target occurrence reached" occurrence !seen; + Alcotest.(check string) + "artifact diagnostic" "artifact.io" + (T.Diagnostic.code_to_string diagnostic.code); + let finals_exist = stage = T.Boundary_effects.Artifact_cleanup in + List.iter + (fun path -> + Alcotest.(check bool) + "final-set invariant" finals_exist (Sys.file_exists path); + Alcotest.(check bool) + "partial-set invariant" true + (Sys.file_exists (path ^ ".partial"))) + [ journal_path; transcript_path ]; + if finals_exist then + List.iter + (fun path -> + Alcotest.(check string) + "final and restored partial agree" + (In_channel.with_open_bin path In_channel.input_all) + (In_channel.with_open_bin (path ^ ".partial") In_channel.input_all)) + [ journal_path; transcript_path ] + +let transaction_cases = + List.concat_map + (fun stage -> + List.map + (fun occurrence -> + Alcotest.test_case + (Printf.sprintf "transaction %s %d" + (T.Boundary_effects.stage_to_string stage) + occurrence) + `Quick + (fun () -> exercise_transaction_failure stage occurrence)) + [ 1; 2 ]) + T.Boundary_effects.[ Artifact_close; Artifact_publish; Artifact_cleanup ] + let initialization () = let instrument = instrument () in T.Strategy_protocol. @@ -205,4 +270,4 @@ let process_cases = let tests = artifact_cases "journal" (module Journal_writer) @ artifact_cases "transcript" (module Transcript_writer) - @ process_cases + @ transaction_cases @ process_cases From 91979401d304bb30ad3f9168386a9c52d3baf840 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 18:20:15 -0400 Subject: [PATCH 10/57] feat: add durable artifact publication --- README.md | 17 ++- bin/main.ml | 75 ++++++++----- docs/architecture.md | 13 ++- docs/diagnostics.md | 2 +- docs/scenario.md | 5 + lib/artifact_writer.ml | 194 ++++++++++++++++++++++++++++++--- lib/artifact_writer.mli | 2 + lib/boundary_effects.ml | 12 ++ lib/boundary_effects.mli | 6 + lib/external_replay.ml | 19 ++-- lib/external_replay.mli | 2 + lib/journal.ml | 4 +- lib/journal.mli | 7 +- lib/replay.ml | 9 +- lib/replay.mli | 2 + lib/strategy_process.ml | 5 +- lib/strategy_process.mli | 1 + lib/strategy_transcript.ml | 5 +- lib/strategy_transcript.mli | 6 +- test/cli.t | 8 +- test/test_boundary_failures.ml | 112 +++++++++++++++++-- 21 files changed, 424 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 2bc75ca..a24ecec 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,9 @@ scenario slices and scheduled or external intents - 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 -- Complete bidirectional strategy transcripts with exclusive partial and no-replace publication +- Complete bidirectional strategy transcripts with coordinated no-replace journal publication - Scenario SHA-256 binding in `run_started` and `run_completed` -- Exclusive partial journal creation and atomic no-replace finalization +- Exclusive partial artifact creation with optional file and directory synchronization - Unit, schema-conformance, scenario, golden-contract, and property tests ## Quick start @@ -142,6 +142,10 @@ CLI binds that exact-byte hash into the journal. It writes to the partial path a requested path only after `run_completed` is fully written and the partial file is closed. An error preserves the partial artifact for diagnosis. +Pass `--durable-artifacts` to synchronize each staged file before publication and synchronize each +containing directory after final links and partial cleanup. The default buffered mode flushes every +record but does not make a restart-durability claim. + ## Execution summary An order emitted after slice `n` cannot execute on slice `n`. It first becomes eligible on a later @@ -174,12 +178,13 @@ The current scope omits: - External execution-report ingestion - Exchange calendars and time-zone databases - Durable reducer snapshots and broker reconciliation -- `fsync` and restart recovery for journals - Tick, trade, and order-book replay -The journal writer flushes each record, creates its partial file exclusively, and finalizes with an -exclusive hard link. It does not call `fsync`, so the journal is an audit artifact rather than a -production recovery log. +Artifact publication requires a filesystem that supports exclusive file creation, hard links, and +atomic unlink. Durable mode additionally requires file and directory synchronization. An +unsupported synchronization operation returns `artifact.io`, never reports success, and preserves +or restores partial names for diagnosis. Durable artifacts strengthen publication persistence; they +do not provide reducer snapshots or restart recovery. ## Architecture and contracts diff --git a/bin/main.ml b/bin/main.ml index 296b043..dad181a 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -17,9 +17,10 @@ let count predicate values = (fun total value -> total + Bool.to_int (predicate value)) 0 values -let run_replay scenario_sha256 scenario journal = +let run_replay scenario_sha256 scenario journal durability = match - Trading_engine.Replay.run ~scenario_sha256 ~journal_path:journal scenario + Trading_engine.Replay.run ~scenario_sha256 ~journal_path:journal ~durability + scenario with | Error message -> Error message | Ok result -> @@ -47,8 +48,10 @@ let run_replay scenario_sha256 scenario journal = Fmt.pr "journal=%s@." journal; Ok () -let run_stream input journal = - match Trading_engine.Replay.run_stream ~journal_path:journal input with +let run_stream input journal durability = + match + Trading_engine.Replay.run_stream ~journal_path:journal ~durability input + with | Error message -> Error message | Ok result -> let active = count Trading_engine.Order.is_active result.orders in @@ -80,12 +83,13 @@ type external_strategy = { transcript : string; } -let run_external_replay environment scenario_sha256 scenario journal strategy = +let run_external_replay environment scenario_sha256 scenario journal strategy + durability = match - Trading_engine.External_replay.run ~env:environment ~scenario_sha256 - ~journal_path:journal ~transcript_path:strategy.transcript - ~strategy_command:strategy.command ~strategy_timeout:strategy.timeout - scenario + Trading_engine.External_replay.run ~durability ~env:environment + ~scenario_sha256 ~journal_path:journal + ~transcript_path:strategy.transcript ~strategy_command:strategy.command + ~strategy_timeout:strategy.timeout scenario with | Error message -> Error message | Ok result -> @@ -114,9 +118,9 @@ let run_external_replay environment scenario_sha256 scenario journal strategy = Fmt.pr "strategy_transcript=%s@." strategy.transcript; Ok () -let run_external_stream environment input journal strategy = +let run_external_stream environment input journal strategy durability = match - Trading_engine.External_replay.run_stream ~env:environment + Trading_engine.External_replay.run_stream ~durability ~env:environment ~journal_path:journal ~transcript_path:strategy.transcript ~strategy_command:strategy.command ~strategy_timeout:strategy.timeout input @@ -147,7 +151,7 @@ let run_external_stream environment input journal strategy = Fmt.pr "strategy_transcript=%s@." strategy.transcript; Ok () -let execute_json environment input journal validate_only strategy = +let execute_json environment input journal validate_only strategy durability = let document = try Ok (In_channel.with_open_bin input In_channel.input_all) with Sys_error message as exception_ -> @@ -189,12 +193,12 @@ let execute_json environment input journal validate_only strategy = "--journal is required unless --validate-only is set") | Some path -> ( match strategy with - | None -> run_replay scenario_sha256 scenario path + | None -> run_replay scenario_sha256 scenario path durability | Some strategy -> run_external_replay environment scenario_sha256 scenario - path strategy))) + path strategy durability))) -let execute_jsonl environment input journal validate_only strategy = +let execute_jsonl environment input journal validate_only strategy durability = if validate_only then match journal with | Some _ -> @@ -215,14 +219,18 @@ let execute_jsonl environment input journal validate_only strategy = Error (cli_error "--journal is required unless --validate-only is set") | Some path -> ( match strategy with - | None -> run_stream input path - | Some strategy -> run_external_stream environment input path strategy) + | None -> run_stream input path durability + | Some strategy -> + run_external_stream environment input path strategy durability) type input_format = Json | Jsonl -let execute_scenario environment input journal validate_only strategy = function - | Json -> execute_json environment input journal validate_only strategy - | Jsonl -> execute_jsonl environment input journal validate_only strategy +let execute_scenario environment input journal validate_only strategy durability + = function + | Json -> + execute_json environment input journal validate_only strategy durability + | Jsonl -> + execute_jsonl environment input journal validate_only strategy durability let external_strategy executable arguments timeout transcript = match (executable, transcript, arguments, timeout) with @@ -244,7 +252,7 @@ let external_strategy executable arguments timeout transcript = let execute environment input journal validate_only capabilities input_format strategy_executable strategy_arguments strategy_timeout strategy_transcript - = + durable_artifacts = if capabilities then match ( input, @@ -253,15 +261,18 @@ let execute environment input journal validate_only capabilities input_format strategy_executable, strategy_arguments, strategy_timeout, - strategy_transcript ) + strategy_transcript, + durable_artifacts ) with - | None, None, false, None, [], None, None -> + | None, None, false, None, [], None, None, false -> Fmt.pr "%s@." (Trading_engine.Contract.capabilities_to_string ()); Ok () | _ -> Error (cli_error "--capabilities cannot be combined with replay or strategy options") + else if validate_only && durable_artifacts then + Error (cli_error "--durable-artifacts cannot be used with --validate-only") else match input with | None -> @@ -277,8 +288,12 @@ let execute environment input journal validate_only capabilities input_format (cli_error "external strategy options cannot be used with --validate-only") | Ok strategy -> + let durability = + if durable_artifacts then Trading_engine.Artifact_writer.Durable + else Trading_engine.Artifact_writer.Buffered + in execute_scenario environment path journal validate_only strategy - input_format) + durability input_format) let input = let doc = "Read the replay scenario from $(docv)." in @@ -297,6 +312,13 @@ let journal = & opt (some string) None & info [ "journal"; "j" ] ~docv:"JOURNAL.jsonl" ~doc) +let durable_artifacts = + let doc = + "Synchronize staged artifact contents and directory metadata before \ + reporting success." + in + Arg.(value & flag & info [ "durable-artifacts" ] ~doc) + let validate_only = let doc = "Validate the scenario and exit without creating a journal." in Arg.(value & flag & info [ "validate-only" ] ~doc) @@ -374,15 +396,16 @@ let command environment = strategy_arguments strategy_timeout strategy_transcript + durable_artifacts diagnostic_format -> ( diagnostic_format, execute environment input journal validate_only capabilities input_format strategy_executable strategy_arguments - strategy_timeout strategy_transcript )) + strategy_timeout strategy_transcript durable_artifacts )) $ input $ journal $ validate_only $ capabilities $ input_format $ strategy_executable $ strategy_argument $ strategy_timeout - $ strategy_transcript $ diagnostic_format) + $ strategy_transcript $ durable_artifacts $ diagnostic_format) let () = Fmt_tty.setup_std_outputs (); diff --git a/docs/architecture.md b/docs/architecture.md index 5b971b4..e2e85ad 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -120,9 +120,16 @@ supervisor launches an explicit argument vector, permits one request at a time, per-exchange timeout and 1 MiB response limit, then requires a clean child exit with no extra standard output. It records every accepted request and response in sequence. Failures preserve the transcript and journal partials. After both writers close, publication links every final path -without replacement before removing any partial path. A close or link failure rolls back final -links created by the transaction. A cleanup failure keeps the complete final set and restores any -partial names already removed. +without replacement before moving any partial path to a reserved cleanup name. Only after every +move succeeds does the transaction unlink those cleanup names. A close or link failure rolls back +final links created by the transaction. A move or cleanup failure keeps the complete final set and +restores every partial name. + +Buffered publication flushes every record. Durable publication also synchronizes each staged file +before close, synchronizes each containing directory after all final links exist, removes partial +links, and synchronizes the directories again. Unsupported file or directory synchronization is an +artifact failure. The failure path rolls back an unpublished final set or restores partial names +beside an already complete final set. ## Invariants diff --git a/docs/diagnostics.md b/docs/diagnostics.md index dd94ca3..2de7ffe 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -39,7 +39,7 @@ Version 1 defines these codes: | `strategy.process` | Strategy spawn, signaling, supervision, or process I/O failed | | `strategy.exit` | Strategy exited with an unsuccessful status | | `artifact.exists` | A final or partial artifact path already exists | -| `artifact.io` | Artifact creation, append, close, or publication failed | +| `artifact.io` | Artifact creation, append, synchronization, close, publication, or cleanup failed | | `artifact.state` | Artifact writer lifecycle operation is invalid | Adding codes or optional context fields does not change the diagnostic version. Removing a code, diff --git a/docs/scenario.md b/docs/scenario.md index 9836204..98065d1 100644 --- a/docs/scenario.md +++ b/docs/scenario.md @@ -196,3 +196,8 @@ A successful replay ends with exactly one `run_completed` record containing the hash, reconciled valuation, and mutually exclusive order-status counts. A journal without that terminal record is incomplete. The requested journal path appears only after exclusive successful finalization; a failed run retains the `.partial` artifact. + +`--durable-artifacts` synchronizes staged contents before publication and containing-directory +metadata after publication and partial cleanup. The filesystem must support hard links plus file +and directory synchronization. Unsupported durability operations fail with `artifact.io` and do +not silently fall back to buffered publication. diff --git a/lib/artifact_writer.ml b/lib/artifact_writer.ml index ffc37af..bf4d2b3 100644 --- a/lib/artifact_writer.ml +++ b/lib/artifact_writer.ml @@ -1,20 +1,25 @@ type open_state type closed_state type published_state +type renamed_state type complete_state +type durability = Buffered | Durable type 'state file = { label : string; final_path : string; partial_path : string; + cleanup_path : string; channel : out_channel; effects : Boundary_effects.t; + durability : durability; } type state = | Open of open_state file | Closed of closed_state file | Published of published_state file + | Renamed of renamed_state file | Complete of complete_state file type t = { mutable state : state } @@ -23,6 +28,7 @@ let state_label = function | Open file -> file.label | Closed file -> file.label | Published file -> file.label + | Renamed file -> file.label | Complete file -> file.label let diagnostic ~code message = @@ -42,17 +48,28 @@ let exception_diagnostic ~label action exception_ = (exception_message exception_)) exception_ +let move_no_replace source_path target_path = + Unix.link source_path target_path; + try Unix.unlink source_path + with exception_ -> + (try Unix.unlink target_path with _ -> ()); + raise exception_ + let transition file = { label = file.label; final_path = file.final_path; partial_path = file.partial_path; + cleanup_path = file.cleanup_path; channel = file.channel; effects = file.effects; + durability = file.durability; } -let create ?(effects = Boundary_effects.direct) ~label final_path = +let create ?(effects = Boundary_effects.direct) ?(durability = Buffered) ~label + final_path = let partial_path = final_path ^ ".partial" in + let cleanup_path = partial_path ^ ".cleanup" in if Sys.file_exists final_path then Error (diagnostic ~code:Diagnostic.Artifact_exists @@ -61,6 +78,10 @@ let create ?(effects = Boundary_effects.direct) ~label final_path = Error (diagnostic ~code:Diagnostic.Artifact_exists (Printf.sprintf "partial %s already exists: %s" label partial_path)) + else if Sys.file_exists cleanup_path then + Error + (diagnostic ~code:Diagnostic.Artifact_exists + (Printf.sprintf "%s cleanup path already exists: %s" label cleanup_path)) else try let channel = @@ -70,12 +91,25 @@ let create ?(effects = Boundary_effects.direct) ~label final_path = [ Open_wronly; Open_creat; Open_excl; Open_binary ] 0o600 partial_path) in - Ok { state = Open { label; final_path; partial_path; channel; effects } } + Ok + { + state = + Open + { + label; + final_path; + partial_path; + cleanup_path; + channel; + effects; + durability; + }; + } with exception_ -> Error (exception_diagnostic ~label "create" exception_) let append artifact contents = match artifact.state with - | Closed _ | Published _ | Complete _ -> + | Closed _ | Published _ | Renamed _ | Complete _ -> Error (diagnostic ~code:Diagnostic.Artifact_state ("cannot append to a closed " ^ state_label artifact.state)) @@ -95,6 +129,12 @@ let close_file artifact file = try Boundary_effects.perform file.effects Boundary_effects.Flush_artifact (fun () -> flush file.channel); + (match file.durability with + | Buffered -> () + | Durable -> + Boundary_effects.perform file.effects + (Boundary_effects.Sync_artifact file.channel) (fun () -> + Unix.fsync (Unix.descr_of_out_channel file.channel))); Boundary_effects.perform file.effects Boundary_effects.Close_artifact (fun () -> close_out file.channel); artifact.state <- Closed closed; @@ -109,7 +149,7 @@ let close_preserving_partial artifact = | Open file -> close_out_noerr file.channel; artifact.state <- Closed (transition file) - | Closed _ | Published _ | Complete _ -> () + | Closed _ | Published _ | Renamed _ | Complete _ -> () let publish artifact = match artifact.state with @@ -123,24 +163,45 @@ let publish artifact = Ok () with exception_ -> Error (exception_diagnostic ~label:file.label "publish" exception_)) - | Open _ | Published _ | Complete _ -> + | Open _ | Published _ | Renamed _ | Complete _ -> Error (diagnostic ~code:Diagnostic.Artifact_state ("cannot publish " ^ state_label artifact.state ^ " from its current state")) -let cleanup_partial artifact = +let rename_partial artifact = match artifact.state with | Published file -> ( try Boundary_effects.perform file.effects - (Boundary_effects.Cleanup_artifact file.partial_path) (fun () -> - Unix.unlink file.partial_path); + (Boundary_effects.Rename_artifact + { + source_path = file.partial_path; + target_path = file.cleanup_path; + }) + (fun () -> move_no_replace file.partial_path file.cleanup_path); + artifact.state <- Renamed (transition file); + Ok () + with exception_ -> + Error (exception_diagnostic ~label:file.label "rename" exception_)) + | Open _ | Closed _ | Renamed _ | Complete _ -> + Error + (diagnostic ~code:Diagnostic.Artifact_state + ("cannot rename " ^ state_label artifact.state + ^ " from its current state")) + +let cleanup_partial artifact = + match artifact.state with + | Renamed file -> ( + try + Boundary_effects.perform file.effects + (Boundary_effects.Cleanup_artifact file.cleanup_path) (fun () -> + Unix.unlink file.cleanup_path); artifact.state <- Complete (transition file); Ok () with exception_ -> Error (exception_diagnostic ~label:file.label "clean up" exception_)) - | Open _ | Closed _ | Complete _ -> + | Open _ | Closed _ | Published _ | Complete _ -> Error (diagnostic ~code:Diagnostic.Artifact_state ("cannot clean up " ^ state_label artifact.state @@ -157,10 +218,24 @@ let rollback artifact = Ok () with exception_ -> Error (exception_diagnostic ~label:file.label "roll back" exception_)) - | Open _ | Closed _ | Complete _ -> Ok () + | Open _ | Closed _ | Renamed _ | Complete _ -> Ok () let restore_partial artifact = match artifact.state with + | Renamed file -> ( + try + if not (Sys.file_exists file.partial_path) then + Boundary_effects.perform file.effects + (Boundary_effects.Rename_artifact + { + source_path = file.cleanup_path; + target_path = file.partial_path; + }) + (fun () -> move_no_replace file.cleanup_path file.partial_path); + artifact.state <- Published (transition file); + Ok () + with exception_ -> + Error (exception_diagnostic ~label:file.label "restore" exception_)) | Complete file -> ( try if not (Sys.file_exists file.partial_path) then @@ -191,6 +266,63 @@ let restore_all artifacts original = (fun diagnostic artifact -> restore_partial artifact |> combine diagnostic) original artifacts +let directory_effects artifacts = + let add directories artifact = + let add_file file = + match file.durability with + | Buffered -> directories + | Durable -> + let directory = Filename.dirname file.final_path in + if + List.exists + (fun (path, _) -> String.equal path directory) + directories + then directories + else (directory, file.effects) :: directories + in + match artifact.state with + | Open file -> add_file file + | Closed file -> add_file file + | Published file -> add_file file + | Renamed file -> add_file file + | Complete file -> add_file file + in + List.fold_left add [] artifacts |> List.rev + +let sync_directory (directory, effects) = + try + Boundary_effects.perform effects (Boundary_effects.Sync_directory directory) + (fun () -> + let descriptor = Unix.openfile directory [ Unix.O_RDONLY ] 0 in + Fun.protect + ~finally:(fun () -> Unix.close descriptor) + (fun () -> Unix.fsync descriptor)); + Ok () + with exception_ -> + Error + (exception_diagnostic + ~label:("artifact directory " ^ directory) + "synchronize" exception_) + +let rec sync_directories = function + | [] -> Ok () + | directory :: rest -> ( + match sync_directory directory with + | Ok () -> sync_directories rest + | Error _ as error -> error) + +let add_cleanup diagnostic = function + | Ok () -> diagnostic + | Error cleanup -> Diagnostic.combine diagnostic cleanup + +let rollback_after_failure artifacts directories diagnostic = + let diagnostic = rollback_all artifacts diagnostic in + sync_directories directories |> add_cleanup diagnostic + +let restore_after_failure artifacts directories diagnostic = + let diagnostic = restore_all artifacts diagnostic in + sync_directories directories |> add_cleanup diagnostic + let rec close_all = function | [] -> Ok () | artifact :: rest -> ( @@ -201,7 +333,7 @@ let rec close_all = function | Error diagnostic -> List.iter close_preserving_partial rest; Error diagnostic) - | Closed _ | Published _ | Complete _ -> + | Closed _ | Published _ | Renamed _ | Complete _ -> List.iter close_preserving_partial rest; Error (diagnostic ~code:Diagnostic.Artifact_state @@ -220,7 +352,17 @@ let cleanup_all artifacts = | artifact :: rest -> ( match cleanup_partial artifact with | Ok () -> loop rest - | Error diagnostic -> Error (restore_all artifacts diagnostic)) + | Error _ as error -> error) + in + loop artifacts + +let rename_all artifacts = + let rec loop = function + | [] -> Ok () + | artifact :: rest -> ( + match rename_partial artifact with + | Ok () -> loop rest + | Error _ as error -> error) in loop artifacts @@ -231,9 +373,33 @@ let commit artifacts = (diagnostic ~code:Diagnostic.Artifact_state "artifact commit requires at least one writer") | _ -> ( + let directories = directory_effects artifacts in match close_all artifacts with | Error _ as error -> error | Ok () -> ( match publish_all [] artifacts with - | Error _ as error -> error - | Ok () -> cleanup_all artifacts)) + | Error diagnostic -> + Error (rollback_after_failure artifacts directories diagnostic) + | Ok () -> ( + match sync_directories directories with + | Error diagnostic -> + Error + (rollback_after_failure artifacts directories diagnostic) + | Ok () -> ( + match rename_all artifacts with + | Error diagnostic -> + Error + (restore_after_failure artifacts directories diagnostic) + | Ok () -> ( + match cleanup_all artifacts with + | Error diagnostic -> + Error + (restore_after_failure artifacts directories + diagnostic) + | Ok () -> ( + match sync_directories directories with + | Ok () -> Ok () + | Error diagnostic -> + Error + (restore_after_failure artifacts directories + diagnostic))))))) diff --git a/lib/artifact_writer.mli b/lib/artifact_writer.mli index 3207cf8..ae74cc6 100644 --- a/lib/artifact_writer.mli +++ b/lib/artifact_writer.mli @@ -1,9 +1,11 @@ (** Exclusive staged-file writer with a typed internal lifecycle. *) type t +type durability = Buffered | Durable val create : ?effects:Boundary_effects.t -> + ?durability:durability -> label:string -> string -> (t, Diagnostic.t) result diff --git a/lib/boundary_effects.ml b/lib/boundary_effects.ml index 0cce29c..9e8d983 100644 --- a/lib/boundary_effects.ml +++ b/lib/boundary_effects.ml @@ -2,10 +2,13 @@ type stage = | Artifact_create | Artifact_write | Artifact_flush + | Artifact_sync_file | Artifact_close | Artifact_publish + | Artifact_rename | Artifact_cleanup | Artifact_restore + | Artifact_sync_directory | Process_spawn | Process_exchange | Process_terminate @@ -15,10 +18,13 @@ type operation = | Create_artifact of string | Write_artifact of { channel : out_channel; contents : string } | Flush_artifact + | Sync_artifact of out_channel | Close_artifact | Publish_artifact of { partial_path : string; final_path : string } + | Rename_artifact of { source_path : string; target_path : string } | Cleanup_artifact of string | Restore_artifact of { final_path : string; partial_path : string } + | Sync_directory of string | Spawn_process | Exchange_process | Terminate_process @@ -35,10 +41,13 @@ let stage = function | Create_artifact _ -> Artifact_create | Write_artifact _ -> Artifact_write | Flush_artifact -> Artifact_flush + | Sync_artifact _ -> Artifact_sync_file | Close_artifact -> Artifact_close | Publish_artifact _ -> Artifact_publish + | Rename_artifact _ -> Artifact_rename | Cleanup_artifact _ -> Artifact_cleanup | Restore_artifact _ -> Artifact_restore + | Sync_directory _ -> Artifact_sync_directory | Spawn_process -> Process_spawn | Exchange_process -> Process_exchange | Terminate_process -> Process_terminate @@ -48,10 +57,13 @@ let stage_to_string = function | Artifact_create -> "artifact create" | Artifact_write -> "artifact write" | Artifact_flush -> "artifact flush" + | Artifact_sync_file -> "artifact file sync" | Artifact_close -> "artifact close" | Artifact_publish -> "artifact publish" + | Artifact_rename -> "artifact rename" | Artifact_cleanup -> "artifact cleanup" | Artifact_restore -> "artifact restore" + | Artifact_sync_directory -> "artifact directory sync" | Process_spawn -> "process spawn" | Process_exchange -> "process exchange" | Process_terminate -> "process terminate" diff --git a/lib/boundary_effects.mli b/lib/boundary_effects.mli index 04827c6..f9dc0cb 100644 --- a/lib/boundary_effects.mli +++ b/lib/boundary_effects.mli @@ -4,10 +4,13 @@ type stage = | Artifact_create | Artifact_write | Artifact_flush + | Artifact_sync_file | Artifact_close | Artifact_publish + | Artifact_rename | Artifact_cleanup | Artifact_restore + | Artifact_sync_directory | Process_spawn | Process_exchange | Process_terminate @@ -17,10 +20,13 @@ type operation = | Create_artifact of string | Write_artifact of { channel : out_channel; contents : string } | Flush_artifact + | Sync_artifact of out_channel | Close_artifact | Publish_artifact of { partial_path : string; final_path : string } + | Rename_artifact of { source_path : string; target_path : string } | Cleanup_artifact of string | Restore_artifact of { final_path : string; partial_path : string } + | Sync_directory of string | Spawn_process | Exchange_process | Terminate_process diff --git a/lib/external_replay.ml b/lib/external_replay.ml index e60bb7f..abb8639 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -120,9 +120,9 @@ let process_slice respond runner market_slice = in drive respond progress -let create_artifacts ~journal_path ~transcript_path = - let* journal = Journal.create journal_path in - match Strategy_transcript.create transcript_path with +let create_artifacts ~durability ~journal_path ~transcript_path = + let* journal = Journal.create ~durability journal_path in + match Strategy_transcript.create ~durability transcript_path with | Ok transcript -> Ok (journal, transcript) | Error _ as error -> Journal.close_preserving_partial journal; @@ -143,8 +143,9 @@ let commit_artifacts (journal, transcript) = Artifact_writer.commit [ Journal.artifact journal; Strategy_transcript.artifact transcript ] -let run ~env ~scenario_sha256 ~journal_path ~transcript_path ~strategy_command - ~strategy_timeout (scenario : Scenario.t) = +let run ?(durability = Artifact_writer.Buffered) ~env ~scenario_sha256 + ~journal_path ~transcript_path ~strategy_command ~strategy_timeout + (scenario : Scenario.t) = if scenario.schedule <> [] then Error (replay "external strategy replay requires an empty scenario schedule") @@ -157,7 +158,7 @@ let run ~env ~scenario_sha256 ~journal_path ~transcript_path ~strategy_command ~initial_cash:scenario.initial_cash in let* journal, transcript = - create_artifacts ~journal_path ~transcript_path + create_artifacts ~durability ~journal_path ~transcript_path in let journal_ref = Some journal in let session_result = @@ -263,8 +264,8 @@ let replay_stream_pass ~scenario_sha256 ~journal ~session channel = let* audit_count = add_audit_count state.audit_count events in Ok (runner, valuation, audit_count, slice_count)) -let run_stream ~env ~journal_path ~transcript_path ~strategy_command - ~strategy_timeout path = +let run_stream ?(durability = Artifact_writer.Buffered) ~env ~journal_path + ~transcript_path ~strategy_command ~strategy_timeout path = let artifacts_ref = ref None in let fail result = Option.iter close_artifacts !artifacts_ref; @@ -284,7 +285,7 @@ let run_stream ~env ~journal_path ~transcript_path ~strategy_command "scenario stream changed during validation") else let* journal, transcript = - create_artifacts ~journal_path ~transcript_path + create_artifacts ~durability ~journal_path ~transcript_path in artifacts_ref := Some (journal, transcript); let session_result = diff --git a/lib/external_replay.mli b/lib/external_replay.mli index 3fbb1df..90a71a5 100644 --- a/lib/external_replay.mli +++ b/lib/external_replay.mli @@ -21,6 +21,7 @@ type streamed_result = private { } val run : + ?durability:Artifact_writer.durability -> env:Eio_unix.Stdenv.base -> scenario_sha256:string -> journal_path:string -> @@ -31,6 +32,7 @@ val run : (result, Diagnostic.t) Stdlib.result val run_stream : + ?durability:Artifact_writer.durability -> env:Eio_unix.Stdenv.base -> journal_path:string -> transcript_path:string -> diff --git a/lib/journal.ml b/lib/journal.ml index 714cf18..2f07698 100644 --- a/lib/journal.ml +++ b/lib/journal.ml @@ -15,8 +15,8 @@ let audit_context event = in (event_id, order_id, causation_ids) -let create ?effects final_path = - Artifact_writer.create ?effects ~label:"journal" final_path +let create ?effects ?durability final_path = + Artifact_writer.create ?effects ?durability ~label:"journal" final_path |> Result.map (fun artifact -> { artifact }) let append journal event = diff --git a/lib/journal.mli b/lib/journal.mli index da22f03..cacb854 100644 --- a/lib/journal.mli +++ b/lib/journal.mli @@ -2,7 +2,12 @@ type t -val create : ?effects:Boundary_effects.t -> string -> (t, Diagnostic.t) result +val create : + ?effects:Boundary_effects.t -> + ?durability:Artifact_writer.durability -> + string -> + (t, Diagnostic.t) result + val append : t -> Audit.t -> (unit, Diagnostic.t) result val close_preserving_partial : t -> unit val commit : t -> (unit, Diagnostic.t) result diff --git a/lib/replay.ml b/lib/replay.ml index 343e72b..1a914f6 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -56,11 +56,12 @@ let add_audit_count count events = Error (replay "audit event count is exhausted") else Ok (Int64.add count added) -let run ~scenario_sha256 ?journal_path scenario = +let run ~scenario_sha256 ?journal_path ?(durability = Artifact_writer.Buffered) + scenario = let journal_result = match journal_path with | None -> Ok None - | Some path -> Journal.create path |> Result.map Option.some + | Some path -> Journal.create ~durability path |> Result.map Option.some in match journal_result with | Error _ as error -> error @@ -215,7 +216,7 @@ let run_stream_pass ~scenario_sha256 ~journal channel = slice_count; }))) -let run_stream ?journal_path path = +let run_stream ?journal_path ?(durability = Artifact_writer.Buffered) path = let journal = ref None in let fail result = Option.iter Journal.close_preserving_partial !journal; @@ -239,7 +240,7 @@ let run_stream ?journal_path path = match journal_path with | None -> Ok validated | Some path -> ( - match Journal.create path with + match Journal.create ~durability path with | Error _ as error -> error | Ok created -> ( journal := Some created; diff --git a/lib/replay.mli b/lib/replay.mli index 8a6447a..d460cde 100644 --- a/lib/replay.mli +++ b/lib/replay.mli @@ -23,10 +23,12 @@ type streamed_result = private { val run : scenario_sha256:string -> ?journal_path:string -> + ?durability:Artifact_writer.durability -> Scenario.t -> (result, Diagnostic.t) Stdlib.result val run_stream : ?journal_path:string -> + ?durability:Artifact_writer.durability -> string -> (streamed_result, Diagnostic.t) Stdlib.result diff --git a/lib/strategy_process.ml b/lib/strategy_process.ml index 7827fb9..beeb754 100644 --- a/lib/strategy_process.ml +++ b/lib/strategy_process.ml @@ -423,12 +423,13 @@ let with_staged_session ?(effects = Boundary_effects.direct) ~env ~command run_session ~effects ~env ~command ~executable ~timeout ~transcript ~initialization use -let with_session ?(effects = Boundary_effects.direct) ~env ~command ~timeout +let with_session ?(effects = Boundary_effects.direct) + ?(durability = Artifact_writer.Buffered) ~env ~command ~timeout ~transcript_path ~initialization use = match validate_configuration ~command ~timeout with | Error _ as error -> error | Ok executable -> ( - match Strategy_transcript.create ~effects transcript_path with + match Strategy_transcript.create ~effects ~durability transcript_path with | Error _ as error -> error | Ok transcript -> ( let fail result = diff --git a/lib/strategy_process.mli b/lib/strategy_process.mli index d236fc5..9971c09 100644 --- a/lib/strategy_process.mli +++ b/lib/strategy_process.mli @@ -14,6 +14,7 @@ val with_staged_session : val with_session : ?effects:Boundary_effects.t -> + ?durability:Artifact_writer.durability -> env:Eio_unix.Stdenv.base -> command:string list -> timeout:float -> diff --git a/lib/strategy_transcript.ml b/lib/strategy_transcript.ml index b46cbf4..4c8d718 100644 --- a/lib/strategy_transcript.ml +++ b/lib/strategy_transcript.ml @@ -3,8 +3,9 @@ type t = { artifact : Artifact_writer.t; mutable next_sequence : int64 } let diagnostic ?sequence ~code message = Diagnostic.make ?sequence ~code ~phase:Diagnostic.Artifact message -let create ?effects final_path = - Artifact_writer.create ?effects ~label:"strategy transcript" final_path +let create ?effects ?durability final_path = + Artifact_writer.create ?effects ?durability ~label:"strategy transcript" + final_path |> Result.map (fun artifact -> { artifact; next_sequence = 1L }) let append transcript ~direction message = diff --git a/lib/strategy_transcript.mli b/lib/strategy_transcript.mli index de95797..55f6998 100644 --- a/lib/strategy_transcript.mli +++ b/lib/strategy_transcript.mli @@ -2,7 +2,11 @@ type t -val create : ?effects:Boundary_effects.t -> string -> (t, Diagnostic.t) result +val create : + ?effects:Boundary_effects.t -> + ?durability:Artifact_writer.durability -> + string -> + (t, Diagnostic.t) result val append : t -> diff --git a/test/cli.t b/test/cli.t index 84696dd..67b946e 100644 --- a/test/cli.t +++ b/test/cli.t @@ -10,7 +10,7 @@ $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v4/fixtures/demo.scenario.jsonl valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=6afe9bbda482265cfa24c35167150f02eea1a457aa5025143f3556b8046ae91b - $ ../bin/main.exe --input-format jsonl --input ../contracts/v4/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl + $ ../bin/main.exe --input-format jsonl --input ../contracts/v4/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts run=demo audits=20 orders=3 active=0 filled=2 rejected=0 cash=9739.76812 equity=10004.76812 gross=265 realized=1.419136 unrealized=3.348984 fees=2.50188 journal=streamed.journal.jsonl @@ -47,13 +47,17 @@ $ test ! -e validation.journal.jsonl + $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v4/fixtures/demo.scenario.json + trading-engine: --durable-artifacts cannot be used with --validate-only + [123] + $ ../bin/main.exe --input ../contracts/v4/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 trading-engine: --strategy-arg, --strategy-timeout, and --strategy-transcript require --strategy-executable [123] $ test ! -e ignored.journal.jsonl $ mkdir external - $ ../bin/main.exe --input ../contracts/strategy/v3/fixtures/external.scenario.json --journal external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external/run.strategy.jsonl --strategy-timeout 5 + $ ../bin/main.exe --input ../contracts/strategy/v3/fixtures/external.scenario.json --journal external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external/run.strategy.jsonl --strategy-timeout 5 --durable-artifacts run=external-demo audits=10 orders=1 active=0 filled=1 rejected=0 cash=9794 equity=10008 gross=214 realized=0 unrealized=8 fees=0 journal=external/run.journal.jsonl diff --git a/test/test_boundary_failures.ml b/test/test_boundary_failures.ml index 9b5dd26..7ed657d 100644 --- a/test/test_boundary_failures.ml +++ b/test/test_boundary_failures.ml @@ -11,7 +11,8 @@ let with_absent_path suffix test = Fun.protect ~finally:(fun () -> remove_if_exists path; - remove_if_exists (path ^ ".partial")) + remove_if_exists (path ^ ".partial"); + remove_if_exists (path ^ ".partial.cleanup")) (fun () -> test path) let injected_effects target = @@ -86,6 +87,7 @@ let artifact_stages = Artifact_flush; Artifact_close; Artifact_publish; + Artifact_rename; Artifact_cleanup; ] @@ -102,12 +104,14 @@ let exercise_artifact_failure (type writer) writer_name match stage with | T.Boundary_effects.Artifact_write | Artifact_flush -> Writer.append writer - | Artifact_close | Artifact_publish | Artifact_cleanup -> ( + | Artifact_close | Artifact_publish | Artifact_rename + | Artifact_cleanup -> ( match Writer.append writer with | Error _ as error -> error | Ok () -> Writer.commit writer) | Artifact_create -> Alcotest.fail "create failure was not injected" - | Artifact_restore -> Alcotest.fail "expected a lifecycle failure" + | Artifact_sync_file | Artifact_restore | Artifact_sync_directory -> + Alcotest.fail "expected a lifecycle failure" | Process_spawn | Process_exchange | Process_terminate | Process_reap -> Alcotest.fail "expected an artifact stage" @@ -122,6 +126,7 @@ let exercise_artifact_failure (type writer) writer_name (T.Diagnostic.code_to_string diagnostic.code); let expected_final = stage = T.Boundary_effects.Artifact_publish + || stage = T.Boundary_effects.Artifact_rename || stage = T.Boundary_effects.Artifact_cleanup in let expected_partial = stage <> T.Boundary_effects.Artifact_create in @@ -133,6 +138,10 @@ let exercise_artifact_failure (type writer) writer_name (writer_name ^ " partial invariant") expected_partial (Sys.file_exists partial_path); + Alcotest.(check bool) + (writer_name ^ " cleanup path invariant") + false + (Sys.file_exists (partial_path ^ ".cleanup")); if stage = T.Boundary_effects.Artifact_write then Alcotest.(check int64) "short write retained" 8L @@ -141,7 +150,10 @@ let exercise_artifact_failure (type writer) writer_name Alcotest.(check string) "publication rival preserved" "rival\n" (In_channel.with_open_bin final_path In_channel.input_all); - if stage = T.Boundary_effects.Artifact_cleanup then + if + stage = T.Boundary_effects.Artifact_rename + || stage = T.Boundary_effects.Artifact_cleanup + then Alcotest.(check string) "published and partial bytes agree" (In_channel.with_open_bin partial_path In_channel.input_all) @@ -184,18 +196,24 @@ let exercise_transaction_failure stage occurrence = T.Artifact_writer.append journal "journal\n" |> ok; T.Artifact_writer.append transcript "transcript\n" |> ok; let diagnostic = T.Artifact_writer.commit [ journal; transcript ] |> error in - Alcotest.(check int) "target occurrence reached" occurrence !seen; + Alcotest.(check bool) "target occurrence reached" true (!seen >= occurrence); Alcotest.(check string) "artifact diagnostic" "artifact.io" (T.Diagnostic.code_to_string diagnostic.code); - let finals_exist = stage = T.Boundary_effects.Artifact_cleanup in + let finals_exist = + stage = T.Boundary_effects.Artifact_rename + || stage = T.Boundary_effects.Artifact_cleanup + in List.iter (fun path -> Alcotest.(check bool) "final-set invariant" finals_exist (Sys.file_exists path); Alcotest.(check bool) "partial-set invariant" true - (Sys.file_exists (path ^ ".partial"))) + (Sys.file_exists (path ^ ".partial")); + Alcotest.(check bool) + "cleanup-set invariant" false + (Sys.file_exists (path ^ ".partial.cleanup"))) [ journal_path; transcript_path ]; if finals_exist then List.iter @@ -218,7 +236,83 @@ let transaction_cases = `Quick (fun () -> exercise_transaction_failure stage occurrence)) [ 1; 2 ]) - T.Boundary_effects.[ Artifact_close; Artifact_publish; Artifact_cleanup ] + T.Boundary_effects. + [ Artifact_close; Artifact_publish; Artifact_rename; Artifact_cleanup ] + +let create_durable_artifacts effects journal_path transcript_path = + let create label path = + T.Artifact_writer.create ~effects ~durability:T.Artifact_writer.Durable + ~label path + |> ok + in + let journal = create "journal" journal_path in + let transcript = create "strategy transcript" transcript_path in + T.Artifact_writer.append journal "journal\n" |> ok; + T.Artifact_writer.append transcript "transcript\n" |> ok; + (journal, transcript) + +let exercise_durability_failure stage occurrence = + with_absent_path ".durable-journal.jsonl" @@ fun journal_path -> + with_absent_path ".durable-strategy.jsonl" @@ fun transcript_path -> + let effects, seen = nth_failure stage occurrence in + let journal, transcript = + create_durable_artifacts effects journal_path transcript_path + in + let diagnostic = T.Artifact_writer.commit [ journal; transcript ] |> error in + Alcotest.(check bool) "target occurrence reached" true (!seen >= occurrence); + Alcotest.(check string) + "durability diagnostic" "artifact.io" + (T.Diagnostic.code_to_string diagnostic.code); + let finals_exist = + stage = T.Boundary_effects.Artifact_sync_directory && occurrence = 2 + in + List.iter + (fun path -> + Alcotest.(check bool) + "durable final-set invariant" finals_exist (Sys.file_exists path); + Alcotest.(check bool) + "durable partial-set invariant" true + (Sys.file_exists (path ^ ".partial")); + Alcotest.(check bool) + "durable cleanup-set invariant" false + (Sys.file_exists (path ^ ".partial.cleanup"))) + [ journal_path; transcript_path ] + +let durable_transaction_succeeds () = + with_absent_path ".durable-journal.jsonl" @@ fun journal_path -> + with_absent_path ".durable-strategy.jsonl" @@ fun transcript_path -> + let journal, transcript = + create_durable_artifacts T.Boundary_effects.direct journal_path + transcript_path + in + T.Artifact_writer.commit [ journal; transcript ] |> ok; + List.iter + (fun path -> + Alcotest.(check bool) "durable final exists" true (Sys.file_exists path); + Alcotest.(check bool) + "durable partial removed" false + (Sys.file_exists (path ^ ".partial")); + Alcotest.(check bool) + "durable cleanup path removed" false + (Sys.file_exists (path ^ ".partial.cleanup")); + Alcotest.(check int) + "private artifact mode" 0o600 + ((Unix.stat path).st_perm land 0o777)) + [ journal_path; transcript_path ] + +let durability_cases = + [ + Alcotest.test_case "durable file sync 1" `Quick (fun () -> + exercise_durability_failure T.Boundary_effects.Artifact_sync_file 1); + Alcotest.test_case "durable file sync 2" `Quick (fun () -> + exercise_durability_failure T.Boundary_effects.Artifact_sync_file 2); + Alcotest.test_case "durable publication directory sync" `Quick (fun () -> + exercise_durability_failure T.Boundary_effects.Artifact_sync_directory 1); + Alcotest.test_case "durable cleanup directory sync" `Quick (fun () -> + exercise_durability_failure T.Boundary_effects.Artifact_sync_directory 2); + Alcotest.test_case "durable transaction succeeds" `Quick + durable_transaction_succeeds; + ] let initialization () = let instrument = instrument () in @@ -270,4 +364,4 @@ let process_cases = let tests = artifact_cases "journal" (module Journal_writer) @ artifact_cases "transcript" (module Transcript_writer) - @ transaction_cases @ process_cases + @ transaction_cases @ durability_cases @ process_cases From fcee24aff2d981e5b32cdf52fdb58959eeeaf665 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 18:38:06 -0400 Subject: [PATCH 11/57] feat: publish runtime resource limits --- README.md | 7 +- contracts/strategy/v3/message.schema.json | 2 + contracts/v3/scenario-stream.schema.json | 6 +- contracts/v3/scenario.schema.json | 5 +- contracts/v4/scenario-stream.schema.json | 6 +- contracts/v4/scenario.schema.json | 5 +- docs/architecture.md | 5 +- docs/diagnostics.md | 1 + docs/scenario.md | 9 +- lib/artifact_writer.ml | 26 ++- lib/contract.ml | 1 + lib/diagnostic.ml | 2 + lib/diagnostic.mli | 1 + lib/engine.ml | 8 +- lib/external_replay.ml | 24 ++- lib/replay.ml | 116 +++++------ lib/resource_limits.ml | 19 ++ lib/resource_limits.mli | 13 ++ lib/scenario.ml | 185 +++++++++++++----- lib/scenario_stream.ml | 223 ++++++++++++++-------- lib/scenario_stream.mli | 2 + lib/strategy_process.ml | 42 +++- lib/strategy_process.mli | 6 + lib/strategy_protocol.ml | 41 +++- test/cli.t | 2 +- test/test_boundary_failures.ml | 25 +++ test/test_diagnostic.ml | 30 +++ test/test_scenario.ml | 142 +++++++++++++- test/test_strategy_protocol.ml | 91 ++++++++- test/validate_schemas.py | 19 +- test/validate_strategy_schema.py | 9 + 31 files changed, 826 insertions(+), 247 deletions(-) create mode 100644 lib/resource_limits.ml create mode 100644 lib/resource_limits.mli diff --git a/README.md b/README.md index a24ecec..0258ddd 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,9 @@ opam exec -- dune exec trading-engine -- --capabilities Clients must confirm that both `scenario_contract_versions` and `journal_contract_versions` contain the scenario's `contract_version` before starting a replay. External clients must also -require their version in `strategy_protocol_versions`. Runtime failures can use the structured +require their version in `strategy_protocol_versions`. The versioned `resource_limits` object +publishes inclusive limits for scenario records, strategy messages, reducer feedback, catalogs, +intent batches, and artifact records. Runtime failures can use the structured diagnostic contract identified by each diagnostic's `diagnostic_version`. Human diagnostics remain the default. Use `--diagnostic-format json` to receive one JSON diagnostic on standard error with a stable code, phase, typed context, and sanitized underlying cause. @@ -142,6 +144,9 @@ CLI binds that exact-byte hash into the journal. It writes to the partial path a requested path only after `run_completed` is fully written and the partial file is closed. An error preserves the partial artifact for diagnosis. +Journal and transcript records are limited to 2 MiB each, including the terminating line feed. +Limit failures use the stable `resource.limit` diagnostic code. + Pass `--durable-artifacts` to synchronize each staged file before publication and synchronize each containing directory after final links and partial cleanup. The default buffered mode flushes every record but does not make a restart-durability claim. diff --git a/contracts/strategy/v3/message.schema.json b/contracts/strategy/v3/message.schema.json index b9a293f..75e0ce4 100644 --- a/contracts/strategy/v3/message.schema.json +++ b/contracts/strategy/v3/message.schema.json @@ -57,6 +57,7 @@ "instruments": { "type": "array", "minItems": 1, + "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/instrument" } }, "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/risk" }, @@ -228,6 +229,7 @@ "properties": { "intents": { "type": "array", + "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/intent" } } } diff --git a/contracts/v3/scenario-stream.schema.json b/contracts/v3/scenario-stream.schema.json index 913ff1d..bd10528 100644 --- a/contracts/v3/scenario-stream.schema.json +++ b/contracts/v3/scenario-stream.schema.json @@ -56,10 +56,10 @@ "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, "initial_cash": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/cashBalance" } }, - "instruments": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/instrument" } }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/instrument" } }, "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/risk" }, "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/execution" }, - "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 4611686018427387903 } + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 } } }, "slicePayload": { @@ -68,7 +68,7 @@ "required": ["market_slice", "intents"], "properties": { "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/intent" } } + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/intent" } } } } } diff --git a/contracts/v3/scenario.schema.json b/contracts/v3/scenario.schema.json index ec76093..8d90cfe 100644 --- a/contracts/v3/scenario.schema.json +++ b/contracts/v3/scenario.schema.json @@ -19,11 +19,12 @@ "instruments": { "type": "array", "minItems": 1, + "maxItems": 4096, "items": { "$ref": "#/$defs/instrument" } }, "risk": { "$ref": "#/$defs/risk" }, "execution": { "$ref": "#/$defs/execution" }, - "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 4611686018427387903 }, + "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.", @@ -108,7 +109,7 @@ "required": ["after_slice_sequence", "intents"], "properties": { "after_slice_sequence": { "$ref": "#/$defs/sequence" }, - "intents": { "type": "array", "items": { "$ref": "#/$defs/intent" } } + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "#/$defs/intent" } } } }, "intent": { diff --git a/contracts/v4/scenario-stream.schema.json b/contracts/v4/scenario-stream.schema.json index ff9e62c..c1da40a 100644 --- a/contracts/v4/scenario-stream.schema.json +++ b/contracts/v4/scenario-stream.schema.json @@ -56,10 +56,10 @@ "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/identifier" }, "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/identifier" }, "initial_cash": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/cashBalance" } }, - "instruments": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/instrument" } }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/instrument" } }, "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/risk" }, "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/execution" }, - "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 4611686018427387903 } + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 } } }, "slicePayload": { @@ -68,7 +68,7 @@ "required": ["market_slice", "intents"], "properties": { "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/intent" } } + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/intent" } } } } } diff --git a/contracts/v4/scenario.schema.json b/contracts/v4/scenario.schema.json index 577db41..d3aea43 100644 --- a/contracts/v4/scenario.schema.json +++ b/contracts/v4/scenario.schema.json @@ -19,11 +19,12 @@ "instruments": { "type": "array", "minItems": 1, + "maxItems": 4096, "items": { "$ref": "#/$defs/instrument" } }, "risk": { "$ref": "#/$defs/risk" }, "execution": { "$ref": "#/$defs/execution" }, - "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 4611686018427387903 }, + "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.", @@ -108,7 +109,7 @@ "required": ["after_slice_sequence", "intents"], "properties": { "after_slice_sequence": { "$ref": "#/$defs/sequence" }, - "intents": { "type": "array", "items": { "$ref": "#/$defs/intent" } } + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "#/$defs/intent" } } } }, "intent": { diff --git a/docs/architecture.md b/docs/architecture.md index e2e85ad..c2c287b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -117,8 +117,9 @@ maps, or audit events. A required terminal record distinguishes completion from External replay requires an empty batch schedule or empty streamed intent batches. The effectful supervisor launches an explicit argument vector, permits one request at a time, enforces a -per-exchange timeout and 1 MiB response limit, then requires a clean child exit with no extra -standard output. It records every accepted request and response in sequence. Failures preserve +per-exchange timeout and 1 MiB message limit, then requires a clean child exit with no extra +standard output. A response may contain at most 4,096 intents. It records every accepted request +and response in sequence. Failures preserve the transcript and journal partials. After both writers close, publication links every final path without replacement before moving any partial path to a reserved cleanup name. Only after every move succeeds does the transaction unlink those cleanup names. A close or link failure rolls back diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 2de7ffe..6a4537c 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -31,6 +31,7 @@ Version 1 defines these codes: | `scenario.unsupported_contract` | Scenario contract version is unsupported | | `scenario_stream.invalid` | Stream envelope, ordering, or payload validation failed | | `scenario_stream.changed` | Stream bytes changed between validation and replay | +| `resource.limit` | A versioned parser, protocol, reducer, or artifact limit was exceeded | | `replay.failed` | Replay orchestration invariant failed | | `reducer.failed` | Pure engine processing rejected the requested transition | | `strategy.invalid_configuration` | Strategy command or timeout is invalid | diff --git a/docs/scenario.md b/docs/scenario.md index 98065d1..3cefc05 100644 --- a/docs/scenario.md +++ b/docs/scenario.md @@ -42,11 +42,11 @@ retains current account, order, target, and latest-bar state required by executi | `run_id` | Stable identity used in generated IDs | | `base_currency` | Reporting currency used for aggregate risk and valuation | | `initial_cash` | One explicit nonnegative balance for every scenario currency | -| `instruments` | Approved executable-instrument catalog | +| `instruments` | Approved executable-instrument catalog, at most 4,096 entries | | `risk` | Signed position, exposure, leverage, margin, and borrow policy | | `execution` | Capacity and fee configuration | -| `max_internal_events` | Positive reducer feedback cap, at most `4611686018427387903` | -| `schedule` | Intents emitted after named slices | +| `max_internal_events` | Positive reducer feedback cap, at most 100,000 | +| `schedule` | Intents emitted after named slices, at most 4,096 per batch | | `slices` | Complete synchronized market observations | Metadata may contain nested JSON values. Duplicate object keys and non-finite numbers are rejected @@ -56,6 +56,9 @@ An external strategy replay requires `schedule: []`. The JSON Lines form likewis slice record's `intents` array to be empty. This keeps one authoritative decision source: either the scenario contract or the separate strategy protocol, never both. +Each JSON Lines record is limited to 1 MiB, excluding its line feed. The reader accepts a final +record without a line feed and drains an oversized record without retaining bytes above the limit. + ## Instruments, risk, and execution Each instrument contains `instrument_id`, `symbol`, `quote_currency`, `tick_size`, and `lot_size`. diff --git a/lib/artifact_writer.ml b/lib/artifact_writer.ml index bf4d2b3..efc6994 100644 --- a/lib/artifact_writer.ml +++ b/lib/artifact_writer.ml @@ -114,15 +114,23 @@ let append artifact contents = (diagnostic ~code:Diagnostic.Artifact_state ("cannot append to a closed " ^ state_label artifact.state)) | Open file -> ( - try - Boundary_effects.perform file.effects - (Boundary_effects.Write_artifact { channel = file.channel; contents }) - (fun () -> output_string file.channel contents); - Boundary_effects.perform file.effects Boundary_effects.Flush_artifact - (fun () -> flush file.channel); - Ok () - with exception_ -> - Error (exception_diagnostic ~label:file.label "append" exception_)) + if String.length contents > Resource_limits.artifact_record_bytes then + Error + (diagnostic ~code:Diagnostic.Resource_limit + (Printf.sprintf "%s record is %d bytes; limit is %d bytes" + file.label (String.length contents) + Resource_limits.artifact_record_bytes)) + else + try + Boundary_effects.perform file.effects + (Boundary_effects.Write_artifact + { channel = file.channel; contents }) + (fun () -> output_string file.channel contents); + Boundary_effects.perform file.effects Boundary_effects.Flush_artifact + (fun () -> flush file.channel); + Ok () + with exception_ -> + Error (exception_diagnostic ~label:file.label "append" exception_)) let close_file artifact file = let closed = transition file in diff --git a/lib/contract.ml b/lib/contract.ml index 6ac079b..d943247 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -16,6 +16,7 @@ let capabilities_to_yojson () = ("journal_formats", strings [ "jsonl" ]); ("execution_models", strings Execution_model.supported); ("strategy_protocol_versions", strings [ strategy_protocol_version ]); + ("resource_limits", Resource_limits.to_yojson ()); ] let capabilities_to_string () = diff --git a/lib/diagnostic.ml b/lib/diagnostic.ml index 9289165..45dfb95 100644 --- a/lib/diagnostic.ml +++ b/lib/diagnostic.ml @@ -8,6 +8,7 @@ type code = | Scenario_unsupported_contract | Scenario_stream_invalid | Scenario_stream_changed + | Resource_limit | Replay_failed | Reducer_failed | Strategy_invalid_configuration @@ -53,6 +54,7 @@ let code_to_string = function | Scenario_unsupported_contract -> "scenario.unsupported_contract" | Scenario_stream_invalid -> "scenario_stream.invalid" | Scenario_stream_changed -> "scenario_stream.changed" + | Resource_limit -> "resource.limit" | Replay_failed -> "replay.failed" | Reducer_failed -> "reducer.failed" | Strategy_invalid_configuration -> "strategy.invalid_configuration" diff --git a/lib/diagnostic.mli b/lib/diagnostic.mli index a1371f1..2358502 100644 --- a/lib/diagnostic.mli +++ b/lib/diagnostic.mli @@ -10,6 +10,7 @@ type code = | Scenario_unsupported_contract | Scenario_stream_invalid | Scenario_stream_changed + | Resource_limit | Replay_failed | Reducer_failed | Strategy_invalid_configuration diff --git a/lib/engine.ml b/lib/engine.ml index 9739914..a70c868 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -12,6 +12,10 @@ let config ~contract_version ~risk ~execution_model ~execution Error "engine contract version is unsupported" else if max_internal_events <= 0 then Error "maximum internal events must be positive" + else if max_internal_events > Resource_limits.internal_events then + Error + (Printf.sprintf "maximum internal events is %d; limit is %d" + max_internal_events Resource_limits.internal_events) else Ok { @@ -778,7 +782,9 @@ module Interactive = struct | [] -> Ok (Drained reduction) | _ when reduction.processed >= reduction.state.config.max_internal_events -> - Error "maximum internal event count exceeded" + Error + (Printf.sprintf "internal event count exceeds configured limit of %d" + reduction.state.config.max_internal_events) | item :: pending -> ( let reduction = { reduction with pending; processed = reduction.processed + 1 } diff --git a/lib/external_replay.ml b/lib/external_replay.ml index abb8639..583f248 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -40,7 +40,16 @@ let replay ?sequence message = ~phase:Diagnostic.Replay message let reducer_result ?sequence result = - Result.map_error (reducer ?sequence) result + Result.map_error + (fun message -> + if + String.starts_with + ~prefix:"internal event count exceeds configured limit" message + then + Diagnostic.make ?sequence ~code:Diagnostic.Resource_limit + ~phase:Diagnostic.Reducer message + else reducer ?sequence message) + result let ( let* ) result function_ = match result with Ok value -> function_ value | Error _ as error -> error @@ -150,6 +159,11 @@ let run ?(durability = Artifact_writer.Buffered) ~env ~scenario_sha256 Error (replay "external strategy replay requires an empty scenario schedule") else + let initialization = initialization_of_scenario ~scenario_sha256 scenario in + let* () = + Strategy_process.validate_configuration ~command:strategy_command + ~timeout:strategy_timeout ~initialization + in let* initial = create_runner ~contract_version:scenario.contract_version ~run_id:scenario.run_id ~scenario_sha256 ~risk:scenario.risk @@ -164,9 +178,7 @@ let run ?(durability = Artifact_writer.Buffered) ~env ~scenario_sha256 let session_result = protect_artifacts (journal, transcript) @@ fun () -> Strategy_process.with_staged_session ~env ~command:strategy_command - ~timeout:strategy_timeout ~transcript - ~initialization:(initialization_of_scenario ~scenario_sha256 scenario) - (fun session -> + ~timeout:strategy_timeout ~transcript ~initialization (fun session -> let respond = Strategy_process.on_event session in let step result market_slice = let* state, audits_rev = result in @@ -284,6 +296,10 @@ let run_stream ?(durability = Artifact_writer.Buffered) ~env ~journal_path ~phase:Diagnostic.Input "scenario stream changed during validation") else + let* () = + Strategy_process.validate_configuration ~command:strategy_command + ~timeout:strategy_timeout ~initialization:validated.initialization + in let* journal, transcript = create_artifacts ~durability ~journal_path ~transcript_path in diff --git a/lib/replay.ml b/lib/replay.ml index 1a914f6..37e5991 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -37,7 +37,19 @@ let replay ?sequence message = ~phase:Diagnostic.Replay message let reducer_result ?sequence result = - Result.map_error (reducer ?sequence) result + Result.map_error + (fun message -> + if + String.starts_with + ~prefix:"internal event count exceeds configured limit" message + then + Diagnostic.make ?sequence ~code:Diagnostic.Resource_limit + ~phase:Diagnostic.Reducer message + else reducer ?sequence message) + result + +let ( let* ) result function_ = + match result with Ok value -> function_ value | Error _ as error -> error let append_events journal events = match journal with @@ -58,6 +70,21 @@ let add_audit_count count events = let run ~scenario_sha256 ?journal_path ?(durability = Artifact_writer.Buffered) scenario = + let* strategy_state = + Scripted_strategy.create scenario.Scenario.schedule |> reducer_result + in + let* config = + Engine.config ~contract_version:scenario.contract_version + ~risk:scenario.risk ~execution_model:scenario.execution_model + ~execution:scenario.execution + ~max_internal_events:scenario.max_internal_events + |> reducer_result + in + let* initial = + Runner.create ~run_id:scenario.run_id ~scenario_sha256 ~config + ~initial_cash:scenario.initial_cash ~strategy_state + |> reducer_result + in let journal_result = match journal_path with | None -> Ok None @@ -78,66 +105,41 @@ let run ~scenario_sha256 ?journal_path ?(durability = Artifact_writer.Buffered) | Ok () -> result | Error _ as error -> error) in - match - Scripted_strategy.create scenario.Scenario.schedule |> reducer_result - with + let step result market_slice = + match result with + | Error _ as error -> error + | Ok (state, audits_rev) -> ( + match + Runner.process_slice state market_slice + |> reducer_result + ~sequence:market_slice.Market_slice.slice_sequence + with + | Error _ as error -> error + | Ok (state, events) -> ( + match append_events journal events with + | Error _ as error -> error + | Ok () -> Ok (state, List.rev_append events audits_rev))) + in + match List.fold_left step (Ok (initial, [])) scenario.slices with | Error _ as error -> fail error - | Ok strategy_state -> ( - match - Engine.config ~contract_version:scenario.contract_version - ~risk:scenario.risk ~execution_model:scenario.execution_model - ~execution:scenario.execution - ~max_internal_events:scenario.max_internal_events - |> reducer_result - with + | Ok (state, audits_rev) -> ( + match Runner.complete state |> reducer_result with | Error _ as error -> fail error - | Ok config -> ( - match - Runner.create ~run_id:scenario.run_id ~scenario_sha256 ~config - ~initial_cash:scenario.initial_cash ~strategy_state - |> reducer_result - with + | Ok (state, valuation, completion_events) -> ( + match append_events journal completion_events with | Error _ as error -> fail error - | Ok initial -> ( - let step result market_slice = - match result with - | Error _ as error -> error - | Ok (state, audits_rev) -> ( - match - Runner.process_slice state market_slice - |> reducer_result - ~sequence: - market_slice.Market_slice.slice_sequence - with - | Error _ as error -> error - | Ok (state, events) -> ( - match append_events journal events with - | Error _ as error -> error - | Ok () -> - Ok (state, List.rev_append events audits_rev))) + | Ok () -> + let audits_rev = + List.rev_append completion_events audits_rev in - match - List.fold_left step (Ok (initial, [])) scenario.slices - with - | Error _ as error -> fail error - | Ok (state, audits_rev) -> ( - match Runner.complete state |> reducer_result with - | Error _ as error -> fail error - | Ok (state, valuation, completion_events) -> ( - match append_events journal completion_events with - | Error _ as error -> fail error - | Ok () -> - let audits_rev = - List.rev_append completion_events audits_rev - in - succeed - (Ok - { - account = Runner.account state; - orders = Oms.orders (Runner.oms state); - valuation; - audits = List.rev audits_rev; - }))))))) + succeed + (Ok + { + account = Runner.account state; + orders = Oms.orders (Runner.oms state); + valuation; + audits = List.rev audits_rev; + })))) let run_stream_pass ~scenario_sha256 ~journal channel = Scenario_stream.fold_channel channel diff --git a/lib/resource_limits.ml b/lib/resource_limits.ml new file mode 100644 index 0000000..e873a82 --- /dev/null +++ b/lib/resource_limits.ml @@ -0,0 +1,19 @@ +let version = "1" +let scenario_record_bytes = 1_048_576 +let strategy_message_bytes = 1_048_576 +let internal_events = 100_000 +let catalog_instruments = 4_096 +let intents_per_batch = 4_096 +let artifact_record_bytes = 2_097_152 + +let to_yojson () = + `Assoc + [ + ("version", `String version); + ("scenario_record_bytes", `Int scenario_record_bytes); + ("strategy_message_bytes", `Int strategy_message_bytes); + ("internal_events", `Int internal_events); + ("catalog_instruments", `Int catalog_instruments); + ("intents_per_batch", `Int intents_per_batch); + ("artifact_record_bytes", `Int artifact_record_bytes); + ] diff --git a/lib/resource_limits.mli b/lib/resource_limits.mli new file mode 100644 index 0000000..e0f4f78 --- /dev/null +++ b/lib/resource_limits.mli @@ -0,0 +1,13 @@ +(** Versioned inclusive limits for boundary and reducer resources. + + Scenario record bytes exclude the line feed. Artifact record bytes include + it. *) + +val version : string +val scenario_record_bytes : int +val strategy_message_bytes : int +val internal_events : int +val catalog_instruments : int +val intents_per_batch : int +val artifact_record_bytes : int +val to_yojson : unit -> Yojson.Safe.t diff --git a/lib/scenario.ml b/lib/scenario.ml index 13aeac8..53466ae 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -38,6 +38,67 @@ module String_set = Set.Make (String) let ( let* ) result function_ = match result with Ok value -> function_ value | Error _ as error -> error +let resource_limit ~json_path ~name ~observed ~allowed = + Diagnostic.make ~code:Diagnostic.Resource_limit ~phase:Diagnostic.Validation + ~json_path + (Printf.sprintf "%s count is %d; limit is %d" name observed allowed) + +let check_list_limit fields field_name ~json_path ~name allowed = + match List.assoc_opt field_name fields with + | Some (`List values) when List.length values > allowed -> + Error + (resource_limit ~json_path ~name ~observed:(List.length values) ~allowed) + | _ -> Ok () + +let check_internal_event_limit fields ~json_path = + match List.assoc_opt "max_internal_events" fields with + | Some (`Int observed) when observed > Resource_limits.internal_events -> + Error + (resource_limit ~json_path ~name:"internal event" ~observed + ~allowed:Resource_limits.internal_events) + | _ -> Ok () + +let check_batch_limits = function + | `Assoc fields -> ( + let* () = + check_list_limit fields "instruments" ~json_path:"$.instruments" + ~name:"catalog instrument" Resource_limits.catalog_instruments + in + let* () = + check_internal_event_limit fields ~json_path:"$.max_internal_events" + in + match List.assoc_opt "schedule" fields with + | Some (`List items) -> + let rec check index = function + | [] -> Ok () + | `Assoc item_fields :: remaining -> + let* () = + check_list_limit item_fields "intents" + ~json_path:(Printf.sprintf "$.schedule[%d].intents" index) + ~name:"intent" Resource_limits.intents_per_batch + in + check (index + 1) remaining + | _ :: remaining -> check (index + 1) remaining + in + check 0 items + | _ -> Ok ()) + | _ -> Ok () + +let check_stream_header_limits = function + | `Assoc fields -> + let* () = + check_list_limit fields "instruments" ~json_path:"$.instruments" + ~name:"catalog instrument" Resource_limits.catalog_instruments + in + check_internal_event_limit fields ~json_path:"$.max_internal_events" + | _ -> Ok () + +let check_stream_item_limits = function + | `Assoc fields -> + check_list_limit fields "intents" ~json_path:"$.intents" ~name:"intent" + Resource_limits.intents_per_batch + | _ -> Ok () + let object_fields ~name ~expected = function | `Assoc fields -> let names = List.map fst fields in @@ -384,8 +445,13 @@ let parse_schedule_item json = let* sequence = parse_int64 ~name:"after_slice_sequence" sequence_json in let* intents_json = field fields "intents" in let* intents_json = list ~name:"intents" intents_json in - let* intents = map_list parse_intent intents_json in - Ok (sequence, intents) + if List.length intents_json > Resource_limits.intents_per_batch then + Error + (Printf.sprintf "intent count is %d; limit is %d" + (List.length intents_json) Resource_limits.intents_per_batch) + else + let* intents = map_list parse_intent intents_json in + Ok (sequence, intents) let parse_volume = function | `Null -> Ok None @@ -813,63 +879,73 @@ let of_yojson_result json = in let* instruments_json = field fields "instruments" in let* instruments_json = list ~name:"instruments" instruments_json in - let* instruments = map_list parse_instrument instruments_json in - if instruments = [] then - Error "scenario must define at least one instrument" + if List.length instruments_json > Resource_limits.catalog_instruments then + Error + (Printf.sprintf "catalog instrument count is %d; limit is %d" + (List.length instruments_json) + Resource_limits.catalog_instruments) else - let currencies = - base_currency - :: List.map - (fun instrument -> instrument.Instrument.quote_currency) - instruments - |> List.sort_uniq String.compare - in - let cash_currencies = - List.map fst initial_cash |> List.sort_uniq String.compare - in - if cash_currencies <> currencies then - Error "initial_cash must contain every scenario currency exactly once" + let* instruments = map_list parse_instrument instruments_json in + if instruments = [] then + Error "scenario must define at least one instrument" else - let catalog = - List.map (fun instrument -> instrument.Instrument.id) instruments - |> Id.Instrument.Set.of_list + let currencies = + base_currency + :: List.map + (fun instrument -> instrument.Instrument.quote_currency) + instruments + |> List.sort_uniq String.compare in - let* risk_json = field fields "risk" in - let* risk = parse_risk base_currency instruments risk_json in - let* execution_json = field fields "execution" in - let* execution_model, execution = parse_execution execution_json in - let* maximum_json = field fields "max_internal_events" in - let* max_internal_events = - integer ~name:"max_internal_events" maximum_json + let cash_currencies = + List.map fst initial_cash |> List.sort_uniq String.compare in - if max_internal_events <= 0 then - Error "max_internal_events must be positive" + if cash_currencies <> currencies then + Error "initial_cash must contain every scenario currency exactly once" else - let* schedule_json = field fields "schedule" in - let* schedule_json = list ~name:"schedule" schedule_json in - let* schedule = map_list parse_schedule_item schedule_json in - let* slices_json = field fields "slices" in - let* slices_json = list ~name:"slices" slices_json in - let* slices = map_list parse_slice slices_json in - let* () = - validate_slices ~base_currency ~currencies ~instruments slices + let catalog = + List.map (fun instrument -> instrument.Instrument.id) instruments + |> Id.Instrument.Set.of_list + in + let* risk_json = field fields "risk" in + let* risk = parse_risk base_currency instruments risk_json in + let* execution_json = field fields "execution" in + let* execution_model, execution = parse_execution execution_json in + let* maximum_json = field fields "max_internal_events" in + let* max_internal_events = + integer ~name:"max_internal_events" maximum_json in - let* () = validate_schedule risk catalog schedule slices in - Ok - { - contract_version; - metadata; - run_id; - base_currency; - initial_cash; - instruments; - risk; - execution_model; - execution; - max_internal_events; - schedule; - slices; - } + if max_internal_events <= 0 then + Error "max_internal_events must be positive" + else if max_internal_events > Resource_limits.internal_events then + Error + (Printf.sprintf "internal event count is %d; limit is %d" + max_internal_events Resource_limits.internal_events) + else + let* schedule_json = field fields "schedule" in + let* schedule_json = list ~name:"schedule" schedule_json in + let* schedule = map_list parse_schedule_item schedule_json in + let* slices_json = field fields "slices" in + let* slices_json = list ~name:"slices" slices_json in + let* slices = map_list parse_slice slices_json in + let* () = + validate_slices ~base_currency ~currencies ~instruments slices + in + let* () = validate_schedule risk catalog schedule slices in + Ok + { + contract_version; + metadata; + run_id; + base_currency; + initial_cash; + instruments; + risk; + execution_model; + execution; + max_internal_events; + schedule; + slices; + } let of_yojson json = let code, json_path = @@ -881,6 +957,7 @@ let of_yojson json = | _ -> (Diagnostic.Scenario_invalid, "$")) | _ -> (Diagnostic.Scenario_invalid, "$") in + let* () = check_batch_limits json in of_yojson_result json |> Result.map_error (fun message -> Diagnostic.make ~code ~phase:Diagnostic.Validation ~json_path message) @@ -1011,11 +1088,13 @@ let stream_header_of_yojson ~contract_version json = (Diagnostic.Scenario_stream_invalid, "$.payload") else (Diagnostic.Scenario_unsupported_contract, "$.contract_version") in + let* () = check_stream_header_limits json in stream_header_of_yojson_result ~contract_version json |> Result.map_error (fun message -> Diagnostic.make ~code ~phase:Diagnostic.Validation ~json_path message) let stream_item_of_yojson header ~previous json = + let* () = check_stream_item_limits json in stream_item_of_yojson_result header ~previous json |> Result.map_error (fun message -> Diagnostic.make ~code:Diagnostic.Scenario_stream_invalid diff --git a/lib/scenario_stream.ml b/lib/scenario_stream.ml index 7b3c728..d11c97c 100644 --- a/lib/scenario_stream.ml +++ b/lib/scenario_stream.ml @@ -129,101 +129,162 @@ let footer_count payload = let* count_json = field fields "slice_count" in int64_string ~name:"slice_count" ~positive:false count_json -let fold_channel channel ~init ~step ~finish = - let line_number = ref 1 in - match In_channel.input_line channel with - | None -> +type bounded_line = End | Line of string | Too_large of int + +let read_bounded_line channel buffer = + let maximum = Bytes.length buffer in + let rec drain observed = + match input_char channel with + | '\n' -> Too_large observed + | _ when observed = Int.max_int -> Too_large observed + | _ -> drain (observed + 1) + | exception End_of_file -> Too_large observed + in + let rec read length = + match input_char channel with + | '\n' -> Line (Bytes.sub_string buffer 0 length) + | character when length < maximum -> + Bytes.set buffer length character; + read (length + 1) + | _ -> drain (maximum + 1) + | exception End_of_file -> + if length = 0 then End else Line (Bytes.sub_string buffer 0 length) + in + read 0 + +let resource_limit ~sequence ~line ~observed ~allowed = + Diagnostic.make ?sequence ~line ~code:Diagnostic.Resource_limit + ~phase:Diagnostic.Input + (Printf.sprintf "scenario stream record is %d bytes; limit is %d bytes" + observed allowed) + +let read_record ?sequence ~line channel buffer = + match read_bounded_line channel buffer with + | End -> Ok None + | Line value -> Ok (Some value) + | Too_large observed -> Error - (invalid ~line:1 ~sequence:1L - "scenario stream must start with scenario_header") - | Some line -> - let* envelope = - parse_envelope ~line_number:1 ~expected_sequence:1L line - in - if not (String.equal envelope.record_type "scenario_header") then + (resource_limit ~sequence ~line ~observed ~allowed:(Bytes.length buffer)) + +let fold_channel ?(max_record_bytes = Resource_limits.scenario_record_bytes) + channel ~init ~step ~finish = + let line_number = ref 1 in + if max_record_bytes <= 0 then + Error + (Diagnostic.make ~code:Diagnostic.Resource_limit + ~phase:Diagnostic.Validation + "scenario record byte limit must be positive") + else if max_record_bytes > Resource_limits.scenario_record_bytes then + Error + (Diagnostic.make ~code:Diagnostic.Resource_limit + ~phase:Diagnostic.Validation + (Printf.sprintf + "scenario record byte limit is %d bytes; maximum is %d bytes" + max_record_bytes Resource_limits.scenario_record_bytes)) + else + let buffer = Bytes.create max_record_bytes in + match read_record ~sequence:1L ~line:1 channel buffer with + | Error _ as error -> error + | Ok None -> Error - (invalid ~line:1 ~sequence:1L ~json_path:"$.record_type" - "scenario_header must be the first scenario stream record") - else - let* header = - Scenario.stream_header_of_yojson - ~contract_version:envelope.contract_version envelope.payload - |> Result.map_error - (Diagnostic.annotate ~line:1 ~sequence:1L ~json_path:"$.payload") + (invalid ~line:1 ~sequence:1L + "scenario stream must start with scenario_header") + | Ok (Some line) -> + let* envelope = + parse_envelope ~line_number:1 ~expected_sequence:1L line in - let* state = init header in - let rec loop state previous slice_count expected_sequence = - incr line_number; - match In_channel.input_line channel with - | None -> - Error - (invalid ~line:!line_number ~sequence:expected_sequence - "scenario_end must terminate the scenario stream") - | Some line -> - let* envelope = - parse_envelope ~line_number:!line_number ~expected_sequence line - in - if - not - (String.equal envelope.contract_version - header.contract_version) - then + if not (String.equal envelope.record_type "scenario_header") then + Error + (invalid ~line:1 ~sequence:1L ~json_path:"$.record_type" + "scenario_header must be the first scenario stream record") + else + let* header = + Scenario.stream_header_of_yojson + ~contract_version:envelope.contract_version envelope.payload + |> Result.map_error + (Diagnostic.annotate ~line:1 ~sequence:1L + ~json_path:"$.payload") + in + let* state = init header in + let rec loop state previous slice_count expected_sequence = + incr line_number; + match + read_record ~sequence:expected_sequence ~line:!line_number channel + buffer + with + | Error _ as error -> error + | Ok None -> Error (invalid ~line:!line_number ~sequence:expected_sequence - ~json_path:"$.contract_version" - "scenario stream contract_version must remain constant") - else if String.equal envelope.record_type "market_slice" then - let* item = - Scenario.stream_item_of_yojson header ~previous - envelope.payload - |> Result.map_error - (Diagnostic.annotate ~line:!line_number - ~sequence:expected_sequence ~json_path:"$.payload") + "scenario_end must terminate the scenario stream") + | Ok (Some line) -> + let* envelope = + parse_envelope ~line_number:!line_number ~expected_sequence + line in - let* state = step state item in - let* expected_sequence = successor expected_sequence in - if Int64.equal slice_count Int64.max_int then + if + not + (String.equal envelope.contract_version + header.contract_version) + then Error (invalid ~line:!line_number ~sequence:expected_sequence - "scenario slice count is exhausted") + ~json_path:"$.contract_version" + "scenario stream contract_version must remain constant") + else if String.equal envelope.record_type "market_slice" then + let* item = + Scenario.stream_item_of_yojson header ~previous + envelope.payload + |> Result.map_error + (Diagnostic.annotate ~line:!line_number + ~sequence:expected_sequence ~json_path:"$.payload") + in + let* state = step state item in + let* expected_sequence = successor expected_sequence in + if Int64.equal slice_count Int64.max_int then + Error + (invalid ~line:!line_number ~sequence:expected_sequence + "scenario slice count is exhausted") + else + loop state (Some item) (Int64.succ slice_count) + expected_sequence + else if String.equal envelope.record_type "scenario_end" then + let* declared_count = + footer_count envelope.payload + |> Result.map_error + (Diagnostic.annotate ~line:!line_number + ~sequence:expected_sequence ~json_path:"$.payload") + in + if not (Int64.equal declared_count slice_count) then + Error + (invalid ~line:!line_number ~sequence:expected_sequence + ~json_path:"$.payload.slice_count" + "scenario_end slice_count differs from streamed \ + market slices") + else + let terminal_line = !line_number + 1 in + match read_record ~line:terminal_line channel buffer with + | Error _ as error -> error + | Ok (Some _) -> + Error + (invalid ~line:terminal_line + "scenario_end must be the terminal scenario \ + stream record") + | Ok None -> finish state ~slice_count else - loop state (Some item) (Int64.succ slice_count) - expected_sequence - else if String.equal envelope.record_type "scenario_end" then - let* declared_count = - footer_count envelope.payload - |> Result.map_error - (Diagnostic.annotate ~line:!line_number - ~sequence:expected_sequence ~json_path:"$.payload") - in - if not (Int64.equal declared_count slice_count) then Error (invalid ~line:!line_number ~sequence:expected_sequence - ~json_path:"$.payload.slice_count" - "scenario_end slice_count differs from streamed market \ - slices") - else - match In_channel.input_line channel with - | Some _ -> - Error - (invalid ~line:(!line_number + 1) - "scenario_end must be the terminal scenario stream \ - record") - | None -> finish state ~slice_count - else - Error - (invalid ~line:!line_number ~sequence:expected_sequence - ~json_path:"$.record_type" - ("unsupported scenario stream record_type: " - ^ envelope.record_type)) - in - let* expected_sequence = successor 1L in - loop state None 0L expected_sequence + ~json_path:"$.record_type" + ("unsupported scenario stream record_type: " + ^ envelope.record_type)) + in + let* expected_sequence = successor 1L in + loop state None 0L expected_sequence -let fold_file path ~init ~step ~finish = +let fold_file ?max_record_bytes path ~init ~step ~finish = try In_channel.with_open_bin path (fun channel -> - fold_channel channel ~init ~step ~finish) + fold_channel ?max_record_bytes channel ~init ~step ~finish) with Sys_error message as exception_ -> Error (Diagnostic.of_exception ~code:Diagnostic.Input_io ~phase:Diagnostic.Input diff --git a/lib/scenario_stream.mli b/lib/scenario_stream.mli index 7cf0f88..eeed704 100644 --- a/lib/scenario_stream.mli +++ b/lib/scenario_stream.mli @@ -1,6 +1,7 @@ (** Bounded-memory reader for versioned JSON Lines replay scenarios. *) val fold_channel : + ?max_record_bytes:int -> in_channel -> init:(Scenario.stream_header -> ('state, Diagnostic.t) result) -> step:('state -> Scenario.stream_item -> ('state, Diagnostic.t) result) -> @@ -8,6 +9,7 @@ val fold_channel : ('result, Diagnostic.t) result val fold_file : + ?max_record_bytes:int -> string -> init:(Scenario.stream_header -> ('state, Diagnostic.t) result) -> step:('state -> Scenario.stream_item -> ('state, Diagnostic.t) result) -> diff --git a/lib/strategy_process.ml b/lib/strategy_process.ml index beeb754..93cdfc4 100644 --- a/lib/strategy_process.ml +++ b/lib/strategy_process.ml @@ -51,8 +51,10 @@ let exception_diagnostic ?sequence stage exception_ = ( Diagnostic.Strategy_protocol, stage ^ ": external strategy closed stdout" ) | Eio.Buf_read.Buffer_limit_exceeded -> - ( Diagnostic.Strategy_protocol, - stage ^ ": strategy response exceeds the maximum message size" ) + ( Diagnostic.Resource_limit, + Printf.sprintf + "%s: strategy response exceeds the maximum message size (%d bytes)" + stage Strategy_protocol.max_message_bytes ) | _ -> ( Diagnostic.Strategy_process, stage ^ ": " ^ Printexc.to_string exception_ ) @@ -187,11 +189,21 @@ let append_cleanup_error result child = | Error original -> Error (Diagnostic.combine original cleanup)) let exchange session ~stage ~expected_sequence request = + let request_document = Strategy_protocol.message_to_string request in + let request_bytes = String.length request_document in + let* () = + if request_bytes <= Strategy_protocol.max_message_bytes then Ok () + else + Error + (diagnostic ~sequence:expected_sequence ~code:Diagnostic.Resource_limit + (Printf.sprintf "%s: strategy message is %d bytes; limit is %d bytes" + stage request_bytes Strategy_protocol.max_message_bytes)) + in let* () = Strategy_transcript.append session.transcript ~direction:Strategy_protocol.Engine_to_strategy request in - let request_line = Strategy_protocol.message_to_string request ^ "\n" in + let request_line = request_document ^ "\n" in let response = try match @@ -329,7 +341,7 @@ let await_exit session = (diagnostic ~code:Diagnostic.Strategy_exit (Printf.sprintf "external strategy was killed by signal %d" signal)) -let validate_configuration ~command ~timeout = +let configured_executable ~command ~timeout ~initialization = if not (valid_timeout timeout) then Error (diagnostic ~code:Diagnostic.Strategy_invalid_configuration @@ -344,7 +356,23 @@ let validate_configuration ~command ~timeout = Error (diagnostic ~code:Diagnostic.Strategy_invalid_configuration "external strategy executable must not be empty") - | executable :: _ -> Ok executable + | executable :: _ -> + let message = + Strategy_protocol.initialize_message ~sequence:1L initialization + |> Strategy_protocol.message_to_string + in + let observed = String.length message in + if observed > Strategy_protocol.max_message_bytes then + Error + (diagnostic ~sequence:1L ~code:Diagnostic.Resource_limit + (Printf.sprintf + "strategy initialization message is %d bytes; limit is %d \ + bytes" + observed Strategy_protocol.max_message_bytes)) + else Ok executable + +let validate_configuration ~command ~timeout ~initialization = + configured_executable ~command ~timeout ~initialization |> Result.map ignore let run_session ~effects ~env ~command ~executable ~timeout ~transcript ~(initialization : Strategy_protocol.initialization) use = @@ -417,7 +445,7 @@ let run_session ~effects ~env ~command ~executable ~timeout ~transcript let with_staged_session ?(effects = Boundary_effects.direct) ~env ~command ~timeout ~transcript ~initialization use = - match validate_configuration ~command ~timeout with + match configured_executable ~command ~timeout ~initialization with | Error _ as error -> error | Ok executable -> run_session ~effects ~env ~command ~executable ~timeout ~transcript @@ -426,7 +454,7 @@ let with_staged_session ?(effects = Boundary_effects.direct) ~env ~command let with_session ?(effects = Boundary_effects.direct) ?(durability = Artifact_writer.Buffered) ~env ~command ~timeout ~transcript_path ~initialization use = - match validate_configuration ~command ~timeout with + match configured_executable ~command ~timeout ~initialization with | Error _ as error -> error | Ok executable -> ( match Strategy_transcript.create ~effects ~durability transcript_path with diff --git a/lib/strategy_process.mli b/lib/strategy_process.mli index 9971c09..c409bf9 100644 --- a/lib/strategy_process.mli +++ b/lib/strategy_process.mli @@ -2,6 +2,12 @@ type t +val validate_configuration : + command:string list -> + timeout:float -> + initialization:Strategy_protocol.initialization -> + (unit, Diagnostic.t) result + val with_staged_session : ?effects:Boundary_effects.t -> env:Eio_unix.Stdenv.base -> diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index 78164d5..9db5baf 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -1,5 +1,5 @@ let version = Contract.strategy_protocol_version -let max_message_bytes = 1_048_576 +let max_message_bytes = Resource_limits.strategy_message_bytes type initialization = { scenario_contract_version : string; @@ -265,6 +265,10 @@ let parse_intents_payload json = in let* intents_json = field fields "intents" in match intents_json with + | `List values when List.length values > Resource_limits.intents_per_batch -> + Error + (Printf.sprintf "intent count is %d; limit is %d" (List.length values) + Resource_limits.intents_per_batch) | `List values -> List.fold_left (fun result value -> @@ -342,18 +346,39 @@ let response_of_yojson ~expected_sequence json = | _ -> "$")) | _ -> "$" in - response_of_yojson_result ~expected_sequence json - |> Result.map_error (fun message -> - Diagnostic.make ~code:Diagnostic.Strategy_protocol - ~phase:Diagnostic.Strategy ~sequence:expected_sequence ~json_path - message) + let intent_count = + match json with + | `Assoc fields -> ( + match List.assoc_opt "payload" fields with + | Some (`Assoc payload_fields) -> ( + match List.assoc_opt "intents" payload_fields with + | Some (`List values) -> Some (List.length values) + | _ -> None) + | _ -> None) + | _ -> None + in + match intent_count with + | Some observed when observed > Resource_limits.intents_per_batch -> + Error + (Diagnostic.make ~code:Diagnostic.Resource_limit + ~phase:Diagnostic.Strategy ~sequence:expected_sequence + ~json_path:"$.payload.intents" + (Printf.sprintf "intent count is %d; limit is %d" observed + Resource_limits.intents_per_batch)) + | _ -> + response_of_yojson_result ~expected_sequence json + |> Result.map_error (fun message -> + Diagnostic.make ~code:Diagnostic.Strategy_protocol + ~phase:Diagnostic.Strategy ~sequence:expected_sequence ~json_path + message) let response_of_string ~expected_sequence document = if String.length document > max_message_bytes then Error - (Diagnostic.make ~code:Diagnostic.Strategy_protocol + (Diagnostic.make ~code:Diagnostic.Resource_limit ~phase:Diagnostic.Strategy ~sequence:expected_sequence - "strategy response exceeds the maximum message size") + (Printf.sprintf "strategy message is %d bytes; limit is %d bytes" + (String.length document) max_message_bytes)) else try let json = Yojson.Safe.from_string document in diff --git a/test/cli.t b/test/cli.t index 67b946e..2991c63 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":["4","3"],"journal_contract_versions":["4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"strategy_protocol_versions":["3"]} + {"engine_version":"1.0.0","scenario_contract_versions":["4","3"],"journal_contract_versions":["4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"strategy_protocol_versions":["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/v4/fixtures/demo.scenario.json valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=991890e8c1cc839a0c321a6d30b2ba20a8b588d4135310f43a548fbc929e9fcf diff --git a/test/test_boundary_failures.ml b/test/test_boundary_failures.ml index 7ed657d..fe0f431 100644 --- a/test/test_boundary_failures.ml +++ b/test/test_boundary_failures.ml @@ -361,7 +361,32 @@ let process_cases = (fun () -> exercise_process_failure stage)) process_stages +let artifact_records_are_bounded () = + with_absent_path ".bounded.jsonl" @@ fun final_path -> + let writer = + T.Artifact_writer.create ~label:"bounded artifact" final_path |> ok + in + T.Artifact_writer.append writer + (String.make T.Resource_limits.artifact_record_bytes 'x') + |> ok; + let diagnostic = + T.Artifact_writer.append writer + (String.make (T.Resource_limits.artifact_record_bytes + 1) 'x') + |> error + in + Alcotest.(check string) + "artifact limit code" "resource.limit" + (T.Diagnostic.code_to_string diagnostic.code); + Alcotest.(check int) + "oversized record was not written" T.Resource_limits.artifact_record_bytes + (In_channel.with_open_bin (final_path ^ ".partial") in_channel_length); + T.Artifact_writer.close_preserving_partial writer + let tests = artifact_cases "journal" (module Journal_writer) @ artifact_cases "transcript" (module Transcript_writer) @ transaction_cases @ durability_cases @ process_cases + @ [ + Alcotest.test_case "artifact records are bounded" `Quick + artifact_records_are_bounded; + ] diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 2380034..9441e4a 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -62,10 +62,40 @@ let preserves_sanitized_exception () = | `String value -> value | _ -> Alcotest.fail "expected target") +let capabilities_publish_versioned_resource_limits () = + let limits = + T.Contract.capabilities_to_yojson () |> field "resource_limits" + in + Alcotest.(check string) + "resource contract version" T.Resource_limits.version + (match field "version" limits with + | `String value -> value + | _ -> Alcotest.fail "expected resource limit version"); + List.iter + (fun (name, expected) -> + Alcotest.(check int) + name expected + (match field name limits with + | `Int value -> value + | _ -> Alcotest.fail ("expected integer limit: " ^ name))) + [ + ("scenario_record_bytes", T.Resource_limits.scenario_record_bytes); + ("strategy_message_bytes", T.Resource_limits.strategy_message_bytes); + ("internal_events", T.Resource_limits.internal_events); + ("catalog_instruments", T.Resource_limits.catalog_instruments); + ("intents_per_batch", T.Resource_limits.intents_per_batch); + ("artifact_record_bytes", T.Resource_limits.artifact_record_bytes); + ]; + Alcotest.(check string) + "resource diagnostic code" "resource.limit" + (T.Diagnostic.code_to_string T.Diagnostic.Resource_limit) + let tests = [ Alcotest.test_case "renders stable machine context" `Quick renders_stable_machine_context; Alcotest.test_case "preserves sanitized exception" `Quick preserves_sanitized_exception; + Alcotest.test_case "versioned resource capabilities" `Quick + capabilities_publish_versioned_resource_limits; ] diff --git a/test/test_scenario.ml b/test/test_scenario.ml index d59771d..6f639c8 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -25,6 +25,22 @@ let with_stream records function_ = output_string channel (String.concat "\n" records ^ "\n")); function_ path) +let with_stream_document document function_ = + let path = Filename.temp_file "trading-engine-scenario" ".jsonl" in + Fun.protect + ~finally:(fun () -> if Sys.file_exists path then Sys.remove path) + (fun () -> + Out_channel.with_open_bin path (fun channel -> + output_string channel document); + function_ path) + +let fold_stream_with_limit maximum path = + In_channel.with_open_bin path (fun channel -> + T.Scenario_stream.fold_channel ~max_record_bytes:maximum channel + ~init:(fun _ -> Ok ()) + ~step:(fun () _ -> Ok ()) + ~finish:(fun () ~slice_count -> Ok slice_count)) + let add_seconds timestamp seconds = Ptime.add_span timestamp (Ptime.Span.of_int_s seconds) |> Option.get @@ -248,6 +264,61 @@ let map_field key change = function fields) | _ -> Alcotest.fail "expected object" +let configured_resources_are_bounded () = + let document = Yojson.Safe.from_string (demo_document ()) in + let check_limit expected_path changed = + let diagnostic = T.Scenario.of_yojson changed |> error in + Alcotest.(check string) + "stable resource code" "resource.limit" + (T.Diagnostic.code_to_string diagnostic.code); + Alcotest.(check (option string)) + "resource path" (Some expected_path) diagnostic.context.json_path + in + check_limit "$.max_internal_events" + (change_field "max_internal_events" + (`Int (T.Resource_limits.internal_events + 1)) + document); + let instrument = + match document with + | `Assoc fields -> ( + match List.assoc "instruments" fields with + | `List (value :: _) -> value + | _ -> Alcotest.fail "expected scenario instruments") + | _ -> Alcotest.fail "expected scenario" + in + check_limit "$.instruments" + (change_field "instruments" + (`List + (List.init (T.Resource_limits.catalog_instruments + 1) (fun _ -> + instrument))) + document); + let schedule_item, intent = + match document with + | `Assoc fields -> ( + match List.assoc "schedule" fields with + | `List ((`Assoc item_fields as item) :: _) -> ( + match List.assoc "intents" item_fields with + | `List (intent :: _) -> (item, intent) + | _ -> Alcotest.fail "expected scheduled intents") + | _ -> Alcotest.fail "expected scenario schedule") + | _ -> Alcotest.fail "expected scenario" + in + let oversized_item = + change_field "intents" + (`List + (List.init (T.Resource_limits.intents_per_batch + 1) (fun _ -> intent))) + schedule_item + in + check_limit "$.schedule[0].intents" + (change_field "schedule" (`List [ oversized_item ]) document); + Alcotest.(check bool) + "reducer configuration limit" true + (Result.is_error + (T.Engine.config ~contract_version:T.Contract.version ~risk:(risk ()) + ~execution_model:(T.Execution_model.find "completed_bar_v1" |> ok) + ~execution:(execution ()) + ~max_internal_events:(T.Resource_limits.internal_events + 1))) + let scenario_with_second_slice_start start_at = map_root (fun fields -> List.map @@ -602,7 +673,7 @@ let journal_finalization_is_exclusive () = (In_channel.with_open_bin path In_channel.input_all); Alcotest.(check bool) "partial preserved" true (Sys.file_exists partial)) -let failed_replay_preserves_partial () = +let invalid_replay_configuration_precedes_artifacts () = let path = Filename.temp_file "trading-engine-failure" ".jsonl" in Sys.remove path; let partial = path ^ ".partial" in @@ -612,11 +683,11 @@ let failed_replay_preserves_partial () = if Sys.file_exists partial then Sys.remove partial) (fun () -> Alcotest.(check bool) - "invalid hash fails after journal creation" true + "invalid hash is rejected" true (Result.is_error (T.Replay.run ~scenario_sha256:"bad" ~journal_path:path (demo ()))); Alcotest.(check bool) "final absent" false (Sys.file_exists path); - Alcotest.(check bool) "partial retained" true (Sys.file_exists partial)) + Alcotest.(check bool) "partial absent" false (Sys.file_exists partial)) let journal_matches_in_memory_events () = let scenario = demo () in @@ -772,6 +843,63 @@ let streamed_intents_are_causal_before_execution () = executable market slice starts" (T.Replay.run_stream path |> diagnostic_message)) +let scenario_stream_records_are_bounded () = + let records = stream_records () in + let maximum = + List.fold_left + (fun current line -> Int.max current (String.length line)) + 0 records + in + with_stream records (fun path -> + Alcotest.(check int64) + "record at exact limit accepted" 4L + (fold_stream_with_limit maximum path |> ok)); + let longest_line, longest_index = + records + |> List.mapi (fun index line -> (line, index + 1)) + |> List.fold_left + (fun ((current, _) as selected) ((candidate, _) as next) -> + if String.length candidate > String.length current then next + else selected) + ("", 0) + in + with_stream records (fun path -> + let diagnostic = fold_stream_with_limit (maximum - 1) path |> error in + Alcotest.(check string) + "oversized record code" "resource.limit" + (T.Diagnostic.code_to_string diagnostic.code); + Alcotest.(check (option int)) + "oversized record line" (Some longest_index) diagnostic.context.line; + Alcotest.(check string) + "observed and allowed bytes" + (Printf.sprintf "scenario stream record is %d bytes; limit is %d bytes" + (String.length longest_line) + (maximum - 1)) + diagnostic.message); + with_stream_document (String.concat "\n" records) (fun path -> + Alcotest.(check int64) + "newline-free terminal record accepted" 4L + (fold_stream_with_limit maximum path |> ok)); + let truncated = List.hd records ^ "\n{\"contract_version\"" in + with_stream_document truncated (fun path -> + let diagnostic = fold_stream_with_limit maximum path |> error in + Alcotest.(check string) + "truncated record remains a JSON error" "scenario.invalid_json" + (T.Diagnostic.code_to_string diagnostic.code); + Alcotest.(check (option int)) + "truncated record line" (Some 2) diagnostic.context.line); + let small_limit = 32 in + let newline_free = String.make (small_limit + 7) 'x' in + with_stream_document newline_free (fun path -> + let diagnostic = fold_stream_with_limit small_limit path |> error in + Alcotest.(check string) + "newline-free oversized code" "resource.limit" + (T.Diagnostic.code_to_string diagnostic.code); + Alcotest.(check string) + "newline-free observed bytes" + "scenario stream record is 39 bytes; limit is 32 bytes" + diagnostic.message) + let large_stream_replay_does_not_retain_audit_history () = let slice_count = 10_000 in let path = Filename.temp_file "trading-engine-large" ".jsonl" in @@ -802,6 +930,8 @@ let tests = duplicate_fields_are_rejected; Alcotest.test_case "metadata validation is recursive" `Quick recursive_metadata_validation; + Alcotest.test_case "configured resources are bounded" `Quick + configured_resources_are_bounded; Alcotest.test_case "invalid schedule sequences rejected" `Quick invalid_schedule_sequences_are_rejected; Alcotest.test_case "duplicate slice bars rejected" `Quick @@ -827,8 +957,8 @@ let tests = journal_is_created_exclusively; Alcotest.test_case "exclusive journal finalization" `Quick journal_finalization_is_exclusive; - Alcotest.test_case "failed replay preserves partial" `Quick - failed_replay_preserves_partial; + Alcotest.test_case "configuration precedes artifacts" `Quick + invalid_replay_configuration_precedes_artifacts; Alcotest.test_case "journal matches events" `Quick journal_matches_in_memory_events; Alcotest.test_case "stream replay matches batch semantics" `Quick @@ -839,6 +969,8 @@ let tests = `Quick streamed_market_slice_timeline_is_non_overlapping; Alcotest.test_case "streamed intents are causal" `Quick streamed_intents_are_causal_before_execution; + Alcotest.test_case "scenario stream records are bounded" `Quick + scenario_stream_records_are_bounded; Alcotest.test_case "large stream avoids retained audit history" `Slow large_stream_replay_does_not_retain_audit_history; ] diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index 76075da..8861c5b 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -236,12 +236,62 @@ let responses_are_strict_and_typed () = (T.Strategy_protocol.response_of_string ~expected_sequence:3L "{" |> diagnostic_message |> String.starts_with ~prefix:"invalid strategy response JSON:"); + let oversized = + T.Strategy_protocol.response_of_string ~expected_sequence:3L + (String.make (T.Strategy_protocol.max_message_bytes + 1) 'x') + |> error + in + Alcotest.(check string) + "oversized response code" "resource.limit" + (T.Diagnostic.code_to_string oversized.code); Alcotest.(check string) "oversized response rejected" - "strategy response exceeds the maximum message size" - (T.Strategy_protocol.response_of_string ~expected_sequence:3L - (String.make (T.Strategy_protocol.max_message_bytes + 1) 'x') - |> diagnostic_message) + (Printf.sprintf "strategy message is %d bytes; limit is %d bytes" + (T.Strategy_protocol.max_message_bytes + 1) + T.Strategy_protocol.max_message_bytes) + oversized.message; + let oversized_intents = + response "intents" + (`Assoc + [ + ( "intents", + `List + (List.init (T.Resource_limits.intents_per_batch + 1) (fun _ -> + `Null)) ); + ]) + |> T.Strategy_protocol.response_of_yojson ~expected_sequence:3L + |> error + in + Alcotest.(check string) + "intent limit code" "resource.limit" + (T.Diagnostic.code_to_string oversized_intents.code); + Alcotest.(check (option string)) + "intent limit path" (Some "$.payload.intents") + oversized_intents.context.json_path + +let strategy_configuration_is_validated_before_use () = + let initialization = + { + (initialization ()) with + metadata = + `Assoc + [ + ( "padding", + `String (String.make T.Resource_limits.strategy_message_bytes 'x') + ); + ]; + } + in + let diagnostic = + T.Strategy_process.validate_configuration ~command:[ "unused" ] ~timeout:1.0 + ~initialization + |> error + in + Alcotest.(check string) + "initialization limit code" "resource.limit" + (T.Diagnostic.code_to_string diagnostic.code); + Alcotest.(check (option int64)) + "initialization sequence" (Some 1L) diagnostic.context.sequence let transcript_records_direction_and_sequence () = let message = T.Strategy_protocol.shutdown_message ~sequence:9L in @@ -286,6 +336,35 @@ let with_process_tree_paths test = remove_if_exists (transcript_path ^ ".partial")) (fun () -> test pid_path transcript_path) +let invalid_configuration_creates_no_transcript_or_process () = + with_process_tree_paths @@ fun _ transcript_path -> + let initialization = + { + (initialization ()) with + metadata = + `Assoc + [ + ( "padding", + `String (String.make T.Resource_limits.strategy_message_bytes 'x') + ); + ]; + } + in + let diagnostic = + Eio_main.run @@ fun env -> + T.Strategy_process.with_session ~env + ~command:[ "/definitely/missing/strategy" ] + ~timeout:1.0 ~transcript_path ~initialization (fun _ -> Ok ()) + |> error + in + Alcotest.(check string) + "configuration fails before spawn" "resource.limit" + (T.Diagnostic.code_to_string diagnostic.code); + Alcotest.(check bool) "no transcript" false (Sys.file_exists transcript_path); + Alcotest.(check bool) + "no partial transcript" false + (Sys.file_exists (transcript_path ^ ".partial")) + let callback_exception_reaps_process_tree () = with_process_tree_paths @@ fun pid_path transcript_path -> let result = @@ -331,6 +410,10 @@ let tests = nonpositive_equity_omits_weights; Alcotest.test_case "responses are strict and typed" `Quick responses_are_strict_and_typed; + Alcotest.test_case "strategy configuration is bounded" `Quick + strategy_configuration_is_validated_before_use; + Alcotest.test_case "configuration precedes transcript and process" `Quick + invalid_configuration_creates_no_transcript_or_process; Alcotest.test_case "transcript records direction" `Quick transcript_records_direction_and_sequence; Alcotest.test_case "callback exception reaps process tree" `Slow diff --git a/test/validate_schemas.py b/test/validate_schemas.py index 3569418..b7a83d0 100644 --- a/test/validate_schemas.py +++ b/test/validate_schemas.py @@ -117,8 +117,15 @@ def main() -> None: expect_invalid(scenario_validator, unsupported_execution_model) if contract_version in {"3", "4"}: excessive_feedback_cap = copy.deepcopy(scenario) - excessive_feedback_cap["max_internal_events"] = 4611686018427387904 + excessive_feedback_cap["max_internal_events"] = 100001 expect_invalid(scenario_validator, excessive_feedback_cap) + excessive_catalog = copy.deepcopy(scenario) + excessive_catalog["instruments"] = [scenario["instruments"][0]] * 4097 + expect_invalid(scenario_validator, excessive_catalog) + excessive_intents = copy.deepcopy(scenario) + intent = scenario["schedule"][0]["intents"][0] + excessive_intents["schedule"][0]["intents"] = [intent] * 4097 + expect_invalid(scenario_validator, excessive_intents) unversioned_stream_record = copy.deepcopy(stream_records[0]) del unversioned_stream_record["contract_version"] expect_invalid(stream_validator, unversioned_stream_record) @@ -128,6 +135,16 @@ def main() -> None: malformed_stream_slice = copy.deepcopy(stream_records[1]) malformed_stream_slice["payload"]["market_slice"]["unexpected"] = True expect_invalid(stream_validator, malformed_stream_slice) + if contract_version in {"3", "4"}: + excessive_stream_catalog = copy.deepcopy(stream_records[0]) + excessive_stream_catalog["payload"]["instruments"] = ( + [stream_records[0]["payload"]["instruments"][0]] * 4097 + ) + expect_invalid(stream_validator, excessive_stream_catalog) + excessive_stream_intents = copy.deepcopy(stream_records[1]) + intent = scenario["schedule"][0]["intents"][0] + excessive_stream_intents["payload"]["intents"] = [intent] * 4097 + expect_invalid(stream_validator, excessive_stream_intents) noncanonical = copy.deepcopy(scenario) if contract_version in {"3", "4"}: noncanonical["initial_cash"][0]["amount"] = "10000.0" diff --git a/test/validate_strategy_schema.py b/test/validate_strategy_schema.py index 5903c41..eb54f5f 100644 --- a/test/validate_strategy_schema.py +++ b/test/validate_strategy_schema.py @@ -90,6 +90,15 @@ def main() -> None: malformed_sequence = copy.deepcopy(records[1]["message"]) malformed_sequence["strategy_sequence"] = "01" expect_invalid(message_validator, malformed_sequence) + intents = next( + copy.deepcopy(record["message"]) + for record in records + if record["message"]["message_type"] == "intents" + and record["message"]["payload"]["intents"] + ) + intent = intents["payload"]["intents"][0] + intents["payload"]["intents"] = [intent] * 4097 + expect_invalid(message_validator, intents) malformed_transcript = copy.deepcopy(records[0]) malformed_transcript["direction"] = "network" expect_invalid(transcript_validator, malformed_transcript) From e0f5e0ed4e6a850aa16347b54fcda535750b8fcd Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 19:08:00 -0400 Subject: [PATCH 12/57] fix: retain rejected strategy evidence --- README.md | 6 + contracts/strategy/v3/README.md | 11 +- contracts/strategy/v3/transcript.schema.json | 113 +++++++++++++++++-- lib/strategy_process.ml | 55 +++++++-- lib/strategy_transcript.ml | 48 +++++++- lib/strategy_transcript.mli | 11 ++ test/cli.t | 54 ++++++++- test/validate_strategy_schema.py | 35 ++++++ 8 files changed, 298 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 0258ddd..7a94959 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,12 @@ CLI binds that exact-byte hash into the journal. It writes to the partial path a requested path only after `run_completed` is fully written and the partial file is closed. An error preserves the partial artifact for diagnosis. +A protocol-invalid strategy response is not stored as an accepted transcript exchange. The partial +transcript instead ends with a versioned rejection record containing its structured diagnostic and +a hexadecimal prefix of at most 256 raw response bytes. This covers malformed fields and JSON, +wrong versions or sequences, EOF, and oversized output without retaining the complete rejected +payload. + Journal and transcript records are limited to 2 MiB each, including the terminating line feed. Limit failures use the stable `resource.limit` diagnostic code. diff --git a/contracts/strategy/v3/README.md b/contracts/strategy/v3/README.md index bafe9ce..a831086 100644 --- a/contracts/strategy/v3/README.md +++ b/contracts/strategy/v3/README.md @@ -29,11 +29,16 @@ Event payloads cover completed market slices, fills, order updates, and rejected intents use the scenario v3 intent shapes. External replay requires an empty batch schedule and empty streamed intent batches. The engine -records both directions in a deterministic transcript. The transcript and audit journal retain -partial files after failure and finalize only after their respective success checks. +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 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 retained transcript records. +- `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/v3/transcript.schema.json b/contracts/strategy/v3/transcript.schema.json index 1e21553..a3cb381 100644 --- a/contracts/strategy/v3/transcript.schema.json +++ b/contracts/strategy/v3/transcript.schema.json @@ -2,21 +2,112 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v3/transcript.schema.json", "title": "Trading Engine external strategy protocol v3 transcript record", - "description": "One ordered request or response retained from a supervised stdio strategy session.", - "type": "object", - "additionalProperties": false, - "required": ["strategy_protocol_version", "transcript_sequence", "direction", "message"], - "properties": { - "strategy_protocol_version": { "const": "3" }, - "transcript_sequence": { + "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]*$" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] + "exchange": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_protocol_version", "transcript_sequence", "direction", "message"], + "properties": { + "strategy_protocol_version": { "const": "3" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v3/message.schema.json" + } + } }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v3/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": { + "type": "object", + "additionalProperties": false, + "required": ["diagnostic_version", "code", "phase", "message", "context", "cause"], + "properties": { + "diagnostic_version": { "const": "1" }, + "code": { "enum": ["strategy.protocol", "resource.limit"] }, + "phase": { "const": "strategy" }, + "message": { "type": "string", "minLength": 1 }, + "context": { "$ref": "#/$defs/diagnosticContext" }, + "cause": { + "oneOf": [ + { "type": "null" }, + { "$ref": "#/$defs/diagnosticCause" } + ] + } + } + }, + "diagnosticContext": { + "type": "object", + "additionalProperties": false, + "properties": { + "json_path": { "type": "string" }, + "line": { "type": "integer" }, + "sequence": { "$ref": "#/$defs/canonicalSequence" }, + "event_id": { "type": "string" }, + "order_id": { "type": "string" }, + "causation_ids": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "diagnosticCause": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "message"], + "properties": { + "kind": { "type": "string", "minLength": 1 }, + "message": { "type": "string" }, + "operation": { "type": "string" }, + "target": { "type": "string" } + } + }, + "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/lib/strategy_process.ml b/lib/strategy_process.ml index 93cdfc4..a6fe339 100644 --- a/lib/strategy_process.ml +++ b/lib/strategy_process.ml @@ -188,6 +188,33 @@ let append_cleanup_error result child = | Ok _ -> Error cleanup | Error original -> Error (Diagnostic.combine original cleanup)) +let rejection_evidence response = + let observed_bytes = String.length response in + let captured_bytes = + Int.min observed_bytes Strategy_transcript.max_rejection_prefix_bytes + in + ( String.sub response 0 captured_bytes, + observed_bytes, + captured_bytes < observed_bytes ) + +let buffered_rejection_evidence output = + let observed_bytes = Eio.Buf_read.buffered_bytes output in + let captured_bytes = + Int.min observed_bytes Strategy_transcript.max_rejection_prefix_bytes + in + ( Eio.Buf_read.take captured_bytes output, + observed_bytes, + captured_bytes < observed_bytes ) + +let reject_response session ~expected_sequence rejection + (raw_prefix, observed_bytes, truncated) = + match + Strategy_transcript.append_rejection session.transcript ~expected_sequence + ~diagnostic:rejection ~raw_prefix ~observed_bytes ~truncated + with + | Ok () -> Error rejection + | Error artifact -> Error (Diagnostic.combine rejection artifact) + let exchange session ~stage ~expected_sequence request = let request_document = Strategy_protocol.message_to_string request in let request_bytes = String.length request_document in @@ -220,20 +247,30 @@ let exchange session ~stage ~expected_sequence request = (diagnostic ~sequence:expected_sequence ~code:Diagnostic.Strategy_timeout (stage ^ ": external strategy timed out")) - with exception_ -> - Error (exception_diagnostic ~sequence:expected_sequence stage exception_) + with + | (End_of_file | Eio.Buf_read.Buffer_limit_exceeded) as exception_ -> + reject_response session ~expected_sequence + (exception_diagnostic ~sequence:expected_sequence stage exception_) + (buffered_rejection_evidence session.output) + | exception_ -> + Error + (exception_diagnostic ~sequence:expected_sequence stage exception_) in let* response = response in - let* response, response_json = + match Strategy_protocol.response_of_string ~expected_sequence response |> Result.map_error (Diagnostic.annotate ~sequence:expected_sequence ~json_path:"$") - in - let* () = - Strategy_transcript.append session.transcript - ~direction:Strategy_protocol.Strategy_to_engine response_json - in - Ok response + with + | Error rejection -> + reject_response session ~expected_sequence rejection + (rejection_evidence response) + | Ok (response, response_json) -> + let* () = + Strategy_transcript.append session.transcript + ~direction:Strategy_protocol.Strategy_to_engine response_json + in + Ok response let exchange_at session ~stage ~sequence make_request = exchange session ~stage ~expected_sequence:sequence (make_request ~sequence) diff --git a/lib/strategy_transcript.ml b/lib/strategy_transcript.ml index 4c8d718..f6911bb 100644 --- a/lib/strategy_transcript.ml +++ b/lib/strategy_transcript.ml @@ -1,5 +1,8 @@ type t = { artifact : Artifact_writer.t; mutable next_sequence : int64 } +let diagnostic_version = "1" +let max_rejection_prefix_bytes = 256 + let diagnostic ?sequence ~code message = Diagnostic.make ?sequence ~code ~phase:Diagnostic.Artifact message @@ -8,23 +11,56 @@ let create ?effects ?durability final_path = final_path |> Result.map (fun artifact -> { artifact; next_sequence = 1L }) -let append transcript ~direction message = +let append_record transcript record = if Int64.equal transcript.next_sequence Int64.max_int then Error (diagnostic ~sequence:transcript.next_sequence ~code:Diagnostic.Artifact_state "strategy transcript sequence is exhausted") else - let record = - Strategy_protocol.transcript_record - ~transcript_sequence:transcript.next_sequence ~direction ~message - in Artifact_writer.append transcript.artifact - (Strategy_protocol.message_to_string record ^ "\n") + (Yojson.Safe.to_string record ^ "\n") |> Result.map (fun () -> transcript.next_sequence <- Int64.succ transcript.next_sequence) |> Result.map_error (Diagnostic.annotate ~sequence:transcript.next_sequence) +let append transcript ~direction message = + Strategy_protocol.transcript_record + ~transcript_sequence:transcript.next_sequence ~direction ~message + |> append_record transcript + +let hex_of_string value = + let digits = "0123456789abcdef" in + String.init + (String.length value * 2) + (fun index -> + let byte = Char.code value.[index / 2] in + if index mod 2 = 0 then digits.[byte lsr 4] else digits.[byte land 0xf]) + +let append_rejection transcript ~expected_sequence ~diagnostic:rejection + ~raw_prefix ~observed_bytes ~truncated = + let raw_prefix = + if String.length raw_prefix <= max_rejection_prefix_bytes then raw_prefix + else String.sub raw_prefix 0 max_rejection_prefix_bytes + in + `Assoc + [ + ("strategy_diagnostic_version", `String diagnostic_version); + ("transcript_sequence", `String (Int64.to_string transcript.next_sequence)); + ("record_type", `String "rejected_strategy_response"); + ("expected_strategy_sequence", `String (Int64.to_string expected_sequence)); + ("diagnostic", Diagnostic.to_yojson rejection); + ( "evidence", + `Assoc + [ + ("encoding", `String "hex"); + ("prefix", `String (hex_of_string raw_prefix)); + ("observed_bytes", `Int observed_bytes); + ("truncated", `Bool truncated); + ] ); + ] + |> append_record transcript + let close_preserving_partial transcript = Artifact_writer.close_preserving_partial transcript.artifact diff --git a/lib/strategy_transcript.mli b/lib/strategy_transcript.mli index 55f6998..8fcae4f 100644 --- a/lib/strategy_transcript.mli +++ b/lib/strategy_transcript.mli @@ -2,6 +2,8 @@ type t +val max_rejection_prefix_bytes : int + val create : ?effects:Boundary_effects.t -> ?durability:Artifact_writer.durability -> @@ -14,6 +16,15 @@ val append : Yojson.Safe.t -> (unit, Diagnostic.t) result +val append_rejection : + t -> + expected_sequence:int64 -> + diagnostic:Diagnostic.t -> + raw_prefix:string -> + observed_bytes:int -> + truncated:bool -> + (unit, Diagnostic.t) result + val close_preserving_partial : t -> unit val commit : t -> (unit, Diagnostic.t) result val artifact : t -> Artifact_writer.t diff --git a/test/cli.t b/test/cli.t index 2991c63..8023904 100644 --- a/test/cli.t +++ b/test/cli.t @@ -100,11 +100,53 @@ > test -e "$directory/run.journal.jsonl.partial" || return 1 > test ! -e "$directory/run.strategy.jsonl" || return 1 > test -e "$directory/run.strategy.jsonl.partial" || return 1 + > if test "$#" -eq 3; then + > python3 - "$directory/run.strategy.jsonl.partial" "$mode" "$3" <<'PY' || return 1 + > import json + > import sys + > from pathlib import Path + > path, mode, expected_code = sys.argv[1:] + > records = [json.loads(line) for line in Path(path).read_text().splitlines()] + > rejection = records[-1] + > assert rejection["strategy_diagnostic_version"] == "1" + > assert rejection["record_type"] == "rejected_strategy_response" + > assert rejection["expected_strategy_sequence"] == "1" + > assert rejection["diagnostic"]["code"] == expected_code + > assert rejection["diagnostic"]["context"]["sequence"] == "1" + > assert "strategy_protocol_version" not in rejection + > assert "direction" not in rejection + > assert "message" not in rejection + > evidence = rejection["evidence"] + > assert evidence["encoding"] == "hex" + > raw = bytes.fromhex(evidence["prefix"]) + > assert len(raw) <= 256 + > if mode == "eof": + > assert raw == b"" and evidence["observed_bytes"] == 0 + > assert evidence["truncated"] is False + > elif mode == "oversized": + > assert raw == b"x" * 256 + > assert evidence["observed_bytes"] == 1_048_577 + > assert evidence["truncated"] is True + > elif mode == "malformed": + > assert raw == b"{" and evidence["observed_bytes"] == 1 + > assert evidence["truncated"] is False + > else: + > assert evidence["observed_bytes"] == len(raw) + > assert evidence["truncated"] is False + > response = json.loads(raw) + > if mode == "bad-sequence": + > assert response["strategy_sequence"] == "999" + > elif mode == "wrong-version": + > assert response["strategy_protocol_version"] == "1" + > elif mode == "unknown-field": + > assert response["unexpected"] is True + > PY + > fi > echo "$mode: rejected" > } - $ check_strategy_failure eof "closed stdout" + $ check_strategy_failure eof "closed stdout" strategy.protocol eof: rejected - $ check_strategy_failure bad-sequence "expected strategy sequence" + $ check_strategy_failure bad-sequence "expected strategy sequence" strategy.protocol bad-sequence: rejected $ check_strategy_failure error "fixture failure" error: rejected @@ -112,13 +154,13 @@ extra-output: rejected $ check_strategy_failure nonzero "exited with code 9" nonzero: rejected - $ check_strategy_failure malformed "invalid strategy response JSON" + $ check_strategy_failure malformed "invalid strategy response JSON" strategy.protocol malformed: rejected - $ check_strategy_failure oversized "exceeds the maximum message size" + $ check_strategy_failure oversized "exceeds the maximum message size" resource.limit oversized: rejected - $ check_strategy_failure wrong-version "unsupported strategy protocol version" + $ check_strategy_failure wrong-version "unsupported strategy protocol version" strategy.protocol wrong-version: rejected - $ check_strategy_failure unknown-field "unknown or missing fields" + $ check_strategy_failure unknown-field "unknown or missing fields" strategy.protocol unknown-field: rejected $ check_process_tree_failure () { diff --git a/test/validate_strategy_schema.py b/test/validate_strategy_schema.py index eb54f5f..ffd6848 100644 --- a/test/validate_strategy_schema.py +++ b/test/validate_strategy_schema.py @@ -103,6 +103,41 @@ def main() -> None: malformed_transcript["direction"] = "network" expect_invalid(transcript_validator, malformed_transcript) + rejected_response = { + "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": None, + }, + "evidence": { + "encoding": "hex", + "prefix": "7b", + "observed_bytes": 1, + "truncated": False, + }, + } + transcript_validator.validate(rejected_response) + assert "direction" not in rejected_response + assert "message" not in rejected_response + + rejection_as_exchange = copy.deepcopy(rejected_response) + rejection_as_exchange["direction"] = "strategy_to_engine" + rejection_as_exchange["message"] = records[1]["message"] + expect_invalid(transcript_validator, rejection_as_exchange) + oversized_prefix = copy.deepcopy(rejected_response) + oversized_prefix["evidence"]["prefix"] = "00" * 257 + expect_invalid(transcript_validator, oversized_prefix) + unversioned_rejection = copy.deepcopy(rejected_response) + del unversioned_rejection["strategy_diagnostic_version"] + expect_invalid(transcript_validator, unversioned_rejection) + if __name__ == "__main__": main() From 464ef19d277954b621d31521f4b50a22002282e2 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 19:51:16 -0400 Subject: [PATCH 13/57] perf: optimize reducer and schedule queues --- README.md | 1 + bench/benchmark_batch_schedule.py | 112 ++++++++++++++++++++++++++++++ docs/architecture.md | 9 +++ docs/performance.md | 47 +++++++++++++ lib/engine.ml | 79 +++++++++++++-------- lib/scenario.ml | 69 +++++++++--------- test/test_reducer.ml | 32 +++++++++ test/test_scenario.ml | 85 ++++++++++++++++++++++- 8 files changed, 369 insertions(+), 65 deletions(-) create mode 100644 bench/benchmark_batch_schedule.py create mode 100644 docs/performance.md diff --git a/README.md b/README.md index 7a94959..d5bd824 100644 --- a/README.md +++ b/README.md @@ -214,5 +214,6 @@ do not provide reducer snapshots or restart recovery. - [Strategy message JSON Schema](contracts/strategy/v3/message.schema.json) - [Strategy transcript JSON Schema](contracts/strategy/v3/transcript.schema.json) - [Execution model](docs/execution-model.md) +- [Performance](docs/performance.md) - [Persistra integration](docs/persistra.md) - [Contributing](CONTRIBUTING.md) diff --git a/bench/benchmark_batch_schedule.py b/bench/benchmark_batch_schedule.py new file mode 100644 index 0000000..e550ba5 --- /dev/null +++ b/bench/benchmark_batch_schedule.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Benchmark dense batch-schedule validation through the public CLI.""" + +from __future__ import annotations + +import argparse +import copy +import json +import statistics +import subprocess +import tempfile +import time +from datetime import datetime, timedelta, timezone +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_EXECUTABLE = ROOT / "_build/default/bin/main.exe" +FIXTURE = ROOT / "contracts/v4/fixtures/demo.scenario.json" + + +def timestamp(value: datetime) -> str: + return value.isoformat(timespec="seconds").replace("+00:00", "Z") + + +def dense_scenario(size: int) -> dict[str, object]: + document = json.loads(FIXTURE.read_text(encoding="utf-8")) + template = document["slices"][0] + base = datetime(2026, 2, 1, tzinfo=timezone.utc) + slices = [] + schedule = [] + for offset in range(size): + sequence = offset + 1 + start = base + timedelta(seconds=offset * 4) + market_slice = copy.deepcopy(template) + market_slice.update( + { + "slice_sequence": str(sequence), + "start_at": timestamp(start), + "end_at": timestamp(start + timedelta(seconds=1)), + "available_at": timestamp(start + timedelta(seconds=2)), + "received_at": timestamp(start + timedelta(seconds=3)), + "corporate_actions": [], + } + ) + slices.append(market_slice) + schedule.append( + { + "after_slice_sequence": str(sequence), + "intents": [ + { + "type": "emit_metric", + "name": "dense_schedule", + "value": str(sequence), + } + ], + } + ) + document["schedule"] = schedule + document["slices"] = slices + return document + + +def measure(executable: Path, scenario: Path, repetitions: int) -> list[float]: + durations = [] + command = [str(executable), "--input", str(scenario), "--validate-only"] + for _ in range(repetitions): + started = time.perf_counter() + subprocess.run( + command, + cwd=ROOT, + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + durations.append(time.perf_counter() - started) + return durations + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--executable", type=Path, default=DEFAULT_EXECUTABLE + ) + parser.add_argument( + "--sizes", type=int, nargs="+", default=[5_000, 10_000, 20_000] + ) + parser.add_argument("--repetitions", type=int, default=3) + args = parser.parse_args() + if args.repetitions <= 0 or any(size <= 0 for size in args.sizes): + parser.error("sizes and repetitions must be positive") + executable = args.executable.resolve() + if not executable.is_file(): + parser.error(f"executable does not exist: {executable}") + + print("slices,schedule,repetitions,median_seconds,min_seconds,max_seconds") + for size in args.sizes: + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", suffix=".scenario.json" + ) as scenario: + json.dump(dense_scenario(size), scenario, separators=(",", ":")) + scenario.flush() + durations = measure(executable, Path(scenario.name), args.repetitions) + print( + f"{size},{size},{args.repetitions}," + f"{statistics.median(durations):.6f},{min(durations):.6f}," + f"{max(durations):.6f}" + ) + + +if __name__ == "__main__": + main() diff --git a/docs/architecture.md b/docs/architecture.md index c2c287b..15a9667 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -82,6 +82,10 @@ Schedule sequences and slice sequences are positive and strictly increasing. Eve anchors to an existing slice. A scheduled order-changing intent must be received no later than the next slice start. +Batch validation indexes each slice together with its successor in an ordered map. Building that +index costs `O(s log s)` for `s` slices, and each of the `m` scheduled sequence lookups costs +`O(log s)`. Validation does not rescan the slice list for each schedule entry. + ## Determinism Determinism depends on: @@ -111,6 +115,11 @@ before any later callback or eligible order, so the next context exposes its eff Positive-equity accounts expose realized portfolio weights; zero- and negative-equity accounts explicitly omit weights. +Reducer feedback uses an immutable two-list queue. Adding generated notifications to the tail and +removing the next item are amortized constant-time operations. Prepending one callback's response +costs only the size of that response. Queue representation changes do not affect processing order +or the exact `max_internal_events` count. + The JSON Lines runner hashes and validates the complete stream before journal creation. It then replays one slice-plus-intents record at a time and does not accumulate market slices, schedule maps, or audit events. A required terminal record distinguishes completion from truncation. diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..4f41b49 --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,47 @@ +# Performance + +The engine treats complexity regressions in validation and pure reduction as correctness risks for +large deterministic replays. Performance tests use contract-sized batches and dense schedules but +do not impose machine-specific time limits in `make check`. + +## Reducer feedback queue + +The reducer stores pending intents and notifications in an immutable two-list queue. Tail insertion +and removal are amortized `O(1)`. Prepending a strategy response is `O(k)` in the response size, +independent of work already pending. Processing `n` queued items is therefore `O(n)` apart from the +domain work performed by each item. + +`test/test_reducer.ml` exercises the maximum 4,096-intent strategy batch. Every intent creates a +notification, and the test completes at the exact 8,193-event feedback limit. On the reference +machine, three warm runs of that focused test had these median wall times: + +| Queue implementation | Median | +|---|---:| +| Pending-list append | 0.11 s | +| Immutable two-list queue | 0.01 s | + +## Dense batch schedules + +Batch validation builds one ordered index that associates every slice sequence with its slice and +successor. For `s` slices and `m` schedule entries, index construction and lookup cost +`O((s + m) log s)`, plus intent validation. The prior complete-list lookup cost `O(s * m)`. + +Run the public-CLI benchmark after building the executable: + +```sh +opam exec -- dune build bin/main.exe +python3 bench/benchmark_batch_schedule.py +``` + +The script generates valid batch scenarios outside the timed section, invokes `--validate-only` +three times per size, and prints medians plus the observed range. These results were measured on +2026-08-21 under Linux/WSL2 on an Intel Core i7-10750H using the default Dune development build: + +| Slices and schedule entries | Complete-list lookup | Ordered index | Speedup | +|---:|---:|---:|---:| +| 5,000 | 0.346 s | 0.216 s | 1.6x | +| 10,000 | 0.911 s | 0.407 s | 2.2x | +| 20,000 | 3.264 s | 0.845 s | 3.9x | + +The exact timings are illustrative rather than service-level targets. The growing baseline ratio +and near-linear indexed results are the relevant regression signal. diff --git a/lib/engine.ml b/lib/engine.ml index a70c868..d33f104 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -66,6 +66,26 @@ module Interactive = struct | Notify of Id.Event.t list * Strategy.event | Act of Id.Event.t list * Strategy.intent + module Pending_queue = struct + type t = { front : pending list; back : pending list } + + let empty = { front = []; back = [] } + let is_empty queue = queue.front = [] && queue.back = [] + + let enqueue queue items = + { queue with back = List.rev_append items queue.back } + + let prepend queue items = { queue with front = items @ queue.front } + + let pop queue = + match queue.front with + | item :: front -> Some (item, { queue with front }) + | [] -> ( + match List.rev queue.back with + | [] -> None + | item :: front -> Some (item, { front; back = [] })) + end + type reduction = { state : t; now : Ptime.t; @@ -73,7 +93,7 @@ module Interactive = struct slice_event_id : Id.Event.t option; causation_ids : Id.Event.t list; audits_rev : Audit.t list; - pending : pending list; + pending : Pending_queue.t; processed : int; } @@ -178,10 +198,10 @@ module Interactive = struct with_causes reduction [ event_id ]) let enqueue reduction items = - { reduction with pending = reduction.pending @ items } + { reduction with pending = Pending_queue.enqueue reduction.pending items } let prepend reduction items = - { reduction with pending = items @ reduction.pending } + { reduction with pending = Pending_queue.prepend reduction.pending items } let value state = let marks = @@ -778,28 +798,31 @@ module Interactive = struct } let rec drain reduction = - match reduction.pending with - | [] -> Ok (Drained reduction) - | _ when reduction.processed >= reduction.state.config.max_internal_events - -> - Error - (Printf.sprintf "internal event count exceeds configured limit of %d" - reduction.state.config.max_internal_events) - | item :: pending -> ( - let reduction = - { reduction with pending; processed = reduction.processed + 1 } - in - match item with - | Notify (causation_ids, event) -> - let reduction = with_causes reduction causation_ids in - let* context = strategy_context reduction.state reduction.now in - Ok (Strategy_requested { reduction; causation_ids; context; event }) - | Act (causation_ids, intent) -> ( - match - handle_intent (with_causes reduction causation_ids) intent - with - | Error _ as error -> error - | Ok reduction -> drain reduction)) + if Pending_queue.is_empty reduction.pending then Ok (Drained reduction) + else if reduction.processed >= reduction.state.config.max_internal_events + then + Error + (Printf.sprintf "internal event count exceeds configured limit of %d" + reduction.state.config.max_internal_events) + else + match Pending_queue.pop reduction.pending with + | None -> Ok (Drained reduction) + | Some (item, pending) -> ( + let reduction = + { reduction with pending; processed = reduction.processed + 1 } + in + match item with + | Notify (causation_ids, event) -> + let reduction = with_causes reduction causation_ids in + let* context = strategy_context reduction.state reduction.now in + Ok + (Strategy_requested { reduction; causation_ids; context; event }) + | Act (causation_ids, intent) -> ( + match + handle_intent (with_causes reduction causation_ids) intent + with + | Error _ as error -> error + | Ok reduction -> drain reduction)) let validate_slice state market_slice = let expected = @@ -1342,7 +1365,7 @@ module Interactive = struct continue Finish_slice reduction | Finish_slice -> let* reduction = assess_margin reduction in - if reduction.pending = [] then + if Pending_queue.is_empty reduction.pending then let* reduction = valuation reduction in Ok (Slice_completed (reduction.state, List.rev reduction.audits_rev)) @@ -1405,7 +1428,7 @@ module Interactive = struct slice_event_id = None; causation_ids = []; audits_rev = []; - pending = []; + pending = Pending_queue.empty; processed = 0; } in @@ -1473,7 +1496,7 @@ module Interactive = struct slice_event_id = None; causation_ids; audits_rev = []; - pending = []; + pending = Pending_queue.empty; processed = 0; } in diff --git a/lib/scenario.ml b/lib/scenario.ml index 53466ae..237cf0c 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -33,6 +33,7 @@ type stream_item = { } module Int64_set = Set.Make (Int64) +module Int64_map = Map.Make (Int64) module String_set = Set.Make (String) let ( let* ) result function_ = @@ -774,50 +775,46 @@ let validate_slices ~base_currency ~currencies ~instruments slices = validate None None None Id.Corporate_action.Set.empty slices let validate_schedule risk catalog schedule slices = - let slice_sequences = - List.fold_left - (fun sequences market_slice -> - Int64_set.add market_slice.Market_slice.slice_sequence sequences) - Int64_set.empty slices - in - let slice_at sequence = - List.find_opt - (fun market_slice -> - Int64.equal market_slice.Market_slice.slice_sequence sequence) - slices - in - let next_slice sequence = - List.find_opt - (fun market_slice -> - Int64.compare market_slice.Market_slice.slice_sequence sequence > 0) - slices + let rec index_slices index = function + | [] -> index + | [ anchor ] -> + Int64_map.add anchor.Market_slice.slice_sequence (anchor, None) index + | anchor :: (next :: _ as remaining) -> + let index = + Int64_map.add anchor.Market_slice.slice_sequence (anchor, Some next) + index + in + index_slices index remaining in + let slice_index = index_slices Int64_map.empty slices in let validate_item sequence intents = if Int64.compare sequence 0L <= 0 then Error "scheduled slice sequence must be positive" - else if not (Int64_set.mem sequence slice_sequences) then - Error - (Printf.sprintf - "scheduled intents refer to missing market slice sequence %Ld" - sequence) else - let* () = - List.fold_left - (fun result intent -> - let* () = result in - validate_portfolio_target risk catalog intent) - (Ok ()) intents - in - match (slice_at sequence, next_slice sequence) with - | Some anchor, Some next - when List.exists changes_orders intents - && Ptime.compare anchor.received_at next.start_at > 0 -> + match Int64_map.find_opt sequence slice_index with + | None -> Error (Printf.sprintf - "scheduled order intent after slice %Ld is received after the \ - next executable market slice starts" + "scheduled intents refer to missing market slice sequence %Ld" sequence) - | _ -> Ok () + | Some (anchor, next) -> ( + let* () = + List.fold_left + (fun result intent -> + let* () = result in + validate_portfolio_target risk catalog intent) + (Ok ()) intents + in + match next with + | Some next + when List.exists changes_orders intents + && Ptime.compare anchor.received_at next.start_at > 0 -> + Error + (Printf.sprintf + "scheduled order intent after slice %Ld is received after \ + the next executable market slice starts" + sequence) + | None | Some _ -> Ok ()) in let rec validate previous = function | [] -> Ok () diff --git a/test/test_reducer.ml b/test/test_reducer.ml index d42c430..630c388 100644 --- a/test/test_reducer.ml +++ b/test/test_reducer.ml @@ -416,6 +416,36 @@ let exact_internal_event_limit_succeeds () = "one callback fits a limit of one" true (Result.is_ok (Runner.process_slice state (market_slice 1L))) +let reducer_feedback_queue_handles_large_batches () = + let batch_size = T.Resource_limits.intents_per_batch in + let invalid_intent = + T.Strategy.Submit_order + (request ~quantity_value:"1" ~origin:T.Order.Target_rebalance ()) + in + let intents = List.init batch_size (fun _ -> invalid_intent) in + let strategy_state = T.Scripted_strategy.create [ (1L, intents) ] |> ok in + let max_internal_events = (2 * batch_size) + 1 in + let config = engine_config ~max_internal_events () in + let state = + Runner.create ~run_id:(run_id "large-feedback") ~scenario_sha256 ~config + ~initial_cash:[ ("USD", money "10000") ] + ~strategy_state + |> ok + in + let _, events = Runner.process_slice state (market_slice 1L) |> ok in + let rejection_count = + List.fold_left + (fun count audit -> + match audit.T.Audit.event with + | T.Audit.Intent_rejected _ -> count + 1 + | _ -> count) + 0 events + in + Alcotest.(check int) "every intent rejected" batch_size rejection_count; + Alcotest.(check int) + "batch completes at the exact feedback limit" (batch_size + 3) + (List.length events) + let completed_run_is_terminal_and_hash_bound () = let state = runner [] in let state, valuation, events = Runner.complete state |> ok in @@ -753,6 +783,8 @@ let tests = internal_feedback_is_capped; Alcotest.test_case "exact internal event limit" `Quick exact_internal_event_limit_succeeds; + Alcotest.test_case "large reducer feedback batch" `Slow + reducer_feedback_queue_handles_large_batches; Alcotest.test_case "completed run is terminal and hash-bound" `Quick completed_run_is_terminal_and_hash_bound; Alcotest.test_case "invalid initial state rejected" `Quick diff --git a/test/test_scenario.ml b/test/test_scenario.ml index 6f639c8..e2bfe03 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -264,6 +264,65 @@ let map_field key change = function fields) | _ -> Alcotest.fail "expected object" +let dense_schedule_document slice_count = + let base = timestamp "2026-02-01T00:00:00Z" in + let slices = + List.init slice_count (fun offset -> + let index = offset + 1 in + let time_offset = offset * 4 in + T.Market_slice.create ~slice_sequence:(Int64.of_int index) + ~start_at:(add_seconds base time_offset) + ~end_at:(add_seconds base (time_offset + 1)) + ~available_at:(add_seconds base (time_offset + 2)) + ~received_at:(add_seconds base (time_offset + 3)) + ~bars: + [ + bar + ~instrument:(instrument_id "demo-equity-acme") + (Int64.of_int index); + ] + ~fx_rates:[ fx_mark () ] + ~corporate_actions:[] + |> ok |> T.Codec.market_slice_to_yojson) + in + let schedule = + List.init slice_count (fun offset -> + let sequence = offset + 1 in + `Assoc + [ + ("after_slice_sequence", `String (string_of_int sequence)); + ( "intents", + `List + [ + `Assoc + [ + ("type", `String "emit_metric"); + ("name", `String "dense_schedule"); + ("value", `String (string_of_int sequence)); + ]; + ] ); + ]) + in + map_root (fun fields -> + List.map + (fun (name, value) -> + if String.equal name "slices" then (name, `List slices) + else if String.equal name "schedule" then (name, `List schedule) + else (name, value)) + fields) + +let dense_batch_schedule_validation_scales () = + let slice_count = 20_000 in + let scenario = + dense_schedule_document slice_count |> T.Scenario.of_yojson |> ok + in + Alcotest.(check int) + "all slices retained" slice_count + (List.length scenario.slices); + Alcotest.(check int) + "all schedule entries retained" slice_count + (List.length scenario.schedule) + let configured_resources_are_bounded () = let document = Yojson.Safe.from_string (demo_document ()) in let check_limit expected_path changed = @@ -390,7 +449,29 @@ let invalid_schedule_sequences_are_rejected () = in Alcotest.(check string) "duplicate schedule rejected" "schedule sequences must increase" - (T.Scenario.of_yojson duplicate |> diagnostic_message) + (T.Scenario.of_yojson duplicate |> diagnostic_message); + let late_anchor = + map_root (fun fields -> + List.map + (fun (name, json) -> + if String.equal name "slices" then + match json with + | `List (first :: second :: rest) -> + ( name, + `List + (first + :: change_field "start_at" + (`String "2026-01-02T21:00:01Z") second + :: rest) ) + | _ -> Alcotest.fail "demo must contain at least two slices" + else (name, json)) + fields) + in + Alcotest.(check string) + "late anchor diagnosed" + "scheduled order intent after slice 1 is received after the next \ + executable market slice starts" + (T.Scenario.of_yojson late_anchor |> diagnostic_message) let duplicate_and_incomplete_slice_bars_are_rejected () = let duplicate = @@ -934,6 +1015,8 @@ let tests = configured_resources_are_bounded; Alcotest.test_case "invalid schedule sequences rejected" `Quick invalid_schedule_sequences_are_rejected; + Alcotest.test_case "dense batch schedule validation" `Slow + dense_batch_schedule_validation_scales; Alcotest.test_case "duplicate slice bars rejected" `Quick duplicate_and_incomplete_slice_bars_are_rejected; Alcotest.test_case "market slice timeline is non-overlapping" `Quick From 27d6cc49d49a30c2ac76360f17a9fe27c2ee595b Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 21:54:43 -0400 Subject: [PATCH 14/57] build: make development environment reproducible --- .github/workflows/ci.yml | 7 +- CONTRIBUTING.md | 19 +++-- Makefile | 16 +++- requirements/schema.in | 1 + requirements/schema.lock | 50 ++++++++++++ scripts/bootstrap-development-environment | 50 ++++++++++++ scripts/check-development-environment | 49 ++++++++++++ scripts/check-schema-environment.py | 94 +++++++++++++++++++++++ test/dune | 8 ++ test/test_development_environment.py | 57 ++++++++++++++ 10 files changed, 334 insertions(+), 17 deletions(-) create mode 100644 requirements/schema.in create mode 100644 requirements/schema.lock create mode 100755 scripts/bootstrap-development-environment create mode 100755 scripts/check-development-environment create mode 100644 scripts/check-schema-environment.py create mode 100644 test/test_development_environment.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6540957..95b48c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,12 +21,7 @@ jobs: - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: python-version: "3.12" - - run: uv venv .venv-schema - - run: >- - uv pip install --python .venv-schema/bin/python - "jsonschema[format-nongpl]==4.26.0" - - run: echo "$GITHUB_WORKSPACE/.venv-schema/bin" >> "$GITHUB_PATH" - - run: opam install . --deps-only --with-test --locked + - run: make bootstrap - run: make check persistra-compatibility: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6671ece..8417454 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,20 +1,25 @@ # Contributing -Use the repository-local opam switch and install development dependencies: +Install `opam`, `uv`, and Python 3, then bootstrap the repository-local development +environment: ```sh -opam install . --deps-only --with-test --locked +make bootstrap ``` -The schema conformance tests also require Python 3 and the JSON Schema format -validators: +The command creates or updates only the repository-local opam switch and +`.venv-schema`. It installs the locked OCaml dependencies and the fully pinned JSON +Schema validator environment. It is safe to run again after either lock changes. + +Check an existing environment without changing it: ```sh -python3 -m venv .venv-schema -.venv-schema/bin/python -m pip install 'jsonschema[format-nongpl]==4.26.0' -export PATH="$PWD/.venv-schema/bin:$PATH" +make environment-check ``` +The check reports missing tools, a missing or incorrect local opam switch, stale +locked dependencies, and an incomplete schema environment with a suggested repair. + Run the complete local gate before committing: ```sh diff --git a/Makefile b/Makefile index 5bb2738..314ee92 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,20 @@ -.PHONY: build test fmt-check check +export PATH := $(CURDIR)/.venv-schema/bin:$(PATH) -build: +.PHONY: bootstrap environment-check build test fmt-check check + +bootstrap: + @./scripts/bootstrap-development-environment + +environment-check: + @./scripts/check-development-environment + +build: environment-check opam exec -- dune build @all -test: +test: environment-check opam exec -- dune runtest -fmt-check: +fmt-check: environment-check opam exec -- dune build @fmt check: fmt-check build test diff --git a/requirements/schema.in b/requirements/schema.in new file mode 100644 index 0000000..9ccf3ef --- /dev/null +++ b/requirements/schema.in @@ -0,0 +1 @@ +jsonschema[format-nongpl]==4.26.0 diff --git a/requirements/schema.lock b/requirements/schema.lock new file mode 100644 index 0000000..9b5e31c --- /dev/null +++ b/requirements/schema.lock @@ -0,0 +1,50 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --python-version 3.12 --no-python-downloads requirements/schema.in --output-file requirements/schema.lock +arrow==1.4.0 + # via isoduration +attrs==26.1.0 + # via + # jsonschema + # referencing +fqdn==1.5.1 + # via jsonschema +idna==3.19 + # via jsonschema +isoduration==20.11.0 + # via jsonschema +jsonpointer==3.1.1 + # via jsonschema +jsonschema==4.26.0 + # via -r requirements/schema.in +jsonschema-specifications==2025.9.1 + # via jsonschema +lark==1.3.1 + # via rfc3987-syntax +python-dateutil==2.9.0.post0 + # via arrow +referencing==0.37.0 + # via + # jsonschema + # jsonschema-specifications +rfc3339-validator==0.1.4 + # via jsonschema +rfc3986-validator==0.1.1 + # via jsonschema +rfc3987-syntax==1.1.0 + # via jsonschema +rpds-py==2026.6.3 + # via + # jsonschema + # referencing +six==1.17.0 + # via + # python-dateutil + # rfc3339-validator +typing-extensions==4.16.0 + # via referencing +tzdata==2026.3 + # via arrow +uri-template==1.3.0 + # via jsonschema +webcolors==25.10.0 + # via jsonschema diff --git a/scripts/bootstrap-development-environment b/scripts/bootstrap-development-environment new file mode 100755 index 0000000..3585cb1 --- /dev/null +++ b/scripts/bootstrap-development-environment @@ -0,0 +1,50 @@ +#!/bin/sh + +set -eu + +repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +schema_environment="$repository_root/.venv-schema" +schema_lock="$repository_root/requirements/schema.lock" + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + printf '%s\n' "error: $1 is required; install it and rerun 'make bootstrap'" >&2 + exit 1 + fi +} + +require_command opam +require_command uv +require_command python3 + +if [ ! -f "$repository_root/_opam/.opam-switch/switch-config" ]; then + printf '%s\n' "Creating the repository-local OCaml 5.5.0 switch..." + opam switch create "$repository_root" 5.5.0 --no-install --yes +fi + +printf '%s\n' "Installing locked OCaml development dependencies..." +opam install "$repository_root" \ + --deps-only \ + --with-test \ + --with-doc \ + --locked \ + --require-checksums \ + --switch "$repository_root" \ + --yes + +if [ ! -x "$schema_environment/bin/python" ]; then + printf '%s\n' "Creating the repository-local schema environment..." + uv venv \ + --python "$(command -v python3)" \ + --no-python-downloads \ + "$schema_environment" +fi + +printf '%s\n' "Installing locked JSON Schema dependencies..." +uv pip sync \ + --python "$schema_environment/bin/python" \ + --no-python-downloads \ + --strict \ + "$schema_lock" + +"$repository_root/scripts/check-development-environment" diff --git a/scripts/check-development-environment b/scripts/check-development-environment new file mode 100755 index 0000000..0c22b65 --- /dev/null +++ b/scripts/check-development-environment @@ -0,0 +1,49 @@ +#!/bin/sh + +set -eu + +repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +local_switch="$repository_root/_opam" +schema_python="$repository_root/.venv-schema/bin/python" + +fail() { + printf '%s\n' "error: $1" >&2 + printf '%s\n' "run 'make bootstrap' to create or update the local environment" >&2 + exit 1 +} + +command -v opam >/dev/null 2>&1 || fail "opam is not installed" +command -v uv >/dev/null 2>&1 || fail "uv is not installed" + +if [ ! -f "$local_switch/.opam-switch/switch-config" ]; then + fail "the repository-local opam switch is missing" +fi + +actual_switch=$(opam var prefix --switch "$repository_root" 2>/dev/null) || + fail "opam cannot read the repository-local switch" +if [ "$actual_switch" != "$local_switch" ]; then + fail "opam selected '$actual_switch' instead of '$local_switch'" +fi + +if ! missing_dependencies=$( + opam install "$repository_root" \ + --deps-only \ + --with-test \ + --with-doc \ + --locked \ + --check \ + --switch "$repository_root" 2>&1 +); then + printf '%s\n' "$missing_dependencies" >&2 + fail "locked OCaml development dependencies are missing" +fi + +if [ ! -x "$schema_python" ]; then + fail "the repository-local schema environment is missing" +fi + +if ! "$schema_python" "$repository_root/scripts/check-schema-environment.py"; then + fail "the schema environment does not match requirements/schema.lock" +fi + +printf '%s\n' "Development environment is ready." diff --git a/scripts/check-schema-environment.py b/scripts/check-schema-environment.py new file mode 100644 index 0000000..49fac52 --- /dev/null +++ b/scripts/check-schema-environment.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import importlib.metadata +import pathlib +import re +import sys + + +PIN = re.compile(r"^([A-Za-z0-9_.-]+)==([^\s;]+)$") + + +def normalized(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower() + + +def locked_versions(lock_path: pathlib.Path) -> dict[str, str]: + locked: dict[str, str] = {} + for raw_line in lock_path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + match = PIN.fullmatch(line) + if match is not None: + locked[normalized(match.group(1))] = match.group(2) + if not locked: + raise ValueError(f"no pinned dependencies found in {lock_path}") + return locked + + +def installed_versions() -> dict[str, str]: + installed: dict[str, str] = {} + for distribution in importlib.metadata.distributions(): + name = distribution.metadata.get("Name") + if name is not None: + installed[normalized(name)] = distribution.version + return installed + + +def dependency_differences( + locked: dict[str, str], installed: dict[str, str] +) -> list[str]: + differences = [ + f"{name}: expected {version}, found {installed.get(name, 'missing')}" + for name, version in sorted(locked.items()) + if installed.get(name) != version + ] + unexpected = sorted(set(installed) - set(locked)) + differences.extend(f"{name}: installed but not locked" for name in unexpected) + return differences + + +def main() -> int: + repository_root = pathlib.Path(__file__).resolve().parent.parent + lock_path = repository_root / "requirements" / "schema.lock" + try: + locked = locked_versions(lock_path) + except (OSError, ValueError) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + installed = installed_versions() + differences = dependency_differences(locked, installed) + if differences: + print("error: schema dependency mismatch:", file=sys.stderr) + for difference in differences: + print(f" - {difference}", file=sys.stderr) + return 1 + + try: + from jsonschema import ( # noqa: PLC0415 + Draft4Validator, + Draft6Validator, + Draft7Validator, + Draft201909Validator, + Draft202012Validator, + FormatChecker, + ) + + validators = ( + Draft4Validator, + Draft6Validator, + Draft7Validator, + Draft201909Validator, + Draft202012Validator, + ) + if not validators or not FormatChecker.checkers: + raise RuntimeError("JSON Schema validators or format checkers are unavailable") + except (ImportError, RuntimeError) as error: + print(f"error: incomplete JSON Schema installation: {error}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/dune b/test/dune index e1c6207..a0fec0d 100644 --- a/test/dune +++ b/test/dune @@ -126,3 +126,11 @@ %{dep:../contracts/strategy/v3/message.schema.json} %{dep:../contracts/strategy/v3/transcript.schema.json} %{dep:../contracts/strategy/v3/fixtures/external.strategy.jsonl}))) + +(rule + (alias runtest) + (deps + test_development_environment.py + ../scripts/check-schema-environment.py) + (action + (run python3 %{dep:test_development_environment.py}))) diff --git a/test/test_development_environment.py b/test/test_development_environment.py new file mode 100644 index 0000000..4534a41 --- /dev/null +++ b/test/test_development_environment.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import importlib.util +import pathlib +import tempfile +import unittest + + +REPOSITORY_ROOT = pathlib.Path(__file__).resolve().parent.parent +MODULE_PATH = REPOSITORY_ROOT / "scripts" / "check-schema-environment.py" +SPEC = importlib.util.spec_from_file_location("check_schema_environment", MODULE_PATH) +if SPEC is None or SPEC.loader is None: + raise RuntimeError(f"cannot load {MODULE_PATH}") +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class SchemaEnvironmentTest(unittest.TestCase): + def test_reads_exact_pins_and_normalizes_names(self) -> None: + with tempfile.TemporaryDirectory() as directory: + lock_path = pathlib.Path(directory) / "schema.lock" + lock_path.write_text( + "# generated\nTyping_Extensions==4.16.0\njsonschema==4.26.0\n", + encoding="utf-8", + ) + + self.assertEqual( + MODULE.locked_versions(lock_path), + {"typing-extensions": "4.16.0", "jsonschema": "4.26.0"}, + ) + + def test_rejects_a_lock_without_exact_pins(self) -> None: + with tempfile.TemporaryDirectory() as directory: + lock_path = pathlib.Path(directory) / "schema.lock" + lock_path.write_text("jsonschema>=4\n", encoding="utf-8") + + with self.assertRaisesRegex(ValueError, "no pinned dependencies"): + MODULE.locked_versions(lock_path) + + def test_reports_missing_mismatched_and_unexpected_packages(self) -> None: + differences = MODULE.dependency_differences( + {"attrs": "26.1.0", "jsonschema": "4.26.0"}, + {"attrs": "25.0.0", "extra": "1.0.0"}, + ) + + self.assertEqual( + differences, + [ + "attrs: expected 26.1.0, found 25.0.0", + "jsonschema: expected 4.26.0, found missing", + "extra: installed but not locked", + ], + ) + + +if __name__ == "__main__": + unittest.main() From 210e94077aa32ab1dc80a057c1e9b641c92959d6 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 21:58:08 -0400 Subject: [PATCH 15/57] fix: align environment checks with supported gate --- Makefile | 8 ++++---- scripts/bootstrap-development-environment | 1 - scripts/check-development-environment | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 314ee92..5a0b8e2 100644 --- a/Makefile +++ b/Makefile @@ -8,13 +8,13 @@ bootstrap: environment-check: @./scripts/check-development-environment -build: environment-check +build: opam exec -- dune build @all -test: environment-check +test: opam exec -- dune runtest -fmt-check: environment-check +fmt-check: opam exec -- dune build @fmt -check: fmt-check build test +check: environment-check fmt-check build test diff --git a/scripts/bootstrap-development-environment b/scripts/bootstrap-development-environment index 3585cb1..54bd75c 100755 --- a/scripts/bootstrap-development-environment +++ b/scripts/bootstrap-development-environment @@ -26,7 +26,6 @@ printf '%s\n' "Installing locked OCaml development dependencies..." opam install "$repository_root" \ --deps-only \ --with-test \ - --with-doc \ --locked \ --require-checksums \ --switch "$repository_root" \ diff --git a/scripts/check-development-environment b/scripts/check-development-environment index 0c22b65..c121e32 100755 --- a/scripts/check-development-environment +++ b/scripts/check-development-environment @@ -29,7 +29,6 @@ if ! missing_dependencies=$( opam install "$repository_root" \ --deps-only \ --with-test \ - --with-doc \ --locked \ --check \ --switch "$repository_root" 2>&1 From c8104910b71c4e242302aa17958b462505f13af3 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 22:02:06 -0400 Subject: [PATCH 16/57] fix: verify the locked opam action plan --- scripts/check-development-environment | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/check-development-environment b/scripts/check-development-environment index c121e32..18b4304 100755 --- a/scripts/check-development-environment +++ b/scripts/check-development-environment @@ -25,16 +25,21 @@ if [ "$actual_switch" != "$local_switch" ]; then fail "opam selected '$actual_switch' instead of '$local_switch'" fi -if ! missing_dependencies=$( +if ! pending_actions=$( opam install "$repository_root" \ --deps-only \ --with-test \ --locked \ - --check \ + --show-actions \ + --color never \ --switch "$repository_root" 2>&1 ); then - printf '%s\n' "$missing_dependencies" >&2 - fail "locked OCaml development dependencies are missing" + printf '%s\n' "$pending_actions" >&2 + fail "opam could not evaluate the locked development dependencies" +fi +if ! printf '%s\n' "$pending_actions" | grep -Fqx "Nothing to do."; then + printf '%s\n' "$pending_actions" >&2 + fail "the locked OCaml development dependencies are stale" fi if [ ! -x "$schema_python" ]; then From 4a745ad618ad29e6c95f64d94f51ecaff3be6b37 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 22:31:04 -0400 Subject: [PATCH 17/57] test: enforce contract conformance --- README.md | 5 +- contracts/conformance/README.md | 35 ++ contracts/conformance/cases.json | 274 ++++++++++++++++ contracts/conformance/dune | 8 + contracts/conformance/frozen.sha256 | 22 ++ contracts/conformance/manifest.json | 175 ++++++++++ contracts/v1/README.md | 4 +- contracts/v2/README.md | 4 +- test/dune | 12 + test/test_contract_conformance.ml | 210 ++++++++++++ test/test_engine.ml | 1 + test/validate_contract_conformance.py | 448 ++++++++++++++++++++++++++ 12 files changed, 1193 insertions(+), 5 deletions(-) create mode 100644 contracts/conformance/README.md create mode 100644 contracts/conformance/cases.json create mode 100644 contracts/conformance/dune create mode 100644 contracts/conformance/frozen.sha256 create mode 100644 contracts/conformance/manifest.json create mode 100644 test/test_contract_conformance.ml create mode 100644 test/validate_contract_conformance.py diff --git a/README.md b/README.md index d5bd824..b06cfff 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,9 @@ scenario slices and scheduled or external intents ## Quick start The project uses a local switch and does not modify the default switch. The complete check also -uses Python's `jsonschema` package to validate the committed scenario and journal fixtures. +uses Python's `jsonschema` package to validate every committed schema and canonical fixture. It +checks the frozen-artifact hashes and runs the current differential corpus against the OCaml +parsers. ```sh cd ~/trading-engine @@ -202,6 +204,7 @@ do not provide reducer snapshots or restart recovery. - [Architecture](docs/architecture.md) - [Diagnostic contract](docs/diagnostics.md) - [Scenario contract](docs/scenario.md) +- [Contract conformance corpus](contracts/conformance/README.md) - [Current contract v4 and conformance fixtures](contracts/v4/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) diff --git a/contracts/conformance/README.md b/contracts/conformance/README.md new file mode 100644 index 0000000..fce76c8 --- /dev/null +++ b/contracts/conformance/README.md @@ -0,0 +1,35 @@ +# Contract conformance corpus + +This directory is the machine-readable entry point for contract consumers. + +- `manifest.json` maps every versioned schema branch to its canonical fixtures. +- `cases.json` records deterministic schema/runtime differential cases for the + versions accepted by the current OCaml runtime. +- `frozen.sha256` protects the schema and fixture bytes for archived scenario + contract v1 and v2 and strategy protocol v1 and v2. + +The artifact manifest treats each schema at each version as a separate branch. +Every branch has positive canonical inputs and generated negative cases for a +missing version, an unsupported version, and an unknown field. Frozen branches +are schema-only: they are never passed to current runtime parsers. + +Every top-level `oneOf` alternative also has a positive witness and a derived +unknown-field rejection. `schema_only_cases` supplies variants that do not occur +in a successful canonical run, such as strategy `error` messages and rejected +response transcript records. + +Differential cases label rules as `structural` or `semantic`. Structural cases +must produce the same result from JSON Schema and the OCaml parser. Semantic +cases explicitly document invariants that JSON Schema cannot express, so an +accepted schema result and a rejected runtime result is intentional. + +Run the complete corpus through the repository gate: + +```sh +make check +``` + +When intentionally changing an archived contract, update `frozen.sha256` in the +same review. Adding a contract version requires a manifest branch, canonical +fixtures, positive and negative validation, and current-runtime differential +cases when the new version is advertised by the engine. diff --git a/contracts/conformance/cases.json b/contracts/conformance/cases.json new file mode 100644 index 0000000..bfc9fbd --- /dev/null +++ b/contracts/conformance/cases.json @@ -0,0 +1,274 @@ +{ + "format_version": "1", + "cases": [ + { + "name": "scenario-v4-valid", + "artifact": "scenario-v4", + "kind": "scenario", + "source": "v4/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-v3-valid", + "artifact": "scenario-v3", + "kind": "scenario", + "source": "v3/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-missing-version", + "artifact": "scenario-v4", + "kind": "scenario", + "source": "v4/fixtures/demo.scenario.json", + "mutations": [{"op": "remove", "path": ["contract_version"]}], + "schema_expectation": "reject", + "runtime_expectation": "reject", + "rule": "structural" + }, + { + "name": "scenario-unknown-field", + "artifact": "scenario-v4", + "kind": "scenario", + "source": "v4/fixtures/demo.scenario.json", + "mutations": [{"op": "add", "path": ["unexpected_contract_field"], "value": true}], + "schema_expectation": "reject", + "runtime_expectation": "reject", + "rule": "structural" + }, + { + "name": "scenario-invalid-scalar-type", + "artifact": "scenario-v4", + "kind": "scenario", + "source": "v4/fixtures/demo.scenario.json", + "mutations": [{"op": "replace", "path": ["initial_cash", 0, "amount"], "value": 10000}], + "schema_expectation": "reject", + "runtime_expectation": "reject", + "rule": "structural" + }, + { + "name": "scenario-duplicate-instrument", + "artifact": "scenario-v4", + "kind": "scenario", + "source": "v4/fixtures/demo.scenario.json", + "mutations": [{"op": "append_copy", "path": ["instruments"], "index": 0}], + "schema_expectation": "accept", + "runtime_expectation": "reject", + "rule": "semantic" + }, + { + "name": "scenario-overlapping-slices", + "artifact": "scenario-v4", + "kind": "scenario", + "source": "v4/fixtures/demo.scenario.json", + "mutations": [{"op": "replace", "path": ["slices", 1, "start_at"], "value": "2026-01-02T20:00:00Z"}], + "schema_expectation": "accept", + "runtime_expectation": "reject", + "rule": "semantic" + }, + { + "name": "scenario-stream-v4-valid", + "artifact": "scenario-stream-v4", + "kind": "scenario_stream", + "source": "v4/fixtures/demo.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-stream-v3-valid", + "artifact": "scenario-stream-v3", + "kind": "scenario_stream", + "source": "v3/fixtures/demo.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-stream-missing-version", + "artifact": "scenario-stream-v4", + "kind": "scenario_stream", + "source": "v4/fixtures/demo.scenario.jsonl", + "record": 1, + "mutations": [{"op": "remove", "path": ["contract_version"]}], + "schema_expectation": "reject", + "runtime_expectation": "reject", + "rule": "structural" + }, + { + "name": "scenario-stream-unknown-field", + "artifact": "scenario-stream-v4", + "kind": "scenario_stream", + "source": "v4/fixtures/demo.scenario.jsonl", + "record": 2, + "mutations": [{"op": "add", "path": ["unexpected_contract_field"], "value": true}], + "schema_expectation": "reject", + "runtime_expectation": "reject", + "rule": "structural" + }, + { + "name": "scenario-stream-out-of-order-sequence", + "artifact": "scenario-stream-v4", + "kind": "scenario_stream", + "source": "v4/fixtures/demo.scenario.jsonl", + "record": 2, + "mutations": [{"op": "replace", "path": ["scenario_sequence"], "value": "3"}], + "schema_expectation": "accept", + "runtime_expectation": "reject", + "rule": "semantic" + }, + { + "name": "strategy-ready-valid", + "artifact": "strategy-message-v3", + "kind": "strategy_response", + "source": "strategy/v3/fixtures/external.strategy.jsonl", + "record": 2, + "extract": ["message"], + "expected_sequence": "1", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-intents-valid", + "artifact": "strategy-message-v3", + "kind": "strategy_response", + "source": "strategy/v3/fixtures/external.strategy.jsonl", + "record": 4, + "extract": ["message"], + "expected_sequence": "2", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-stopped-valid", + "artifact": "strategy-message-v3", + "kind": "strategy_response", + "source": "strategy/v3/fixtures/external.strategy.jsonl", + "record": 14, + "extract": ["message"], + "expected_sequence": "7", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-error-valid", + "artifact": "strategy-message-v3", + "kind": "strategy_response", + "source": "strategy/v3/fixtures/external.strategy.jsonl", + "record": 14, + "extract": ["message"], + "expected_sequence": "7", + "mutations": [ + {"op": "replace", "path": ["message_type"], "value": "error"}, + {"op": "replace", "path": ["payload"], "value": {"message": "intentional conformance error"}} + ], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-missing-version", + "artifact": "strategy-message-v3", + "kind": "strategy_response", + "source": "strategy/v3/fixtures/external.strategy.jsonl", + "record": 2, + "extract": ["message"], + "expected_sequence": "1", + "mutations": [{"op": "remove", "path": ["strategy_protocol_version"]}], + "schema_expectation": "reject", + "runtime_expectation": "reject", + "rule": "structural" + }, + { + "name": "strategy-unknown-field", + "artifact": "strategy-message-v3", + "kind": "strategy_response", + "source": "strategy/v3/fixtures/external.strategy.jsonl", + "record": 2, + "extract": ["message"], + "expected_sequence": "1", + "mutations": [{"op": "add", "path": ["unexpected_contract_field"], "value": true}], + "schema_expectation": "reject", + "runtime_expectation": "reject", + "rule": "structural" + }, + { + "name": "strategy-wrong-sequence", + "artifact": "strategy-message-v3", + "kind": "strategy_response", + "source": "strategy/v3/fixtures/external.strategy.jsonl", + "record": 2, + "extract": ["message"], + "expected_sequence": "2", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "reject", + "rule": "semantic" + } + ], + "schema_only_cases": [ + { + "name": "strategy-v1-error-branch", + "artifact": "strategy-message-v1", + "source": "strategy/v1/fixtures/external.strategy.jsonl", + "record": 14, + "extract": ["message"], + "mutations": [ + {"op": "replace", "path": ["message_type"], "value": "error"}, + {"op": "replace", "path": ["payload"], "value": {"message": "intentional conformance error"}} + ], + "schema_expectation": "accept" + }, + { + "name": "strategy-v2-error-branch", + "artifact": "strategy-message-v2", + "source": "strategy/v2/fixtures/external.strategy.jsonl", + "record": 14, + "extract": ["message"], + "mutations": [ + {"op": "replace", "path": ["message_type"], "value": "error"}, + {"op": "replace", "path": ["payload"], "value": {"message": "intentional conformance error"}} + ], + "schema_expectation": "accept" + }, + { + "name": "strategy-v3-rejected-response-branch", + "artifact": "strategy-transcript-v3", + "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/dune b/contracts/conformance/dune new file mode 100644 index 0000000..d1eb67f --- /dev/null +++ b/contracts/conformance/dune @@ -0,0 +1,8 @@ +(install + (section share) + (package trading_engine) + (files + (README.md as contracts/conformance/README.md) + (cases.json as contracts/conformance/cases.json) + (frozen.sha256 as contracts/conformance/frozen.sha256) + (manifest.json as contracts/conformance/manifest.json))) diff --git a/contracts/conformance/frozen.sha256 b/contracts/conformance/frozen.sha256 new file mode 100644 index 0000000..a17e5ab --- /dev/null +++ b/contracts/conformance/frozen.sha256 @@ -0,0 +1,22 @@ +78df448f65b0d784d51951108cacb1db247d9d2dd03d93886f43e4acc0e83e1d contracts/strategy/v1/fixtures/external.scenario.json +135cc392abcf1f9dae9e2f190bb96c68ef9b39c44e23ac9250482d93912e326d contracts/strategy/v1/fixtures/external.scenario.jsonl +c93f085b131c1af6aadd84a8fda6bebe9520e5e11c03945b47577846bca7cced contracts/strategy/v1/fixtures/external.strategy.jsonl +9bba3babb59ec025bbcc159b24b99b2f260341456e12caafc1ea05d95a4c9742 contracts/strategy/v1/message.schema.json +64aedf3ea18319c65ad8c3d5a7775e5acff9712170e9ce2211839573f84ddbfb contracts/strategy/v1/transcript.schema.json +78df448f65b0d784d51951108cacb1db247d9d2dd03d93886f43e4acc0e83e1d contracts/strategy/v2/fixtures/external.scenario.json +135cc392abcf1f9dae9e2f190bb96c68ef9b39c44e23ac9250482d93912e326d contracts/strategy/v2/fixtures/external.scenario.jsonl +c35b69d323a35d1637955034b357d83215a67eb0f89513686e271d61af6a8cb4 contracts/strategy/v2/fixtures/external.strategy.jsonl +4666cc7ee98a1420a4003150f080a3f3a538e4366dfa1b7d80b0595518af97a7 contracts/strategy/v2/message.schema.json +c0c974c7aa57e16179462a5ef2a34924b8593d6823b928b2c2f51ce2817c7db5 contracts/strategy/v2/transcript.schema.json +2b4c0b2608fff14fe32cc1366a307e181993f81e14eaf16632ec72ca4eef8bda contracts/v1/fixtures/demo.journal.jsonl +a782fbbee8b89332ee6491d9d9be36bf7f2aaacbd440d30c20ed033fd7566d19 contracts/v1/fixtures/demo.scenario.json +d83b7c23d5e44793f9b71c542738cf4fed0612117e8399a61059677974f3382c contracts/v1/fixtures/demo.scenario.jsonl +9dd286602774f360b149f28828b56ccb1a8676dad8c5e2843d9df5499697fddf contracts/v1/journal.schema.json +9ee2f673c15c410e7cf9943a93526761a2eead592e5ac6948c9abc9d005d3c4e contracts/v1/scenario-stream.schema.json +cdd3102b873206c882f80496bc50f013b82ae62411025c5aa06fffafc692cb07 contracts/v1/scenario.schema.json +7c04bdff8488de02bf6e95238b91b348b0a36eecbb808030ac9e346552790f62 contracts/v2/fixtures/demo.journal.jsonl +21834e964dd6daab292e6285924384970b3341f7166ead1d38f8edb284541e44 contracts/v2/fixtures/demo.scenario.json +576573a119da2fecb9188cdf309bd21888b34d594c416c69e5d69c55255e1765 contracts/v2/fixtures/demo.scenario.jsonl +e222fb136b636b6281186512078bc781d234cefe3c2ede82b1c792daf66231cf contracts/v2/journal.schema.json +fb7e5c459977d6351f4b40938ce898441b0afc268b048b4e48bb094954186f46 contracts/v2/scenario-stream.schema.json +0340a0f5305810acd102894cf8557e312534bbad6eafcf5c27ae88b1dfaca147 contracts/v2/scenario.schema.json diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json new file mode 100644 index 0000000..40fe4e4 --- /dev/null +++ b/contracts/conformance/manifest.json @@ -0,0 +1,175 @@ +{ + "format_version": "1", + "artifacts": [ + { + "name": "scenario-v1", + "schema": "v1/scenario.schema.json", + "version_field": "contract_version", + "version": "1", + "sources": [ + {"path": "v1/fixtures/demo.scenario.json", "format": "json"} + ] + }, + { + "name": "scenario-stream-v1", + "schema": "v1/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "1", + "sources": [ + {"path": "v1/fixtures/demo.scenario.jsonl", "format": "jsonl"} + ] + }, + { + "name": "journal-v1", + "schema": "v1/journal.schema.json", + "version_field": "contract_version", + "version": "1", + "sources": [ + {"path": "v1/fixtures/demo.journal.jsonl", "format": "jsonl"} + ] + }, + { + "name": "scenario-v2", + "schema": "v2/scenario.schema.json", + "version_field": "contract_version", + "version": "2", + "sources": [ + {"path": "v2/fixtures/demo.scenario.json", "format": "json"} + ] + }, + { + "name": "scenario-stream-v2", + "schema": "v2/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "2", + "sources": [ + {"path": "v2/fixtures/demo.scenario.jsonl", "format": "jsonl"} + ] + }, + { + "name": "journal-v2", + "schema": "v2/journal.schema.json", + "version_field": "contract_version", + "version": "2", + "sources": [ + {"path": "v2/fixtures/demo.journal.jsonl", "format": "jsonl"} + ] + }, + { + "name": "scenario-v3", + "schema": "v3/scenario.schema.json", + "version_field": "contract_version", + "version": "3", + "sources": [ + {"path": "v3/fixtures/demo.scenario.json", "format": "json"}, + {"path": "strategy/v1/fixtures/external.scenario.json", "format": "json"}, + {"path": "strategy/v2/fixtures/external.scenario.json", "format": "json"}, + {"path": "strategy/v3/fixtures/external.scenario.json", "format": "json"} + ] + }, + { + "name": "scenario-stream-v3", + "schema": "v3/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "3", + "sources": [ + {"path": "v3/fixtures/demo.scenario.jsonl", "format": "jsonl"}, + {"path": "strategy/v1/fixtures/external.scenario.jsonl", "format": "jsonl"}, + {"path": "strategy/v2/fixtures/external.scenario.jsonl", "format": "jsonl"}, + {"path": "strategy/v3/fixtures/external.scenario.jsonl", "format": "jsonl"} + ] + }, + { + "name": "journal-v3", + "schema": "v3/journal.schema.json", + "version_field": "contract_version", + "version": "3", + "sources": [ + {"path": "v3/fixtures/demo.journal.jsonl", "format": "jsonl"} + ] + }, + { + "name": "scenario-v4", + "schema": "v4/scenario.schema.json", + "version_field": "contract_version", + "version": "4", + "sources": [ + {"path": "v4/fixtures/demo.scenario.json", "format": "json"}, + {"path": "v4/fixtures/fill-clipped.scenario.json", "format": "json"} + ] + }, + { + "name": "scenario-stream-v4", + "schema": "v4/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "4", + "sources": [ + {"path": "v4/fixtures/demo.scenario.jsonl", "format": "jsonl"} + ] + }, + { + "name": "journal-v4", + "schema": "v4/journal.schema.json", + "version_field": "contract_version", + "version": "4", + "sources": [ + {"path": "v4/fixtures/demo.journal.jsonl", "format": "jsonl"}, + {"path": "v4/fixtures/fill-clipped.journal.jsonl", "format": "jsonl"} + ] + }, + { + "name": "strategy-message-v1", + "schema": "strategy/v1/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "1", + "sources": [ + {"path": "strategy/v1/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"]} + ] + }, + { + "name": "strategy-transcript-v1", + "schema": "strategy/v1/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "1", + "sources": [ + {"path": "strategy/v1/fixtures/external.strategy.jsonl", "format": "jsonl"} + ] + }, + { + "name": "strategy-message-v2", + "schema": "strategy/v2/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "2", + "sources": [ + {"path": "strategy/v2/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"]} + ] + }, + { + "name": "strategy-transcript-v2", + "schema": "strategy/v2/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "2", + "sources": [ + {"path": "strategy/v2/fixtures/external.strategy.jsonl", "format": "jsonl"} + ] + }, + { + "name": "strategy-message-v3", + "schema": "strategy/v3/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "3", + "sources": [ + {"path": "strategy/v3/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"]} + ] + }, + { + "name": "strategy-transcript-v3", + "schema": "strategy/v3/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "3", + "sources": [ + {"path": "strategy/v3/fixtures/external.strategy.jsonl", "format": "jsonl"} + ] + } + ] +} diff --git a/contracts/v1/README.md b/contracts/v1/README.md index e925ceb..e3bf5bb 100644 --- a/contracts/v1/README.md +++ b/contracts/v1/README.md @@ -1,8 +1,8 @@ # Trading Engine contract v1 This frozen directory preserves the historical v1 process and file contract. The current runtime -emits and advertises v3 only; these artifacts remain available for provenance and compatibility -testing by older consumers. +advertises v4 and v3 only; these artifacts remain available for provenance and schema-only +compatibility testing by older consumers. - `scenario.schema.json` validates batch replay inputs. - `scenario-stream.schema.json` validates each JSON Lines scenario-stream record. diff --git a/contracts/v2/README.md b/contracts/v2/README.md index cb8b424..06dd6f3 100644 --- a/contracts/v2/README.md +++ b/contracts/v2/README.md @@ -1,8 +1,8 @@ # Trading Engine contract v2 This frozen directory preserves the historical v2 process and file contract. The current runtime -emits and advertises v3 only; these artifacts remain available for provenance and compatibility -testing by older consumers. +advertises v4 and v3 only; these artifacts remain available for provenance and schema-only +compatibility testing by older consumers. - `scenario.schema.json` validates batch replay inputs. - `scenario-stream.schema.json` validates each JSON Lines scenario-stream record. diff --git a/test/dune b/test/dune index a0fec0d..2d3ba68 100644 --- a/test/dune +++ b/test/dune @@ -9,6 +9,7 @@ test_reducer test_checkpoint4 test_strategy_protocol + test_contract_conformance test_boundary_failures test_scenario test_engine) @@ -23,6 +24,9 @@ ../contracts/v4/scenario.schema.json ../contracts/v3/fixtures/demo.journal.jsonl ../contracts/v3/fixtures/demo.scenario.json + ../contracts/v3/fixtures/demo.scenario.jsonl + ../contracts/conformance/cases.json + ../contracts/strategy/v3/fixtures/external.strategy.jsonl fake_strategy.py) (libraries trading_engine @@ -134,3 +138,11 @@ ../scripts/check-schema-environment.py) (action (run python3 %{dep:test_development_environment.py}))) + +(rule + (alias runtest) + (deps + validate_contract_conformance.py + (source_tree ../contracts)) + (action + (run python3 %{dep:validate_contract_conformance.py}))) diff --git a/test/test_contract_conformance.ml b/test/test_contract_conformance.ml new file mode 100644 index 0000000..65e75dd --- /dev/null +++ b/test/test_contract_conformance.ml @@ -0,0 +1,210 @@ +open Test_support +module T = Trading_engine + +type path_component = Field of string | Index of int + +let field name = function + | `Assoc fields -> List.assoc name fields + | _ -> Alcotest.failf "%s must be a JSON object field" name + +let string_field name json = + match field name json with + | `String value -> value + | _ -> Alcotest.failf "%s must be a JSON string" name + +let optional_field name = function + | `Assoc fields -> List.assoc_opt name fields + | _ -> Alcotest.failf "%s must be read from a JSON object" name + +let list_field name json = + match field name json with + | `List values -> values + | _ -> Alcotest.failf "%s must be a JSON array" name + +let path_of_yojson = function + | `List components -> + List.map + (function + | `String name -> Field name + | `Int index -> Index index + | _ -> Alcotest.fail "mutation paths contain only fields and indexes") + components + | _ -> Alcotest.fail "mutation path must be a JSON array" + +let rec find_path path json = + match (path, json) with + | [], value -> value + | Field name :: remaining, `Assoc fields -> + find_path remaining (List.assoc name fields) + | Index index :: remaining, `List values -> + find_path remaining (List.nth values index) + | _ -> Alcotest.fail "mutation path does not select a value" + +let rec set_path path replacement json = + match (path, json) with + | [], _ -> replacement + | Field name :: remaining, `Assoc fields -> + let found = ref false in + let fields = + List.map + (fun (candidate, value) -> + if String.equal candidate name then ( + found := true; + (candidate, set_path remaining replacement value)) + else (candidate, value)) + fields + in + let fields = + if !found then fields + else + match remaining with + | [] -> fields @ [ (name, replacement) ] + | _ -> Alcotest.fail "mutation cannot add a nested missing field" + in + `Assoc fields + | Index index :: remaining, `List values -> + `List + (List.mapi + (fun candidate value -> + if candidate = index then set_path remaining replacement value + else value) + values) + | _ -> Alcotest.fail "mutation path cannot be replaced" + +let rec remove_path path json = + match (path, json) with + | [ Field name ], `Assoc fields -> + `Assoc + (List.filter + (fun (candidate, _) -> not (String.equal name candidate)) + fields) + | Field name :: remaining, `Assoc fields -> + `Assoc + (List.map + (fun (candidate, value) -> + if String.equal candidate name then + (candidate, remove_path remaining value) + else (candidate, value)) + fields) + | Index index :: remaining, `List values -> + `List + (List.mapi + (fun candidate value -> + if candidate = index then remove_path remaining value else value) + values) + | _ -> Alcotest.fail "mutation path cannot be removed" + +let apply_mutation document mutation = + let operation = string_field "op" mutation in + let path = field "path" mutation |> path_of_yojson in + match operation with + | "remove" -> remove_path path document + | "add" | "replace" -> set_path path (field "value" mutation) document + | "append_copy" -> ( + let index = + match field "index" mutation with + | `Int value -> value + | _ -> Alcotest.fail "append_copy index must be an integer" + in + match find_path path document with + | `List values -> + set_path path (`List (values @ [ List.nth values index ])) document + | _ -> Alcotest.fail "append_copy path must select an array") + | value -> Alcotest.failf "unsupported mutation operation %s" value + +let apply_mutations case document = + List.fold_left apply_mutation document (list_field "mutations" case) + +let contract_path relative = Filename.concat "../contracts" relative +let read_json relative = contract_path relative |> Yojson.Safe.from_file + +let read_jsonl relative = + In_channel.with_open_bin (contract_path relative) In_channel.input_lines + |> List.filter (fun line -> not (String.equal line "")) + |> List.map Yojson.Safe.from_string + +let selected_record case records = + match optional_field "record" case with + | Some (`Int line_number) -> List.nth records (line_number - 1) + | _ -> Alcotest.fail "differential case must select a source record" + +let extracted case document = + match optional_field "extract" case with + | Some path -> find_path (path_of_yojson path) document + | None -> document + +let with_stream records function_ = + let path = Filename.temp_file "trading-engine-conformance" ".jsonl" in + Fun.protect + ~finally:(fun () -> if Sys.file_exists path then Sys.remove path) + (fun () -> + Out_channel.with_open_bin path (fun channel -> + List.iter + (fun record -> + Yojson.Safe.to_channel channel record; + output_char channel '\n') + records); + function_ path) + +let parse_stream path = + In_channel.with_open_bin path (fun channel -> + T.Scenario_stream.fold_channel + ~max_record_bytes:T.Resource_limits.scenario_record_bytes channel + ~init:(fun _ -> Ok ()) + ~step:(fun () _ -> Ok ()) + ~finish:(fun () ~slice_count:_ -> Ok ())) + +let runtime_result case = + let source = string_field "source" case in + match string_field "kind" case with + | "scenario" -> + read_json source |> apply_mutations case |> T.Scenario.of_yojson + |> Result.map (fun _ -> ()) + | "scenario_stream" -> + let records = read_jsonl source in + let records = + match optional_field "record" case with + | None -> records + | Some (`Int line_number) -> + List.mapi + (fun index record -> + if index = line_number - 1 then apply_mutations case record + else record) + records + | _ -> Alcotest.fail "stream record must be an integer" + in + with_stream records parse_stream + | "strategy_response" -> + let response = + read_jsonl source |> selected_record case |> extracted case + |> apply_mutations case + in + let expected_sequence = + string_field "expected_sequence" case |> Int64.of_string + in + T.Strategy_protocol.response_of_yojson ~expected_sequence response + |> Result.map (fun _ -> ()) + | kind -> Alcotest.failf "unsupported differential runtime kind %s" kind + +let check_case case = + let name = string_field "name" case in + let expected = string_field "runtime_expectation" case in + let accepted = Result.is_ok (runtime_result case) in + Alcotest.(check bool) + (name ^ " runtime expectation") + (String.equal expected "accept") + accepted + +let cases () = + match read_json "conformance/cases.json" with + | `Assoc fields -> ( + match List.assoc "cases" fields with + | `List values -> values + | _ -> Alcotest.fail "differential corpus cases must be an array") + | _ -> Alcotest.fail "differential corpus must be an object" + +let tests = + cases () + |> List.map (fun case -> + Alcotest.test_case (string_field "name" case) `Quick (fun () -> + check_case case)) diff --git a/test/test_engine.ml b/test/test_engine.ml index 685e469..03c0940 100644 --- a/test/test_engine.ml +++ b/test/test_engine.ml @@ -8,6 +8,7 @@ let () = ("reducer", Test_reducer.tests); ("checkpoint4", Test_checkpoint4.tests); ("strategy-protocol", Test_strategy_protocol.tests); + ("contract-conformance", Test_contract_conformance.tests); ("boundary-failures", Test_boundary_failures.tests); ("scenario", Test_scenario.tests); ] diff --git a/test/validate_contract_conformance.py b/test/validate_contract_conformance.py new file mode 100644 index 0000000..3aa3047 --- /dev/null +++ b/test/validate_contract_conformance.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 +"""Validate every committed contract branch and the differential corpus.""" + +from __future__ import annotations + +import copy +import hashlib +import json +from pathlib import Path +from typing import Any + +from jsonschema.exceptions import ValidationError +from jsonschema.validators import validator_for +from referencing import Registry, Resource + + +ROOT = Path(__file__).resolve().parents[1] +CONTRACTS = ROOT / "contracts" +CONFORMANCE = CONTRACTS / "conformance" + + +def loads(document: str, label: str) -> Any: + def object_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ValueError(f"duplicate key {key!r} in {label}") + result[key] = value + return result + + return json.loads( + document, + object_pairs_hook=object_pairs, + parse_constant=lambda value: (_ for _ in ()).throw( + ValueError(f"non-finite number {value!r} in {label}") + ), + ) + + +def load(path: Path) -> Any: + return loads(path.read_text(encoding="utf-8"), str(path.relative_to(ROOT))) + + +def extract(instance: Any, path: list[str | int]) -> Any | None: + current = instance + for component in path: + if isinstance(component, str) and isinstance(current, dict): + if component not in current: + return None + current = current[component] + elif isinstance(component, int) and isinstance(current, list): + current = current[component] + else: + raise AssertionError(f"cannot extract {path!r} from {instance!r}") + return current + + +def source_instances(source: dict[str, Any]) -> list[tuple[int, Any]]: + path = CONTRACTS / source["path"] + if source["format"] == "json": + records = [(1, load(path))] + elif source["format"] == "jsonl": + records = [ + (line_number, loads(line, f"{path.relative_to(ROOT)}:{line_number}")) + for line_number, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), start=1 + ) + if line + ] + else: + raise AssertionError(f"unsupported source format {source['format']!r}") + extraction = source.get("extract", []) + selected = [ + (line_number, extracted) + for line_number, record in records + if (extracted := extract(record, extraction)) is not None + ] + if not selected: + raise AssertionError(f"{source['path']} selected no conformance inputs") + return selected + + +def schema_paths() -> set[Path]: + return { + path + for path in CONTRACTS.rglob("*.schema.json") + if "conformance" not in path.parts + } + + +def fixture_paths() -> set[Path]: + return { + path + for path in CONTRACTS.rglob("fixtures/*") + if path.is_file() and "conformance" not in path.parts + } + + +def schema_registry() -> tuple[dict[str, Any], Registry[Any]]: + schemas = { + str(path.relative_to(CONTRACTS)): load(path) for path in schema_paths() + } + resources = [] + for relative, schema in schemas.items(): + if not isinstance(schema, dict): + raise AssertionError(f"{relative} must contain a JSON object") + if "$schema" not in schema or "$id" not in schema: + raise AssertionError(f"{relative} must declare $schema and $id") + validator_class = validator_for(schema) + if schema["$schema"] != validator_class.META_SCHEMA.get("$id"): + raise AssertionError( + f"{relative} declares unsupported draft {schema['$schema']!r}" + ) + validator_class.check_schema(schema) + resources.append((schema["$id"], Resource.from_contents(schema))) + registry: Registry[Any] = Registry().with_resources(resources) + for relative, schema in schemas.items(): + resolver = registry.resolver(schema["$id"]) + for reference in references(schema): + try: + resolver.lookup(reference) + except Exception as error: + raise AssertionError( + f"broken reference {reference!r} in {relative}" + ) from error + return schemas, registry + + +def references(value: Any) -> list[str]: + if isinstance(value, dict): + found = [value["$ref"]] if isinstance(value.get("$ref"), str) else [] + return found + [ + reference + for child in value.values() + for reference in references(child) + ] + if isinstance(value, list): + return [reference for child in value for reference in references(child)] + return [] + + +def make_validator( + schema: dict[str, Any], registry: Registry[Any] +) -> Any: + validator_class = validator_for(schema) + return validator_class( + schema, + format_checker=validator_class.FORMAT_CHECKER, + registry=registry, + ) + + +def expect_invalid(validator: Any, instance: Any, label: str) -> None: + try: + validator.validate(instance) + except ValidationError: + return + raise AssertionError(f"{label} unexpectedly satisfied its schema") + + +def verify_manifest( + schemas: dict[str, Any], registry: Registry[Any] +) -> tuple[dict[str, dict[str, Any]], dict[str, Any]]: + manifest = load(CONFORMANCE / "manifest.json") + if manifest.get("format_version") != "1": + raise AssertionError("unsupported contract manifest version") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or not artifacts: + raise AssertionError("contract manifest must list artifacts") + by_name = {artifact["name"]: artifact for artifact in artifacts} + if len(by_name) != len(artifacts): + raise AssertionError("contract manifest artifact names must be unique") + + declared_schemas = {artifact["schema"] for artifact in artifacts} + discovered_schemas = { + str(path.relative_to(CONTRACTS)) for path in schema_paths() + } + if declared_schemas != discovered_schemas: + raise AssertionError( + "contract manifest schema set differs from committed schemas: " + f"declared={sorted(declared_schemas)} " + f"committed={sorted(discovered_schemas)}" + ) + declared_sources = { + source["path"] for artifact in artifacts for source in artifact["sources"] + } + discovered_sources = { + str(path.relative_to(CONTRACTS)) for path in fixture_paths() + } + if declared_sources != discovered_sources: + raise AssertionError( + "contract manifest fixture set differs from committed fixtures: " + f"declared={sorted(declared_sources)} " + f"committed={sorted(discovered_sources)}" + ) + + for artifact in artifacts: + schema = schemas[artifact["schema"]] + validator = make_validator(schema, registry) + inputs = [ + instance + for source in artifact["sources"] + for _, instance in source_instances(source) + ] + for index, instance in enumerate(inputs, start=1): + validator.validate(instance) + if not isinstance(instance, dict): + raise AssertionError(f"{artifact['name']} input must be an object") + version = instance.get(artifact["version_field"]) + if version != artifact["version"]: + raise AssertionError( + f"{artifact['name']} input {index} has version {version!r}" + ) + + representative = inputs[0] + missing_version = copy.deepcopy(representative) + del missing_version[artifact["version_field"]] + expect_invalid(validator, missing_version, f"{artifact['name']} missing version") + unsupported_version = copy.deepcopy(representative) + unsupported_version[artifact["version_field"]] = "__unsupported__" + expect_invalid( + validator, unsupported_version, f"{artifact['name']} unsupported version" + ) + unknown_field = copy.deepcopy(representative) + unknown_field["unexpected_contract_field"] = True + expect_invalid(validator, unknown_field, f"{artifact['name']} unknown field") + return by_name, manifest + + +def resolve_parent(instance: Any, path: list[str | int]) -> tuple[Any, str | int]: + if not path: + raise AssertionError("mutation path must not be empty") + current = instance + for component in path[:-1]: + if isinstance(component, str) and isinstance(current, dict): + current = current[component] + elif isinstance(component, int) and isinstance(current, list): + current = current[component] + else: + raise AssertionError(f"invalid mutation path {path!r}") + return current, path[-1] + + +def apply_mutations(instance: Any, mutations: list[dict[str, Any]]) -> Any: + result = copy.deepcopy(instance) + for mutation in mutations: + operation = mutation["op"] + path = mutation["path"] + if operation == "append_copy": + target = extract(result, path) + if not isinstance(target, list): + raise AssertionError(f"append_copy target {path!r} is not an array") + target.append(copy.deepcopy(target[mutation["index"]])) + continue + parent, component = resolve_parent(result, path) + if operation == "remove": + if not isinstance(parent, dict) or not isinstance(component, str): + raise AssertionError("remove currently requires an object field") + del parent[component] + elif operation in {"add", "replace"}: + value = copy.deepcopy(mutation["value"]) + if isinstance(parent, dict) and isinstance(component, str): + parent[component] = value + elif isinstance(parent, list) and isinstance(component, int): + parent[component] = value + else: + raise AssertionError(f"invalid mutation target {path!r}") + else: + raise AssertionError(f"unsupported mutation operation {operation!r}") + return result + + +def case_instances(case: dict[str, Any]) -> list[Any]: + if "instance" in case: + return [apply_mutations(case["instance"], case["mutations"])] + source = { + "path": case["source"], + "format": "jsonl" if case["source"].endswith(".jsonl") else "json", + "extract": case.get("extract", []), + } + inputs = source_instances(source) + record = case.get("record") + if record is not None: + inputs = [instance for line, instance in inputs if line == record] + if len(inputs) != 1: + raise AssertionError( + f"{case['name']} did not select exactly one source record" + ) + else: + inputs = [instance for _, instance in inputs] + mutations = case["mutations"] + if mutations and len(inputs) != 1: + raise AssertionError(f"{case['name']} mutates more than one source record") + return [apply_mutations(instance, mutations) for instance in inputs] + + +def verify_cases( + artifacts: dict[str, dict[str, Any]], + schemas: dict[str, Any], + registry: Registry[Any], +) -> dict[str, list[Any]]: + corpus = load(CONFORMANCE / "cases.json") + if corpus.get("format_version") != "1": + raise AssertionError("unsupported differential corpus version") + cases = corpus.get("cases") + if not isinstance(cases, list) or not cases: + raise AssertionError("differential corpus must list cases") + names = {case["name"] for case in cases} + if len(names) != len(cases): + raise AssertionError("differential case names must be unique") + schema_only_cases = corpus.get("schema_only_cases") + if not isinstance(schema_only_cases, list): + raise AssertionError("differential corpus must list schema-only cases") + all_cases = [*cases, *schema_only_cases] + all_names = {case["name"] for case in all_cases} + if len(all_names) != len(all_cases): + raise AssertionError("all conformance case names must be unique") + accepted_by_artifact: dict[str, list[Any]] = {} + for case in all_cases: + artifact = artifacts[case["artifact"]] + validator = make_validator(schemas[artifact["schema"]], registry) + inputs = case_instances(case) + schema_accepts = True + try: + for instance in inputs: + validator.validate(instance) + except ValidationError: + schema_accepts = False + expected_schema = case["schema_expectation"] == "accept" + if schema_accepts != expected_schema: + raise AssertionError( + f"{case['name']} schema expectation was " + f"{case['schema_expectation']}" + ) + if schema_accepts: + accepted_by_artifact.setdefault(case["artifact"], []).extend(inputs) + if case in schema_only_cases: + continue + schema_expectation = case["schema_expectation"] + runtime_expectation = case["runtime_expectation"] + if case["rule"] == "structural" and schema_expectation != runtime_expectation: + raise AssertionError( + f"{case['name']} structural expectations must agree" + ) + if case["rule"] == "semantic" and ( + schema_expectation != "accept" or runtime_expectation != "reject" + ): + raise AssertionError( + f"{case['name']} semantic case must document schema accept/runtime reject" + ) + return accepted_by_artifact + + +def verify_top_level_branches( + artifacts: dict[str, dict[str, Any]], + schemas: dict[str, Any], + registry: Registry[Any], + accepted_cases: dict[str, list[Any]], +) -> None: + for artifact_name, artifact in artifacts.items(): + schema = schemas[artifact["schema"]] + branches = schema.get("oneOf", []) + if not branches: + continue + candidates = [ + instance + for source in artifact["sources"] + for _, instance in source_instances(source) + ] + candidates.extend(accepted_cases.get(artifact_name, [])) + full_validator = make_validator(schema, registry) + for branch_index, branch in enumerate(branches, start=1): + branch_schema = copy.deepcopy(schema) + branch_schema["oneOf"] = [branch] + branch_validator = make_validator(branch_schema, registry) + witness = next( + (candidate for candidate in candidates if branch_validator.is_valid(candidate)), + None, + ) + if witness is None: + raise AssertionError( + f"{artifact_name} oneOf branch {branch_index} has no positive case" + ) + if not isinstance(witness, dict): + raise AssertionError( + f"{artifact_name} oneOf branch {branch_index} is not an object" + ) + negative = copy.deepcopy(witness) + negative["unexpected_contract_field"] = True + expect_invalid( + full_validator, + negative, + f"{artifact_name} oneOf branch {branch_index} negative case", + ) + + +def verify_frozen_integrity() -> None: + ledger_path = CONFORMANCE / "frozen.sha256" + expected: dict[str, str] = {} + for line_number, line in enumerate( + ledger_path.read_text(encoding="utf-8").splitlines(), start=1 + ): + try: + digest, relative = line.split(" ", maxsplit=1) + except ValueError as error: + raise AssertionError( + f"{ledger_path.relative_to(ROOT)}:{line_number} is malformed" + ) from error + if relative in expected: + raise AssertionError(f"duplicate frozen artifact {relative}") + expected[relative] = digest + frozen_roots = [ + CONTRACTS / "v1", + CONTRACTS / "v2", + CONTRACTS / "strategy" / "v1", + CONTRACTS / "strategy" / "v2", + ] + discovered = { + str(path.relative_to(ROOT)) + for root in frozen_roots + for path in root.rglob("*") + if path.is_file() + and (path.name.endswith(".schema.json") or "fixtures" in path.parts) + } + if set(expected) != discovered: + raise AssertionError( + "frozen integrity ledger differs from archived artifacts: " + f"ledger={sorted(expected)} archived={sorted(discovered)}" + ) + for relative, digest in expected.items(): + actual = hashlib.sha256((ROOT / relative).read_bytes()).hexdigest() + if actual != digest: + raise AssertionError( + f"frozen artifact changed: {relative}; " + "update frozen.sha256 only for an intentional contract revision" + ) + + +def main() -> None: + schemas, registry = schema_registry() + artifacts, _ = verify_manifest(schemas, registry) + accepted_cases = verify_cases(artifacts, schemas, registry) + verify_top_level_branches(artifacts, schemas, registry, accepted_cases) + verify_frozen_integrity() + + +if __name__ == "__main__": + main() From d055a8d3fa17cb95192b8c3ced4049fade56bb42 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 22:41:38 -0400 Subject: [PATCH 18/57] refactor: separate scenario validation layers --- docs/architecture.md | 10 +- docs/scenario.md | 7 +- lib/dune | 1 + lib/scenario.ml | 654 ++++++++++-------------------------- lib/scenario_shape.ml | 140 ++++++++ lib/scenario_shape.mli | 28 ++ lib/scenario_validation.ml | 422 +++++++++++++++++++++++ lib/scenario_validation.mli | 32 ++ test/test_scenario.ml | 58 ++++ 9 files changed, 865 insertions(+), 487 deletions(-) create mode 100644 lib/scenario_shape.ml create mode 100644 lib/scenario_shape.mli create mode 100644 lib/scenario_validation.ml create mode 100644 lib/scenario_validation.mli diff --git a/docs/architecture.md b/docs/architecture.md index 15a9667..6ceeece 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -17,7 +17,10 @@ journal files, and the runtime shell. | `Execution`, `Execution_model` | Pluggable synchronized-slice matching, capacity allocation, and fees | | `Account` | Currency ledgers, signed positions, attribution, average cost, fees, P&L, and valuation | | `Engine` | Sequencing, portfolio reconciliation, and pure suspend/resume orchestration | -| `Scenario`, `Scenario_stream`, `Replay` | Strict batch and bounded-memory scripted runners | +| `Scenario_shape` | Exact batch, stream-header, and stream-item JSON fields | +| `Scenario` | Domain construction shared by batch and stream inputs | +| `Scenario_validation` | Shared cross-field and cross-record scenario invariants | +| `Scenario_stream`, `Replay` | Bounded-memory stream adaptation and scripted runners | | `Strategy_protocol`, `Strategy_process`, `External_replay` | Versioned child supervision and external runners | | `Sha256`, `Codec`, `Diagnostic`, `Artifact_writer`, `Journal`, `Strategy_transcript` | Input identity, stable diagnostics and audit JSON, and file publication | @@ -26,6 +29,11 @@ and reducer internals keep plain errors inside the deterministic boundary; repla stable codes, phases, source locations, event causality, and sanitized exception details before returning an error to callers. +Batch and stream headers use the same domain construction and static semantic checks. Stream +items reuse the batch slice, intent, timeline, and catalog validators against the prior item; +they do not construct temporary batch documents. Shape, construction, and semantic errors retain +their precise JSON path, while the stream adapter adds the record line and sequence. + The journal and strategy transcript share one typed-state artifact lifecycle for exclusive staging, append, close, no-replace publication, and cleanup. Artifact writers and the process supervisor route their minimal operating-system operations through one boundary dispatcher. Production executes diff --git a/docs/scenario.md b/docs/scenario.md index 3cefc05..e01f12f 100644 --- a/docs/scenario.md +++ b/docs/scenario.md @@ -6,7 +6,8 @@ JSON integers. Unknown, missing, duplicate, noncanonical, and non-finite values Use [the v4 demo](../contracts/v4/fixtures/demo.scenario.json) as the canonical complete example. The [scenario JSON Schema](../contracts/v4/scenario.schema.json) provides structural validation. -The engine parser also enforces cross-field and cross-record invariants. +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. ```sh trading-engine --input scenario.json --validate-only @@ -27,6 +28,10 @@ adjacent to their decision slice rather than stored in a future-looking global s replay, the reader checks each intent-bearing slice against the next slice's start time while retaining only those two records. +The batch object and stream header share one domain-construction path and the same static semantic +checks. Stream items reuse the batch slice and intent validators directly; no synthetic batch +scenario is constructed. + The [stream record JSON Schema](../contracts/v4/scenario-stream.schema.json) validates each line, and [the v4 stream fixture](../contracts/v4/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 diff --git a/lib/dune b/lib/dune index ecba7b6..3af5c6e 100644 --- a/lib/dune +++ b/lib/dune @@ -1,6 +1,7 @@ (library (name trading_engine) (public_name trading_engine) + (private_modules scenario_shape scenario_validation) (foreign_stubs (language c) (names process_tree_stubs)) diff --git a/lib/scenario.ml b/lib/scenario.ml index 237cf0c..4ab64de 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -32,10 +32,6 @@ type stream_item = { action_ids : Id.Corporate_action.Set.t; } -module Int64_set = Set.Make (Int64) -module Int64_map = Map.Make (Int64) -module String_set = Set.Make (String) - let ( let* ) result function_ = match result with Ok value -> function_ value | Error _ as error -> error @@ -153,6 +149,22 @@ let map_list parse values = in List.fold_left step (Ok []) values |> Result.map List.rev +let at json_path result = + Result.map_error + (fun message -> Scenario_shape.error ~json_path message) + result + +let map_list_at root parse values = + let step result (index, value) = + let* values = result in + let* value = parse value |> at (Printf.sprintf "%s[%d]" root index) in + Ok (value :: values) + in + values + |> List.mapi (fun index value -> (index, value)) + |> List.fold_left step (Ok []) + |> Result.map List.rev + let parse_id parse ~name json = let* value = string ~name json in parse value @@ -571,393 +583,137 @@ let parse_slice json = Market_slice.create ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions -let changes_orders = function - | Strategy.Target_weights _ | Strategy.Target_quantities _ - | Strategy.Submit_order _ | Strategy.Cancel_order _ -> - true - | Strategy.Emit_metric _ -> false - -let validate_portfolio_target risk catalog = function - | Strategy.Target_weights targets -> - let ids = - List.map - (fun (target : Strategy.weight_target) -> target.instrument_id) - targets - in - let unique = List.sort_uniq Id.Instrument.compare ids in - if List.length unique <> List.length ids then - Error "target_weights must contain each instrument exactly once" - else if - not (Id.Instrument.Set.equal catalog (Id.Instrument.Set.of_list ids)) - then Error "target_weights must cover every configured instrument" - else - let gross = - List.fold_left - (fun result (target : Strategy.weight_target) -> - let* total = result in - let* absolute = Scalar.Weight.absolute target.Strategy.weight in - Scalar.Weight.add total absolute) - (Ok Scalar.Weight.zero) targets - in - let* gross = gross in - if - Int64.compare - (Scalar.Weight.to_micros gross) - (Scalar.Ratio.to_micros (Risk.max_leverage risk)) - > 0 - then Error "target gross weight exceeds maximum leverage" - else Ok () - | Strategy.Target_quantities targets -> - let ids = - List.map - (fun (target : Strategy.quantity_target) -> target.instrument_id) - targets - in - let unique = List.sort_uniq Id.Instrument.compare ids in - if List.length unique <> List.length ids then - Error "target_quantities must contain each instrument exactly once" - else if - not (Id.Instrument.Set.equal catalog (Id.Instrument.Set.of_list ids)) - then Error "target_quantities must cover every configured instrument" - else - List.fold_left - (fun result (target : Strategy.quantity_target) -> - let* () = result in - match Risk.instrument risk target.Strategy.instrument_id with - | None -> Error "target quantity refers to an unknown instrument" - | Some instrument -> - if - not - (Scalar.Quantity.is_multiple target.quantity - ~lot:instrument.Instrument.lot_size) - then - Error "target quantity is not aligned to its instrument lot" - else Risk.check_position risk target.quantity) - (Ok ()) targets - | Strategy.Submit_order request -> ( - if not (Id.Instrument.Set.mem request.Order.instrument_id catalog) then - Error "order refers to an unknown instrument" - else if - Scalar.Quantity.compare request.quantity (Risk.max_order_quantity risk) - > 0 - then Error "order exceeds the maximum order quantity" - else - match Risk.instrument risk request.instrument_id with - | None -> Error "order refers to an unknown instrument" - | Some instrument -> ( - if - not - (Scalar.Quantity.is_multiple request.quantity - ~lot:instrument.Instrument.lot_size) - then - Error "order quantity is not aligned to the instrument lot size" - else - match request.kind with - | Order.Market -> Ok () - | Order.Limit price -> - if Scalar.Price.is_multiple price ~tick:instrument.tick_size - then Ok () - else - Error - "limit price is not aligned to the instrument tick size")) - | Strategy.Cancel_order _ | Strategy.Emit_metric _ -> Ok () - -let validate_slices ~base_currency ~currencies ~instruments slices = - let catalog = - List.map (fun instrument -> instrument.Instrument.id) instruments - |> Id.Instrument.Set.of_list - in - let instrument_map = - List.fold_left - (fun map instrument -> - Id.Instrument.Map.add instrument.Instrument.id instrument map) - Id.Instrument.Map.empty instruments - in - let expected_currencies = String_set.of_list currencies in - let one = Scalar.Price.of_decimal_string "1" |> Result.get_ok in - let rec validate previous_sequence previous_end previous_received action_ids = - function - | [] -> Ok () - | market_slice :: remaining -> - let ids = - List.map - (fun bar -> bar.Bar.instrument_id) - market_slice.Market_slice.bars - |> Id.Instrument.Set.of_list - in - let fx_currencies = - List.map - (fun mark -> mark.Market_slice.currency) - market_slice.Market_slice.fx_rates - |> String_set.of_list - in - let actions_valid = - List.for_all - (fun action -> - Id.Instrument.Set.mem action.Corporate_action.instrument_id - catalog) - market_slice.corporate_actions - in - let duplicate_action = - List.find_opt - (fun action -> - Id.Corporate_action.Set.mem action.Corporate_action.id action_ids) - market_slice.corporate_actions - in - let bars_aligned = - List.for_all - (fun bar -> - match - Id.Instrument.Map.find_opt bar.Bar.instrument_id instrument_map - with - | None -> false - | Some instrument -> - List.for_all - (fun price -> - Scalar.Price.is_multiple price ~tick:instrument.tick_size) - [ - bar.open_price; - bar.high_price; - bar.low_price; - bar.close_price; - ] - && Option.for_all - (fun volume -> - Scalar.Quantity.is_multiple volume - ~lot:instrument.lot_size) - bar.volume) - market_slice.bars - in - if not (Id.Instrument.Set.equal catalog ids) then - Error "each market slice must contain every configured instrument" - else if not (String_set.equal expected_currencies fx_currencies) then - Error "each market slice must contain every scenario currency FX rate" - else if - not - (Option.exists - (fun rate -> Scalar.Price.equal rate one) - (Market_slice.fx_rate market_slice base_currency)) - then Error "the base-currency FX rate must equal one" - else if not actions_valid then - Error "corporate action refers to an unknown instrument" - else if Option.is_some duplicate_action then - Error "corporate action IDs must be unique across the scenario" - else if not bars_aligned then - Error - "market prices and volumes must align with instrument increments" - else if - Option.exists - (fun sequence -> - Int64.compare market_slice.slice_sequence sequence <= 0) - previous_sequence - then Error "market slice sequence must increase" - else if - Option.exists - (fun end_at -> Ptime.compare market_slice.start_at end_at < 0) - previous_end - then Error "market slice start must not precede previous end" - else if - Option.exists - (fun received_at -> - Ptime.compare market_slice.received_at received_at < 0) - previous_received - then Error "market slice receipt time must not move backward" - else - let action_ids = - List.fold_left - (fun ids action -> - Id.Corporate_action.Set.add action.Corporate_action.id ids) - action_ids market_slice.corporate_actions - in - validate (Some market_slice.slice_sequence) (Some market_slice.end_at) - (Some market_slice.received_at) action_ids remaining - in - validate None None None Id.Corporate_action.Set.empty slices - -let validate_schedule risk catalog schedule slices = - let rec index_slices index = function - | [] -> index - | [ anchor ] -> - Int64_map.add anchor.Market_slice.slice_sequence (anchor, None) index - | anchor :: (next :: _ as remaining) -> - let index = - Int64_map.add anchor.Market_slice.slice_sequence (anchor, Some next) - index - in - index_slices index remaining - in - let slice_index = index_slices Int64_map.empty slices in - let validate_item sequence intents = - if Int64.compare sequence 0L <= 0 then - Error "scheduled slice sequence must be positive" - else - match Int64_map.find_opt sequence slice_index with - | None -> - Error - (Printf.sprintf - "scheduled intents refer to missing market slice sequence %Ld" - sequence) - | Some (anchor, next) -> ( - let* () = - List.fold_left - (fun result intent -> - let* () = result in - validate_portfolio_target risk catalog intent) - (Ok ()) intents - in - match next with - | Some next - when List.exists changes_orders intents - && Ptime.compare anchor.received_at next.start_at > 0 -> - Error - (Printf.sprintf - "scheduled order intent after slice %Ld is received after \ - the next executable market slice starts" - sequence) - | None | Some _ -> Ok ()) - in - let rec validate previous = function - | [] -> Ok () - | (sequence, intents) :: remaining -> - if - Option.exists - (fun prior -> Int64.compare sequence prior <= 0) - previous - then Error "schedule sequences must increase" - else - let* () = validate_item sequence intents in - validate (Some sequence) remaining - in - validate None schedule +let child root field = root ^ "." ^ field -let of_yojson_result json = - let* fields = - object_fields ~name:"scenario" - ~expected: - [ - "contract_version"; - "metadata"; - "run_id"; - "base_currency"; - "initial_cash"; - "instruments"; - "risk"; - "execution"; - "max_internal_events"; - "schedule"; - "slices"; - ] - json - in - let* contract_json = field fields "contract_version" in - let* contract_version = string ~name:"contract_version" contract_json in +let construct_header ~root ~contract_path ~contract_version + (shape : Scenario_shape.common) = if not (Contract.is_supported contract_version) then Error - (Printf.sprintf - "unsupported scenario contract_version %S (expected one of %s)" - contract_version - (String.concat ", " Contract.supported_versions)) + (Scenario_shape.error ~json_path:contract_path + (Printf.sprintf + "unsupported scenario contract_version %S (expected one of %s)" + contract_version + (String.concat ", " Contract.supported_versions))) else - let* metadata = field fields "metadata" in let* () = - match metadata with - | `Assoc _ -> validate_metadata metadata - | _ -> Error "metadata must be a JSON object" + match shape.metadata with + | `Assoc _ -> + validate_metadata shape.metadata |> at (child root "metadata") + | _ -> + Error + (Scenario_shape.error ~json_path:(child root "metadata") + "metadata must be a JSON object") in - let* run_json = field fields "run_id" in - let* run_id = parse_id Id.Run.of_string ~name:"run_id" run_json in - let* currency_json = field fields "base_currency" in - let* base_currency = string ~name:"base_currency" currency_json in - let* cash_json = field fields "initial_cash" in - let* cash_json = list ~name:"initial_cash" cash_json in - let* initial_cash = map_list parse_cash_balance cash_json in - let* () = - Account.create ~base_currency ~initial_cash |> Result.map (fun _ -> ()) + let metadata = shape.metadata in + let* run_id = + parse_id Id.Run.of_string ~name:"run_id" shape.run_id + |> at (child root "run_id") in - let* instruments_json = field fields "instruments" in - let* instruments_json = list ~name:"instruments" instruments_json in - if List.length instruments_json > Resource_limits.catalog_instruments then - Error - (Printf.sprintf "catalog instrument count is %d; limit is %d" - (List.length instruments_json) - Resource_limits.catalog_instruments) - else - let* instruments = map_list parse_instrument instruments_json in - if instruments = [] then - Error "scenario must define at least one instrument" - else - let currencies = - base_currency - :: List.map - (fun instrument -> instrument.Instrument.quote_currency) - instruments - |> List.sort_uniq String.compare - in - let cash_currencies = - List.map fst initial_cash |> List.sort_uniq String.compare - in - if cash_currencies <> currencies then - Error "initial_cash must contain every scenario currency exactly once" - else - let catalog = - List.map (fun instrument -> instrument.Instrument.id) instruments - |> Id.Instrument.Set.of_list - in - let* risk_json = field fields "risk" in - let* risk = parse_risk base_currency instruments risk_json in - let* execution_json = field fields "execution" in - let* execution_model, execution = parse_execution execution_json in - let* maximum_json = field fields "max_internal_events" in - let* max_internal_events = - integer ~name:"max_internal_events" maximum_json - in - if max_internal_events <= 0 then - Error "max_internal_events must be positive" - else if max_internal_events > Resource_limits.internal_events then - Error - (Printf.sprintf "internal event count is %d; limit is %d" - max_internal_events Resource_limits.internal_events) - else - let* schedule_json = field fields "schedule" in - let* schedule_json = list ~name:"schedule" schedule_json in - let* schedule = map_list parse_schedule_item schedule_json in - let* slices_json = field fields "slices" in - let* slices_json = list ~name:"slices" slices_json in - let* slices = map_list parse_slice slices_json in - let* () = - validate_slices ~base_currency ~currencies ~instruments slices - in - let* () = validate_schedule risk catalog schedule slices in - Ok - { - contract_version; - metadata; - run_id; - base_currency; - initial_cash; - instruments; - risk; - execution_model; - execution; - max_internal_events; - schedule; - slices; - } + let* base_currency = + string ~name:"base_currency" shape.base_currency + |> at (child root "base_currency") + in + let* initial_cash_json = + list ~name:"initial_cash" shape.initial_cash + |> at (child root "initial_cash") + in + let* initial_cash = + map_list_at + (child root "initial_cash") + parse_cash_balance initial_cash_json + in + let* instruments_json = + list ~name:"instruments" shape.instruments + |> at (child root "instruments") + in + let* instruments = + map_list_at (child root "instruments") parse_instrument instruments_json + in + let* max_internal_events = + integer ~name:"max_internal_events" shape.max_internal_events + |> at (child root "max_internal_events") + in + let* currencies, catalog = + Scenario_validation.header ~root ~base_currency ~initial_cash ~instruments + ~max_internal_events + in + let* risk = + parse_risk base_currency instruments shape.risk |> at (child root "risk") + in + let* execution_model, execution = + parse_execution shape.execution |> at (child root "execution") + in + let header : stream_header = + { + contract_version; + metadata; + run_id; + base_currency; + initial_cash; + instruments; + risk; + execution_model; + execution; + max_internal_events; + } + in + Ok (header, currencies, catalog) + +let construct_batch (shape : Scenario_shape.batch) = + let root = "$" in + let contract_path = "$.contract_version" in + let* contract_version = + string ~name:"contract_version" shape.contract_version |> at contract_path + in + let* header, currencies, catalog = + construct_header ~root ~contract_path ~contract_version shape.common + in + let* schedule_json = + list ~name:"schedule" shape.schedule |> at "$.schedule" + in + let* schedule = map_list_at "$.schedule" parse_schedule_item schedule_json in + let* slices_json = list ~name:"slices" shape.slices |> at "$.slices" in + let* slices = map_list_at "$.slices" parse_slice slices_json in + let* () = + Scenario_validation.batch ~root ~base_currency:header.base_currency + ~currencies ~instruments:header.instruments ~risk:header.risk ~catalog + ~schedule ~slices + in + Ok + { + contract_version = header.contract_version; + metadata = header.metadata; + run_id = header.run_id; + base_currency = header.base_currency; + initial_cash = header.initial_cash; + instruments = header.instruments; + risk = header.risk; + execution_model = header.execution_model; + execution = header.execution; + max_internal_events = header.max_internal_events; + schedule; + slices; + } + +let diagnostic code (error : Scenario_shape.error) = + Diagnostic.make ~code ~phase:Diagnostic.Validation ~json_path:error.json_path + error.message let of_yojson json = - let code, json_path = + let code = match json with | `Assoc fields -> ( match List.assoc_opt "contract_version" fields with | Some (`String supplied) when not (Contract.is_supported supplied) -> - (Diagnostic.Scenario_unsupported_contract, "$.contract_version") - | _ -> (Diagnostic.Scenario_invalid, "$")) - | _ -> (Diagnostic.Scenario_invalid, "$") + Diagnostic.Scenario_unsupported_contract + | _ -> Diagnostic.Scenario_invalid) + | _ -> Diagnostic.Scenario_invalid in let* () = check_batch_limits json in - of_yojson_result json - |> Result.map_error (fun message -> - Diagnostic.make ~code ~phase:Diagnostic.Validation ~json_path message) + let* shape = + Scenario_shape.batch json |> Result.map_error (diagnostic code) + in + construct_batch shape |> Result.map_error (diagnostic code) let of_string document = try Yojson.Safe.from_string document |> of_yojson @@ -976,123 +732,51 @@ let read_file path = ~message:("could not read scenario: " ^ message) exception_) -let stream_header_of_yojson_result ~contract_version json = - let* fields = - object_fields ~name:"scenario stream header payload" - ~expected: - [ - "metadata"; - "run_id"; - "base_currency"; - "initial_cash"; - "instruments"; - "risk"; - "execution"; - "max_internal_events"; - ] - json +let stream_header_of_yojson ~contract_version json = + let code = + if Contract.is_supported contract_version then + Diagnostic.Scenario_stream_invalid + else Diagnostic.Scenario_unsupported_contract in - let scenario_json = - `Assoc - ((("contract_version", `String contract_version) :: fields) - @ [ ("schedule", `List []); ("slices", `List []) ]) + let* () = check_stream_header_limits json in + let* shape = + Scenario_shape.stream_header json |> Result.map_error (diagnostic code) in - let* scenario = of_yojson_result scenario_json in - Ok - { - contract_version = scenario.contract_version; - metadata = scenario.metadata; - run_id = scenario.run_id; - base_currency = scenario.base_currency; - initial_cash = scenario.initial_cash; - instruments = scenario.instruments; - risk = scenario.risk; - execution_model = scenario.execution_model; - execution = scenario.execution; - max_internal_events = scenario.max_internal_events; - } + construct_header ~root:"$.payload" ~contract_path:"$.contract_version" + ~contract_version shape + |> Result.map (fun (header, _, _) -> header) + |> Result.map_error (diagnostic code) -let stream_item_of_yojson_result header ~previous json = - let* fields = - object_fields ~name:"scenario stream slice payload" - ~expected:[ "market_slice"; "intents" ] - json +let stream_item_of_yojson header ~previous json = + let* () = check_stream_item_limits json in + let code = Diagnostic.Scenario_stream_invalid in + let* shape = + Scenario_shape.stream_item json |> Result.map_error (diagnostic code) in - let* slice_json = field fields "market_slice" in - let* market_slice = parse_slice slice_json in - let* intents_json = field fields "intents" in - let* intents_json = list ~name:"intents" intents_json in - let* intents = map_list parse_intent intents_json in - let catalog = - List.map (fun instrument -> instrument.Instrument.id) header.instruments - |> Id.Instrument.Set.of_list + let* market_slice = + parse_slice shape.market_slice + |> at "$.payload.market_slice" + |> Result.map_error (diagnostic code) in - let slices = - match previous with - | None -> [ market_slice ] - | Some item -> [ item.market_slice; market_slice ] + let* intents_json = + list ~name:"intents" shape.intents + |> at "$.payload.intents" + |> Result.map_error (diagnostic code) + in + let* intents = + map_list_at "$.payload.intents" parse_intent intents_json + |> Result.map_error (diagnostic code) in - let prior_action_ids = + let previous_slice, previous_intents, prior_action_ids = match previous with - | None -> Id.Corporate_action.Set.empty - | Some item -> item.action_ids + | None -> (None, [], Id.Corporate_action.Set.empty) + | Some item -> (Some item.market_slice, item.intents, item.action_ids) in let* action_ids = - List.fold_left - (fun result action -> - let* ids = result in - if Id.Corporate_action.Set.mem action.Corporate_action.id ids then - Error "corporate action IDs must be unique across the scenario stream" - else Ok (Id.Corporate_action.Set.add action.id ids)) - (Ok prior_action_ids) market_slice.corporate_actions - in - let currencies = - header.base_currency - :: List.map - (fun instrument -> instrument.Instrument.quote_currency) - header.instruments - |> List.sort_uniq String.compare - in - let* () = - validate_slices ~base_currency:header.base_currency ~currencies - ~instruments:header.instruments slices - in - let* () = - List.fold_left - (fun result intent -> - let* () = result in - validate_portfolio_target header.risk catalog intent) - (Ok ()) intents - in - let* () = - match previous with - | Some item - when List.exists changes_orders item.intents - && Ptime.compare item.market_slice.received_at market_slice.start_at - > 0 -> - Error - (Printf.sprintf - "scheduled order intent after slice %Ld is received after the \ - next executable market slice starts" - item.market_slice.slice_sequence) - | None | Some _ -> Ok () + Scenario_validation.stream_item ~root:"$.payload" + ~base_currency:header.base_currency ~instruments:header.instruments + ~risk:header.risk ~previous_slice ~previous_intents ~prior_action_ids + ~market_slice ~intents + |> Result.map_error (diagnostic code) in Ok { market_slice; intents; action_ids } - -let stream_header_of_yojson ~contract_version json = - let code, json_path = - if Contract.is_supported contract_version then - (Diagnostic.Scenario_stream_invalid, "$.payload") - else (Diagnostic.Scenario_unsupported_contract, "$.contract_version") - in - let* () = check_stream_header_limits json in - stream_header_of_yojson_result ~contract_version json - |> Result.map_error (fun message -> - Diagnostic.make ~code ~phase:Diagnostic.Validation ~json_path message) - -let stream_item_of_yojson header ~previous json = - let* () = check_stream_item_limits json in - stream_item_of_yojson_result header ~previous json - |> Result.map_error (fun message -> - Diagnostic.make ~code:Diagnostic.Scenario_stream_invalid - ~phase:Diagnostic.Validation ~json_path:"$.payload" message) diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml new file mode 100644 index 0000000..5890664 --- /dev/null +++ b/lib/scenario_shape.ml @@ -0,0 +1,140 @@ +type error = { json_path : string; message : string } + +type common = { + metadata : Yojson.Safe.t; + run_id : Yojson.Safe.t; + base_currency : Yojson.Safe.t; + initial_cash : Yojson.Safe.t; + instruments : Yojson.Safe.t; + risk : Yojson.Safe.t; + execution : Yojson.Safe.t; + max_internal_events : Yojson.Safe.t; +} + +type batch = { + contract_version : Yojson.Safe.t; + common : common; + schedule : Yojson.Safe.t; + slices : Yojson.Safe.t; +} + +type stream_item = { market_slice : Yojson.Safe.t; intents : Yojson.Safe.t } + +let error ~json_path message = { json_path; message } + +let ( let* ) result function_ = + match result with Ok value -> function_ value | Error _ as error -> error + +let object_fields ~json_path ~name ~expected = function + | `Assoc fields -> + let names = List.map fst fields in + let actual = List.sort_uniq String.compare names in + let expected = List.sort_uniq String.compare expected in + if List.length names <> List.length actual then + let duplicates = + List.filter + (fun key -> List.length (List.filter (String.equal key) names) > 1) + actual + in + Error + (error ~json_path + (Printf.sprintf "%s has duplicate JSON fields: [%s]" name + (String.concat "," duplicates))) + else if actual = expected then Ok fields + else + let missing = + List.filter (fun key -> not (List.mem key actual)) expected + in + let extra = + List.filter (fun key -> not (List.mem key expected)) actual + in + Error + (error ~json_path + (Printf.sprintf "%s fields differ: missing=[%s], extra=[%s]" name + (String.concat "," missing) + (String.concat "," extra))) + | _ -> Error (error ~json_path (name ^ " must be a JSON object")) + +let field ~root fields name = + match List.assoc_opt name fields with + | Some value -> Ok value + | None -> + Error + (error ~json_path:(root ^ "." ^ name) ("missing JSON field: " ^ name)) + +let common ~root fields = + let* metadata = field ~root fields "metadata" in + let* run_id = field ~root fields "run_id" in + let* base_currency = field ~root fields "base_currency" in + let* initial_cash = field ~root fields "initial_cash" in + let* instruments = field ~root fields "instruments" in + let* risk = field ~root fields "risk" in + let* execution = field ~root fields "execution" in + let* max_internal_events = field ~root fields "max_internal_events" in + Ok + { + metadata; + run_id; + base_currency; + initial_cash; + instruments; + risk; + execution; + max_internal_events; + } + +let batch json = + let root = "$" in + let* fields = + object_fields ~json_path:root ~name:"scenario" + ~expected: + [ + "contract_version"; + "metadata"; + "run_id"; + "base_currency"; + "initial_cash"; + "instruments"; + "risk"; + "execution"; + "max_internal_events"; + "schedule"; + "slices"; + ] + json + in + let* contract_version = field ~root fields "contract_version" in + let* common = common ~root fields in + let* schedule = field ~root fields "schedule" in + let* slices = field ~root fields "slices" in + Ok { contract_version; common; schedule; slices } + +let stream_header json = + let root = "$.payload" in + let* fields = + object_fields ~json_path:root ~name:"scenario stream header payload" + ~expected: + [ + "metadata"; + "run_id"; + "base_currency"; + "initial_cash"; + "instruments"; + "risk"; + "execution"; + "max_internal_events"; + ] + json + in + common ~root fields + +let stream_item json = + let root = "$.payload" in + let* fields = + object_fields ~json_path:root ~name:"scenario stream slice payload" + ~expected:[ "market_slice"; "intents" ] + json + in + let* market_slice = field ~root fields "market_slice" in + let* intents = field ~root fields "intents" in + Ok { market_slice; intents } diff --git a/lib/scenario_shape.mli b/lib/scenario_shape.mli new file mode 100644 index 0000000..01b5554 --- /dev/null +++ b/lib/scenario_shape.mli @@ -0,0 +1,28 @@ +(** Strict top-level JSON shapes for batch and streamed scenarios. *) + +type error = { json_path : string; message : string } + +type common = { + metadata : Yojson.Safe.t; + run_id : Yojson.Safe.t; + base_currency : Yojson.Safe.t; + initial_cash : Yojson.Safe.t; + instruments : Yojson.Safe.t; + risk : Yojson.Safe.t; + execution : Yojson.Safe.t; + max_internal_events : Yojson.Safe.t; +} + +type batch = { + contract_version : Yojson.Safe.t; + common : common; + schedule : Yojson.Safe.t; + slices : Yojson.Safe.t; +} + +type stream_item = { market_slice : Yojson.Safe.t; intents : Yojson.Safe.t } + +val error : json_path:string -> string -> error +val batch : Yojson.Safe.t -> (batch, error) result +val stream_header : Yojson.Safe.t -> (common, error) result +val stream_item : Yojson.Safe.t -> (stream_item, error) result diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml new file mode 100644 index 0000000..231f9c0 --- /dev/null +++ b/lib/scenario_validation.ml @@ -0,0 +1,422 @@ +module Int64_map = Map.Make (Int64) +module String_set = Set.Make (String) + +let ( let* ) result function_ = + match result with Ok value -> function_ value | Error _ as error -> error + +let fail ~json_path message = Error (Scenario_shape.error ~json_path message) + +let at json_path result = + Result.map_error + (fun message -> Scenario_shape.error ~json_path message) + result + +let child root field = root ^ "." ^ field + +let header ~root ~base_currency ~initial_cash ~instruments ~max_internal_events + = + let* () = + Account.create ~base_currency ~initial_cash + |> Result.map (fun _ -> ()) + |> at (child root "initial_cash") + in + if instruments = [] then + fail ~json_path:(child root "instruments") + "scenario must define at least one instrument" + else + let catalog = + List.map (fun instrument -> instrument.Instrument.id) instruments + |> Id.Instrument.Set.of_list + in + if Id.Instrument.Set.cardinal catalog <> List.length instruments then + fail ~json_path:(child root "instruments") "instrument IDs must be unique" + else + let currencies = + base_currency + :: List.map + (fun instrument -> instrument.Instrument.quote_currency) + instruments + |> List.sort_uniq String.compare + in + let cash_currencies = + List.map fst initial_cash |> List.sort_uniq String.compare + in + if cash_currencies <> currencies then + fail + ~json_path:(child root "initial_cash") + "initial_cash must contain every scenario currency exactly once" + else if max_internal_events <= 0 then + fail + ~json_path:(child root "max_internal_events") + "max_internal_events must be positive" + else if max_internal_events > Resource_limits.internal_events then + fail + ~json_path:(child root "max_internal_events") + (Printf.sprintf "internal event count is %d; limit is %d" + max_internal_events Resource_limits.internal_events) + else Ok (currencies, catalog) + +let changes_orders = function + | Strategy.Target_weights _ | Strategy.Target_quantities _ + | Strategy.Submit_order _ | Strategy.Cancel_order _ -> + true + | Strategy.Emit_metric _ -> false + +let validate_portfolio_target ~json_path risk catalog = function + | Strategy.Target_weights targets -> + let ids = + List.map + (fun (target : Strategy.weight_target) -> target.instrument_id) + targets + in + let unique = List.sort_uniq Id.Instrument.compare ids in + if List.length unique <> List.length ids then + fail ~json_path + "target_weights must contain each instrument exactly once" + else if + not (Id.Instrument.Set.equal catalog (Id.Instrument.Set.of_list ids)) + then + fail ~json_path "target_weights must cover every configured instrument" + else + let gross = + List.fold_left + (fun result (target : Strategy.weight_target) -> + let* total = result in + let* absolute = + Scalar.Weight.absolute target.Strategy.weight |> at json_path + in + Scalar.Weight.add total absolute |> at json_path) + (Ok Scalar.Weight.zero) targets + in + let* gross = gross in + if + Int64.compare + (Scalar.Weight.to_micros gross) + (Scalar.Ratio.to_micros (Risk.max_leverage risk)) + > 0 + then fail ~json_path "target gross weight exceeds maximum leverage" + else Ok () + | Strategy.Target_quantities targets -> + let ids = + List.map + (fun (target : Strategy.quantity_target) -> target.instrument_id) + targets + in + let unique = List.sort_uniq Id.Instrument.compare ids in + if List.length unique <> List.length ids then + fail ~json_path + "target_quantities must contain each instrument exactly once" + else if + not (Id.Instrument.Set.equal catalog (Id.Instrument.Set.of_list ids)) + then + fail ~json_path + "target_quantities must cover every configured instrument" + else + List.fold_left + (fun result (target : Strategy.quantity_target) -> + let* () = result in + match Risk.instrument risk target.Strategy.instrument_id with + | None -> + fail ~json_path + "target quantity refers to an unknown instrument" + | Some instrument -> + if + not + (Scalar.Quantity.is_multiple target.quantity + ~lot:instrument.Instrument.lot_size) + then + fail ~json_path + "target quantity is not aligned to its instrument lot" + else Risk.check_position risk target.quantity |> at json_path) + (Ok ()) targets + | Strategy.Submit_order request -> ( + if not (Id.Instrument.Set.mem request.Order.instrument_id catalog) then + fail ~json_path "order refers to an unknown instrument" + else if + Scalar.Quantity.compare request.quantity (Risk.max_order_quantity risk) + > 0 + then fail ~json_path "order exceeds the maximum order quantity" + else + match Risk.instrument risk request.instrument_id with + | None -> fail ~json_path "order refers to an unknown instrument" + | Some instrument -> ( + if + not + (Scalar.Quantity.is_multiple request.quantity + ~lot:instrument.Instrument.lot_size) + then + fail ~json_path + "order quantity is not aligned to the instrument lot size" + else + match request.kind with + | Order.Market -> Ok () + | Order.Limit price -> + if Scalar.Price.is_multiple price ~tick:instrument.tick_size + then Ok () + else + fail ~json_path + "limit price is not aligned to the instrument tick size")) + | Strategy.Cancel_order _ | Strategy.Emit_metric _ -> Ok () + +let validate_slices_at ~paths ~base_currency ~currencies ~instruments slices = + let catalog = + List.map (fun instrument -> instrument.Instrument.id) instruments + |> Id.Instrument.Set.of_list + in + let instrument_map = + List.fold_left + (fun map instrument -> + Id.Instrument.Map.add instrument.Instrument.id instrument map) + Id.Instrument.Map.empty instruments + in + let expected_currencies = String_set.of_list currencies in + let one = Scalar.Price.of_decimal_string "1" |> Result.get_ok in + let rec validate index previous_sequence previous_end previous_received + action_ids = function + | [] -> Ok () + | market_slice :: remaining -> + let root = List.nth paths index in + let ids = + List.map + (fun bar -> bar.Bar.instrument_id) + market_slice.Market_slice.bars + |> Id.Instrument.Set.of_list + in + let fx_currencies = + List.map + (fun mark -> mark.Market_slice.currency) + market_slice.Market_slice.fx_rates + |> String_set.of_list + in + let actions_valid = + List.for_all + (fun action -> + Id.Instrument.Set.mem action.Corporate_action.instrument_id + catalog) + market_slice.corporate_actions + in + let duplicate_action = + List.find_opt + (fun action -> + Id.Corporate_action.Set.mem action.Corporate_action.id action_ids) + market_slice.corporate_actions + in + let bars_aligned = + List.for_all + (fun bar -> + match + Id.Instrument.Map.find_opt bar.Bar.instrument_id instrument_map + with + | None -> false + | Some instrument -> + List.for_all + (fun price -> + Scalar.Price.is_multiple price ~tick:instrument.tick_size) + [ + bar.open_price; + bar.high_price; + bar.low_price; + bar.close_price; + ] + && Option.for_all + (fun volume -> + Scalar.Quantity.is_multiple volume + ~lot:instrument.lot_size) + bar.volume) + market_slice.bars + in + if not (Id.Instrument.Set.equal catalog ids) then + fail ~json_path:(child root "bars") + "each market slice must contain every configured instrument" + else if not (String_set.equal expected_currencies fx_currencies) then + fail ~json_path:(child root "fx_rates") + "each market slice must contain every scenario currency FX rate" + else if + not + (Option.exists + (fun rate -> Scalar.Price.equal rate one) + (Market_slice.fx_rate market_slice base_currency)) + then + fail ~json_path:(child root "fx_rates") + "the base-currency FX rate must equal one" + else if not actions_valid then + fail + ~json_path:(child root "corporate_actions") + "corporate action refers to an unknown instrument" + else if Option.is_some duplicate_action then + fail + ~json_path:(child root "corporate_actions") + "corporate action IDs must be unique across the scenario" + else if not bars_aligned then + fail ~json_path:(child root "bars") + "market prices and volumes must align with instrument increments" + else if + Option.exists + (fun sequence -> + Int64.compare market_slice.slice_sequence sequence <= 0) + previous_sequence + then + fail + ~json_path:(child root "slice_sequence") + "market slice sequence must increase" + else if + Option.exists + (fun end_at -> Ptime.compare market_slice.start_at end_at < 0) + previous_end + then + fail ~json_path:(child root "start_at") + "market slice start must not precede previous end" + else if + Option.exists + (fun received_at -> + Ptime.compare market_slice.received_at received_at < 0) + previous_received + then + fail ~json_path:(child root "received_at") + "market slice receipt time must not move backward" + else + let action_ids = + List.fold_left + (fun ids action -> + Id.Corporate_action.Set.add action.Corporate_action.id ids) + action_ids market_slice.corporate_actions + in + validate (index + 1) (Some market_slice.slice_sequence) + (Some market_slice.end_at) (Some market_slice.received_at) + action_ids remaining + in + validate 0 None None None Id.Corporate_action.Set.empty slices + +let validate_schedule ~root risk catalog schedule slices = + let rec index_slices index = function + | [] -> index + | [ anchor ] -> + Int64_map.add anchor.Market_slice.slice_sequence (anchor, None) index + | anchor :: (next :: _ as remaining) -> + let index = + Int64_map.add anchor.Market_slice.slice_sequence (anchor, Some next) + index + in + index_slices index remaining + in + let slice_index = index_slices Int64_map.empty slices in + let validate_item index sequence intents = + let item_root = Printf.sprintf "%s[%d]" root index in + if Int64.compare sequence 0L <= 0 then + fail + ~json_path:(child item_root "after_slice_sequence") + "scheduled slice sequence must be positive" + else + match Int64_map.find_opt sequence slice_index with + | None -> + fail + ~json_path:(child item_root "after_slice_sequence") + (Printf.sprintf + "scheduled intents refer to missing market slice sequence %Ld" + sequence) + | Some (anchor, next) -> ( + let intents_path = child item_root "intents" in + let* () = + List.fold_left + (fun result intent -> + let* () = result in + validate_portfolio_target ~json_path:intents_path risk catalog + intent) + (Ok ()) intents + in + match next with + | Some next + when List.exists changes_orders intents + && Ptime.compare anchor.received_at next.start_at > 0 -> + fail ~json_path:intents_path + (Printf.sprintf + "scheduled order intent after slice %Ld is received after \ + the next executable market slice starts" + sequence) + | None | Some _ -> Ok ()) + in + let rec validate index previous = function + | [] -> Ok () + | (sequence, intents) :: remaining -> + if + Option.exists + (fun prior -> Int64.compare sequence prior <= 0) + previous + then + fail + ~json_path:(Printf.sprintf "%s[%d].after_slice_sequence" root index) + "schedule sequences must increase" + else + let* () = validate_item index sequence intents in + validate (index + 1) (Some sequence) remaining + in + validate 0 None schedule + +let batch ~root ~base_currency ~currencies ~instruments ~risk ~catalog ~schedule + ~slices = + let slice_paths = + List.mapi (fun index _ -> Printf.sprintf "%s.slices[%d]" root index) slices + in + let* () = + validate_slices_at ~paths:slice_paths ~base_currency ~currencies + ~instruments slices + in + validate_schedule ~root:(child root "schedule") risk catalog schedule slices + +let stream_item ~root ~base_currency ~instruments ~risk ~previous_slice + ~previous_intents ~prior_action_ids ~(market_slice : Market_slice.t) + ~intents = + let catalog = + List.map (fun instrument -> instrument.Instrument.id) instruments + |> Id.Instrument.Set.of_list + in + let current_slice_path = child root "market_slice" in + let* action_ids = + List.fold_left + (fun result action -> + let* ids = result in + if Id.Corporate_action.Set.mem action.Corporate_action.id ids then + fail + ~json_path:(child current_slice_path "corporate_actions") + "corporate action IDs must be unique across the scenario stream" + else Ok (Id.Corporate_action.Set.add action.id ids)) + (Ok prior_action_ids) market_slice.corporate_actions + in + let currencies = + base_currency + :: List.map + (fun instrument -> instrument.Instrument.quote_currency) + instruments + |> List.sort_uniq String.compare + in + let slices, paths = + match previous_slice with + | None -> ([ market_slice ], [ current_slice_path ]) + | Some previous -> + ([ previous; market_slice ], [ current_slice_path; current_slice_path ]) + in + let* () = + validate_slices_at ~paths ~base_currency ~currencies ~instruments slices + in + let intents_path = child root "intents" in + let* () = + List.fold_left + (fun result intent -> + let* () = result in + validate_portfolio_target ~json_path:intents_path risk catalog intent) + (Ok ()) intents + in + let* () = + match previous_slice with + | Some previous + when List.exists changes_orders previous_intents + && Ptime.compare previous.received_at market_slice.start_at > 0 -> + fail + ~json_path:(child current_slice_path "start_at") + (Printf.sprintf + "scheduled order intent after slice %Ld is received after the \ + next executable market slice starts" + previous.slice_sequence) + | None | Some _ -> Ok () + in + Ok action_ids diff --git a/lib/scenario_validation.mli b/lib/scenario_validation.mli new file mode 100644 index 0000000..8b7f3ec --- /dev/null +++ b/lib/scenario_validation.mli @@ -0,0 +1,32 @@ +(** Cross-field and cross-record scenario semantics. *) + +val header : + root:string -> + base_currency:string -> + initial_cash:(string * Scalar.Money.t) list -> + instruments:Instrument.t list -> + max_internal_events:int -> + (string list * Id.Instrument.Set.t, Scenario_shape.error) result + +val batch : + root:string -> + base_currency:string -> + currencies:string list -> + instruments:Instrument.t list -> + risk:Risk.t -> + catalog:Id.Instrument.Set.t -> + schedule:(int64 * Strategy.intent list) list -> + slices:Market_slice.t list -> + (unit, Scenario_shape.error) result + +val stream_item : + root:string -> + base_currency:string -> + instruments:Instrument.t list -> + risk:Risk.t -> + previous_slice:Market_slice.t option -> + previous_intents:Strategy.intent list -> + prior_action_ids:Id.Corporate_action.Set.t -> + market_slice:Market_slice.t -> + intents:Strategy.intent list -> + (Id.Corporate_action.Set.t, Scenario_shape.error) result diff --git a/test/test_scenario.ml b/test/test_scenario.ml index e2bfe03..ee12a60 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -264,6 +264,62 @@ let map_field key change = function fields) | _ -> Alcotest.fail "expected object" +let validation_layers_report_precise_context () = + let document = Yojson.Safe.from_string (demo_document ()) in + let instruments = + match document with + | `Assoc fields -> ( + match List.assoc "instruments" fields with + | `List (instrument :: _) -> [ instrument; instrument ] + | _ -> Alcotest.fail "demo instruments must be nonempty") + | _ -> Alcotest.fail "demo must be an object" + in + let batch = + change_field "instruments" (`List instruments) document + |> T.Scenario.of_yojson |> error + in + Alcotest.(check string) + "batch shared validation" "instrument IDs must be unique" batch.message; + Alcotest.(check (option string)) + "batch semantic path" (Some "$.instruments") batch.context.json_path; + let stream_header_payload = + stream_records () |> List.hd |> Yojson.Safe.from_string |> function + | `Assoc fields -> List.assoc "payload" fields + | _ -> Alcotest.fail "stream header must be an object" + in + let stream_header = + change_field "instruments" (`List instruments) stream_header_payload + |> T.Scenario.stream_header_of_yojson ~contract_version:T.Contract.version + |> error + in + Alcotest.(check string) + "stream shares header semantics" batch.message stream_header.message; + Alcotest.(check (option string)) + "stream semantic path" (Some "$.payload.instruments") + stream_header.context.json_path; + let malformed_stream = + stream_records () + |> List.mapi (fun index line -> + if index <> 1 then line + else + Yojson.Safe.from_string line + |> map_field "payload" + (map_field "intents" (function + | `List (`Assoc fields :: remaining) -> + `List + (`Assoc (("unexpected", `Bool true) :: fields) + :: remaining) + | _ -> Alcotest.fail "stream intents must be nonempty")) + |> Yojson.Safe.to_string) + in + with_stream malformed_stream (fun path -> + let diagnostic = T.Replay.run_stream path |> error in + Alcotest.(check (option int)) + "stream record line" (Some 2) diagnostic.context.line; + Alcotest.(check (option string)) + "stream item path" (Some "$.payload.intents[0]") + diagnostic.context.json_path) + let dense_schedule_document slice_count = let base = timestamp "2026-02-01T00:00:00Z" in let slices = @@ -1011,6 +1067,8 @@ let tests = duplicate_fields_are_rejected; Alcotest.test_case "metadata validation is recursive" `Quick recursive_metadata_validation; + Alcotest.test_case "validation layers report precise context" `Quick + validation_layers_report_precise_context; Alcotest.test_case "configured resources are bounded" `Quick configured_resources_are_bounded; Alcotest.test_case "invalid schedule sequences rejected" `Quick From 5f1d6cdad7884b7b06ff23ae8dd4a98f0b1d290f Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 22:48:21 -0400 Subject: [PATCH 19/57] test: fuzz protocol boundaries --- CONTRIBUTING.md | 3 + Makefile | 11 +- README.md | 1 + docs/fuzzing.md | 38 ++++ test/dune | 13 ++ test/fuzz_protocol.ml | 487 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 552 insertions(+), 1 deletion(-) create mode 100644 docs/fuzzing.md create mode 100644 test/fuzz_protocol.ml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8417454..8b164bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,9 @@ Run the complete local gate before committing: make check ``` +The gate includes the fixed protocol-fuzzing smoke corpus. For longer deterministic +campaigns and reproduction controls, see [Protocol fuzzing](docs/fuzzing.md). + The gate formats a copy check, builds every target, and runs all tests. Keep commits small, coherent, and working. Use subject-only conventional commit messages such as `feat: implement deterministic order matching`. diff --git a/Makefile b/Makefile index 5a0b8e2..9bcaf74 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ export PATH := $(CURDIR)/.venv-schema/bin:$(PATH) -.PHONY: bootstrap environment-check build test fmt-check check +.PHONY: bootstrap environment-check build test fuzz-smoke fuzz fmt-check check + +FUZZ_SEED ?= 20260821 +FUZZ_CASES ?= 10000 bootstrap: @./scripts/bootstrap-development-environment @@ -14,6 +17,12 @@ build: test: opam exec -- dune runtest +fuzz-smoke: + opam exec -- dune exec test/fuzz_protocol.exe -- --seed 20260821 --cases 256 + +fuzz: + opam exec -- dune exec test/fuzz_protocol.exe -- --seed $(FUZZ_SEED) --cases $(FUZZ_CASES) + fmt-check: opam exec -- dune build @fmt diff --git a/README.md b/README.md index b06cfff..5183f4d 100644 --- a/README.md +++ b/README.md @@ -218,5 +218,6 @@ do not provide reducer snapshots or restart recovery. - [Strategy transcript JSON Schema](contracts/strategy/v3/transcript.schema.json) - [Execution model](docs/execution-model.md) - [Performance](docs/performance.md) +- [Protocol fuzzing](docs/fuzzing.md) - [Persistra integration](docs/persistra.md) - [Contributing](CONTRIBUTING.md) diff --git a/docs/fuzzing.md b/docs/fuzzing.md new file mode 100644 index 0000000..fd584f4 --- /dev/null +++ b/docs/fuzzing.md @@ -0,0 +1,38 @@ +# Protocol fuzzing + +The deterministic protocol harness exercises every untrusted parsing boundary: + +- batch scenario JSON and bounded JSON Lines streams +- external strategy responses +- RFC 3339 timestamps +- fixed-point decimals +- opaque identifiers +- raw JSON used by canonical journal and transcript fixtures + +The seed corpus includes every file under a committed contract `fixtures/` +directory and every materialized case in `contracts/conformance/cases.json`. +Before mutating inputs, the harness replays that complete corpus and fixed hostile +inputs for malformed UTF-8, 512-level nesting, a token larger than one MiB, +duplicate keys, and truncation. + +Run the bounded campaign used by `make check` and CI: + +```sh +make fuzz-smoke +``` + +Run a longer local campaign by choosing the seed and mutation count: + +```sh +FUZZ_SEED=1401 FUZZ_CASES=1000000 make fuzz +``` + +Each mutation truncates, flips, inserts, deletes, or duplicates bytes. A failure +prints the boundary, source name, input length, hexadecimal prefix, and exception. +Repeat the same command with the reported seed and at least the reported case +index to reproduce it. Keep the seed and minimized regression input when adding a +failure-path test. + +The harness checks parser totality, not acceptance. Invalid input may return any +documented diagnostic, but it must not raise an exception, abort, or bypass the +stream and strategy size limits. diff --git a/test/dune b/test/dune index 2d3ba68..0ee73c2 100644 --- a/test/dune +++ b/test/dune @@ -39,6 +39,19 @@ eio eio_main)) +(executable + (name fuzz_protocol) + (modules fuzz_protocol) + (libraries trading_engine yojson unix)) + +(rule + (alias runtest) + (deps + fuzz_protocol.exe + (source_tree ../contracts)) + (action + (run ./fuzz_protocol.exe --seed 20260821 --cases 256))) + (cram (deps ../bin/main.exe diff --git a/test/fuzz_protocol.ml b/test/fuzz_protocol.ml new file mode 100644 index 0000000..65442b8 --- /dev/null +++ b/test/fuzz_protocol.ml @@ -0,0 +1,487 @@ +module T = Trading_engine + +type boundary = + | Batch + | Stream + | Strategy + | Timestamp + | Decimal + | Identifier + | Json + +type seed = { name : string; boundary : boundary; input : string } +type path_component = Field of string | Index of int + +let boundary_name = function + | Batch -> "batch" + | Stream -> "stream" + | Strategy -> "strategy" + | Timestamp -> "timestamp" + | Decimal -> "decimal" + | Identifier -> "identifier" + | Json -> "json" + +let contracts = + if Sys.file_exists "../contracts/conformance/cases.json" then "../contracts" + else if Sys.file_exists "contracts/conformance/cases.json" then "contracts" + else failwith "could not locate the contract corpus" + +let contract_path relative = Filename.concat contracts relative + +let field name = function + | `Assoc fields -> List.assoc name fields + | _ -> failwith (name ^ " must be read from an object") + +let optional_field name = function + | `Assoc fields -> List.assoc_opt name fields + | _ -> failwith (name ^ " must be read from an object") + +let string_field name json = + match field name json with + | `String value -> value + | _ -> failwith (name ^ " must be a string") + +let list_field name json = + match field name json with + | `List values -> values + | _ -> failwith (name ^ " must be an array") + +let path_of_yojson = function + | `List components -> + List.map + (function + | `String name -> Field name + | `Int index -> Index index + | _ -> failwith "mutation path contains an invalid component") + components + | _ -> failwith "mutation path must be an array" + +let rec find_path path json = + match (path, json) with + | [], value -> value + | Field name :: remaining, `Assoc fields -> + find_path remaining (List.assoc name fields) + | Index index :: remaining, `List values -> + find_path remaining (List.nth values index) + | _ -> failwith "mutation path does not select a value" + +let rec set_path path replacement json = + match (path, json) with + | [], _ -> replacement + | Field name :: remaining, `Assoc fields -> + let found = ref false in + let fields = + List.map + (fun (candidate, value) -> + if String.equal candidate name then ( + found := true; + (candidate, set_path remaining replacement value)) + else (candidate, value)) + fields + in + let fields = + if !found then fields + else + match remaining with + | [] -> fields @ [ (name, replacement) ] + | _ -> failwith "mutation cannot add a nested missing field" + in + `Assoc fields + | Index index :: remaining, `List values -> + `List + (List.mapi + (fun candidate value -> + if candidate = index then set_path remaining replacement value + else value) + values) + | _ -> failwith "mutation path cannot be replaced" + +let rec remove_path path json = + match (path, json) with + | [ Field name ], `Assoc fields -> + `Assoc + (List.filter + (fun (candidate, _) -> not (String.equal name candidate)) + fields) + | Field name :: remaining, `Assoc fields -> + `Assoc + (List.map + (fun (candidate, value) -> + if String.equal candidate name then + (candidate, remove_path remaining value) + else (candidate, value)) + fields) + | Index index :: remaining, `List values -> + `List + (List.mapi + (fun candidate value -> + if candidate = index then remove_path remaining value else value) + values) + | _ -> failwith "mutation path cannot be removed" + +let apply_mutation document mutation = + let operation = string_field "op" mutation in + let path = field "path" mutation |> path_of_yojson in + match operation with + | "remove" -> remove_path path document + | "add" | "replace" -> set_path path (field "value" mutation) document + | "append_copy" -> ( + let index = + match field "index" mutation with + | `Int value -> value + | _ -> failwith "append_copy index must be an integer" + in + match find_path path document with + | `List values -> + set_path path (`List (values @ [ List.nth values index ])) document + | _ -> failwith "append_copy target must be an array") + | value -> failwith ("unsupported mutation operation " ^ value) + +let apply_mutations case document = + List.fold_left apply_mutation document (list_field "mutations" case) + +let read_json relative = Yojson.Safe.from_file (contract_path relative) + +let read_jsonl relative = + In_channel.with_open_bin (contract_path relative) In_channel.input_lines + |> List.filter (fun line -> not (String.equal line "")) + |> List.map Yojson.Safe.from_string + +let select_record case records = + match optional_field "record" case with + | Some (`Int line_number) -> List.nth records (line_number - 1) + | _ -> failwith "conformance case must select a record" + +let extract case document = + match optional_field "extract" case with + | Some path -> find_path (path_of_yojson path) document + | None -> document + +let materialize_case case = + let name = "conformance/" ^ string_field "name" case in + match optional_field "kind" case with + | Some (`String "scenario") -> + { + name; + boundary = Batch; + input = + read_json (string_field "source" case) + |> apply_mutations case |> Yojson.Safe.to_string; + } + | Some (`String "scenario_stream") -> + let records = read_jsonl (string_field "source" case) in + let records = + match optional_field "record" case with + | None -> records + | Some (`Int line_number) -> + List.mapi + (fun index record -> + if index = line_number - 1 then apply_mutations case record + else record) + records + | _ -> failwith "stream record must be an integer" + in + { + name; + boundary = Stream; + input = + ( records |> List.map Yojson.Safe.to_string |> String.concat "\n" + |> fun document -> document ^ "\n" ); + } + | Some (`String "strategy_response") -> + { + name; + boundary = Strategy; + input = + read_jsonl (string_field "source" case) + |> select_record case |> extract case |> apply_mutations case + |> Yojson.Safe.to_string; + } + | Some (`String kind) -> failwith ("unsupported conformance kind " ^ kind) + | Some _ -> failwith "conformance kind must be a string" + | None -> + let document = + match optional_field "instance" case with + | Some value -> value + | None -> + read_jsonl (string_field "source" case) + |> select_record case |> extract case + in + { + name; + boundary = + (if + String.starts_with ~prefix:"strategy-message" + (string_field "artifact" case) + then Strategy + else Json); + input = apply_mutations case document |> Yojson.Safe.to_string; + } + +let rec fixture_files directory = + Sys.readdir directory |> Array.to_list |> List.sort String.compare + |> List.concat_map (fun name -> + let path = Filename.concat directory name in + if Sys.is_directory path then fixture_files path else [ path ]) + +let relative_to_contracts path = + let prefix = contracts ^ Filename.dir_sep in + String.sub path (String.length prefix) + (String.length path - String.length prefix) + +let canonical_seeds () = + fixture_files contracts + |> List.filter (fun path -> + String.split_on_char '/' path |> List.mem "fixtures") + |> List.concat_map (fun path -> + let relative = relative_to_contracts path in + let contents = In_channel.with_open_bin path In_channel.input_all in + if String.ends_with ~suffix:".scenario.json" path then + [ { name = relative; boundary = Batch; input = contents } ] + else if String.ends_with ~suffix:".scenario.jsonl" path then + [ { name = relative; boundary = Stream; input = contents } ] + else if String.ends_with ~suffix:".strategy.jsonl" path then + contents |> String.split_on_char '\n' + |> List.filter (fun line -> not (String.equal line "")) + |> List.mapi (fun index line -> + let record = Yojson.Safe.from_string line in + [ + { + name = Printf.sprintf "%s:%d" relative (index + 1); + boundary = Json; + input = line; + }; + ] + @ + match optional_field "message" record with + | Some message -> + [ + { + name = Printf.sprintf "%s:%d/message" relative (index + 1); + boundary = Strategy; + input = Yojson.Safe.to_string message; + }; + ] + | None -> []) + |> List.flatten + else + contents |> String.split_on_char '\n' + |> List.filter (fun line -> not (String.equal line "")) + |> List.mapi (fun index line -> + { + name = Printf.sprintf "%s:%d" relative (index + 1); + boundary = Json; + input = line; + })) + +let conformance_seeds () = + match read_json "conformance/cases.json" with + | `Assoc fields -> + let cases name = + match List.assoc name fields with + | `List values -> values + | _ -> failwith (name ^ " must be an array") + in + List.map materialize_case (cases "cases" @ cases "schema_only_cases") + | _ -> failwith "conformance cases must be an object" + +let primitive_seeds = + [ + { + name = "timestamp/canonical"; + boundary = Timestamp; + input = "2026-01-02T14:30:00Z"; + }; + { + name = "timestamp/offset"; + boundary = Timestamp; + input = "2026-01-02T14:30:00-05:00"; + }; + { + name = "timestamp/leap-second"; + boundary = Timestamp; + input = "2026-01-02T14:30:60Z"; + }; + { name = "decimal/zero"; boundary = Decimal; input = "0" }; + { name = "decimal/signed"; boundary = Decimal; input = "-1.000001" }; + { name = "decimal/noncanonical"; boundary = Decimal; input = "01.0" }; + { + name = "identifier/canonical"; + boundary = Identifier; + input = "fuzz-id_01"; + }; + { name = "identifier/empty"; boundary = Identifier; input = "" }; + { name = "identifier/control"; boundary = Identifier; input = "bad\000id" }; + ] + +let deep_json depth = String.make depth '[' ^ "null" ^ String.make depth ']' + +let hostile_seeds () = + let huge_json_string = "\"" ^ String.make 1_048_577 'x' ^ "\"" in + let json_inputs = + [ + ("malformed-utf8", String.make 1 (Char.chr 255)); + ("deep-nesting", deep_json 512); + ("huge-token", huge_json_string); + ( "duplicate-key", + "{\"contract_version\":\"4\",\"contract_version\":\"4\"}" ); + ("truncation", "{\"contract_version\":"); + ] + in + List.concat_map + (fun boundary -> + List.map + (fun (name, input) -> { name = "hostile/" ^ name; boundary; input }) + json_inputs) + [ Batch; Stream; Strategy; Json ] + @ [ + { + name = "timestamp/huge"; + boundary = Timestamp; + input = String.make 4096 '9'; + }; + { + name = "decimal/huge"; + boundary = Decimal; + input = String.make 4096 '9'; + }; + { + name = "identifier/huge"; + boundary = Identifier; + input = String.make 4096 'x'; + }; + ] + +let with_stream document function_ = + let path = Filename.temp_file "trading-engine-fuzz" ".jsonl" in + Fun.protect + ~finally:(fun () -> if Sys.file_exists path then Sys.remove path) + (fun () -> + Out_channel.with_open_bin path (fun channel -> + output_string channel document); + function_ path) + +let expected_sequence document = + try + match Yojson.Safe.from_string document with + | `Assoc fields -> ( + match List.assoc_opt "strategy_sequence" fields with + | Some (`String value) -> + Option.value (Int64.of_string_opt value) ~default:1L + | _ -> 1L) + | _ -> 1L + with Yojson.Json_error _ -> 1L + +let exercise boundary input = + match boundary with + | Batch -> ignore (T.Scenario.of_string input) + | Stream -> + with_stream input (fun path -> + ignore + (T.Scenario_stream.fold_file path + ~init:(fun _ -> Ok ()) + ~step:(fun () _ -> Ok ()) + ~finish:(fun () ~slice_count:_ -> Ok ()))) + | Strategy -> + ignore + (T.Strategy_protocol.response_of_string + ~expected_sequence:(expected_sequence input) input) + | Timestamp -> ignore (T.Codec.ptime_of_string input) + | Decimal -> + ignore (T.Scalar.Price.of_decimal_string input); + ignore (T.Scalar.Quantity.of_decimal_string input); + ignore (T.Scalar.Money.of_decimal_string input); + ignore (T.Scalar.Weight.of_decimal_string input); + ignore (T.Scalar.Ratio.of_decimal_string input) + | Identifier -> + ignore (T.Id.Run.of_string input); + ignore (T.Id.Instrument.of_string input); + ignore (T.Id.Order.of_string input); + ignore (T.Id.Fill.of_string input); + ignore (T.Id.Strategy.of_string input); + ignore (T.Id.Event.of_string input); + ignore (T.Id.Corporate_action.of_string input) + | Json -> ( + try ignore (Yojson.Safe.from_string input) + with Yojson.Json_error _ -> ()) + +let hex_prefix input = + let length = Int.min 128 (String.length input) in + String.init (length * 2) (fun index -> + let byte = Char.code input.[index / 2] in + let nibble = if index mod 2 = 0 then byte lsr 4 else byte land 15 in + "0123456789abcdef".[nibble]) + +let run seed = + try exercise seed.boundary seed.input + with exception_ -> + Printf.eprintf + "fuzz failure boundary=%s name=%s bytes=%d prefix=%s exception=%s\n%!" + (boundary_name seed.boundary) + seed.name (String.length seed.input) (hex_prefix seed.input) + (Printexc.to_string exception_); + exit 1 + +let mutate state input = + let length = String.length input in + match Random.State.int state 5 with + | 0 -> + if length = 0 then input + else String.sub input 0 (Random.State.int state length) + | 1 -> + if length = 0 then String.make 1 (Char.chr (Random.State.int state 256)) + else + let bytes = Bytes.of_string input in + let index = Random.State.int state length in + Bytes.set bytes index (Char.chr (Random.State.int state 256)); + Bytes.unsafe_to_string bytes + | 2 -> + let index = Random.State.int state (length + 1) in + let byte = String.make 1 (Char.chr (Random.State.int state 256)) in + String.sub input 0 index ^ byte ^ String.sub input index (length - index) + | 3 -> + if length = 0 then input + else + let start = Random.State.int state length in + let count = 1 + Random.State.int state (length - start) in + String.sub input 0 start + ^ String.sub input (start + count) (length - start - count) + | _ -> + if length = 0 then input + else + let start = Random.State.int state length in + let maximum = Int.min 64 (length - start) in + let count = 1 + Random.State.int state maximum in + let insertion = Random.State.int state (length + 1) in + String.sub input 0 insertion + ^ String.sub input start count + ^ String.sub input insertion (length - insertion) + +let () = + let random_seed = ref 20_260_821 in + let cases = ref 256 in + Arg.parse + [ + ("--seed", Arg.Set_int random_seed, "deterministic random seed"); + ("--cases", Arg.Set_int cases, "number of byte-mutation cases"); + ] + (fun argument -> raise (Arg.Bad ("unexpected argument " ^ argument))) + "fuzz_protocol [--seed INTEGER] [--cases INTEGER]"; + if !cases < 0 then raise (Arg.Bad "--cases must be nonnegative"); + let corpus = canonical_seeds () @ conformance_seeds () @ primitive_seeds in + List.iter run corpus; + List.iter run (hostile_seeds ()); + let state = Random.State.make [| !random_seed |] in + for index = 1 to !cases do + let source = + List.nth corpus (Random.State.int state (List.length corpus)) + in + run + { + source with + name = Printf.sprintf "mutation/%d/%s" index source.name; + input = mutate state source.input; + } + done; + Printf.printf "fuzz seed=%d cases=%d corpus=%d status=ok\n" !random_seed + !cases (List.length corpus) From 04f971ee03ccf499692f43e4f8a8f8257fa260b2 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 23:00:02 -0400 Subject: [PATCH 20/57] test: add reducer model properties --- CONTRIBUTING.md | 2 + README.md | 3 +- docs/reducer-property-testing.md | 50 ++ test/dune | 1 + test/test_engine.ml | 1 + test/test_reducer_properties.ml | 940 +++++++++++++++++++++++++++++++ 6 files changed, 996 insertions(+), 1 deletion(-) create mode 100644 docs/reducer-property-testing.md create mode 100644 test/test_reducer_properties.ml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b164bd..ef4956e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,8 @@ make check The gate includes the fixed protocol-fuzzing smoke corpus. For longer deterministic campaigns and reproduction controls, see [Protocol fuzzing](docs/fuzzing.md). +Reducer model properties also print reproducible seeds and shrink failures into scenario-like +traces; see [Reducer property testing](docs/reducer-property-testing.md). The gate formats a copy check, builds every target, and runs all tests. Keep commits small, coherent, and working. Use subject-only conventional commit messages such as diff --git a/README.md b/README.md index 5183f4d..edd2897 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ scenario slices and scheduled or external intents - Complete bidirectional strategy transcripts with coordinated no-replace journal publication - Scenario SHA-256 binding in `run_started` and `run_completed` - Exclusive partial artifact creation with optional file and directory synchronization -- Unit, schema-conformance, scenario, golden-contract, and property tests +- Unit, schema-conformance, scenario, golden-contract, reducer model-property, and protocol-fuzz tests ## Quick start @@ -218,6 +218,7 @@ do not provide reducer snapshots or restart recovery. - [Strategy transcript JSON Schema](contracts/strategy/v3/transcript.schema.json) - [Execution model](docs/execution-model.md) - [Performance](docs/performance.md) +- [Reducer property testing](docs/reducer-property-testing.md) - [Protocol fuzzing](docs/fuzzing.md) - [Persistra integration](docs/persistra.md) - [Contributing](CONTRIBUTING.md) diff --git a/docs/reducer-property-testing.md b/docs/reducer-property-testing.md new file mode 100644 index 0000000..4301ecf --- /dev/null +++ b/docs/reducer-property-testing.md @@ -0,0 +1,50 @@ +# Reducer property testing + +The reducer property suite builds valid, shrinkable multi-asset traces and runs them through the +same public transitions used by normal replay. A trace contains four to fourteen synchronized +slices with independent prices, volumes, and EUR/USD marks. Generated strategy commands include +direct market and limit orders, cancellations selected from the live working-order inventory, +quantity and weight targets, and metrics. Matching turns eligible orders into partial or complete +fills. Traces may also include cash dividends and one split, and vary participation, leverage, +initial margin, and maintenance margin. + +Direct-order generation consults the current strategy context. It avoids overlapping working +orders for the same instrument and bounds position-reducing orders so one fill cannot cross through +zero. Invalid business requests may still be generated intentionally: the reducer must express +those as deterministic rejection events rather than corrupting state or escaping the transition. + +## Checked properties + +After every completed slice, `test/test_reducer_properties.ml` checks: + +- cash, equity, net value, long and short value, gross exposure, cost basis, realized and + unrealized P&L, dividends, and fees against their per-currency and per-position attributions; +- native and base-currency conversions, including the defined per-field rounding boundaries; +- initial and maintenance requirements, excess, and margin-call state against the configured risk + model; +- order quantities, fill totals, statuses, active inventory, fill notionals, and fill ownership; +- contiguous engine sequences, derived event IDs, canonical causal references, and the rule that + every cause names an earlier event. + +A second property drives every generated trace through both `Engine.Make` and +`Engine.Interactive`. Audit records are compared as exact serialized bytes after each slice and at +completion; account and order snapshots are compared independently. + +## Reproducing failures + +QCheck prints its random seed and shrinks a failure by removing slices and commands and reducing +numeric inputs. The final report includes the smallest scenario-like JSON trace it found. Re-run a +seed through the complete test gate with: + +```sh +QCHECK_SEED=123456 make test +``` + +Increase the generated case count without changing the checked-in defaults with: + +```sh +QCHECK_SEED=123456 REDUCER_PROPERTY_CASES=5000 make test +``` + +The seed reproduces generation; the printed shrunk trace is the durable debugging artifact when +generator behavior later changes. diff --git a/test/dune b/test/dune index 0ee73c2..4ccd062 100644 --- a/test/dune +++ b/test/dune @@ -7,6 +7,7 @@ test_accounting test_execution test_reducer + test_reducer_properties test_checkpoint4 test_strategy_protocol test_contract_conformance diff --git a/test/test_engine.ml b/test/test_engine.ml index 03c0940..e10d1de 100644 --- a/test/test_engine.ml +++ b/test/test_engine.ml @@ -6,6 +6,7 @@ let () = ("accounting", Test_accounting.tests); ("execution", Test_execution.tests); ("reducer", Test_reducer.tests); + ("reducer-properties", Test_reducer_properties.tests); ("checkpoint4", Test_checkpoint4.tests); ("strategy-protocol", Test_strategy_protocol.tests); ("contract-conformance", Test_contract_conformance.tests); diff --git a/test/test_reducer_properties.ml b/test/test_reducer_properties.ml new file mode 100644 index 0000000..0ca0c95 --- /dev/null +++ b/test/test_reducer_properties.ml @@ -0,0 +1,940 @@ +open Test_support +module T = Trading_engine + +let ( let* ) result function_ = + match result with Ok value -> function_ value | Error _ as error -> error + +type asset = Primary | Foreign + +type command = + | Submit of { + asset : asset; + side : T.Order.side; + quantity : int; + limit : int option; + } + | Cancel_working of asset + | Target_quantities of { primary : int; foreign : int } + | Target_weights of { primary_bps : int; foreign_bps : int } + | Emit_metric of int + +type dividend = { asset : asset; cents : int } + +type step = { + primary_price : int; + foreign_price : int; + euro_rate_bps : int; + primary_volume : int; + foreign_volume : int; + dividend : dividend option; + commands : command list; +} + +type trace = { + leverage_tenths : int; + initial_margin_bps : int; + maintenance_margin_bps : int; + participation_bps : int; + split_asset : asset option; + steps : step list; +} + +let primary = instrument ~id:"property-primary" ~symbol:"PRIMARY" () + +let foreign = + instrument ~id:"property-foreign" ~symbol:"FOREIGN" ~currency:"EUR" () + +let instrument_for_asset = function Primary -> primary | Foreign -> foreign +let asset_name = function Primary -> "primary" | Foreign -> "foreign" +let side_name = function T.Order.Buy -> "buy" | T.Order.Sell -> "sell" + +let decimal_of_scaled value scale = + let sign = if value < 0 then "-" else "" in + let magnitude = abs value in + let whole = magnitude / scale in + let remainder = magnitude mod scale in + let digits = String.length (string_of_int (scale - 1)) in + let fraction = Printf.sprintf "%0*d" digits remainder in + let rec trim index = + if index < 0 then "" + else if Char.equal fraction.[index] '0' then trim (index - 1) + else String.sub fraction 0 (index + 1) + in + match trim (String.length fraction - 1) with + | "" -> Printf.sprintf "%s%d" sign whole + | fraction -> Printf.sprintf "%s%d.%s" sign whole fraction + +let json_of_asset asset = `String (asset_name asset) + +let json_of_command = function + | Submit { asset; side; quantity; limit } -> + `Assoc + [ + ("kind", `String "submit_order"); + ("asset", json_of_asset asset); + ("side", `String (side_name side)); + ("quantity", `Int quantity); + ( "limit", + Option.fold ~none:`Null ~some:(fun value -> `Int value) limit ); + ] + | Cancel_working asset -> + `Assoc + [ ("kind", `String "cancel_working"); ("asset", json_of_asset asset) ] + | Target_quantities { primary; foreign } -> + `Assoc + [ + ("kind", `String "target_quantities"); + ("primary", `Int primary); + ("foreign", `Int foreign); + ] + | Target_weights { primary_bps; foreign_bps } -> + `Assoc + [ + ("kind", `String "target_weights"); + ("primary_bps", `Int primary_bps); + ("foreign_bps", `Int foreign_bps); + ] + | Emit_metric value -> + `Assoc [ ("kind", `String "emit_metric"); ("value", `Int value) ] + +let json_of_step sequence step = + let dividend = + Option.fold step.dividend ~none:`Null ~some:(fun dividend -> + `Assoc + [ + ("asset", json_of_asset dividend.asset); + ("cents", `Int dividend.cents); + ]) + in + `Assoc + [ + ("slice_sequence", `Int sequence); + ("primary_price", `Int step.primary_price); + ("foreign_price", `Int step.foreign_price); + ("euro_rate_bps", `Int step.euro_rate_bps); + ("primary_volume", `Int step.primary_volume); + ("foreign_volume", `Int step.foreign_volume); + ("dividend", dividend); + ("commands", `List (List.map json_of_command step.commands)); + ] + +let print_trace trace = + let steps = List.mapi (fun index -> json_of_step (index + 1)) trace.steps in + `Assoc + [ + ("contract_version", `String T.Contract.version); + ("leverage_tenths", `Int trace.leverage_tenths); + ("initial_margin_bps", `Int trace.initial_margin_bps); + ("maintenance_margin_bps", `Int trace.maintenance_margin_bps); + ("participation_bps", `Int trace.participation_bps); + ( "split_on_slice_four", + Option.fold trace.split_asset ~none:`Null ~some:json_of_asset ); + ("steps", `List steps); + ] + |> Yojson.Safe.pretty_to_string + +let gen_asset = + QCheck2.Gen.map + (fun foreign -> if foreign then Foreign else Primary) + QCheck2.Gen.bool + +let gen_side = + QCheck2.Gen.map + (fun sell -> if sell then T.Order.Sell else T.Order.Buy) + QCheck2.Gen.bool + +let gen_command = + let open QCheck2.Gen in + oneof_weighted + [ + ( 7, + map + (fun (asset, side, quantity, limit) -> + Submit { asset; side; quantity; limit }) + (quad gen_asset gen_side (int_range 1 50) + (option ~ratio:0.45 (int_range 10 240))) ); + (2, map (fun asset -> Cancel_working asset) gen_asset); + ( 4, + map + (fun (primary, foreign) -> Target_quantities { primary; foreign }) + (pair (int_range (-100) 100) (int_range (-100) 100)) ); + ( 2, + map + (fun (primary_bps, foreign_bps) -> + Target_weights { primary_bps; foreign_bps }) + (pair (int_range (-12_500) 12_500) (int_range (-12_500) 12_500)) ); + (1, map (fun value -> Emit_metric value) (int_range (-1000) 1000)); + ] + +let gen_dividend = + let open QCheck2.Gen in + option ~ratio:0.25 + (map + (fun (asset, cents) -> { asset; cents }) + (pair gen_asset (int_range 1 250))) + +let gen_step = + let open QCheck2.Gen in + map + (fun ( (primary_price, foreign_price, euro_rate_bps, primary_volume), + (foreign_volume, dividend, commands) ) -> + { + primary_price; + foreign_price; + euro_rate_bps; + primary_volume; + foreign_volume; + dividend; + commands; + }) + (pair + (quad (int_range 20 220) (int_range 20 220) (int_range 4_000 20_000) + (int_range 1 80)) + (triple (int_range 1 80) gen_dividend + (list_size (int_range 0 3) gen_command))) + +let gen_trace = + let open QCheck2.Gen in + bind (int_range 2_500 10_000) (fun initial_margin_bps -> + map + (fun ( ( leverage_tenths, + maintenance_margin_bps, + participation_bps, + split_asset ), + steps ) -> + { + leverage_tenths; + initial_margin_bps; + maintenance_margin_bps; + participation_bps; + split_asset; + steps; + }) + (pair + (quad (int_range 10 30) + (int_range 1_000 initial_margin_bps) + (int_range 2_500 10_000) + (option ~ratio:0.5 gen_asset)) + (list_size (int_range 4 14) gen_step))) + +let make_risk trace = + T.Risk.create ~base_currency:"USD" ~instruments:[ primary; foreign ] + ~max_order_quantity:(quantity "50") ~max_long_position:(quantity "100") + ~max_short_position:(quantity "100") ~max_gross_exposure:(money "50000") + ~max_leverage: + (T.Scalar.Ratio.of_decimal_string + (decimal_of_scaled trace.leverage_tenths 10) + |> ok) + ~initial_margin_bps:trace.initial_margin_bps + ~maintenance_margin_bps:trace.maintenance_margin_bps ~short_borrow_bps:250 + |> ok + +let make_config trace risk = + engine_config ~risk + ~execution: + (execution ~participation_bps:trace.participation_bps ~fixed_fee:"0.25" + ~fee_bps:5 ()) + ~max_internal_events:5000 () + +let initial_cash = [ ("USD", money "10000"); ("EUR", money "5000") ] + +let permitted_submit_quantity context asset side requested = + let instrument_id = (instrument_for_asset asset).T.Instrument.id in + let has_working_order = + T.Strategy.working_orders context + |> List.exists (fun order -> + T.Id.Instrument.equal order.T.Order.request.instrument_id instrument_id) + in + if has_working_order then None + else + let current = T.Strategy.position context instrument_id in + let current_micros = T.Scalar.Quantity.to_micros current in + let crosses_current = + (Int64.compare current_micros 0L > 0 && side = T.Order.Sell) + || (Int64.compare current_micros 0L < 0 && side = T.Order.Buy) + in + let permitted = + if not crosses_current then requested + else + min requested + (Int64.div (Int64.abs current_micros) T.Scalar.Quantity.scale + |> Int64.to_int) + in + if permitted = 0 then None else Some permitted + +let command_intents context command = + match command with + | Submit { asset; side; quantity = quantity_value; limit } -> + permitted_submit_quantity context asset side quantity_value + |> Option.fold ~none:[] ~some:(fun quantity_value -> + let instrument = (instrument_for_asset asset).T.Instrument.id in + let kind = + Option.fold limit ~none:T.Order.Market ~some:(fun value -> + T.Order.Limit (price (string_of_int value))) + in + let request = + request ~instrument ~side + ~quantity_value:(string_of_int quantity_value) + ~kind () + in + [ T.Strategy.Submit_order request ]) + | Cancel_working asset -> + let instrument_id = (instrument_for_asset asset).id in + T.Strategy.working_orders context + |> List.find_opt (fun order -> + T.Id.Instrument.equal order.T.Order.request.instrument_id + instrument_id) + |> Option.fold ~none:[] ~some:(fun order -> + [ T.Strategy.Cancel_order order.T.Order.id ]) + | Target_quantities { primary = primary_value; foreign = foreign_value } -> + [ + T.Strategy.Target_quantities + [ + T.Strategy. + { + instrument_id = primary.id; + quantity = quantity (string_of_int primary_value); + }; + T.Strategy. + { + instrument_id = foreign.id; + quantity = quantity (string_of_int foreign_value); + }; + ]; + ] + | Target_weights { primary_bps; foreign_bps } -> + [ + T.Strategy.Target_weights + [ + T.Strategy. + { + instrument_id = primary.id; + weight = weight (decimal_of_scaled primary_bps 10_000); + }; + T.Strategy. + { + instrument_id = foreign.id; + weight = weight (decimal_of_scaled foreign_bps 10_000); + }; + ]; + ] + | Emit_metric value -> + [ + T.Strategy.Emit_metric + { name = "generated.reducer.metric"; value = string_of_int value }; + ] + +module Asset_set = Set.Make (struct + type t = asset + + let compare = Stdlib.compare +end) + +let commands_to_intents context commands = + List.fold_left + (fun (intents, submitted_assets) command -> + match command with + | Submit { asset; _ } when Asset_set.mem asset submitted_assets -> + (intents, submitted_assets) + | Submit { asset; _ } -> + ( intents @ command_intents context command, + Asset_set.add asset submitted_assets ) + | _ -> (intents @ command_intents context command, submitted_assets)) + ([], Asset_set.empty) commands + |> fst + +module Generated_strategy = struct + type state = (int64 * command list) list + + let name = "generated-script" + + let take sequence schedule = + let rec loop reversed = function + | [] -> ([], List.rev reversed) + | (candidate, commands) :: rest when Int64.equal sequence candidate -> + (commands, List.rev_append reversed rest) + | item :: rest -> loop (item :: reversed) rest + in + loop [] schedule + + let on_event state context = function + | T.Strategy.Market_slice_closed slice -> + let commands, state = take slice.T.Market_slice.slice_sequence state in + (state, commands_to_intents context commands) + | T.Strategy.Fill_received _ | T.Strategy.Order_updated _ + | T.Strategy.Intent_rejected _ -> + (state, []) +end + +module Generated_runner = T.Engine.Make (Generated_strategy) + +let schedule trace = + List.mapi + (fun index step -> (Int64.of_int (index + 1), step.commands)) + trace.steps + +let corporate_actions trace index step = + let dividend = + Option.to_list step.dividend + |> List.map (fun dividend -> + let instrument = instrument_for_asset dividend.asset in + T.Corporate_action.cash_dividend + ~id: + (T.Id.Corporate_action.of_string_exn + (Printf.sprintf "property-dividend-%d" index)) + ~instrument_id:instrument.id + ~amount_per_unit:(money (decimal_of_scaled dividend.cents 100)) + |> ok) + in + match (index, trace.split_asset) with + | 4, Some asset -> + let instrument = instrument_for_asset asset in + T.Corporate_action.split + ~id:(T.Id.Corporate_action.of_string_exn "property-split") + ~instrument_id:instrument.id ~numerator:2L ~denominator:1L + |> ok + |> fun split -> split :: dividend + | _ -> dividend + +let make_bar instrument close_value volume_value = + let low_value = max 1 (close_value - 3) in + T.Bar.create ~instrument_id:instrument.T.Instrument.id + ~open_price:(price (string_of_int close_value)) + ~high_price:(price (string_of_int (close_value + 3))) + ~low_price:(price (string_of_int low_value)) + ~close_price:(price (string_of_int close_value)) + ~volume:(Some (quantity (string_of_int volume_value))) + |> ok + +let make_slice trace index step = + market_slice + ~bars: + [ + make_bar primary step.primary_price step.primary_volume; + make_bar foreign step.foreign_price step.foreign_volume; + ] + ~fx_rates: + [ + fx_mark (); + fx_mark ~currency:"EUR" + ~rate:(decimal_of_scaled step.euro_rate_bps 10_000) + (); + ] + ~corporate_actions:(corporate_actions trace index step) + (Int64.of_int index) + +let ensure condition message = if condition then Ok () else Error message + +let money_equal label expected actual = + ensure + (T.Scalar.Money.equal expected actual) + (Format.asprintf "%s: expected %a, got %a" label T.Scalar.Money.pp expected + T.Scalar.Money.pp actual) + +let sum_money values = + List.fold_left + (fun result value -> + let* total = result in + T.Scalar.Money.add total value) + (Ok T.Scalar.Money.zero) values + +let check_position_attribution position = + let* market_value = + T.Scalar.Money.notional position.T.Account.mark position.quantity + in + let* () = + money_equal "position market value" market_value position.market_value + in + let* unrealized = + T.Scalar.Money.subtract position.market_value position.cost_basis + in + let* () = + money_equal "position unrealized P&L" unrealized position.unrealized_pnl + in + let* total_fees = + T.Scalar.Money.add position.execution_fees position.borrow_fees + in + let* () = money_equal "position total fees" total_fees position.total_fees in + let* base_market = + T.Scalar.Money.convert position.market_value ~rate:position.fx_rate + in + let* () = + money_equal "position base market value" base_market + position.base_market_value + in + let* base_basis = + T.Scalar.Money.convert position.cost_basis ~rate:position.fx_rate + in + let* () = + money_equal "position base cost basis" base_basis position.base_cost_basis + in + let* base_realized = + T.Scalar.Money.convert position.realized_pnl ~rate:position.fx_rate + in + let* () = + money_equal "position base realized P&L" base_realized + position.base_realized_pnl + in + let* base_unrealized = + T.Scalar.Money.convert position.unrealized_pnl ~rate:position.fx_rate + in + let* () = + money_equal "position base unrealized P&L" base_unrealized + position.base_unrealized_pnl + in + let* base_total_fees = + T.Scalar.Money.convert position.total_fees ~rate:position.fx_rate + in + let* () = + money_equal "position base total fees" base_total_fees + position.base_total_fees + in + let* () = + ensure + (T.Scalar.Money.compare position.execution_fees T.Scalar.Money.zero >= 0) + "execution fees became negative" + in + ensure + (T.Scalar.Money.compare position.borrow_fees T.Scalar.Money.zero >= 0) + "borrow fees became negative" + +let check_valuation risk (valuation : T.Audit.valuation) = + let account = valuation.account in + let* () = + List.fold_left + (fun result position -> + let* () = result in + check_position_attribution position) + (Ok ()) account.positions + in + let* cash = + account.cash_balances + |> List.map (fun balance -> balance.T.Account.base_value) + |> sum_money + in + let* () = money_equal "cash attribution" cash account.cash in + let sum_position field = account.positions |> List.map field |> sum_money in + let* net = + sum_position (fun position -> position.T.Account.base_market_value) + in + let* () = money_equal "net market value" net account.net_market_value in + let* basis = + sum_position (fun position -> position.T.Account.base_cost_basis) + in + let* () = money_equal "cost basis" basis account.cost_basis in + let* realized = + sum_position (fun position -> position.T.Account.base_realized_pnl) + in + let* () = money_equal "realized P&L" realized account.realized_pnl in + let* unrealized = + sum_position (fun position -> position.T.Account.base_unrealized_pnl) + in + let* () = money_equal "unrealized P&L" unrealized account.unrealized_pnl in + let* execution_fees = + sum_position (fun position -> position.T.Account.base_execution_fees) + in + let* () = + money_equal "execution fee attribution" execution_fees + account.execution_fees + in + let* borrow_fees = + sum_position (fun position -> position.T.Account.base_borrow_fees) + in + let* () = + money_equal "borrow fee attribution" borrow_fees account.borrow_fees + in + let* total_fees = + sum_position (fun position -> position.T.Account.base_total_fees) + in + let* () = money_equal "total fee attribution" total_fees account.total_fees in + let* gross = + T.Scalar.Money.add account.long_market_value account.short_market_value + in + let* () = money_equal "gross exposure" gross account.gross_exposure in + let* negative_short = T.Scalar.Money.negate account.short_market_value in + let* signed_exposure = + T.Scalar.Money.add account.long_market_value negative_short + in + let* () = + money_equal "signed exposure" signed_exposure account.net_market_value + in + let* equity = T.Scalar.Money.add account.cash account.net_market_value in + let* () = money_equal "equity" equity account.equity in + let* expected_margin = T.Risk.margin_snapshot risk account in + let actual_margin = valuation.margin in + let* () = + money_equal "initial margin requirement" expected_margin.initial_requirement + actual_margin.initial_requirement + in + let* () = + money_equal "maintenance margin requirement" + expected_margin.maintenance_requirement + actual_margin.maintenance_requirement + in + let* () = + money_equal "initial margin excess" expected_margin.initial_excess + actual_margin.initial_excess + in + let* () = + money_equal "maintenance margin excess" expected_margin.maintenance_excess + actual_margin.maintenance_excess + in + ensure + (Bool.equal expected_margin.margin_call actual_margin.margin_call) + "margin call state disagrees with the account valuation" + +type audit_history = { next_sequence : int64; seen : T.Id.Event.Set.t } + +let empty_history = { next_sequence = 1L; seen = T.Id.Event.Set.empty } + +let check_audits history audits = + List.fold_left + (fun result audit -> + let* history = result in + let* () = + ensure + (Int64.equal audit.T.Audit.engine_sequence history.next_sequence) + "audit sequence is not contiguous" + in + let expected_id = + T.Audit.event_id ~run_id:audit.run_id + ~engine_sequence:audit.engine_sequence + in + let* () = + ensure + (T.Id.Event.equal expected_id audit.event_id) + "audit event ID is not derived from its sequence" + in + let canonical = List.sort_uniq T.Id.Event.compare audit.causation_ids in + let* () = + ensure + (List.equal T.Id.Event.equal canonical audit.causation_ids) + "audit causation IDs are not canonical" + in + let* () = + ensure + (List.for_all + (fun cause -> T.Id.Event.Set.mem cause history.seen) + audit.causation_ids) + "audit causation refers to a non-prior event" + in + Ok + { + next_sequence = Int64.succ history.next_sequence; + seen = T.Id.Event.Set.add audit.event_id history.seen; + }) + (Ok history) audits + +let check_order history order = + let request_quantity = order.T.Order.request.quantity in + let* () = + ensure + (T.Scalar.Quantity.is_positive request_quantity) + "order request quantity is not positive" + in + let* () = + ensure + (T.Scalar.Quantity.is_nonnegative order.filled_quantity) + "order filled quantity is negative" + in + let* () = + ensure + (T.Scalar.Quantity.compare order.filled_quantity request_quantity <= 0) + "order filled quantity exceeds its request" + in + let* () = + ensure + (T.Id.Event.Set.mem order.created_event_id history.seen) + "order creation event is absent from the audit history" + in + let* () = + ensure + (T.Id.Event.Set.mem order.updated_event_id history.seen) + "order update event is absent from the audit history" + in + match order.status with + | T.Order.Working -> + ensure + (T.Scalar.Quantity.compare order.filled_quantity request_quantity < 0) + "working order is completely filled" + | T.Order.Partially_filled -> + ensure + (T.Scalar.Quantity.is_positive order.filled_quantity + && T.Scalar.Quantity.compare order.filled_quantity request_quantity < 0 + ) + "partial order has an invalid fill quantity" + | T.Order.Filled -> + ensure + (T.Scalar.Quantity.equal order.filled_quantity request_quantity) + "filled order has a remainder" + | T.Order.Cancelled | T.Order.Rejected _ -> Ok () + +let check_orders history oms = + let orders = T.Oms.orders oms in + let* () = + List.fold_left + (fun result order -> + let* () = result in + check_order history order) + (Ok ()) orders + in + let expected_active = List.filter T.Order.is_active orders in + ensure + (List.length expected_active = List.length (T.Oms.active_orders oms)) + "active order inventory is inconsistent" + +let slice_valuation audits = + List.filter_map + (fun audit -> + match audit.T.Audit.event with + | T.Audit.Valuation value -> Some value + | _ -> None) + audits + |> function + | [ valuation ] -> Ok valuation + | _ -> Error "slice did not emit exactly one valuation" + +let check_fill oms audit = + match audit.T.Audit.event with + | T.Audit.Fill_applied fill -> + let* expected_notional = + T.Scalar.Money.notional fill.T.Fill.price fill.quantity + in + let* () = money_equal "fill notional" expected_notional fill.notional in + let* () = + ensure + (T.Scalar.Quantity.is_positive fill.quantity) + "fill quantity is not positive" + in + let* () = + ensure + (T.Scalar.Money.compare fill.fee T.Scalar.Money.zero >= 0) + "fill fee is negative" + in + ensure + (Option.is_some (T.Oms.find oms fill.order_id)) + "fill refers to an absent order" + | _ -> Ok () + +let check_slice risk history state audits = + let* history = check_audits history audits in + let* valuation = slice_valuation audits in + let* () = check_valuation risk valuation in + let oms = Generated_runner.oms state in + let* () = check_orders history oms in + let* () = + List.fold_left + (fun result audit -> + let* () = result in + check_fill oms audit) + (Ok ()) audits + in + Ok history + +let property_failure trace slice message = + QCheck2.Test.fail_reportf "slice %d: %s\nshrunk scenario:\n%s" slice message + (print_trace trace) + +let reducer_invariants_hold trace = + let risk = make_risk trace in + let config = make_config trace risk in + let state = + Generated_runner.create ~run_id:(run_id "property-run") ~scenario_sha256 + ~config ~initial_cash ~strategy_state:(schedule trace) + |> ok + in + let rec loop index history state = function + | [] -> ( + match Generated_runner.complete state with + | Error message -> property_failure trace index message + | Ok (_, valuation, audits) -> ( + match check_audits history audits with + | Error message -> property_failure trace index message + | Ok _ -> ( + let completed = + List.find_map + (fun audit -> + match audit.T.Audit.event with + | T.Audit.Run_completed { valuation; _ } -> Some valuation + | _ -> None) + audits + in + match completed with + | None -> + property_failure trace index "missing completion audit" + | Some completed -> ( + match + ( money_equal "completion equity" valuation.equity + completed.account.equity, + check_valuation risk completed ) + with + | Ok (), Ok () -> true + | Error message, _ | _, Error message -> + property_failure trace index message)))) + | step :: rest -> ( + let slice = make_slice trace index step in + match Generated_runner.process_slice state slice with + | Error message -> property_failure trace index message + | Ok (state, audits) -> ( + match check_slice risk history state audits with + | Error message -> property_failure trace index message + | Ok history -> loop (index + 1) history state rest)) + in + loop 1 empty_history state trace.steps + +let account_signature account = + let cash = + T.Account.cash_balances account + |> List.map (fun (currency, amount) -> + currency ^ "=" ^ T.Scalar.Money.to_decimal_string amount) + in + let positions = + T.Account.positions account + |> List.map (fun (instrument_id, (position : T.Account.position)) -> + String.concat ":" + [ + T.Id.Instrument.to_string instrument_id; + T.Scalar.Quantity.to_decimal_string position.T.Account.quantity; + T.Scalar.Money.to_decimal_string position.cost_basis; + T.Scalar.Money.to_decimal_string position.realized_pnl; + T.Scalar.Money.to_decimal_string position.dividend_pnl; + T.Scalar.Money.to_decimal_string position.execution_fees; + T.Scalar.Money.to_decimal_string position.borrow_fees; + ]) + in + cash @ positions + +let order_signature oms = + T.Oms.orders oms + |> List.map (fun order -> + String.concat ":" + [ + T.Id.Order.to_string order.T.Order.id; + T.Id.Instrument.to_string order.request.instrument_id; + side_name order.request.side; + T.Scalar.Quantity.to_decimal_string order.request.quantity; + T.Scalar.Quantity.to_decimal_string order.filled_quantity; + T.Scalar.Money.to_decimal_string order.filled_notional; + T.Order.status_to_string order.status; + ]) + +let rec drive_interactive strategy_state progress = + match T.Engine.Interactive.strategy_request progress with + | Some (context, event) -> + let strategy_state, intents = + Generated_strategy.on_event strategy_state context event + in + let* progress = T.Engine.Interactive.resume progress intents in + drive_interactive strategy_state progress + | None -> ( + match T.Engine.Interactive.slice_result progress with + | Some (state, audits) -> Ok (state, strategy_state, audits) + | None -> Error "interactive reducer reached an invalid progress state") + +let reducers_agree trace = + let risk = make_risk trace in + let config = make_config trace risk in + let strategy_state = schedule trace in + let scripted = + Generated_runner.create + ~run_id:(run_id "property-equivalence") + ~scenario_sha256 ~config ~initial_cash ~strategy_state + |> ok + in + let interactive = + T.Engine.Interactive.create + ~run_id:(run_id "property-equivalence") + ~scenario_sha256 ~config ~initial_cash + |> ok + in + let compare_slice index scripted interactive audits scripted_audits = + let expected = List.map T.Codec.audit_to_string scripted_audits in + let actual = List.map T.Codec.audit_to_string audits in + if not (List.equal String.equal expected actual) then + property_failure trace index "scripted and interactive audits differ" + else if + not + (List.equal String.equal + (account_signature (Generated_runner.account scripted)) + (account_signature (T.Engine.Interactive.account interactive))) + then property_failure trace index "scripted and interactive accounts differ" + else if + not + (List.equal String.equal + (order_signature (Generated_runner.oms scripted)) + (order_signature (T.Engine.Interactive.oms interactive))) + then property_failure trace index "scripted and interactive orders differ" + else true + in + let rec loop index scripted interactive strategy_state = function + | [] -> ( + match + ( Generated_runner.complete scripted, + T.Engine.Interactive.complete interactive ) + with + | ( Ok (_, scripted_valuation, scripted_audits), + Ok (_, interactive_valuation, interactive_audits) ) -> + if + List.equal String.equal + (List.map T.Codec.audit_to_string scripted_audits) + (List.map T.Codec.audit_to_string interactive_audits) + && T.Scalar.Money.equal scripted_valuation.equity + interactive_valuation.equity + then true + else property_failure trace index "completion results differ" + | Error message, _ | _, Error message -> + property_failure trace index message) + | step :: rest -> ( + let slice = make_slice trace index step in + match + ( Generated_runner.process_slice scripted slice, + T.Engine.Interactive.process_slice interactive slice ) + with + | Ok (scripted, scripted_audits), Ok progress -> ( + match drive_interactive strategy_state progress with + | Error message -> property_failure trace index message + | Ok (interactive, strategy_state, audits) -> + if + compare_slice index scripted interactive audits + scripted_audits + then loop (index + 1) scripted interactive strategy_state rest + else false) + | Error message, _ | _, Error message -> + property_failure trace index message) + in + loop 1 scripted interactive strategy_state trace.steps + +let trace_shape trace = + let command_count = + List.fold_left + (fun total step -> total + List.length step.commands) + 0 trace.steps + in + Printf.sprintf "steps=%d commands=%d split=%b" (List.length trace.steps) + command_count + (Option.is_some trace.split_asset) + +let property_count default = + match Sys.getenv_opt "REDUCER_PROPERTY_CASES" with + | None -> default + | Some value -> ( + match int_of_string_opt value with + | Some count when count > 0 -> count + | _ -> invalid_arg "REDUCER_PROPERTY_CASES must be a positive integer") + +let reducer_invariant_property = + QCheck2.Test.make ~name:"generated reducer traces reconcile after every slice" + ~count:(property_count 300) ~print:print_trace ~collect:trace_shape + gen_trace reducer_invariants_hold + +let reducer_equivalence_property = + QCheck2.Test.make + ~name:"generated scripted and interactive reducer traces agree" + ~count:(property_count 250) ~print:print_trace ~collect:trace_shape + gen_trace reducers_agree + +let tests = + [ + QCheck_alcotest.to_alcotest ~speed_level:`Quick reducer_invariant_property; + QCheck_alcotest.to_alcotest ~speed_level:`Quick reducer_equivalence_property; + ] From 336139eb12878682ae987580a870f994d592ebc2 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 23:08:13 -0400 Subject: [PATCH 21/57] refactor: split reducer phases --- README.md | 1 + docs/architecture.md | 8 ++ lib/dune | 2 +- lib/engine.ml | 242 +++++++++++++++++++++++------------------ lib/reducer_phases.ml | 120 ++++++++++++++++++++ lib/reducer_phases.mli | 93 ++++++++++++++++ test/test_reducer.ml | 32 ++++++ 7 files changed, 393 insertions(+), 105 deletions(-) create mode 100644 lib/reducer_phases.ml create mode 100644 lib/reducer_phases.mli diff --git a/README.md b/README.md index edd2897..b9b37c2 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ scenario slices and scheduled or external intents - Separate market event, availability, receipt, slice, and engine ordering - Pure strategy callbacks with causal, immutable context snapshots - Pure suspend/resume strategy requests with equivalent scripted and external reducers +- Explicit immutable reducer phases behind one internal transition contract - Portfolio weight and quantity targets covering the complete instrument catalog - Current-equity weight sizing at synchronized closing marks with lot rounding - Persistent target reconciliation through bounded market-order attempts diff --git a/docs/architecture.md b/docs/architecture.md index 6ceeece..c08b42f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -42,6 +42,14 @@ writes, without introducing files, pipes, processes, or fault state into the red ## Reducer phases +The private `Reducer_phases` transition contract gives each phase an opaque immutable state input +and an explicit result. Validation, initialization, corporate actions, borrow accrual, +notifications, matching, target reconciliation, margin, and valuation are separate phase modules. +The transition machine is the only layer that selects the next phase or resumes a suspended +strategy request; individual phase modules cannot select, skip, or reorder their neighbors. +`Engine` retains the existing scripted and interactive APIs while supplying the domain operations +behind that contract. + For each synchronized market slice, the engine: 1. Validates catalog coverage, slice order, receipt order, and market time. diff --git a/lib/dune b/lib/dune index 3af5c6e..cbc839f 100644 --- a/lib/dune +++ b/lib/dune @@ -1,7 +1,7 @@ (library (name trading_engine) (public_name trading_engine) - (private_modules scenario_shape scenario_validation) + (private_modules reducer_phases scenario_shape scenario_validation) (foreign_stubs (language c) (names process_tree_stubs)) diff --git a/lib/engine.ml b/lib/engine.ml index d33f104..3b8c12d 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -1313,100 +1313,27 @@ module Interactive = struct (Option.to_list reduction.slice_event_id) else Ok reduction - type phase = - | Match_slice of Market_slice.t * Execution.cursor - | Reconcile_targets - | Finish_slice - - type progress = - | Awaiting_strategy of { - reduction : reduction; - phase : phase; - causation_ids : Id.Event.t list; - context : Strategy.context; - event : Strategy.event; - } - | Slice_completed of t * Audit.t list - - let rec continue phase reduction = - let* drained = drain reduction in - match drained with - | Strategy_requested { reduction; causation_ids; context; event } -> - Ok - (Awaiting_strategy { reduction; phase; causation_ids; context; event }) - | Drained reduction -> ( - match phase with - | Match_slice (market_slice, cursor) -> ( - match Execution.next cursor ~oms:reduction.state.oms with - | Error _ as error -> error - | Ok (Execution.Proposed (proposed, advance)) -> - let* reduction, applied_quantity = - apply_proposed_fill reduction market_slice proposed - in - let* cursor = advance applied_quantity in - continue (Match_slice (market_slice, cursor)) reduction - | Ok (Execution.Finished market_ioc_orders) -> - let* slice_event_id = - match reduction.slice_event_id with - | Some value -> Ok value - | None -> Error "matching slice has no audit event" - in - let reduction = with_causes reduction [ slice_event_id ] in - let* reduction = - cancel_market_remainders reduction market_ioc_orders - in - let* pending = - notification reduction ~causation_ids:[ slice_event_id ] - (Strategy.Market_slice_closed market_slice) - in - continue Reconcile_targets (enqueue reduction [ pending ])) - | Reconcile_targets -> - let* reduction = reconcile_targets reduction in - continue Finish_slice reduction - | Finish_slice -> - let* reduction = assess_margin reduction in - if Pending_queue.is_empty reduction.pending then - let* reduction = valuation reduction in - Ok - (Slice_completed (reduction.state, List.rev reduction.audits_rev)) - else continue Finish_slice reduction) - - let strategy_request = function - | Awaiting_strategy { context; event; _ } -> Some (context, event) - | Slice_completed _ -> None - - let slice_result = function - | Awaiting_strategy _ -> None - | Slice_completed (state, audits) -> Some (state, audits) - - let resume progress intents = - match progress with - | Slice_completed _ -> - Error "completed slice cannot accept strategy intents" - | Awaiting_strategy { reduction; phase; causation_ids; _ } -> - let actions = - List.map (fun intent -> Act (causation_ids, intent)) intents - in - continue phase (prepend reduction actions) + module Validation_phase = struct + let run state market_slice = + if state.completed then + Error "completed engine cannot process another market slice" + else validate_slice state market_slice + end - let process_slice state market_slice = - if state.completed then - Error "completed engine cannot process another market slice" - else - let* () = validate_slice state market_slice in + module Initialize_phase = struct + let run state market_slice = + let applied_action_ids = + List.fold_left + (fun ids action -> + Id.Corporate_action.Set.add action.Corporate_action.id ids) + state.applied_action_ids market_slice.Market_slice.corporate_actions + in + let latest_bars = + List.fold_left + (fun bars bar -> Id.Instrument.Map.add bar.Bar.instrument_id bar bars) + state.latest_bars market_slice.bars + in let state = - let applied_action_ids = - List.fold_left - (fun ids action -> - Id.Corporate_action.Set.add action.Corporate_action.id ids) - state.applied_action_ids market_slice.corporate_actions - in - let latest_bars = - List.fold_left - (fun bars bar -> - Id.Instrument.Map.add bar.Bar.instrument_id bar bars) - state.latest_bars market_slice.bars - in { state with last_slice_sequence = Some market_slice.slice_sequence; @@ -1437,24 +1364,131 @@ module Interactive = struct emit_with_id (with_causes reduction []) (Audit.Market_slice_received market_slice) in - let reduction = + Ok { reduction with slice_event_id = Some slice_event_id; causation_ids = [ slice_event_id ]; } + end + + module Actions_phase = struct + let run market_slice reduction = + apply_corporate_actions reduction + market_slice.Market_slice.corporate_actions + end + + module Borrow_phase = struct + let run market_slice reduction = apply_borrow_fees reduction market_slice + end + + module Notifications_phase = struct + type request = { + reduction : reduction; + causation_ids : Id.Event.t list; + context : Strategy.context; + event : Strategy.event; + } + + type outcome = Drained of reduction | Awaiting of request + + let run reduction = + match drain reduction with + | Error _ as error -> error + | Ok result -> ( + match (result : drain_result) with + | Drained reduction -> Ok (Drained reduction) + | Strategy_requested { reduction; causation_ids; context; event } -> + Ok (Awaiting { reduction; causation_ids; context; event })) + + let has_pending reduction = not (Pending_queue.is_empty reduction.pending) + let payload request = (request.context, request.event) + + let resume request intents = + let actions = + List.map (fun intent -> Act (request.causation_ids, intent)) intents in - let* reduction = - apply_corporate_actions reduction market_slice.corporate_actions - in - let* reduction = apply_borrow_fees reduction market_slice in - let* cursor = - Execution_model.start_slice reduction.state.config.execution_model - reduction.state.config.execution - ~instruments:(configured_instruments reduction.state) - ~oms:reduction.state.oms market_slice - in - continue (Match_slice (market_slice, cursor)) reduction + prepend request.reduction actions + end + + module Matching_phase = struct + type outcome = + | Continue of reduction * Execution.cursor + | Complete of reduction + + let start market_slice reduction = + Execution_model.start_slice reduction.state.config.execution_model + reduction.state.config.execution + ~instruments:(configured_instruments reduction.state) + ~oms:reduction.state.oms market_slice + + let run market_slice cursor reduction = + match Execution.next cursor ~oms:reduction.state.oms with + | Error _ as error -> error + | Ok (Execution.Proposed (proposed, advance)) -> + let* reduction, applied_quantity = + apply_proposed_fill reduction market_slice proposed + in + let* cursor = advance applied_quantity in + Ok (Continue (reduction, cursor)) + | Ok (Execution.Finished market_ioc_orders) -> + let* slice_event_id = + match reduction.slice_event_id with + | Some value -> Ok value + | None -> Error "matching slice has no audit event" + in + let reduction = with_causes reduction [ slice_event_id ] in + let* reduction = + cancel_market_remainders reduction market_ioc_orders + in + let* pending = + notification reduction ~causation_ids:[ slice_event_id ] + (Strategy.Market_slice_closed market_slice) + in + Ok (Complete (enqueue reduction [ pending ])) + end + + module Targets_phase = struct + let run = reconcile_targets + end + + module Margin_phase = struct + let run = assess_margin + end + + module Valuation_phase = struct + let run reduction = + let* reduction = valuation reduction in + Ok (reduction.state, List.rev reduction.audits_rev) + end + + module Phase_machine = Reducer_phases.Make (struct + type nonrec state = t + type nonrec reduction = reduction + type market_slice = Market_slice.t + type cursor = Execution.cursor + type audit = Audit.t + type context = Strategy.context + type event = Strategy.event + type intent = Strategy.intent + + module Validation = Validation_phase + module Initialize = Initialize_phase + module Actions = Actions_phase + module Borrow = Borrow_phase + module Notifications = Notifications_phase + module Matching = Matching_phase + module Targets = Targets_phase + module Margin = Margin_phase + module Valuation = Valuation_phase + end) + + type progress = Phase_machine.progress + + let strategy_request = Phase_machine.strategy_request + let slice_result = Phase_machine.slice_result + let resume = Phase_machine.resume + let process_slice = Phase_machine.process_slice let order_counts orders = List.fold_left diff --git a/lib/reducer_phases.ml b/lib/reducer_phases.ml new file mode 100644 index 0000000..b49b409 --- /dev/null +++ b/lib/reducer_phases.ml @@ -0,0 +1,120 @@ +module type CONTRACT = sig + type state + type reduction + type market_slice + type cursor + type audit + type context + type event + type intent + + module Validation : sig + val run : state -> market_slice -> (unit, string) result + end + + module Initialize : sig + val run : state -> market_slice -> (reduction, string) result + end + + module Actions : sig + val run : market_slice -> reduction -> (reduction, string) result + end + + module Borrow : sig + val run : market_slice -> reduction -> (reduction, string) result + end + + module Notifications : sig + type request + type outcome = Drained of reduction | Awaiting of request + + val run : reduction -> (outcome, string) result + val has_pending : reduction -> bool + val payload : request -> context * event + val resume : request -> intent list -> reduction + end + + module Matching : sig + type outcome = Continue of reduction * cursor | Complete of reduction + + val start : market_slice -> reduction -> (cursor, string) result + val run : market_slice -> cursor -> reduction -> (outcome, string) result + end + + module Targets : sig + val run : reduction -> (reduction, string) result + end + + module Margin : sig + val run : reduction -> (reduction, string) result + end + + module Valuation : sig + val run : reduction -> (state * audit list, string) result + end +end + +module Make (Contract : CONTRACT) = struct + let ( let* ) result function_ = + match result with Ok value -> function_ value | Error _ as error -> error + + type phase = + | Matching of Contract.market_slice * Contract.cursor + | Targets + | Finish + + type progress = + | Awaiting_strategy of Contract.Notifications.request * phase + | Slice_completed of Contract.state * Contract.audit list + + let rec transition phase reduction = + let* drained = Contract.Notifications.run reduction in + match drained with + | Contract.Notifications.Awaiting request -> + Ok (Awaiting_strategy (request, phase)) + | Contract.Notifications.Drained reduction -> ( + match phase with + | Matching (market_slice, cursor) -> ( + let* outcome = + Contract.Matching.run market_slice cursor reduction + in + match outcome with + | Contract.Matching.Continue (reduction, cursor) -> + transition (Matching (market_slice, cursor)) reduction + | Contract.Matching.Complete reduction -> + transition Targets reduction) + | Targets -> + let* reduction = Contract.Targets.run reduction in + transition Finish reduction + | Finish -> + let* reduction = Contract.Margin.run reduction in + if Contract.Notifications.has_pending reduction then + transition Finish reduction + else + let* state, audits = Contract.Valuation.run reduction in + Ok (Slice_completed (state, audits))) + + let process_slice state market_slice = + let* () = Contract.Validation.run state market_slice in + let* reduction = Contract.Initialize.run state market_slice in + let* reduction = Contract.Actions.run market_slice reduction in + let* reduction = Contract.Borrow.run market_slice reduction in + let* cursor = Contract.Matching.start market_slice reduction in + transition (Matching (market_slice, cursor)) reduction + + let strategy_request = function + | Awaiting_strategy (request, _) -> + Some (Contract.Notifications.payload request) + | Slice_completed _ -> None + + let resume progress intents = + match progress with + | Slice_completed _ -> + Error "completed slice cannot accept strategy intents" + | Awaiting_strategy (request, phase) -> + transition phase (Contract.Notifications.resume request intents) + + let slice_result = function + | Awaiting_strategy _ -> None + | Slice_completed (state, audits) -> Some (state, audits) +end diff --git a/lib/reducer_phases.mli b/lib/reducer_phases.mli new file mode 100644 index 0000000..da26345 --- /dev/null +++ b/lib/reducer_phases.mli @@ -0,0 +1,93 @@ +(** Pure reducer-phase sequencing behind one transition contract. + + The contract keeps domain state opaque. Every phase receives an immutable + reduction and returns either a replacement reduction or a completed slice; + only this module decides which phase runs next. *) + +module type CONTRACT = sig + type state + type reduction + type market_slice + type cursor + type audit + type context + type event + type intent + + module Validation : sig + val run : state -> market_slice -> (unit, string) result + (** Read-only ingress check. Success guarantees that later phases receive a + new, ordered, catalog-complete slice. *) + end + + module Initialize : sig + val run : state -> market_slice -> (reduction, string) result + (** Captures the slice snapshot and creates its reduction. The result owns + one received-slice audit and an empty feedback queue. *) + end + + module Actions : sig + val run : market_slice -> reduction -> (reduction, string) result + (** Applies the complete corporate-action batch in source order before any + borrow accrual or matching. *) + end + + module Borrow : sig + val run : market_slice -> reduction -> (reduction, string) result + (** Accrues deterministic short-borrow fees against the action-adjusted + account before matching. *) + end + + module Notifications : sig + type request + type outcome = Drained of reduction | Awaiting of request + + val run : reduction -> (outcome, string) result + (** Drains accepted intents until empty or until a strategy callback must + suspend the transition. No later phase runs with pending feedback. *) + + val has_pending : reduction -> bool + val payload : request -> context * event + val resume : request -> intent list -> reduction + end + + module Matching : sig + type outcome = Continue of reduction * cursor | Complete of reduction + + val start : market_slice -> reduction -> (cursor, string) result + (** Fixes the eligible-order cursor after actions and borrow accrual. *) + + val run : market_slice -> cursor -> reduction -> (outcome, string) result + (** Executes at most one cursor step. [Continue] retains the same slice; + [Complete] has cancelled market remainders and queued the close + callback. *) + end + + module Targets : sig + val run : reduction -> (reduction, string) result + (** Reconciles persistent targets exactly once after the close callback. *) + end + + module Margin : sig + val run : reduction -> (reduction, string) result + (** Assesses the post-target account. Generated order notifications are + drained before this phase is reassessed. *) + end + + module Valuation : sig + val run : reduction -> (state * audit list, string) result + (** Terminates the slice with exactly one valuation and returns audits in + publication order. *) + end +end + +module Make (Contract : CONTRACT) : sig + type progress + + val process_slice : + Contract.state -> Contract.market_slice -> (progress, string) result + + val strategy_request : progress -> (Contract.context * Contract.event) option + val resume : progress -> Contract.intent list -> (progress, string) result + val slice_result : progress -> (Contract.state * Contract.audit list) option +end diff --git a/test/test_reducer.ml b/test/test_reducer.ml index 630c388..3bb45fa 100644 --- a/test/test_reducer.ml +++ b/test/test_reducer.ml @@ -756,6 +756,36 @@ let interactive_reducer_matches_scripted_strategy () = "completed slice cannot accept strategy intents" (T.Engine.Interactive.resume completed_progress [] |> error) +let explicit_phase_order_is_stable () = + let dividend = + T.Corporate_action.cash_dividend + ~id:(T.Id.Corporate_action.of_string_exn "phase-dividend") + ~instrument_id:(instrument_id "test-equity") + ~amount_per_unit:(money "1") + |> ok + in + let metric = + T.Strategy.Emit_metric { name = "phase.boundary"; value = "reached" } + in + let state = runner [ (1L, [ target "2" ]); (2L, [ metric; target "0" ]) ] in + let state, _ = Runner.process_slice state (market_slice 1L) |> ok in + let _, events = + Runner.process_slice state (market_slice ~corporate_actions:[ dividend ] 2L) + |> ok + in + Alcotest.(check (list string)) + "actions, matching, notifications, targets, and valuation stay ordered" + [ + "market_slice_received"; + "cash_dividend_applied"; + "fill_applied"; + "metric_emitted"; + "target_portfolio_requested"; + "order_accepted"; + "valuation"; + ] + (event_names events) + let tests = [ Alcotest.test_case "market target retries after partial fill" `Quick @@ -796,4 +826,6 @@ let tests = `Quick callbacks_use_current_slice_and_apply_responses_before_matching; Alcotest.test_case "interactive reducer matches scripted strategy" `Quick interactive_reducer_matches_scripted_strategy; + Alcotest.test_case "explicit phase order is stable" `Quick + explicit_phase_order_is_stable; ] From dbe84649c6416affce6a224ebd7739c1cf6ec39e Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 23:22:45 -0400 Subject: [PATCH 22/57] test: enforce OCaml coverage reporting --- .github/workflows/ci.yml | 25 +++++ .gitignore | 1 + CONTRIBUTING.md | 4 + Makefile | 5 +- README.md | 1 + bin/dune | 2 + coverage/ocaml-policy.json | 12 +++ docs/coverage.md | 48 +++++++++ lib/dune | 2 + scripts/bootstrap-development-environment | 7 ++ scripts/check-development-environment | 9 ++ scripts/check-ocaml-coverage | 42 ++++++++ scripts/check-ocaml-coverage.py | 122 ++++++++++++++++++++++ test/test_domain.ml | 72 +++++++++++++ trading_engine.opam | 7 ++ trading_engine.opam.locked | 12 ++- 16 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 coverage/ocaml-policy.json create mode 100644 docs/coverage.md create mode 100755 scripts/check-ocaml-coverage create mode 100755 scripts/check-ocaml-coverage.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95b48c8..c5f19d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,31 @@ jobs: - run: make bootstrap - run: make check + ocaml-coverage: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: ocaml/setup-ocaml@605a7e998e76e035b82c14d618a6e1010732c4ce # v3.7.1 + with: + ocaml-compiler: "5.5.0" + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + python-version: "3.12" + - run: make bootstrap + - run: make coverage + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ocaml-coverage + path: | + _coverage/cobertura.xml + _coverage/html + _coverage/summary.txt + if-no-files-found: error + retention-days: 14 + persistra-compatibility: runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/.gitignore b/.gitignore index 58211b8..d87103d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /_build/ +/_coverage/ /_opam/ /.venv-schema/ /.direnv/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef4956e..961c06c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,10 @@ campaigns and reproduction controls, see [Protocol fuzzing](docs/fuzzing.md). Reducer model properties also print reproducible seeds and shrink failures into scenario-like traces; see [Reducer property testing](docs/reducer-property-testing.md). +Run `make coverage` to enforce the OCaml coverage floor and generate per-module, control-flow +HTML, and Cobertura reports. See [OCaml coverage](docs/coverage.md) for report locations, +instrumentation scope, and the explained-threshold-change policy. + The gate formats a copy check, builds every target, and runs all tests. Keep commits small, coherent, and working. Use subject-only conventional commit messages such as `feat: implement deterministic order matching`. diff --git a/Makefile b/Makefile index 9bcaf74..105d905 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ export PATH := $(CURDIR)/.venv-schema/bin:$(PATH) -.PHONY: bootstrap environment-check build test fuzz-smoke fuzz fmt-check check +.PHONY: bootstrap environment-check build test coverage fuzz-smoke fuzz fmt-check check FUZZ_SEED ?= 20260821 FUZZ_CASES ?= 10000 @@ -17,6 +17,9 @@ build: test: opam exec -- dune runtest +coverage: environment-check + @./scripts/check-ocaml-coverage + fuzz-smoke: opam exec -- dune exec test/fuzz_protocol.exe -- --seed 20260821 --cases 256 diff --git a/README.md b/README.md index b9b37c2..e91bbaa 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,7 @@ do not provide reducer snapshots or restart recovery. - [Strategy message JSON Schema](contracts/strategy/v3/message.schema.json) - [Strategy transcript JSON Schema](contracts/strategy/v3/transcript.schema.json) - [Execution model](docs/execution-model.md) +- [OCaml coverage](docs/coverage.md) - [Performance](docs/performance.md) - [Reducer property testing](docs/reducer-property-testing.md) - [Protocol fuzzing](docs/fuzzing.md) diff --git a/bin/dune b/bin/dune index 28dcdff..cb97f95 100644 --- a/bin/dune +++ b/bin/dune @@ -2,4 +2,6 @@ (name main) (public_name trading-engine) (package trading_engine) + (instrumentation + (backend bisect_ppx)) (libraries trading_engine cmdliner eio_main fmt.tty logs.fmt logs.cli)) diff --git a/coverage/ocaml-policy.json b/coverage/ocaml-policy.json new file mode 100644 index 0000000..ddc5b3b --- /dev/null +++ b/coverage/ocaml-policy.json @@ -0,0 +1,12 @@ +{ + "format_version": 1, + "minimum_coverage_percent": 86.0, + "threshold_history": [ + { + "minimum_coverage_percent": 86.0, + "reason": "Initial floor after covering bar and corporate-action validation boundaries.", + "issue_url": "https://github.com/fallblu/trading-engine/issues/17" + } + ], + "excluded_paths": [] +} diff --git a/docs/coverage.md b/docs/coverage.md new file mode 100644 index 0000000..ed20df3 --- /dev/null +++ b/docs/coverage.md @@ -0,0 +1,48 @@ +# OCaml coverage + +The OCaml coverage gate instruments the production library and command-line executable with +Bisect_ppx while running the normal Dune test aliases. Normal `make build`, `make test`, and +`make check` targets remain uninstrumented, so coverage cannot change deterministic journals, +transcripts, diagnostics, or other contract output. + +The opam manifests and bootstrap script pin Bisect_ppx to one upstream commit that supports the +project's OCaml 5.5 and ppxlib toolchain. The environment check verifies that exact source pin as +well as the locked dependency versions, preventing a local fallback to an incompatible release. + +Run the gate from a bootstrapped development environment: + +```sh +make coverage +``` + +The command cleans Dune's generated build tree, recreates `_coverage/`, and writes three views of +the same run. Cleaning first ensures every test executable and cram invocation contributes fresh +instrumentation data: + +- `summary.txt` lists every production module and the project-wide instrumented-point result. +- `html/index.html` highlights expression and control-flow points, making unvisited match arms, + conditions, and exception paths directly inspectable. +- `cobertura.xml` provides line-oriented machine-readable data for CI and external analysis. + +CI publishes these files as the `ocaml-coverage` artifact for 14 days. The report command uses +`--expect bin/` and `--expect lib/`, so a production module that silently disappears from the +instrumented report fails the job. + +## Threshold policy + +`coverage/ocaml-policy.json` records the active project-wide minimum and its complete change +history. Every entry requires both an explanation and a repository issue. A lower minimum must be +appended as a new explained history entry; editing the active number alone fails the policy check. +Raise the minimum when sustained coverage permits it. + +There are currently no excluded production paths. Tests, generated build files, vendored +dependencies, schemas, and Python validation tools are outside the OCaml instrumentation scope; +they are exercised by the same Dune aliases but do not contribute points. If a production +expression must use `[@coverage off]` or a production path must be excluded later, add the path and +a concrete reason to `excluded_paths` in the policy before using the exclusion. Do not exclude +defensive failures merely because they are difficult to trigger. + +The initial floor was measured only after adding focused tests for bar price/volume invariants and +corporate-action validation, comparison, and rendering. These paths guard market-data integrity +and split/dividend semantics, so their coverage was addressed before adopting the project-wide +minimum. diff --git a/lib/dune b/lib/dune index cbc839f..6fbb4d3 100644 --- a/lib/dune +++ b/lib/dune @@ -2,6 +2,8 @@ (name trading_engine) (public_name trading_engine) (private_modules reducer_phases scenario_shape scenario_validation) + (instrumentation + (backend bisect_ppx)) (foreign_stubs (language c) (names process_tree_stubs)) diff --git a/scripts/bootstrap-development-environment b/scripts/bootstrap-development-environment index 54bd75c..d609327 100755 --- a/scripts/bootstrap-development-environment +++ b/scripts/bootstrap-development-environment @@ -5,6 +5,7 @@ set -eu repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) schema_environment="$repository_root/.venv-schema" schema_lock="$repository_root/requirements/schema.lock" +bisect_ppx_source="git+https://github.com/aantron/bisect_ppx.git#7061d643ff492b0045796357ee6917ded21fb1f0" require_command() { if ! command -v "$1" >/dev/null 2>&1; then @@ -22,6 +23,12 @@ if [ ! -f "$repository_root/_opam/.opam-switch/switch-config" ]; then opam switch create "$repository_root" 5.5.0 --no-install --yes fi +printf '%s\n' "Pinning the OCaml 5.5-compatible coverage backend..." +opam pin add bisect_ppx "$bisect_ppx_source" \ + --no-action \ + --switch "$repository_root" \ + --yes + printf '%s\n' "Installing locked OCaml development dependencies..." opam install "$repository_root" \ --deps-only \ diff --git a/scripts/check-development-environment b/scripts/check-development-environment index 18b4304..d7d646c 100755 --- a/scripts/check-development-environment +++ b/scripts/check-development-environment @@ -5,6 +5,7 @@ set -eu repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) local_switch="$repository_root/_opam" schema_python="$repository_root/.venv-schema/bin/python" +bisect_ppx_source="git+https://github.com/aantron/bisect_ppx.git#7061d643ff492b0045796357ee6917ded21fb1f0" fail() { printf '%s\n' "error: $1" >&2 @@ -25,6 +26,14 @@ if [ "$actual_switch" != "$local_switch" ]; then fail "opam selected '$actual_switch' instead of '$local_switch'" fi +bisect_ppx_pin=$( + opam pin list --switch "$repository_root" 2>/dev/null \ + | awk '$1 == "bisect_ppx.dev" { print $3 }' +) +if [ "$bisect_ppx_pin" != "$bisect_ppx_source" ]; then + fail "bisect_ppx is not pinned to the locked OCaml 5.5-compatible source" +fi + if ! pending_actions=$( opam install "$repository_root" \ --deps-only \ diff --git a/scripts/check-ocaml-coverage b/scripts/check-ocaml-coverage new file mode 100755 index 0000000..bbf22eb --- /dev/null +++ b/scripts/check-ocaml-coverage @@ -0,0 +1,42 @@ +#!/bin/sh +set -eu + +repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +coverage_root="$repository_root/_coverage" +coverage_data="$coverage_root/data" + +if [ "$coverage_root" != "$repository_root/_coverage" ]; then + echo "refusing to clean an unexpected coverage directory" >&2 + exit 1 +fi + +rm -rf -- "$coverage_root" +mkdir -p "$coverage_data" + +export BISECT_FILE="$coverage_data/bisect" +cd "$repository_root" + +opam exec --switch . -- dune clean +opam exec --switch . -- dune runtest --instrument-with bisect_ppx --force +opam exec --switch . -- bisect-ppx-report summary \ + --coverage-path "$coverage_data" \ + --per-file \ + --expect bin/ \ + --expect lib/ \ + | tee "$coverage_root/summary.txt" +opam exec --switch . -- bisect-ppx-report html \ + --coverage-path "$coverage_data" \ + --expect bin/ \ + --expect lib/ \ + --sort-by-stats \ + --tree \ + --title "Trading Engine OCaml coverage" \ + -o "$coverage_root/html" +opam exec --switch . -- bisect-ppx-report cobertura \ + --coverage-path "$coverage_data" \ + --expect bin/ \ + --expect lib/ \ + "$coverage_root/cobertura.xml" +python3 scripts/check-ocaml-coverage.py \ + coverage/ocaml-policy.json \ + "$coverage_root/summary.txt" diff --git a/scripts/check-ocaml-coverage.py b/scripts/check-ocaml-coverage.py new file mode 100755 index 0000000..ea869f1 --- /dev/null +++ b/scripts/check-ocaml-coverage.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Validate the OCaml coverage result and its change history.""" + +from __future__ import annotations + +import json +import re +import sys +from decimal import Decimal +from pathlib import Path + + +SUMMARY_PATTERN = re.compile( + r"^\s*(?P\d+(?:\.\d+)?)\s+%\s+" + r"(?P\d+)/(?P\d+)\s+Project coverage\s*$", + re.MULTILINE, +) +ISSUE_PREFIX = "https://github.com/fallblu/trading-engine/issues/" + + +def fail(message: str) -> None: + raise SystemExit(f"coverage policy error: {message}") + + +def load_policy(path: Path) -> dict[str, object]: + try: + policy = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + fail(f"cannot read {path}: {error}") + if not isinstance(policy, dict): + fail("policy must be a JSON object") + return policy + + +def decimal_field(value: object, name: str) -> Decimal: + if isinstance(value, bool) or not isinstance(value, (int, float, str)): + fail(f"{name} must be a number") + try: + result = Decimal(str(value)) + except Exception as error: + fail(f"{name} is invalid: {error}") + if result < 0 or result > 100: + fail(f"{name} must be between 0 and 100") + return result + + +def validate_policy(policy: dict[str, object]) -> Decimal: + if policy.get("format_version") != 1: + fail("format_version must be 1") + minimum = decimal_field( + policy.get("minimum_coverage_percent"), "minimum_coverage_percent" + ) + history = policy.get("threshold_history") + if not isinstance(history, list) or not history: + fail("threshold_history must contain at least one entry") + + previous: Decimal | None = None + for index, entry in enumerate(history): + if not isinstance(entry, dict): + fail(f"threshold_history[{index}] must be an object") + entry_minimum = decimal_field( + entry.get("minimum_coverage_percent"), + f"threshold_history[{index}].minimum_coverage_percent", + ) + reason = entry.get("reason") + issue_url = entry.get("issue_url") + if not isinstance(reason, str) or not reason.strip(): + fail(f"threshold_history[{index}] must explain the change") + if not isinstance(issue_url, str) or not issue_url.startswith(ISSUE_PREFIX): + fail(f"threshold_history[{index}] must link a repository issue") + if previous is not None and entry_minimum < previous and len(reason.strip()) < 20: + fail(f"threshold_history[{index}] must explain the threshold reduction") + previous = entry_minimum + + if previous != minimum: + fail("the latest threshold history entry must match the active minimum") + + exclusions = policy.get("excluded_paths") + if not isinstance(exclusions, list): + fail("excluded_paths must be a list") + for index, exclusion in enumerate(exclusions): + if not isinstance(exclusion, dict): + fail(f"excluded_paths[{index}] must be an object") + path = exclusion.get("path") + reason = exclusion.get("reason") + if not isinstance(path, str) or not path.strip(): + fail(f"excluded_paths[{index}] must name a path") + if not isinstance(reason, str) or not reason.strip(): + fail(f"excluded_paths[{index}] must explain the exclusion") + return minimum + + +def main() -> None: + if len(sys.argv) != 3: + fail("usage: check-ocaml-coverage.py POLICY SUMMARY") + policy_path, summary_path = map(Path, sys.argv[1:]) + minimum = validate_policy(load_policy(policy_path)) + try: + summary = summary_path.read_text(encoding="utf-8") + except OSError as error: + fail(f"cannot read {summary_path}: {error}") + match = SUMMARY_PATTERN.search(summary) + if match is None: + fail("summary does not contain project coverage") + covered = int(match.group("covered")) + total = int(match.group("total")) + if total <= 0 or covered > total: + fail("summary contains invalid coverage counts") + actual = Decimal(covered * 100) / Decimal(total) + if actual < minimum: + fail( + f"{covered}/{total} points ({actual:.2f}%) is below the " + f"{minimum}% minimum" + ) + print( + f"OCaml coverage {covered}/{total} points ({actual:.2f}%) " + f"meets the {minimum}% minimum" + ) + + +if __name__ == "__main__": + main() diff --git a/test/test_domain.ml b/test/test_domain.ml index f47b310..83a09f0 100644 --- a/test/test_domain.ml +++ b/test/test_domain.ml @@ -100,6 +100,74 @@ let market_slice_validation () = Alcotest.(check bool) "premature availability rejected" true (Result.is_error result) +let bar_validation_boundaries () = + let instrument_id = instrument_id "bar-validation" in + let create ?(open_price = "100") ?(high_price = "110") ?(low_price = "90") + ?(close_price = "105") ?(volume = Some "10") () = + T.Bar.create ~instrument_id ~open_price:(price open_price) + ~high_price:(price high_price) ~low_price:(price low_price) + ~close_price:(price close_price) + ~volume:(Option.map quantity volume) + in + let rejects label expected result = + Alcotest.(check string) label expected (error result) + in + rejects "inverted range" "bar low must not exceed its high" + (create ~high_price:"90" ~low_price:"100" ()); + rejects "open below range" "bar open must lie inside its low-high range" + (create ~open_price:"89" ()); + rejects "open above range" "bar open must lie inside its low-high range" + (create ~open_price:"111" ()); + rejects "close below range" "bar close must lie inside its low-high range" + (create ~close_price:"89" ()); + rejects "close above range" "bar close must lie inside its low-high range" + (create ~close_price:"111" ()); + rejects "negative volume" "bar volume must be nonnegative" + (create ~volume:(Some "-1") ()); + let valid = create ~volume:None () |> ok in + Alcotest.(check string) + "rendered close" "bar[bar-validation] close=105" + (Format.asprintf "%a" T.Bar.pp valid) + +let corporate_action_validation_boundaries () = + let id value = T.Id.Corporate_action.of_string_exn value in + let instrument_id = instrument_id "action-validation" in + let split ?(numerator = 2L) ?(denominator = 1L) action_id = + T.Corporate_action.split ~id:(id action_id) ~instrument_id ~numerator + ~denominator + in + Alcotest.(check string) + "zero numerator" "split numerator and denominator must be positive" + (error (split ~numerator:0L "zero-numerator")); + Alcotest.(check string) + "zero denominator" "split numerator and denominator must be positive" + (error (split ~denominator:0L "zero-denominator")); + Alcotest.(check string) + "unchanged units" "split ratio must change the instrument units" + (error (split ~numerator:1L "identity-split")); + let split_action = split "split" |> ok in + let invalid_dividend = + T.Corporate_action.cash_dividend ~id:(id "invalid-dividend") ~instrument_id + ~amount_per_unit:(money "0") + in + Alcotest.(check string) + "zero dividend" "cash dividend amount per unit must be positive" + (error invalid_dividend); + let dividend = + T.Corporate_action.cash_dividend ~id:(id "dividend") ~instrument_id + ~amount_per_unit:(money "0.25") + |> ok + in + Alcotest.(check bool) + "actions compare by ID" true + (T.Corporate_action.compare dividend split_action < 0); + Alcotest.(check string) + "split rendering" "split split 2:1 action-validation" + (Format.asprintf "%a" T.Corporate_action.pp split_action); + Alcotest.(check string) + "dividend rendering" "dividend dividend 0.25 action-validation" + (Format.asprintf "%a" T.Corporate_action.pp dividend) + let sha256_vectors () = Alcotest.(check string) "empty" "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" @@ -230,6 +298,10 @@ let tests = Alcotest.test_case "portfolio weight rounds toward zero" `Quick portfolio_weight_rounds_toward_zero; Alcotest.test_case "market slice validation" `Quick market_slice_validation; + Alcotest.test_case "bar validation boundaries" `Quick + bar_validation_boundaries; + Alcotest.test_case "corporate action validation boundaries" `Quick + corporate_action_validation_boundaries; Alcotest.test_case "SHA-256 vectors" `Quick sha256_vectors; Alcotest.test_case "OMS partial and duplicate fills" `Quick oms_partial_fill_and_duplicate; diff --git a/trading_engine.opam b/trading_engine.opam index 6d647a8..e61b41b 100644 --- a/trading_engine.opam +++ b/trading_engine.opam @@ -15,6 +15,12 @@ build: [ ["dune" "subst"] {dev} ["dune" "build" "-p" name "-j" jobs] ] +pin-depends: [ + [ + "bisect_ppx.dev" + "git+https://github.com/aantron/bisect_ppx.git#7061d643ff492b0045796357ee6917ded21fb1f0" + ] +] depends: [ "ocaml" {>= "5.5.0" & < "5.6"} "dune" {>= "3.24"} @@ -30,5 +36,6 @@ depends: [ "alcotest" {with-test & >= "1.9.1" & < "2.0"} "qcheck-core" {with-test & >= "0.91" & < "1.0"} "qcheck-alcotest" {with-test & >= "0.91" & < "1.0"} + "bisect_ppx" {with-test} "odoc" {with-doc} ] diff --git a/trading_engine.opam.locked b/trading_engine.opam.locked index 1e3eb40..8592c08 100644 --- a/trading_engine.opam.locked +++ b/trading_engine.opam.locked @@ -20,6 +20,7 @@ depends: [ "base-threads" {= "base"} "base-unix" {= "base"} "bigstringaf" {= "0.10.0"} + "bisect_ppx" {= "dev" & with-test} "camlp-streams" {= "5.0.1" & with-test} "cmdliner" {= "2.1.1"} "compiler-cloning" {= "enabled"} @@ -54,6 +55,7 @@ depends: [ "ocaml" {= "5.5.0"} "ocaml-base-compiler" {= "5.5.0"} "ocaml-compiler" {= "5.5.0"} + "ocaml-compiler-libs" {= "v0.17.0" & with-test} "ocaml-options-vanilla" {= "1"} "ocaml-syntax-shims" {= "1.0.0" & with-test} "ocaml-version" {= "4.1.3" & with-test} @@ -66,6 +68,8 @@ depends: [ "odoc" {= "3.2.1" & with-doc} "odoc-parser" {= "3.2.1" & with-doc} "optint" {= "0.3.0"} + "ppx_derivers" {= "1.2.1" & with-test} + "ppxlib" {= "0.38.0" & with-test} "psq" {= "0.2.1"} "ptime" {= "1.2.0"} "qcheck-alcotest" {= "0.91" & with-test} @@ -89,6 +93,12 @@ build: [ ["dune" "subst"] {dev} ["dune" "build" "-p" name "-j" jobs] ] +pin-depends: [ + [ + "bisect_ppx.dev" + "git+https://github.com/aantron/bisect_ppx.git#7061d643ff492b0045796357ee6917ded21fb1f0" + ] +] license: "MIT" homepage: "https://github.com/fallblu/trading-engine" -bug-reports: "https://github.com/fallblu/trading-engine/issues" \ No newline at end of file +bug-reports: "https://github.com/fallblu/trading-engine/issues" From 5bb440d6a8e4c37254fa9eeac9a2d442ce35ffad Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 23:29:59 -0400 Subject: [PATCH 23/57] build: verify coverage backend archive --- .github/workflows/ci.yml | 2 +- README.md | 3 +- docs/coverage.md | 7 ++-- .../packages/bisect_ppx/bisect_ppx.dev/opam | 33 +++++++++++++++++++ opam-repository/repo | 1 + scripts/bootstrap-development-environment | 16 ++++++--- scripts/check-development-environment | 21 ++++++++---- trading_engine.opam | 6 ---- trading_engine.opam.locked | 6 ---- 9 files changed, 67 insertions(+), 28 deletions(-) create mode 100644 opam-repository/packages/bisect_ppx/bisect_ppx.dev/opam create mode 100644 opam-repository/repo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5f19d8..3a63995 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: with: ocaml-compiler: "5.5.0" - working-directory: trading-engine - run: opam install . --deps-only --with-test --locked + run: make bootstrap - working-directory: trading-engine run: make build - working-directory: persistra diff --git a/README.md b/README.md index e91bbaa..396f135 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,7 @@ parsers. ```sh cd ~/trading-engine -opam switch set . -opam install . --deps-only --with-test --locked +make bootstrap make check ``` diff --git a/docs/coverage.md b/docs/coverage.md index ed20df3..fcacf4b 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -5,9 +5,10 @@ Bisect_ppx while running the normal Dune test aliases. Normal `make build`, `mak `make check` targets remain uninstrumented, so coverage cannot change deterministic journals, transcripts, diagnostics, or other contract output. -The opam manifests and bootstrap script pin Bisect_ppx to one upstream commit that supports the -project's OCaml 5.5 and ppxlib toolchain. The environment check verifies that exact source pin as -well as the locked dependency versions, preventing a local fallback to an incompatible release. +The repository-local opam package source selects one upstream Bisect_ppx commit that supports the +project's OCaml 5.5 and ppxlib toolchain and verifies its archive with SHA-256. Bootstrap registers +that package source before installing the lock. The environment check verifies the package source +and locked dependency versions, preventing a local fallback to an incompatible release. Run the gate from a bootstrapped development environment: diff --git a/opam-repository/packages/bisect_ppx/bisect_ppx.dev/opam b/opam-repository/packages/bisect_ppx/bisect_ppx.dev/opam new file mode 100644 index 0000000..8d78a8b --- /dev/null +++ b/opam-repository/packages/bisect_ppx/bisect_ppx.dev/opam @@ -0,0 +1,33 @@ +opam-version: "2.0" +name: "bisect_ppx" +version: "dev" +synopsis: "Code coverage for OCaml" +license: "MIT" +homepage: "https://github.com/aantron/bisect_ppx" +bug-reports: "https://github.com/aantron/bisect_ppx/issues" +dev-repo: "git+https://github.com/aantron/bisect_ppx.git" +authors: [ + "Xavier Clerc " + "Leonid Rozenberg " + "Anton Bachin " +] +maintainer: [ + "Anton Bachin " + "Leonid Rozenberg " +] +depends: [ + "base-unix" + "cmdliner" {>= "1.3.0"} + "dune" {>= "2.9.0"} + "ocaml" {>= "4.03.0"} + "ppxlib" {>= "0.36.0"} +] +build: [ + ["dune" "build" "-p" name "-j" jobs] +] +url { + src: + "https://github.com/aantron/bisect_ppx/archive/7061d643ff492b0045796357ee6917ded21fb1f0.tar.gz" + checksum: + "sha256=7ea9ec62296768c6f7a29b48b7e3a5c10fc3ae8a162c29886f404529a4f1686a" +} diff --git a/opam-repository/repo b/opam-repository/repo new file mode 100644 index 0000000..013b84d --- /dev/null +++ b/opam-repository/repo @@ -0,0 +1 @@ +opam-version: "2.0" diff --git a/scripts/bootstrap-development-environment b/scripts/bootstrap-development-environment index d609327..ca7101e 100755 --- a/scripts/bootstrap-development-environment +++ b/scripts/bootstrap-development-environment @@ -5,7 +5,8 @@ set -eu repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) schema_environment="$repository_root/.venv-schema" schema_lock="$repository_root/requirements/schema.lock" -bisect_ppx_source="git+https://github.com/aantron/bisect_ppx.git#7061d643ff492b0045796357ee6917ded21fb1f0" +coverage_repository_name="trading-engine-coverage" +coverage_repository="$repository_root/opam-repository" require_command() { if ! command -v "$1" >/dev/null 2>&1; then @@ -23,12 +24,19 @@ if [ ! -f "$repository_root/_opam/.opam-switch/switch-config" ]; then opam switch create "$repository_root" 5.5.0 --no-install --yes fi -printf '%s\n' "Pinning the OCaml 5.5-compatible coverage backend..." -opam pin add bisect_ppx "$bisect_ppx_source" \ - --no-action \ +printf '%s\n' "Registering the checksummed OCaml coverage package..." +opam repository add "$coverage_repository_name" "$coverage_repository" \ + --rank 1 \ --switch "$repository_root" \ --yes +if opam pin list --switch "$repository_root" --short | grep -Fqx "bisect_ppx"; then + opam pin remove bisect_ppx \ + --no-action \ + --switch "$repository_root" \ + --yes +fi + printf '%s\n' "Installing locked OCaml development dependencies..." opam install "$repository_root" \ --deps-only \ diff --git a/scripts/check-development-environment b/scripts/check-development-environment index d7d646c..9ad4f33 100755 --- a/scripts/check-development-environment +++ b/scripts/check-development-environment @@ -5,7 +5,8 @@ set -eu repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) local_switch="$repository_root/_opam" schema_python="$repository_root/.venv-schema/bin/python" -bisect_ppx_source="git+https://github.com/aantron/bisect_ppx.git#7061d643ff492b0045796357ee6917ded21fb1f0" +coverage_repository_name="trading-engine-coverage" +coverage_repository_url="file://$repository_root/opam-repository" fail() { printf '%s\n' "error: $1" >&2 @@ -26,12 +27,20 @@ if [ "$actual_switch" != "$local_switch" ]; then fail "opam selected '$actual_switch' instead of '$local_switch'" fi -bisect_ppx_pin=$( - opam pin list --switch "$repository_root" 2>/dev/null \ - | awk '$1 == "bisect_ppx.dev" { print $3 }' +if ! opam repository list --switch "$repository_root" --short 2>/dev/null \ + | grep -Fqx "$coverage_repository_name"; then + fail "the checksummed OCaml coverage package repository is not registered" +fi +coverage_repository_actual=$( + opam repository list --switch "$repository_root" 2>/dev/null \ + | awk '$2 == "trading-engine-coverage" { print $3 }' ) -if [ "$bisect_ppx_pin" != "$bisect_ppx_source" ]; then - fail "bisect_ppx is not pinned to the locked OCaml 5.5-compatible source" +if [ "$coverage_repository_actual" != "$coverage_repository_url" ]; then + fail "the OCaml coverage package repository has an unexpected source" +fi +if opam pin list --switch "$repository_root" --short 2>/dev/null \ + | grep -Fqx "bisect_ppx"; then + fail "bisect_ppx must resolve from the checksummed package repository" fi if ! pending_actions=$( diff --git a/trading_engine.opam b/trading_engine.opam index e61b41b..392a9f0 100644 --- a/trading_engine.opam +++ b/trading_engine.opam @@ -15,12 +15,6 @@ build: [ ["dune" "subst"] {dev} ["dune" "build" "-p" name "-j" jobs] ] -pin-depends: [ - [ - "bisect_ppx.dev" - "git+https://github.com/aantron/bisect_ppx.git#7061d643ff492b0045796357ee6917ded21fb1f0" - ] -] depends: [ "ocaml" {>= "5.5.0" & < "5.6"} "dune" {>= "3.24"} diff --git a/trading_engine.opam.locked b/trading_engine.opam.locked index 8592c08..71703a4 100644 --- a/trading_engine.opam.locked +++ b/trading_engine.opam.locked @@ -93,12 +93,6 @@ build: [ ["dune" "subst"] {dev} ["dune" "build" "-p" name "-j" jobs] ] -pin-depends: [ - [ - "bisect_ppx.dev" - "git+https://github.com/aantron/bisect_ppx.git#7061d643ff492b0045796357ee6917ded21fb1f0" - ] -] license: "MIT" homepage: "https://github.com/fallblu/trading-engine" bug-reports: "https://github.com/fallblu/trading-engine/issues" From 7f21bea9f09d88686f45ddb444accd3a487f27f6 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 23:33:28 -0400 Subject: [PATCH 24/57] build: parse repository state without color --- scripts/check-development-environment | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/check-development-environment b/scripts/check-development-environment index 9ad4f33..c9f9653 100755 --- a/scripts/check-development-environment +++ b/scripts/check-development-environment @@ -27,18 +27,18 @@ if [ "$actual_switch" != "$local_switch" ]; then fail "opam selected '$actual_switch' instead of '$local_switch'" fi -if ! opam repository list --switch "$repository_root" --short 2>/dev/null \ +if ! opam repository list --switch "$repository_root" --short --color never 2>/dev/null \ | grep -Fqx "$coverage_repository_name"; then fail "the checksummed OCaml coverage package repository is not registered" fi coverage_repository_actual=$( - opam repository list --switch "$repository_root" 2>/dev/null \ + opam repository list --switch "$repository_root" --color never 2>/dev/null \ | awk '$2 == "trading-engine-coverage" { print $3 }' ) if [ "$coverage_repository_actual" != "$coverage_repository_url" ]; then fail "the OCaml coverage package repository has an unexpected source" fi -if opam pin list --switch "$repository_root" --short 2>/dev/null \ +if opam pin list --switch "$repository_root" --short --color never 2>/dev/null \ | grep -Fqx "bisect_ppx"; then fail "bisect_ppx must resolve from the checksummed package repository" fi From d345db3051209a9d7a0ddd987d88222e0235b5ff Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 23:39:16 -0400 Subject: [PATCH 25/57] fix: isolate coverage build artifacts --- .gitignore | 1 + docs/coverage.md | 7 ++++--- scripts/check-ocaml-coverage | 13 +++++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index d87103d..8077895 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /_build/ +/_build-coverage/ /_coverage/ /_opam/ /.venv-schema/ diff --git a/docs/coverage.md b/docs/coverage.md index fcacf4b..ceeb1f4 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -16,9 +16,10 @@ Run the gate from a bootstrapped development environment: make coverage ``` -The command cleans Dune's generated build tree, recreates `_coverage/`, and writes three views of -the same run. Cleaning first ensures every test executable and cram invocation contributes fresh -instrumentation data: +The command recreates the isolated `_build-coverage/` and `_coverage/` directories, then writes +three views of the same run. A fresh isolated build ensures every test executable and cram +invocation contributes new instrumentation data without changing the normal `_build/` tree or +source-root editor artifacts: - `summary.txt` lists every production module and the project-wide instrumented-point result. - `html/index.html` highlights expression and control-flow points, making unvisited match arms, diff --git a/scripts/check-ocaml-coverage b/scripts/check-ocaml-coverage index bbf22eb..f2a338d 100755 --- a/scripts/check-ocaml-coverage +++ b/scripts/check-ocaml-coverage @@ -4,20 +4,25 @@ set -eu repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) coverage_root="$repository_root/_coverage" coverage_data="$coverage_root/data" +coverage_build="$repository_root/_build-coverage" -if [ "$coverage_root" != "$repository_root/_coverage" ]; then - echo "refusing to clean an unexpected coverage directory" >&2 +if [ "$coverage_root" != "$repository_root/_coverage" ] \ + || [ "$coverage_build" != "$repository_root/_build-coverage" ]; then + echo "refusing to clean an unexpected generated directory" >&2 exit 1 fi rm -rf -- "$coverage_root" +rm -rf -- "$coverage_build" mkdir -p "$coverage_data" export BISECT_FILE="$coverage_data/bisect" cd "$repository_root" -opam exec --switch . -- dune clean -opam exec --switch . -- dune runtest --instrument-with bisect_ppx --force +opam exec --switch . -- dune runtest \ + --build-dir "$coverage_build" \ + --instrument-with bisect_ppx \ + --force opam exec --switch . -- bisect-ppx-report summary \ --coverage-path "$coverage_data" \ --per-file \ From ddc6fe18c11548b801657f5a67b9787d71e4ccf7 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Fri, 21 Aug 2026 23:49:30 -0400 Subject: [PATCH 26/57] test: add replay performance benchmarks --- .gitignore | 2 + Makefile | 11 +- README.md | 4 + bench/baselines/linux-x86_64.json | 159 ++++++++++ bench/benchmark_replay.py | 498 ++++++++++++++++++++++++++++++ bench/latency_strategy.py | 49 +++ docs/performance.md | 61 ++++ test/dune | 9 + test/test_benchmark_replay.py | 86 ++++++ 9 files changed, 877 insertions(+), 2 deletions(-) create mode 100644 bench/baselines/linux-x86_64.json create mode 100644 bench/benchmark_replay.py create mode 100644 bench/latency_strategy.py create mode 100644 test/test_benchmark_replay.py diff --git a/.gitignore b/.gitignore index 8077895..12dd915 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ /_build/ /_build-coverage/ /_coverage/ +/benchmark-results/ +__pycache__/ /_opam/ /.venv-schema/ /.direnv/ diff --git a/Makefile b/Makefile index 105d905..03df759 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,10 @@ export PATH := $(CURDIR)/.venv-schema/bin:$(PATH) -.PHONY: bootstrap environment-check build test coverage fuzz-smoke fuzz fmt-check check +.PHONY: bootstrap environment-check build test coverage benchmark-smoke benchmark fuzz-smoke fuzz fmt-check check FUZZ_SEED ?= 20260821 FUZZ_CASES ?= 10000 +BENCHMARK_OUTPUT ?= benchmark-results/replay.json bootstrap: @./scripts/bootstrap-development-environment @@ -20,6 +21,12 @@ test: coverage: environment-check @./scripts/check-ocaml-coverage +benchmark-smoke: build + python3 bench/benchmark_replay.py --suite smoke --repetitions 1 --warmups 0 + +benchmark: build + python3 bench/benchmark_replay.py --output $(BENCHMARK_OUTPUT) + fuzz-smoke: opam exec -- dune exec test/fuzz_protocol.exe -- --seed 20260821 --cases 256 @@ -29,4 +36,4 @@ fuzz: fmt-check: opam exec -- dune build @fmt -check: environment-check fmt-check build test +check: environment-check fmt-check build test benchmark-smoke diff --git a/README.md b/README.md index 396f135..3f7808a 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,10 @@ make bootstrap make check ``` +Run the advisory batch, stream, dense-OMS, and external-strategy performance matrix with +`make benchmark`. See [Performance](docs/performance.md) for workload definitions, reported +metrics, and the baseline tolerance policy. + Validate the included scenario with an in-memory replay: ```sh diff --git a/bench/baselines/linux-x86_64.json b/bench/baselines/linux-x86_64.json new file mode 100644 index 0000000..71269aa --- /dev/null +++ b/bench/baselines/linux-x86_64.json @@ -0,0 +1,159 @@ +{ + "schema_version": 1, + "captured_at": "2026-08-21", + "environment": { + "system": "Linux/WSL2", + "machine": "Intel Core i7-10750H x86_64", + "python": "3.12.3", + "engine": "1.0.0", + "dune_profile": "dev", + "repetitions": 3, + "warmups": 1 + }, + "policy": { + "mode": "advisory", + "rationale": "Performance gates remain opt-in until multiple runners establish stable distributions." + }, + "cases": { + "batch-standard": { + "metrics": { + "median_wall_seconds": 0.03820421, + "median_peak_rss_kib": 18176, + "median_events_per_second": 26227.475977, + "median_artifact_bytes_per_second": 24804282.041168 + }, + "tolerances": { + "median_wall_seconds": 0.35, + "median_peak_rss_kib": 0.25, + "median_events_per_second": 0.30, + "median_artifact_bytes_per_second": 0.30 + } + }, + "stream-standard": { + "metrics": { + "median_wall_seconds": 0.0729861, + "median_peak_rss_kib": 18276, + "median_events_per_second": 13728.641481, + "median_artifact_bytes_per_second": 13017999.317678 + }, + "tolerances": { + "median_wall_seconds": 0.35, + "median_peak_rss_kib": 0.25, + "median_events_per_second": 0.30, + "median_artifact_bytes_per_second": 0.30 + } + }, + "batch-large-catalog": { + "metrics": { + "median_wall_seconds": 0.36514346, + "median_peak_rss_kib": 28792, + "median_events_per_second": 553.207224, + "median_artifact_bytes_per_second": 20197740.909833 + }, + "tolerances": { + "median_wall_seconds": 0.35, + "median_peak_rss_kib": 0.25, + "median_events_per_second": 0.30, + "median_artifact_bytes_per_second": 0.30 + } + }, + "stream-large-catalog": { + "metrics": { + "median_wall_seconds": 0.61600429, + "median_peak_rss_kib": 23908, + "median_events_per_second": 327.919794, + "median_artifact_bytes_per_second": 11973257.523905 + }, + "tolerances": { + "median_wall_seconds": 0.35, + "median_peak_rss_kib": 0.25, + "median_events_per_second": 0.30, + "median_artifact_bytes_per_second": 0.30 + } + }, + "batch-dense-oms": { + "metrics": { + "median_wall_seconds": 0.0348315, + "median_peak_rss_kib": 23908, + "median_events_per_second": 13149.017412, + "median_artifact_bytes_per_second": 11377230.380546 + }, + "tolerances": { + "median_wall_seconds": 0.35, + "median_peak_rss_kib": 0.25, + "median_events_per_second": 0.30, + "median_artifact_bytes_per_second": 0.30 + } + }, + "stream-dense-oms": { + "metrics": { + "median_wall_seconds": 0.06674844, + "median_peak_rss_kib": 23908, + "median_events_per_second": 6861.583582, + "median_artifact_bytes_per_second": 5967585.160043 + }, + "tolerances": { + "median_wall_seconds": 0.35, + "median_peak_rss_kib": 0.25, + "median_events_per_second": 0.30, + "median_artifact_bytes_per_second": 0.30 + } + }, + "external-batch-zero-latency": { + "metrics": { + "median_wall_seconds": 0.06543207, + "median_peak_rss_kib": 23908, + "median_events_per_second": 3087.171169, + "median_artifact_bytes_per_second": 5267310.051478 + }, + "tolerances": { + "median_wall_seconds": 0.35, + "median_peak_rss_kib": 0.25, + "median_events_per_second": 0.30, + "median_artifact_bytes_per_second": 0.30 + } + }, + "external-stream-zero-latency": { + "metrics": { + "median_wall_seconds": 0.07694489, + "median_peak_rss_kib": 23908, + "median_events_per_second": 2625.255556, + "median_artifact_bytes_per_second": 4485781.966808 + }, + "tolerances": { + "median_wall_seconds": 0.35, + "median_peak_rss_kib": 0.25, + "median_events_per_second": 0.30, + "median_artifact_bytes_per_second": 0.30 + } + }, + "external-batch-five-ms": { + "metrics": { + "median_wall_seconds": 0.66649209, + "median_peak_rss_kib": 23908, + "median_events_per_second": 303.079366, + "median_artifact_bytes_per_second": 513308.417509 + }, + "tolerances": { + "median_wall_seconds": 0.35, + "median_peak_rss_kib": 0.25, + "median_events_per_second": 0.30, + "median_artifact_bytes_per_second": 0.30 + } + }, + "external-stream-five-ms": { + "metrics": { + "median_wall_seconds": 0.68144857, + "median_peak_rss_kib": 23908, + "median_events_per_second": 296.427359, + "median_artifact_bytes_per_second": 502786.292442 + }, + "tolerances": { + "median_wall_seconds": 0.35, + "median_peak_rss_kib": 0.25, + "median_events_per_second": 0.30, + "median_artifact_bytes_per_second": 0.30 + } + } + } +} diff --git a/bench/benchmark_replay.py b/bench/benchmark_replay.py new file mode 100644 index 0000000..acdda0d --- /dev/null +++ b/bench/benchmark_replay.py @@ -0,0 +1,498 @@ +#!/usr/bin/env python3 +"""Benchmark representative batch, stream, OMS, and strategy workloads.""" + +from __future__ import annotations + +import argparse +import copy +import json +import os +import platform +import re +import statistics +import subprocess +import sys +import tempfile +import time +from dataclasses import asdict, dataclass +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_EXECUTABLE = ROOT / "_build/default/bin/main.exe" +DEFAULT_BASELINE = ROOT / "bench/baselines/linux-x86_64.json" +FIXTURE = ROOT / "contracts/v4/fixtures/demo.scenario.json" +STRATEGY = ROOT / "bench/latency_strategy.py" +SUMMARY_PATTERN = re.compile( + r"\baudits=(?P[0-9]+).*\bactive=(?P[0-9]+)" +) +METRIC_DIRECTIONS = { + "median_wall_seconds": "higher", + "median_peak_rss_kib": "higher", + "median_events_per_second": "lower", + "median_artifact_bytes_per_second": "lower", +} + + +@dataclass(frozen=True) +class BenchmarkCase: + name: str + replay_format: str + catalog_size: int + slice_count: int + active_order_count: int = 0 + strategy_latency_ms: float | None = None + + @property + def uses_external_strategy(self) -> bool: + return self.strategy_latency_ms is not None + + +@dataclass(frozen=True) +class Sample: + wall_seconds: float + peak_rss_kib: int + audit_events: int + artifact_bytes: int + events_per_second: float + artifact_bytes_per_second: float + + +SMOKE_CASES = ( + BenchmarkCase("smoke-batch", "batch", 2, 8, active_order_count=4), + BenchmarkCase("smoke-stream", "stream", 2, 8, active_order_count=4), + BenchmarkCase("smoke-external-batch", "batch", 1, 4, strategy_latency_ms=1.0), + BenchmarkCase("smoke-external-stream", "stream", 1, 4, strategy_latency_ms=1.0), +) + +FULL_CASES = ( + BenchmarkCase("batch-standard", "batch", 1, 500), + BenchmarkCase("stream-standard", "stream", 1, 500), + BenchmarkCase("batch-large-catalog", "batch", 128, 100), + BenchmarkCase("stream-large-catalog", "stream", 128, 100), + BenchmarkCase("batch-dense-oms", "batch", 1, 100, active_order_count=256), + BenchmarkCase("stream-dense-oms", "stream", 1, 100, active_order_count=256), + BenchmarkCase("external-batch-zero-latency", "batch", 1, 100, strategy_latency_ms=0.0), + BenchmarkCase("external-stream-zero-latency", "stream", 1, 100, strategy_latency_ms=0.0), + BenchmarkCase("external-batch-five-ms", "batch", 1, 100, strategy_latency_ms=5.0), + BenchmarkCase("external-stream-five-ms", "stream", 1, 100, strategy_latency_ms=5.0), +) + + +def timestamp(value: datetime) -> str: + return value.isoformat(timespec="seconds").replace("+00:00", "Z") + + +def _instrument(index: int) -> dict[str, object]: + return { + "instrument_id": f"benchmark-equity-{index:04d}", + "symbol": f"B{index:04d}", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1", + } + + +def build_scenario(case: BenchmarkCase) -> dict[str, object]: + """Build a deterministic scenario whose declared dimensions match a case.""" + if case.replay_format not in {"batch", "stream"}: + raise ValueError("replay_format must be batch or stream") + if case.catalog_size <= 0 or case.slice_count <= 0: + raise ValueError("catalog_size and slice_count must be positive") + if not 0 <= case.active_order_count <= 4096: + raise ValueError("active_order_count must be between 0 and 4096") + if case.uses_external_strategy and case.active_order_count: + raise ValueError("external-strategy cases cannot contain a schedule") + + document = json.loads(FIXTURE.read_text(encoding="utf-8")) + template = document["slices"][0] + instruments = [_instrument(index + 1) for index in range(case.catalog_size)] + base = datetime(2026, 8, 21, tzinfo=timezone.utc) + slices = [] + for offset in range(case.slice_count): + start = base + timedelta(seconds=offset * 4) + market_slice = copy.deepcopy(template) + market_slice.update( + { + "slice_sequence": str(offset + 1), + "start_at": timestamp(start), + "end_at": timestamp(start + timedelta(seconds=1)), + "available_at": timestamp(start + timedelta(seconds=2)), + "received_at": timestamp(start + timedelta(seconds=3)), + "bars": [ + { + "instrument_id": instrument["instrument_id"], + "open": "100", + "high": "101", + "low": "99", + "close": "100", + "volume": "1000000", + } + for instrument in instruments + ], + "corporate_actions": [], + } + ) + slices.append(market_slice) + + schedule = [] + if case.active_order_count: + schedule.append( + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": instruments[0]["instrument_id"], + "side": "buy", + "quantity": "1", + "order_kind": "limit", + "limit_price": "1", + } + for _ in range(case.active_order_count) + ], + } + ) + + document.update( + { + "metadata": { + "producer": "trading-engine-benchmark", + "benchmark_case": case.name, + }, + "run_id": f"benchmark-{case.name}", + "instruments": instruments, + "risk": { + "max_order_quantity": "1000000", + "max_long_position": "1000000", + "max_short_position": "1000000", + "max_gross_exposure": "1000000000", + "max_leverage": "1000000", + "initial_margin_bps": 1, + "maintenance_margin_bps": 1, + "short_borrow_bps": 0, + }, + "execution": { + "model": "completed_bar_v1", + "participation_bps": 10000, + "fixed_fee": "0", + "fee_bps": 0, + }, + "max_internal_events": max(1000, case.active_order_count * 4 + 16), + "schedule": schedule, + "slices": slices, + } + ) + return document + + +def stream_records(document: dict[str, object]) -> list[dict[str, object]]: + """Convert a batch document to the semantically equivalent stream records.""" + schedule = { + item["after_slice_sequence"]: item["intents"] + for item in document["schedule"] + } + header_fields = ( + "metadata", + "run_id", + "base_currency", + "initial_cash", + "instruments", + "risk", + "execution", + "max_internal_events", + ) + records = [ + { + "contract_version": document["contract_version"], + "scenario_sequence": "1", + "record_type": "scenario_header", + "payload": {field: document[field] for field in header_fields}, + } + ] + for index, market_slice in enumerate(document["slices"], start=2): + records.append( + { + "contract_version": document["contract_version"], + "scenario_sequence": str(index), + "record_type": "market_slice", + "payload": { + "market_slice": market_slice, + "intents": schedule.get(market_slice["slice_sequence"], []), + }, + } + ) + records.append( + { + "contract_version": document["contract_version"], + "scenario_sequence": str(len(records) + 1), + "record_type": "scenario_end", + "payload": {"slice_count": str(len(document["slices"]))}, + } + ) + return records + + +def write_input(case: BenchmarkCase, directory: Path) -> Path: + document = build_scenario(case) + if case.replay_format == "batch": + path = directory / "scenario.json" + path.write_text( + json.dumps(document, separators=(",", ":")) + "\n", encoding="utf-8" + ) + else: + path = directory / "scenario.jsonl" + with path.open("w", encoding="utf-8") as channel: + for record in stream_records(document): + channel.write(json.dumps(record, separators=(",", ":")) + "\n") + return path + + +def parse_summary(stdout: str) -> tuple[int, int]: + match = SUMMARY_PATTERN.search(stdout) + if match is None: + raise ValueError(f"could not parse replay summary: {stdout.strip()}") + return int(match.group("audits")), int(match.group("active")) + + +def _peak_rss_kib(usage: Any) -> int: + peak = int(usage.ru_maxrss) + return peak // 1024 if sys.platform == "darwin" else peak + + +def run_once( + executable: Path, case: BenchmarkCase, scenario: Path, directory: Path, run: int +) -> Sample: + journal = directory / f"journal-{run}.jsonl" + transcript = directory / f"strategy-{run}.jsonl" + command = [str(executable), "--input", str(scenario), "--journal", str(journal)] + if case.replay_format == "stream": + command.extend(("--input-format", "jsonl")) + if case.uses_external_strategy: + command.extend( + ( + "--strategy-executable", + sys.executable, + "--strategy-arg", + str(STRATEGY), + "--strategy-arg", + str(case.strategy_latency_ms), + "--strategy-timeout", + "30", + "--strategy-transcript", + str(transcript), + ) + ) + + environment = os.environ.copy() + environment["PYTHONDONTWRITEBYTECODE"] = "1" + with tempfile.TemporaryFile() as stdout_file, tempfile.TemporaryFile() as stderr_file: + started = time.perf_counter_ns() + process = subprocess.Popen( + command, + cwd=ROOT, + env=environment, + stdout=stdout_file, + stderr=stderr_file, + ) + _, status, usage = os.wait4(process.pid, 0) + elapsed = (time.perf_counter_ns() - started) / 1_000_000_000 + process.returncode = os.waitstatus_to_exitcode(status) + stdout_file.seek(0) + stderr_file.seek(0) + stdout = stdout_file.read().decode("utf-8", errors="replace") + stderr = stderr_file.read().decode("utf-8", errors="replace") + if process.returncode != 0: + raise RuntimeError( + f"benchmark command failed with exit {process.returncode}: {stderr.strip()}" + ) + + audits, active = parse_summary(stdout) + if active != case.active_order_count: + raise RuntimeError( + f"{case.name} retained {active} active orders; expected " + f"{case.active_order_count}" + ) + artifact_paths = [journal] + if case.uses_external_strategy: + artifact_paths.append(transcript) + if any(not path.is_file() for path in artifact_paths): + raise RuntimeError(f"{case.name} did not publish every expected artifact") + journal_events = sum(1 for _ in journal.open("rb")) + if journal_events != audits: + raise RuntimeError( + f"{case.name} reported {audits} audits but wrote {journal_events} journal records" + ) + artifact_bytes = sum(path.stat().st_size for path in artifact_paths) + return Sample( + wall_seconds=elapsed, + peak_rss_kib=_peak_rss_kib(usage), + audit_events=audits, + artifact_bytes=artifact_bytes, + events_per_second=audits / elapsed, + artifact_bytes_per_second=artifact_bytes / elapsed, + ) + + +def summarize(case: BenchmarkCase, samples: list[Sample]) -> dict[str, object]: + if not samples: + raise ValueError("at least one sample is required") + audit_counts = {sample.audit_events for sample in samples} + artifact_sizes = {sample.artifact_bytes for sample in samples} + if len(audit_counts) != 1 or len(artifact_sizes) != 1: + raise RuntimeError(f"{case.name} produced nondeterministic artifacts") + return { + "case": asdict(case), + "audit_events": samples[0].audit_events, + "artifact_bytes": samples[0].artifact_bytes, + "median_wall_seconds": statistics.median( + sample.wall_seconds for sample in samples + ), + "median_peak_rss_kib": statistics.median( + sample.peak_rss_kib for sample in samples + ), + "median_events_per_second": statistics.median( + sample.events_per_second for sample in samples + ), + "median_artifact_bytes_per_second": statistics.median( + sample.artifact_bytes_per_second for sample in samples + ), + "samples": [asdict(sample) for sample in samples], + } + + +def find_regressions( + result: dict[str, object], baseline: dict[str, object] +) -> list[str]: + regressions = [] + tolerances = baseline["tolerances"] + metrics = baseline["metrics"] + for metric, direction in METRIC_DIRECTIONS.items(): + observed = float(result[metric]) + reference = float(metrics[metric]) + tolerance = float(tolerances[metric]) + threshold = reference * (1 + tolerance if direction == "higher" else 1 - tolerance) + regressed = observed > threshold if direction == "higher" else observed < threshold + if regressed: + regressions.append( + f"{metric}={observed:.3f} crossed advisory threshold {threshold:.3f}" + ) + return regressions + + +def benchmark_environment(executable: Path) -> dict[str, str]: + version = subprocess.run( + [str(executable), "--version"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + return { + "system": platform.system(), + "machine": platform.machine(), + "python": platform.python_version(), + "engine": version, + "dune_profile": os.environ.get("DUNE_PROFILE", "dev"), + } + + +def render(results: list[dict[str, object]]) -> None: + print( + "case | format | catalog | slices | active | latency ms | wall s | " + "peak MiB | events/s | artifact MiB/s | baseline" + ) + print("--- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---") + for result in results: + case = result["case"] + regressions = result.get("advisory_regressions", []) + latency = case["strategy_latency_ms"] + print( + f"{case['name']} | {case['replay_format']} | {case['catalog_size']} | " + f"{case['slice_count']} | {case['active_order_count']} | " + f"{'-' if latency is None else latency} | " + f"{result['median_wall_seconds']:.4f} | " + f"{result['median_peak_rss_kib'] / 1024:.1f} | " + f"{result['median_events_per_second']:.0f} | " + f"{result['median_artifact_bytes_per_second'] / 1048576:.2f} | " + f"{'advisory regression' if regressions else result.get('baseline_status', 'not compared')}" + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--executable", type=Path, default=DEFAULT_EXECUTABLE) + parser.add_argument("--suite", choices=("smoke", "full"), default="full") + parser.add_argument("--repetitions", type=int, default=3) + parser.add_argument("--warmups", type=int, default=1) + parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE) + parser.add_argument("--no-baseline", action="store_true") + parser.add_argument("--enforce", action="store_true") + parser.add_argument("--output", type=Path) + args = parser.parse_args() + if args.repetitions <= 0 or args.warmups < 0: + parser.error("repetitions must be positive and warmups cannot be negative") + if args.enforce and args.no_baseline: + parser.error("--enforce requires baseline comparison") + executable = args.executable.resolve() + if not executable.is_file(): + parser.error(f"executable does not exist: {executable}") + + baseline = None + if not args.no_baseline and args.baseline.is_file(): + baseline = json.loads(args.baseline.read_text(encoding="utf-8")) + if args.enforce and baseline is None: + parser.error(f"baseline does not exist: {args.baseline}") + cases = SMOKE_CASES if args.suite == "smoke" else FULL_CASES + results = [] + with tempfile.TemporaryDirectory(prefix="trading-engine-benchmark-") as raw_directory: + root = Path(raw_directory) + for case in cases: + case_directory = root / case.name + case_directory.mkdir() + scenario = write_input(case, case_directory) + for warmup in range(args.warmups): + run_once(executable, case, scenario, case_directory, -(warmup + 1)) + samples = [ + run_once(executable, case, scenario, case_directory, repetition) + for repetition in range(args.repetitions) + ] + result = summarize(case, samples) + baseline_case = None if baseline is None else baseline["cases"].get(case.name) + if baseline_case is None: + result["baseline_status"] = "not compared" + else: + result["baseline_status"] = "within tolerance" + result["advisory_regressions"] = find_regressions(result, baseline_case) + results.append(result) + + report = { + "schema_version": 1, + "generated_at": datetime.now(timezone.utc).isoformat(), + "suite": args.suite, + "repetitions": args.repetitions, + "warmups": args.warmups, + "environment": benchmark_environment(executable), + "results": results, + } + render(results) + if args.output is not None: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + print(f"wrote {args.output}") + regression_count = sum( + len(result.get("advisory_regressions", [])) for result in results + ) + if regression_count: + print( + f"{regression_count} advisory regression(s) detected; " + "use --enforce to make them fatal", + file=sys.stderr, + ) + return 1 if args.enforce and regression_count else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bench/latency_strategy.py b/bench/latency_strategy.py new file mode 100644 index 0000000..8f4ff06 --- /dev/null +++ b/bench/latency_strategy.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""No-op strategy that adds deterministic per-event response latency.""" + +from __future__ import annotations + +import json +import sys +import time + + +LATENCY_SECONDS = float(sys.argv[1]) / 1000 if len(sys.argv) > 1 else 0.0 +if LATENCY_SECONDS < 0: + raise ValueError("latency must not be negative") + + +for line in sys.stdin: + request = json.loads(line) + message_type = request["message_type"] + if message_type == "initialize": + response_type = "ready" + payload = { + "strategy_name": "benchmark-latency", + "strategy_version": "1", + } + elif message_type == "event": + if LATENCY_SECONDS: + time.sleep(LATENCY_SECONDS) + response_type = "intents" + payload = {"intents": []} + elif message_type == "shutdown": + response_type = "stopped" + payload = {} + else: + response_type = "error" + payload = {"message": "unsupported request"} + print( + json.dumps( + { + "strategy_protocol_version": "3", + "strategy_sequence": request["strategy_sequence"], + "message_type": response_type, + "payload": payload, + }, + separators=(",", ":"), + ), + flush=True, + ) + if message_type == "shutdown": + break diff --git a/docs/performance.md b/docs/performance.md index 4f41b49..de45431 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -45,3 +45,64 @@ three times per size, and prints medians plus the observed range. These results The exact timings are illustrative rather than service-level targets. The growing baseline ratio and near-linear indexed results are the relevant regression signal. + +## Replay regression suite + +The replay suite measures complete public-CLI runs, including scenario parsing, deterministic +reduction, and artifact publication. It generates every input before the timed interval and covers +the batch and JSON Lines paths independently. + +```sh +make benchmark +``` + +The default run performs one warmup and records the median of three samples in +`benchmark-results/replay.json`. Generated reports are ignored by Git. The table printed to the +terminal and the JSON report include: + +- Wall-clock seconds measured with the monotonic high-resolution clock. +- Peak resident memory of the direct engine process. On Linux this is `ru_maxrss`, normalized to + KiB; an external strategy's own resident memory is intentionally excluded. +- Audit-event throughput, using the CLI's audit count checked against journal line count. +- Artifact-byte throughput, using the published journal size and, for external cases, the strategy + transcript size. + +The full matrix holds all unlisted dimensions constant while varying the source of likely +regressions: + +| Workload pair | Catalog | Slices | Active orders | Strategy latency | +|---|---:|---:|---:|---:| +| Standard history | 1 | 500 | 0 | none | +| Large catalog | 128 | 100 | 0 | none | +| Dense OMS | 1 | 100 | 256 | none | +| External strategy | 1 | 100 | 0 | 0 ms/event | +| Latent external strategy | 1 | 100 | 0 | 5 ms/event | + +Every workload is run in both batch and stream form. Dense-OMS cases submit persistent buy limits +far below the market after the first slice and verify that exactly 256 orders remain active. The +latency strategy returns no intents and sleeps only before each event response, keeping protocol +initialization and shutdown outside the modeled per-event delay. + +### Baseline and tolerance policy + +`bench/baselines/linux-x86_64.json` stores the initial Linux/WSL2 development-build baseline from +the reference machine described in that file. Wall time allows a 35% increase; peak RSS allows a +25% increase; event and artifact throughput allow a 30% decrease. These deliberately broad +tolerances account for scheduler, filesystem-cache, and allocator noise while the project gathers +measurements across more runners. + +Baseline comparison is advisory by default and therefore cannot make `make benchmark` fail. A +reported regression is a prompt to repeat the run on comparable hardware and profile the affected +dimension. On a controlled, baseline-compatible runner, opt into a failing gate with: + +```sh +python3 bench/benchmark_replay.py --enforce +``` + +Refresh a baseline only after explaining an intentional workload or performance change and +recording the engine version, build profile, machine, warmup count, and repetition count. Do not +replace a baseline solely to clear an advisory regression. + +`make check` runs a one-sample smoke matrix with tiny versions of all four replay routes. It checks +input generation, external-strategy protocol behavior, active-order retention, audit counts, and +artifact publication without comparing timing values. diff --git a/test/dune b/test/dune index 4ccd062..b95ed06 100644 --- a/test/dune +++ b/test/dune @@ -160,3 +160,12 @@ (source_tree ../contracts)) (action (run python3 %{dep:validate_contract_conformance.py}))) + +(rule + (alias runtest) + (deps + test_benchmark_replay.py + ../bench/benchmark_replay.py + ../contracts/v4/fixtures/demo.scenario.json) + (action + (run python3 %{dep:test_benchmark_replay.py}))) diff --git a/test/test_benchmark_replay.py b/test/test_benchmark_replay.py new file mode 100644 index 0000000..2ef0a75 --- /dev/null +++ b/test/test_benchmark_replay.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Focused contract tests for the replay benchmark harness.""" + +from __future__ import annotations + +import importlib.util +import sys +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "benchmark_replay", ROOT / "bench/benchmark_replay.py" +) +assert SPEC is not None and SPEC.loader is not None +BENCHMARK = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = BENCHMARK +SPEC.loader.exec_module(BENCHMARK) + + +class BenchmarkReplayTest(unittest.TestCase): + def test_generated_batch_has_requested_dimensions(self) -> None: + case = BENCHMARK.BenchmarkCase("unit", "batch", 3, 4, 2) + scenario = BENCHMARK.build_scenario(case) + + self.assertEqual(3, len(scenario["instruments"])) + self.assertEqual(4, len(scenario["slices"])) + self.assertTrue(all(len(item["bars"]) == 3 for item in scenario["slices"])) + self.assertEqual(2, len(scenario["schedule"][0]["intents"])) + self.assertTrue( + all( + intent["limit_price"] == "1" + for intent in scenario["schedule"][0]["intents"] + ) + ) + + def test_stream_preserves_slices_and_scheduled_intents(self) -> None: + case = BENCHMARK.BenchmarkCase("unit", "stream", 2, 3, 5) + records = BENCHMARK.stream_records(BENCHMARK.build_scenario(case)) + + self.assertEqual("scenario_header", records[0]["record_type"]) + self.assertEqual("scenario_end", records[-1]["record_type"]) + self.assertEqual("3", records[-1]["payload"]["slice_count"]) + self.assertEqual(5, len(records[1]["payload"]["intents"])) + self.assertEqual([], records[2]["payload"]["intents"]) + + def test_summary_parser_reads_batch_and_stream_counts(self) -> None: + self.assertEqual( + (321, 17), + BENCHMARK.parse_summary( + "run=benchmark audits=321 orders=17 active=17 filled=0 rejected=0\n" + ), + ) + + def test_tolerance_comparison_checks_both_metric_directions(self) -> None: + result = { + "median_wall_seconds": 1.31, + "median_peak_rss_kib": 120.0, + "median_events_per_second": 79.0, + "median_artifact_bytes_per_second": 90.0, + } + baseline = { + "metrics": { + "median_wall_seconds": 1.0, + "median_peak_rss_kib": 100.0, + "median_events_per_second": 100.0, + "median_artifact_bytes_per_second": 100.0, + }, + "tolerances": { + "median_wall_seconds": 0.30, + "median_peak_rss_kib": 0.25, + "median_events_per_second": 0.20, + "median_artifact_bytes_per_second": 0.20, + }, + } + + regressions = BENCHMARK.find_regressions(result, baseline) + + self.assertEqual(2, len(regressions)) + self.assertTrue(any("wall" in regression for regression in regressions)) + self.assertTrue(any("events" in regression for regression in regressions)) + + +if __name__ == "__main__": + unittest.main() From 612fda350ad2b8c8a4a4ac97718e81292feef9d2 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 01:57:42 -0400 Subject: [PATCH 27/57] chore: add repository planning metadata --- .github/ISSUE_TEMPLATE/bug.yml | 67 +++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/contract-change.yml | 64 ++++++++++ .github/ISSUE_TEMPLATE/cross-repository.yml | 59 ++++++++++ .github/ISSUE_TEMPLATE/feature.yml | 60 ++++++++++ .github/SUPPORT.md | 13 +++ .github/labels.json | 33 ++++++ .github/pull_request_template.md | 7 ++ .github/repository.json | 14 +++ .github/workflows/ci.yml | 58 +++++++++- CHANGELOG.md | 6 + CONTRIBUTING.md | 13 +++ Makefile | 7 +- README.md | 3 + docs/persistra.md | 37 ++++++ test/test_repository_metadata.py | 122 ++++++++++++++++++++ 16 files changed, 565 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/contract-change.yml create mode 100644 .github/ISSUE_TEMPLATE/cross-repository.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/SUPPORT.md create mode 100644 .github/labels.json create mode 100644 .github/pull_request_template.md create mode 100644 .github/repository.json create mode 100644 test/test_repository_metadata.py diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..0c62f08 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,67 @@ +name: Bug report +description: Report reproducible incorrect engine, CLI, artifact, or contract behavior. +title: "fix: " +labels: ["bug"] +assignees: [] +body: + - type: markdown + attributes: + value: Thanks for providing a small, sanitized reproduction. + - type: dropdown + id: component + attributes: + label: Component + options: + - Accounting and valuation + - Artifacts and publication + - CLI and diagnostics + - Contracts and schemas + - Execution and order management + - Reducer and sequencing + - Risk and margin + - Strategy protocol + - CI and development tooling + validations: + required: true + - type: input + id: version + attributes: + label: Engine revision or version + placeholder: v1.0.0 or a full commit SHA + validations: + required: true + - type: input + id: contracts + attributes: + label: Contract versions + description: Include scenario, journal, diagnostic, and strategy versions that apply. + placeholder: scenario v4, journal v4, strategy v3 + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Provide exact commands and the smallest sanitized input or fixture. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: Include stable diagnostic codes and bounded output when available. + validations: + required: true + - type: checkboxes + id: safety + attributes: + label: Safe report + options: + - label: I removed credentials, customer data, proprietary strategies, and licensed data. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..22fc544 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Support and usage guidance + url: https://github.com/fallblu/trading-engine/blob/develop/.github/SUPPORT.md + about: Review supported scope, public boundaries, and safe issue-reporting guidance. diff --git a/.github/ISSUE_TEMPLATE/contract-change.yml b/.github/ISSUE_TEMPLATE/contract-change.yml new file mode 100644 index 0000000..c195f1b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/contract-change.yml @@ -0,0 +1,64 @@ +name: Contract change +description: Propose a versioned scenario, journal, diagnostic, capability, or strategy change. +title: "feat: evolve contract " +labels: ["enhancement", "component: contracts"] +assignees: [] +body: + - type: dropdown + id: family + attributes: + label: Contract family + options: + - Scenario and journal + - Scenario stream + - Capabilities + - Diagnostics + - External strategy protocol and transcript + validations: + required: true + - type: input + id: versions + attributes: + label: Affected versions + placeholder: current v4; frozen v1-v3 unchanged + validations: + required: true + - type: dropdown + id: compatibility + attributes: + label: Compatibility class + options: + - Additive within the current version + - New version required + - Clarification with no wire-format change + - Not yet known + validations: + required: true + - type: textarea + id: invariant + attributes: + label: Semantic invariant + description: State the runtime rule and what JSON Schema can and cannot enforce. + validations: + required: true + - type: textarea + id: wire-change + attributes: + label: Wire-format change + description: Show the smallest representative before-and-after records. + validations: + required: true + - type: textarea + id: migration + attributes: + label: Compatibility and migration + description: Explain frozen artifacts, parser behavior, and producer/consumer updates. + validations: + required: true + - type: textarea + id: verification + attributes: + label: Conformance evidence + description: List schemas, canonical fixtures, negative fixtures, and differential tests. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/cross-repository.yml b/.github/ISSUE_TEMPLATE/cross-repository.yml new file mode 100644 index 0000000..5660287 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/cross-repository.yml @@ -0,0 +1,59 @@ +name: Cross-repository compatibility +description: Report or propose coordinated behavior between Trading Engine and Persistra. +title: "chore: coordinate compatibility " +labels: ["enhancement", "dependency: persistra"] +assignees: [] +body: + - type: input + id: engine-revision + attributes: + label: Trading Engine revision + placeholder: Full commit SHA + validations: + required: true + - type: input + id: persistra-revision + attributes: + label: Persistra revision + placeholder: Full commit SHA + validations: + required: true + - type: input + id: contracts + attributes: + label: Contract versions + placeholder: scenario v3, journal v3, strategy v3 + validations: + required: true + - type: dropdown + id: owner + attributes: + label: Owning boundary + options: + - Trading Engine consumer/runtime behavior + - Persistra producer/host behavior + - Versioned contract shared by both repositories + - Not yet known + validations: + required: true + - type: textarea + id: behavior + attributes: + label: Compatibility behavior + description: Describe the expected handoff and the observed failure or proposed change. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction and evidence + description: Include exact commands, sanitized fixtures, diagnostics, and CI links. + validations: + required: true + - type: textarea + id: coordination + attributes: + label: Coordinated update + description: Identify which pin, tests, contracts, and documentation each repository changes. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..61c83f9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,60 @@ +name: Feature proposal +description: Propose one scoped capability without assigning a release commitment. +title: "feat: " +labels: ["enhancement"] +assignees: [] +body: + - type: dropdown + id: component + attributes: + label: Component + options: + - Accounting and valuation + - Artifacts and publication + - CLI and diagnostics + - Contracts and schemas + - Execution and order management + - Reducer and sequencing + - Risk and margin + - Strategy protocol + - CI and development tooling + validations: + required: true + - type: textarea + id: problem + attributes: + label: Problem + description: Describe the concrete unsupported workflow or invariant. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed behavior + description: Define scope, boundaries, and observable behavior without a delivery date. + validations: + required: true + - type: dropdown + id: contract-impact + attributes: + label: Contract impact + options: + - No public contract change + - Additive current-contract change + - Breaking versioned-contract change + - Not yet known + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives and tradeoffs + validations: + required: true + - type: textarea + id: verification + attributes: + label: Verification expectations + description: List unit, property, conformance, failure-path, or performance evidence. + validations: + required: true diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..955e8e7 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,13 @@ +# Support + +Start with the [README](../README.md) for setup, supported scope, and command examples. The +[architecture](../docs/architecture.md), [scenario contract](../docs/scenario.md), and +[Persistra integration guide](../docs/persistra.md) describe the public boundaries in detail. + +Use the structured issue forms for reproducible bugs, feature proposals, versioned contract +changes, and cross-repository compatibility failures. Search existing issues first. Include the +engine version, relevant contract versions, exact commands, sanitized inputs, and the smallest +reproduction that demonstrates the behavior. + +Do not post credentials, customer data, proprietary strategies, or licensed market data. This +repository cannot provide private trading, deployment, or strategy-development support. diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..5a62bca --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,33 @@ +[ + {"category": "component", "name": "component: accounting", "color": "1d76db", "description": "Cash, positions, valuation, fees, and reconciliation"}, + {"category": "component", "name": "component: artifacts", "color": "1d76db", "description": "Journals, transcripts, manifests, and durable publication"}, + {"category": "component", "name": "component: ci", "color": "1d76db", "description": "Continuous integration, builds, and development tooling"}, + {"category": "component", "name": "component: cli", "color": "1d76db", "description": "Command-line input, output, diagnostics, and process behavior"}, + {"category": "component", "name": "component: contracts", "color": "1d76db", "description": "Versioned scenarios, journals, schemas, and conformance"}, + {"category": "component", "name": "component: execution", "color": "1d76db", "description": "Order management, matching, fills, and execution models"}, + {"category": "component", "name": "component: reducer", "color": "1d76db", "description": "Pure state transitions, phases, and deterministic sequencing"}, + {"category": "component", "name": "component: repository", "color": "1d76db", "description": "Repository policy, documentation, and community health"}, + {"category": "component", "name": "component: risk", "color": "1d76db", "description": "Pre-trade limits, margin, borrow, and liquidation"}, + {"category": "component", "name": "component: strategy", "color": "1d76db", "description": "Built-in and external strategy lifecycle and protocol"}, + + {"category": "priority", "name": "priority: critical", "color": "b60205", "description": "Immediate correctness, security, or data-integrity impact"}, + {"category": "priority", "name": "priority: high", "color": "d93f0b", "description": "Important work selected ahead of normal backlog items"}, + {"category": "priority", "name": "priority: medium", "color": "fbca04", "description": "Normal backlog priority after explicit triage"}, + {"category": "priority", "name": "priority: low", "color": "0e8a16", "description": "Useful work with no current urgency"}, + + {"category": "effort", "name": "effort: small", "color": "c2e0c6", "description": "Focused change with a narrow verification surface"}, + {"category": "effort", "name": "effort: medium", "color": "fef2c0", "description": "Multi-file change with moderate design or testing work"}, + {"category": "effort", "name": "effort: large", "color": "f9d0c4", "description": "Broad change that should be split into reviewed increments"}, + + {"category": "contract", "name": "contract: scenario-v1", "color": "5319e7", "description": "Frozen scenario and journal contract version 1"}, + {"category": "contract", "name": "contract: scenario-v2", "color": "5319e7", "description": "Frozen scenario and journal contract version 2"}, + {"category": "contract", "name": "contract: scenario-v3", "color": "5319e7", "description": "Transitional scenario and journal contract version 3"}, + {"category": "contract", "name": "contract: scenario-v4", "color": "5319e7", "description": "Current scenario and journal contract version 4"}, + {"category": "contract", "name": "contract: strategy-v1", "color": "7057ff", "description": "Historical external strategy protocol version 1"}, + {"category": "contract", "name": "contract: strategy-v2", "color": "7057ff", "description": "Historical external strategy protocol version 2"}, + {"category": "contract", "name": "contract: strategy-v3", "color": "7057ff", "description": "Current external strategy protocol version 3"}, + + {"category": "dependency", "name": "dependency: persistra", "color": "006b75", "description": "Requires coordinated behavior or validation in Persistra"}, + {"category": "dependency", "name": "dependency: upstream", "color": "006b75", "description": "Depends on an external project or toolchain"}, + {"category": "dependency", "name": "dependency: blocked", "color": "b60205", "description": "Cannot proceed until a named dependency is resolved"} +] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..83135b0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +## Summary + + + +## Test plan + + diff --git a/.github/repository.json b/.github/repository.json new file mode 100644 index 0000000..2e0013a --- /dev/null +++ b/.github/repository.json @@ -0,0 +1,14 @@ +{ + "description": "Deterministic event-driven OCaml execution engine with versioned replay contracts and causal audit journals", + "homepage": "https://github.com/fallblu/trading-engine#readme", + "topics": [ + "backtesting", + "deterministic", + "event-driven", + "execution-simulator", + "json-schema", + "ocaml", + "quantitative-finance", + "trading-engine" + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a63995..fc2f871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,13 @@ name: CI on: push: pull_request: + workflow_dispatch: + inputs: + persistra_latest_head: + description: Run the nonrequired Persistra develop compatibility canary + required: false + default: false + type: boolean permissions: contents: read @@ -52,6 +59,8 @@ jobs: persistra-compatibility: runs-on: ubuntu-latest timeout-minutes: 30 + env: + PERSISTRA_COMPAT_REVISION: ade8c05e435c56d8df8eba88fed1284652fd731b steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -61,8 +70,55 @@ jobs: with: persist-credentials: false repository: fallblu/persistra - ref: ${{ vars.PERSISTRA_COMPAT_REF || 'develop' }} + ref: ${{ env.PERSISTRA_COMPAT_REVISION }} path: persistra + - name: Report Persistra compatibility revision + working-directory: persistra + run: | + actual_revision="$(git rev-parse HEAD)" + test "$actual_revision" = "$PERSISTRA_COMPAT_REVISION" + echo "Persistra compatibility revision: $actual_revision" + printf '### Persistra compatibility\n\n`%s`\n' "$actual_revision" >> "$GITHUB_STEP_SUMMARY" + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + python-version: "3.12" + - uses: ocaml/setup-ocaml@605a7e998e76e035b82c14d618a6e1010732c4ce # v3.7.1 + with: + ocaml-compiler: "5.5.0" + - working-directory: trading-engine + run: make bootstrap + - working-directory: trading-engine + run: make build + - working-directory: persistra + run: uv sync --group dev + - working-directory: persistra + env: + PERSISTRA_TRADING_ENGINE_BINARY: ${{ github.workspace }}/trading-engine/_build/default/bin/main.exe + PERSISTRA_TRADING_ENGINE_CONTRACT_DIR: ${{ github.workspace }}/trading-engine/contracts/v3 + run: uv run pytest --no-cov tests/integration/test_trading_engine.py + + persistra-latest-head: + if: ${{ github.event_name == 'workflow_dispatch' && inputs.persistra_latest_head }} + continue-on-error: true + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + path: trading-engine + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + repository: fallblu/persistra + ref: develop + path: persistra + - name: Report Persistra canary revision + working-directory: persistra + run: | + actual_revision="$(git rev-parse HEAD)" + echo "Persistra latest-head canary revision: $actual_revision" + printf '### Persistra latest-head canary\n\n`%s`\n' "$actual_revision" >> "$GITHUB_STEP_SUMMARY" - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: python-version: "3.12" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b95cfc..5c93ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +- Add structured issue and pull-request intake, reviewed planning-label and repository metadata, + explicit compatibility guarantees, a pinned required Persistra baseline, and a manual + nonrequired latest-head signal. + ## 1.0.0 — 2026-08-21 - Release the deterministic completed-bar execution engine with exact checked arithmetic, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 961c06c..2fd6429 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,3 +40,16 @@ coherent, and working. Use subject-only conventional commit messages such as `feat: implement deterministic order matching`. Do not add secrets, provider credentials, or customer account data to fixtures or journals. + +## Intake and planning metadata + +Use the structured bug, feature, contract-change, or cross-repository issue form. Pull requests +retain the `Summary` and `Test plan` sections from the repository template. + +Component, contract-version, and dependency labels describe stable scope. Priority and effort +labels are assigned only during explicit triage; they do not promise a release, date, or roadmap +position. Do not encode delivery commitments in labels. The reviewed label definitions and desired +repository metadata live under `.github/` and must agree with the GitHub settings. + +For reciprocal Persistra compatibility guarantees and the pin-advancement procedure, read +[Persistra integration](docs/persistra.md#compatibility-guarantees). diff --git a/Makefile b/Makefile index 03df759..9082ab5 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ export PATH := $(CURDIR)/.venv-schema/bin:$(PATH) -.PHONY: bootstrap environment-check build test coverage benchmark-smoke benchmark fuzz-smoke fuzz fmt-check check +.PHONY: bootstrap environment-check build test metadata-check coverage benchmark-smoke benchmark fuzz-smoke fuzz fmt-check check FUZZ_SEED ?= 20260821 FUZZ_CASES ?= 10000 @@ -18,6 +18,9 @@ build: test: opam exec -- dune runtest +metadata-check: + python3 test/test_repository_metadata.py + coverage: environment-check @./scripts/check-ocaml-coverage @@ -36,4 +39,4 @@ fuzz: fmt-check: opam exec -- dune build @fmt -check: environment-check fmt-check build test benchmark-smoke +check: environment-check fmt-check build test metadata-check benchmark-smoke diff --git a/README.md b/README.md index 3f7808a..ab37999 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,8 @@ do not provide reducer snapshots or restart recovery. ## Architecture and contracts +- [Support and issue guidance](.github/SUPPORT.md) +- [Contributing](CONTRIBUTING.md) - [Architecture](docs/architecture.md) - [Diagnostic contract](docs/diagnostics.md) - [Scenario contract](docs/scenario.md) @@ -218,6 +220,7 @@ do not provide reducer snapshots or restart recovery. - [External 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/v3/message.schema.json) - [Strategy transcript JSON Schema](contracts/strategy/v3/transcript.schema.json) - [Execution model](docs/execution-model.md) diff --git a/docs/persistra.md b/docs/persistra.md index d1746d7..9e77188 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -69,6 +69,43 @@ Persistra must answer each callback before the engine continues matching. Every slice uses the slice receipt time and complete bar and FX snapshot. A later callback therefore includes accepted intents returned from an earlier callback at that same replay clock. +## Compatibility guarantees + +Compatibility is defined by versioned wire contracts and an explicitly tested pair of repository +revisions. A branch name, package version, or successful build in only one repository is not a +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 v4 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 v3; 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 + recorded revision pair and advertised versions. + +The required `persistra-compatibility` job pins the full Persistra commit stored as +`PERSISTRA_COMPAT_REVISION` in `.github/workflows/ci.yml`. It asserts the resolved checkout and +writes the SHA to the log and job summary. It never follows a repository variable or moving branch. +Persistra owns the reciprocal required pin to a reviewed Trading Engine commit. + +To advance either baseline, the repository changing its pin selects a green full commit from the +other repository, builds both exact checkouts, runs the cross-repository integration suite, and +updates the one workflow SHA in a reviewed pull request. When a contract or host/runtime behavior +changes, both repositories update their fixtures, documentation, and pins in dependency order. +Neither repository silently advances the other's required baseline. + +Maintainers can manually dispatch CI with `persistra_latest_head` enabled to test Persistra +`develop`. The `persistra-latest-head` job is nonrequired and allowed to fail, so it provides an +early signal without changing the reproducible baseline or blocking an unrelated engine change. + ## Time mapping - Intraday UTC timestamps map to slice event times. diff --git a/test/test_repository_metadata.py b/test/test_repository_metadata.py new file mode 100644 index 0000000..00a3d7b --- /dev/null +++ b/test/test_repository_metadata.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +import collections +import json +import pathlib +import re +import unittest + + +REPOSITORY_ROOT = pathlib.Path(__file__).resolve().parent.parent +GITHUB = REPOSITORY_ROOT / ".github" +FORM_NAMES = {"bug.yml", "contract-change.yml", "cross-repository.yml", "feature.yml"} +DEFAULT_LABELS = {"bug", "enhancement"} + + +class RepositoryMetadataTest(unittest.TestCase): + def test_repository_profile_is_specific_and_bounded(self) -> None: + profile = json.loads((GITHUB / "repository.json").read_text(encoding="utf-8")) + + self.assertEqual( + profile["description"], + "Deterministic event-driven OCaml execution engine with versioned replay contracts " + "and causal audit journals", + ) + self.assertEqual(profile["homepage"], "https://github.com/fallblu/trading-engine#readme") + self.assertEqual( + profile["topics"], + [ + "backtesting", + "deterministic", + "event-driven", + "execution-simulator", + "json-schema", + "ocaml", + "quantitative-finance", + "trading-engine", + ], + ) + self.assertEqual(len(profile["topics"]), len(set(profile["topics"]))) + + def test_label_manifest_covers_stable_planning_dimensions(self) -> None: + labels = json.loads((GITHUB / "labels.json").read_text(encoding="utf-8")) + names = [label["name"] for label in labels] + categories = collections.Counter(label["category"] for label in labels) + + self.assertEqual(len(names), len(set(names))) + self.assertEqual( + categories, + {"component": 10, "priority": 4, "effort": 3, "contract": 7, "dependency": 3}, + ) + self.assertTrue(all(re.fullmatch(r"[0-9a-f]{6}", label["color"]) for label in labels)) + self.assertTrue(all(label["description"].strip() for label in labels)) + self.assertIn("dependency: persistra", names) + self.assertIn("contract: scenario-v4", names) + self.assertIn("contract: strategy-v3", names) + + def test_structured_forms_reference_defined_labels_and_require_evidence(self) -> None: + template_directory = GITHUB / "ISSUE_TEMPLATE" + forms = {path.name: path for path in template_directory.glob("*.yml") if path.name != "config.yml"} + labels = json.loads((GITHUB / "labels.json").read_text(encoding="utf-8")) + allowed_labels = DEFAULT_LABELS | {label["name"] for label in labels} + + self.assertEqual(set(forms), FORM_NAMES) + for name, path in forms.items(): + text = path.read_text(encoding="utf-8") + self.assertRegex(text, r"(?m)^name: .+$", name) + self.assertRegex(text, r"(?m)^description: .+$", name) + self.assertIn("\nbody:\n", text, name) + self.assertIn("validations:\n required: true", text, name) + label_match = re.search(r"(?m)^labels: \[(.+)\]$", text) + self.assertIsNotNone(label_match, name) + assigned = set(json.loads(f"[{label_match.group(1)}]")) + self.assertLessEqual(assigned, allowed_labels, name) + ids = re.findall(r"(?m)^ id: ([a-z0-9-]+)$", text) + self.assertEqual(len(ids), len(set(ids)), name) + self.assertNotIn("priority:", "\n".join(path.read_text() for path in forms.values())) + self.assertNotIn("effort:", "\n".join(path.read_text() for path in forms.values())) + + config = (template_directory / "config.yml").read_text(encoding="utf-8") + self.assertIn("blank_issues_enabled: false", config) + self.assertIn(".github/SUPPORT.md", config) + + def test_pull_request_and_support_templates_preserve_required_sections(self) -> None: + pull_request = (GITHUB / "pull_request_template.md").read_text(encoding="utf-8") + self.assertEqual(pull_request.count("## Summary"), 1) + self.assertEqual(pull_request.count("## Test plan"), 1) + + support = (GITHUB / "SUPPORT.md").read_text(encoding="utf-8") + self.assertIn("structured issue forms", support) + self.assertIn("Do not post credentials", support) + + def test_compatibility_gate_is_pinned_and_canary_is_optional(self) -> None: + workflow = (GITHUB / "workflows/ci.yml").read_text(encoding="utf-8") + revision = "ade8c05e435c56d8df8eba88fed1284652fd731b" + + self.assertIn(f"PERSISTRA_COMPAT_REVISION: {revision}", workflow) + self.assertIn("ref: ${{ env.PERSISTRA_COMPAT_REVISION }}", workflow) + self.assertNotIn("PERSISTRA_COMPAT_REF", workflow) + self.assertNotIn("vars.", workflow) + self.assertIn('test "$actual_revision" = "$PERSISTRA_COMPAT_REVISION"', workflow) + self.assertIn("Persistra compatibility revision: $actual_revision", workflow) + self.assertIn('>> "$GITHUB_STEP_SUMMARY"', workflow) + + canary = workflow.split(" persistra-latest-head:\n", 1)[1] + self.assertIn("workflow_dispatch", workflow) + self.assertIn("inputs.persistra_latest_head", canary) + self.assertIn("continue-on-error: true", canary) + self.assertIn("ref: develop", canary) + self.assertIn("Persistra latest-head canary revision", canary) + + revisions = re.findall(r"uses: [^@\s]+@([^\s]+)", workflow) + self.assertTrue(revisions) + self.assertTrue(all(re.fullmatch(r"[0-9a-f]{40}", revision) for revision in revisions)) + + compatibility = (REPOSITORY_ROOT / "docs/persistra.md").read_text(encoding="utf-8") + for guarantee in ("Engine", "Scenario", "Journal", "Strategy", "Persistra"): + self.assertIn(f"**{guarantee}:**", compatibility) + self.assertIn("Neither repository silently advances", compatibility) + + +if __name__ == "__main__": + unittest.main() From 795bbd6d14f64ea3ffeaff09b12a9e42531f42ea Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 02:06:29 -0400 Subject: [PATCH 28/57] ci: verify dependency bands and deterministic journals --- .github/workflows/ci.yml | 44 ++++++++++++++++++ CHANGELOG.md | 2 + CONTRIBUTING.md | 5 +++ Makefile | 9 +++- README.md | 1 + docs/continuous-integration.md | 30 +++++++++++++ scripts/bootstrap-development-environment | 55 +++++++++++++++++++---- scripts/check-deterministic-journals | 52 +++++++++++++++++++++ 8 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 docs/continuous-integration.md create mode 100755 scripts/check-deterministic-journals diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc2f871..8e20411 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,10 @@ on: permissions: contents: read +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.ref_type == 'branch' && !github.ref_protected) }} + jobs: check: runs-on: ubuntu-latest @@ -31,6 +35,46 @@ jobs: - run: make bootstrap - run: make check + environment-matrix: + name: environment (${{ matrix.name }}) + continue-on-error: ${{ matrix.informational }} + strategy: + fail-fast: false + max-parallel: 2 + matrix: + include: + - name: lowest-ubuntu + runner: ubuntu-latest + dependency-band: lowest + target: dependency-band-check + informational: false + - name: highest-ubuntu + runner: ubuntu-latest + dependency-band: highest + target: dependency-band-check + informational: false + - name: highest-macos + runner: macos-15 + dependency-band: highest + target: determinism-check + informational: true + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: ocaml/setup-ocaml@605a7e998e76e035b82c14d618a6e1010732c4ce # v3.7.1 + with: + ocaml-compiler: "5.5.0" + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + python-version: "3.12" + - name: Install ${{ matrix.dependency-band }} dependency band + run: ./scripts/bootstrap-development-environment "${{ matrix.dependency-band }}" + - name: Run ${{ matrix.target }} + run: make "${{ matrix.target }}" + ocaml-coverage: runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c93ae1..0e1634b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Add structured issue and pull-request intake, reviewed planning-label and repository metadata, explicit compatibility guarantees, a pinned required Persistra baseline, and a manual nonrequired latest-head signal. +- Verify exact canonical journal bytes across locked, dependency-bound, and operating-system CI + cells, with safe concurrency cancellation and documented required versus informational gates. ## 1.0.0 — 2026-08-21 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2fd6429..8ac7f79 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,11 @@ Run `make coverage` to enforce the OCaml coverage floor and generate per-module, HTML, and Cobertura reports. See [OCaml coverage](docs/coverage.md) for report locations, instrumentation scope, and the explained-threshold-change policy. +CI additionally resolves the lowest and highest supported dependency bands and compares canonical +journal bytes on Linux and macOS. See [Continuous integration](docs/continuous-integration.md) for +the required and informational cells. Use `make dependency-band-check` only after bootstrapping a +nonlocked CI band; normal development continues to use `make check` and the exact lock. + The gate formats a copy check, builds every target, and runs all tests. Keep commits small, coherent, and working. Use subject-only conventional commit messages such as `feat: implement deterministic order matching`. diff --git a/Makefile b/Makefile index 9082ab5..6e1f24d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ export PATH := $(CURDIR)/.venv-schema/bin:$(PATH) -.PHONY: bootstrap environment-check build test metadata-check coverage benchmark-smoke benchmark fuzz-smoke fuzz fmt-check check +.PHONY: bootstrap environment-check build test metadata-check determinism-check dependency-band-check coverage benchmark-smoke benchmark fuzz-smoke fuzz fmt-check check FUZZ_SEED ?= 20260821 FUZZ_CASES ?= 10000 @@ -21,6 +21,11 @@ test: metadata-check: python3 test/test_repository_metadata.py +determinism-check: build + @./scripts/check-deterministic-journals + +dependency-band-check: fmt-check build test metadata-check determinism-check benchmark-smoke + coverage: environment-check @./scripts/check-ocaml-coverage @@ -39,4 +44,4 @@ fuzz: fmt-check: opam exec -- dune build @fmt -check: environment-check fmt-check build test metadata-check benchmark-smoke +check: environment-check fmt-check build test metadata-check determinism-check benchmark-smoke diff --git a/README.md b/README.md index ab37999..683d9f6 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,7 @@ do not provide reducer snapshots or restart recovery. - [Strategy transcript JSON Schema](contracts/strategy/v3/transcript.schema.json) - [Execution model](docs/execution-model.md) - [OCaml coverage](docs/coverage.md) +- [Continuous integration and portability matrix](docs/continuous-integration.md) - [Performance](docs/performance.md) - [Reducer property testing](docs/reducer-property-testing.md) - [Protocol fuzzing](docs/fuzzing.md) diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md new file mode 100644 index 0000000..1e848d4 --- /dev/null +++ b/docs/continuous-integration.md @@ -0,0 +1,30 @@ +# Continuous integration + +CI tests a small, explicit environment matrix instead of an accidental Cartesian product. The +public package bounds in `trading_engine.opam` define supported dependencies. The repository lock +defines the reproducible development baseline. + +| Cell | Operating system | OCaml | Dependencies | Gate | Status | +| --- | --- | --- | --- | --- | --- | +| `check` | Ubuntu latest | 5.5.0 | Exact lock | Full `make check` | Required | +| `lowest-ubuntu` | Ubuntu latest | 5.5.0 | Oldest solver-valid versions inside declared bounds | Full dependency-band check | Required | +| `highest-ubuntu` | Ubuntu latest | 5.5.0 | Newest solver-valid versions inside declared bounds | Full dependency-band check | Required | +| `highest-macos` | macOS 15 | 5.5.0 | Newest solver-valid versions inside declared bounds | Build and exact journal comparison | Informational | + +The lower and upper cells resolve against the current opam repository. They deliberately test the +range declared by the package rather than pretending to be reproducible locks. A failure in either +required Ubuntu cell means the declared support bounds or the implementation must change. The +macOS cell is an early portability signal while Ubuntu remains the supported build platform. + +Every runtime cell replays the v3 demo, v4 demo, and v4 risk-limited fill scenarios under `TZ=UTC` +and the C locale. It compares the resulting journal files byte for byte with their 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. + +Coverage runs once in the exact locked Ubuntu environment. The required Persistra job also runs +once against its full pinned commit; it is not repeated across dependency or operating-system +cells. The manually dispatched Persistra moving-head job remains informational. + +Pull requests and unprotected branch pushes cancel superseded runs. Tags and protected branches do +not, so durable integration evidence is not discarded. Pull-request concurrency keys use the pull +request number; all other events use the exact Git ref. diff --git a/scripts/bootstrap-development-environment b/scripts/bootstrap-development-environment index ca7101e..35a9ca2 100755 --- a/scripts/bootstrap-development-environment +++ b/scripts/bootstrap-development-environment @@ -7,6 +7,16 @@ schema_environment="$repository_root/.venv-schema" schema_lock="$repository_root/requirements/schema.lock" coverage_repository_name="trading-engine-coverage" coverage_repository="$repository_root/opam-repository" +dependency_band=${1:-locked} + +case "$dependency_band" in + locked | lowest | highest) ;; + *) + printf '%s\n' \ + "error: dependency band must be one of: locked, lowest, highest" >&2 + exit 2 + ;; +esac require_command() { if ! command -v "$1" >/dev/null 2>&1; then @@ -37,14 +47,36 @@ if opam pin list --switch "$repository_root" --short | grep -Fqx "bisect_ppx"; t --yes fi -printf '%s\n' "Installing locked OCaml development dependencies..." -opam install "$repository_root" \ - --deps-only \ - --with-test \ - --locked \ - --require-checksums \ - --switch "$repository_root" \ - --yes +printf '%s\n' "Installing $dependency_band OCaml development dependencies..." +case "$dependency_band" in + locked) + opam install "$repository_root" \ + --deps-only \ + --with-test \ + --locked \ + --require-checksums \ + --switch "$repository_root" \ + --yes + ;; + lowest) + opam install "$repository_root" \ + --criteria='+count[version-lag,solution]' \ + --deps-only \ + --with-test \ + --require-checksums \ + --switch "$repository_root" \ + --yes + ;; + highest) + opam install "$repository_root" \ + --criteria='-count[version-lag,solution]' \ + --deps-only \ + --with-test \ + --require-checksums \ + --switch "$repository_root" \ + --yes + ;; +esac if [ ! -x "$schema_environment/bin/python" ]; then printf '%s\n' "Creating the repository-local schema environment..." @@ -61,4 +93,9 @@ uv pip sync \ --strict \ "$schema_lock" -"$repository_root/scripts/check-development-environment" +if [ "$dependency_band" = locked ]; then + "$repository_root/scripts/check-development-environment" +else + printf '%s\n' \ + "$dependency_band dependency band is ready; the locked environment check does not apply." +fi diff --git a/scripts/check-deterministic-journals b/scripts/check-deterministic-journals new file mode 100755 index 0000000..551c703 --- /dev/null +++ b/scripts/check-deterministic-journals @@ -0,0 +1,52 @@ +#!/bin/sh + +set -eu + +repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +engine="$repository_root/_build/default/bin/main.exe" +temporary_root=$(mktemp -d "${TMPDIR:-/tmp}/trading-engine-determinism.XXXXXX") + +cleanup() { + rm -rf -- "$temporary_root" +} +trap cleanup EXIT HUP INT TERM + +export LC_ALL=C +export TZ=UTC + +compare_journal() { + name=$1 + scenario=$2 + expected=$3 + actual="$temporary_root/$name.journal.jsonl" + standard_output="$temporary_root/$name.stdout" + standard_error="$temporary_root/$name.stderr" + + "$engine" \ + --input "$repository_root/$scenario" \ + --journal "$actual" \ + >"$standard_output" \ + 2>"$standard_error" + + if ! cmp -s "$repository_root/$expected" "$actual"; then + printf '%s\n' "error: $name journal differs from its canonical bytes" >&2 + diff -u "$repository_root/$expected" "$actual" >&2 || true + exit 1 + fi + + byte_count=$(wc -c <"$actual" | tr -d ' ') + printf '%s\n' "$name: exact journal match ($byte_count bytes)" +} + +compare_journal \ + v3-demo \ + contracts/v3/fixtures/demo.scenario.json \ + contracts/v3/fixtures/demo.journal.jsonl +compare_journal \ + v4-demo \ + contracts/v4/fixtures/demo.scenario.json \ + contracts/v4/fixtures/demo.journal.jsonl +compare_journal \ + v4-fill-clipped \ + contracts/v4/fixtures/fill-clipped.scenario.json \ + contracts/v4/fixtures/fill-clipped.journal.jsonl From 912ea75fe10193dabebcb32f0763bec03f95df9d Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 02:07:13 -0400 Subject: [PATCH 29/57] ci: deduplicate branch and pull request runs --- .github/workflows/ci.yml | 2 +- docs/continuous-integration.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e20411..8a04764 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ permissions: contents: read concurrency: - group: ci-${{ github.event.pull_request.number || github.ref }} + group: ci-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.ref_type == 'branch' && !github.ref_protected) }} jobs: diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 1e848d4..1ee50a5 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -26,5 +26,6 @@ once against its full pinned commit; it is not repeated across dependency or ope cells. The manually dispatched Persistra moving-head job remains informational. Pull requests and unprotected branch pushes cancel superseded runs. Tags and protected branches do -not, so durable integration evidence is not discarded. Pull-request concurrency keys use the pull -request number; all other events use the exact Git ref. +not, so durable integration evidence is not discarded. The key combines the source repository and +source branch, so a pull request cancels its duplicate feature-branch push without colliding with a +fork or another branch. From e15af048f529c28801f57fe8fbbd2ca1ca919a6f Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 02:18:19 -0400 Subject: [PATCH 30/57] ci: run feature validation on pull requests --- .github/workflows/ci.yml | 4 ++++ docs/continuous-integration.md | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a04764..8e48c83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,10 @@ name: CI on: push: + branches: + - develop + tags: + - "*" pull_request: workflow_dispatch: inputs: diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 1ee50a5..e9da298 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -25,7 +25,8 @@ Coverage runs once in the exact locked Ubuntu environment. The required Persistr once against its full pinned commit; it is not repeated across dependency or operating-system cells. The manually dispatched Persistra moving-head job remains informational. -Pull requests and unprotected branch pushes cancel superseded runs. Tags and protected branches do -not, so durable integration evidence is not discarded. The key combines the source repository and -source branch, so a pull request cancels its duplicate feature-branch push without colliding with a -fork or another branch. +Feature-branch pushes do not start CI; the pull-request event owns that validation and avoids a +duplicate check set. Pull requests cancel superseded commits. Push validation runs only on +`develop` and tags, where it is never cancelled, so durable integration evidence is not discarded. +The concurrency key combines the source repository and source branch without colliding with a fork +or another branch. From c1118a7746b8b40d32b4c2f53d6257df24664c70 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 02:36:58 -0400 Subject: [PATCH 31/57] docs: publish architecture contract and API site --- .github/repository.json | 2 +- .github/workflows/docs.yml | 92 +++++++++ .github/workflows/external-links.yml | 59 ++++++ .gitignore | 2 + CHANGELOG.md | 2 + CONTRIBUTING.md | 5 + Makefile | 15 +- README.md | 4 + docs/api-reference.md | 10 + docs/documentation-platform.md | 38 ++++ dune | 1 + lychee.toml | 21 ++ mkdocs.yml | 58 ++++++ requirements/docs.in | 2 + requirements/docs.lock | 75 ++++++++ scripts/bootstrap-documentation-environment | 36 ++++ scripts/build-documentation-site | 35 ++++ scripts/check-documentation.py | 201 ++++++++++++++++++++ test/dune | 6 + test/test_documentation.py | 51 +++++ test/test_repository_metadata.py | 2 +- trading_engine.opam | 2 +- trading_engine.opam.locked | 2 +- 23 files changed, 715 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/external-links.yml create mode 100644 docs/api-reference.md create mode 100644 docs/documentation-platform.md create mode 100644 dune create mode 100644 lychee.toml create mode 100644 mkdocs.yml create mode 100644 requirements/docs.in create mode 100644 requirements/docs.lock create mode 100755 scripts/bootstrap-documentation-environment create mode 100755 scripts/build-documentation-site create mode 100644 scripts/check-documentation.py create mode 100644 test/test_documentation.py diff --git a/.github/repository.json b/.github/repository.json index 2e0013a..d465d7f 100644 --- a/.github/repository.json +++ b/.github/repository.json @@ -1,6 +1,6 @@ { "description": "Deterministic event-driven OCaml execution engine with versioned replay contracts and causal audit journals", - "homepage": "https://github.com/fallblu/trading-engine#readme", + "homepage": "https://fallblu.github.io/trading-engine/", "topics": [ "backtesting", "deterministic", diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..afb38b6 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,92 @@ +name: Documentation + +on: + pull_request: + paths: + - .github/workflows/docs.yml + - .github/SUPPORT.md + - contracts/** + - docs/** + - dune + - dune-project + - lib/dune + - lib/**/*.mli + - requirements/docs.* + - scripts/bootstrap-documentation-environment + - scripts/build-documentation-site + - scripts/check-documentation.py + - CHANGELOG.md + - CONTRIBUTING.md + - Makefile + - README.md + - mkdocs.yml + - trading_engine.opam + - trading_engine.opam.locked + push: + branches: + - develop + paths: + - .github/workflows/docs.yml + - .github/SUPPORT.md + - contracts/** + - docs/** + - dune + - dune-project + - lib/dune + - lib/**/*.mli + - requirements/docs.* + - scripts/bootstrap-documentation-environment + - scripts/build-documentation-site + - scripts/check-documentation.py + - CHANGELOG.md + - CONTRIBUTING.md + - Makefile + - README.md + - mkdocs.yml + - trading_engine.opam + - trading_engine.opam.locked + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: documentation-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + name: documentation-build + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: ocaml/setup-ocaml@605a7e998e76e035b82c14d618a6e1010732c4ce # v3.7.1 + with: + ocaml-compiler: "5.5.0" + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + python-version: "3.12" + - run: make bootstrap + - run: make docs-build + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: site + + deploy: + name: documentation-deploy + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + - id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/external-links.yml b/.github/workflows/external-links.yml new file mode 100644 index 0000000..2b2abc6 --- /dev/null +++ b/.github/workflows/external-links.yml @@ -0,0 +1,59 @@ +name: External links + +on: + pull_request: + paths: + - .github/workflows/external-links.yml + - .github/SUPPORT.md + - contracts/**/*.md + - docs/**/*.md + - CHANGELOG.md + - CONTRIBUTING.md + - README.md + - lychee.toml + push: + branches: + - develop + paths: + - .github/workflows/external-links.yml + - .github/SUPPORT.md + - contracts/**/*.md + - docs/**/*.md + - CHANGELOG.md + - CONTRIBUTING.md + - README.md + - lychee.toml + schedule: + - cron: "17 7 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: external-links-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + check: + name: external-links + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 + with: + args: >- + --config lychee.toml --verbose --no-progress + 'README.md' 'CONTRIBUTING.md' 'CHANGELOG.md' '.github/SUPPORT.md' + 'docs/**/*.md' 'contracts/**/README.md' + checkbox: false + fail: true + failIfEmpty: true + format: detailed + jobSummary: false + lycheeVersion: v0.24.2 + output: lychee-report.txt + token: "" diff --git a/.gitignore b/.gitignore index 12dd915..bff82e9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ __pycache__/ /_opam/ /.venv-schema/ +/.venv-docs/ +/site/ /.direnv/ /.envrc *.install diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e1634b..4a07b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ nonrequired latest-head signal. - Verify exact canonical journal bytes across locked, dependency-bound, and operating-system CI cells, with safe concurrency cancellation and documented required versus informational gates. +- Publish one strict documentation site for architecture, versioned contracts, and generated OCaml + APIs, with offline topology checks and bounded external-link validation. ## 1.0.0 — 2026-08-21 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ac7f79..8bbd88e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,11 @@ journal bytes on Linux and macOS. See [Continuous integration](docs/continuous-i the required and informational cells. Use `make dependency-band-check` only after bootstrapping a nonlocked CI band; normal development continues to use `make check` and the exact lock. +Run `make docs-build` to create the strict local site under `site/`. It installs only the locked +documentation tools in `.venv-docs`, stages versioned contracts without modifying them, generates +the public OCaml interfaces, and validates the complete output. See the +[documentation platform](docs/documentation-platform.md) for publication and link-checking policy. + The gate formats a copy check, builds every target, and runs all tests. Keep commits small, coherent, and working. Use subject-only conventional commit messages such as `feat: implement deterministic order matching`. diff --git a/Makefile b/Makefile index 6e1f24d..4822855 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ export PATH := $(CURDIR)/.venv-schema/bin:$(PATH) -.PHONY: bootstrap environment-check build test metadata-check determinism-check dependency-band-check coverage benchmark-smoke benchmark fuzz-smoke fuzz fmt-check check +.PHONY: bootstrap environment-check build test metadata-check docs-bootstrap docs-source-check docs-check docs-build determinism-check dependency-band-check coverage benchmark-smoke benchmark fuzz-smoke fuzz fmt-check check FUZZ_SEED ?= 20260821 FUZZ_CASES ?= 10000 @@ -21,6 +21,17 @@ test: metadata-check: python3 test/test_repository_metadata.py +docs-bootstrap: + @./scripts/bootstrap-documentation-environment + +docs-source-check: + python3 scripts/check-documentation.py source + +docs-check: docs-source-check + +docs-build: docs-bootstrap docs-check + @./scripts/build-documentation-site + determinism-check: build @./scripts/check-deterministic-journals @@ -44,4 +55,4 @@ fuzz: fmt-check: opam exec -- dune build @fmt -check: environment-check fmt-check build test metadata-check determinism-check benchmark-smoke +check: environment-check fmt-check build test metadata-check docs-source-check determinism-check benchmark-smoke diff --git a/README.md b/README.md index 683d9f6..b0fa402 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ Trading Engine is a deterministic, event-driven OCaml execution engine. It runs through pre-trade risk, order management, synchronized completed-bar execution, exact accounting, valuation, and a hash-bound JSON Lines audit journal. +The [documentation site](docs/documentation-platform.md) connects the architecture, execution and +scenario contracts, stable versioned artifacts, and generated OCaml API reference. + The engine is replay-first. Its pure kernel and explicit source, strategy, execution, and journal layers keep networking, files, and wall-clock state outside the reducer. @@ -226,6 +229,7 @@ do not provide reducer snapshots or restart recovery. - [Execution model](docs/execution-model.md) - [OCaml coverage](docs/coverage.md) - [Continuous integration and portability matrix](docs/continuous-integration.md) +- [Documentation platform and generated API](docs/documentation-platform.md) - [Performance](docs/performance.md) - [Reducer property testing](docs/reducer-property-testing.md) - [Protocol fuzzing](docs/fuzzing.md) diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..525c90e --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,10 @@ +# OCaml API reference + +The public OCaml interfaces are generated from the checked-in `.mli` files with `odoc`. The +documentation build inserts a link to the generated module index at the marker below and verifies +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/v4/README.md) remain authoritative for process boundaries. diff --git a/docs/documentation-platform.md b/docs/documentation-platform.md new file mode 100644 index 0000000..787fc7a --- /dev/null +++ b/docs/documentation-platform.md @@ -0,0 +1,38 @@ +# Documentation platform + +The canonical documentation site is published at `https://fallblu.github.io/trading-engine/` from +one immutable GitHub Pages artifact. It combines project guides, versioned contract sources, and +generated OCaml API pages without committing generated HTML. + +## Toolchain + +[MkDocs](https://www.mkdocs.org/user-guide/configuration/) 1.6 and Material for MkDocs 9 render the +Markdown navigation and search index. [odoc](https://ocaml.github.io/odoc/odoc/odoc_for_authors.html) +renders the public `.mli` interfaces through Dune's `@doc` target. Exact Python package versions +live in `requirements/docs.lock`; the exact odoc version lives in `trading_engine.opam.locked`. + +The build stages the repository Markdown and entire `contracts/` tree under `_build`, adds the odoc +HTML tree, and then runs `mkdocs build --strict`. Staging publishes contract README files, schemas, +and fixtures directly from their source locations. Frozen v1 and v2 pages therefore keep stable +versioned URLs and cannot diverge from the repository copies. + +`make docs-check` performs the deterministic offline source check. `make docs-build` bootstraps the +locked documentation tools, builds odoc, runs strict MkDocs, checks every generated local link, and +confirms that public modules and contract assets are present. + +## Deployment boundary + +The [GitHub Pages custom workflow](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages) +uses a read-only build job. Pull requests build and package the complete site but cannot deploy it. +Only a push to `develop` enables the separate deployment job, whose only elevated permissions are +`pages: write` and `id-token: write`. The `github-pages` environment records the deployed URL. + +All actions use full commit pins. Pages deployment never writes a generated branch, repository +commit, tag, or release artifact. + +## Link validation + +Offline checks validate every repository-relative Markdown target and every generated HTML link. +External HTTPS links run through a separate pinned Lychee workflow on relevant pull requests, +`develop` changes, a weekly schedule, and manual dispatch. It uses no token, rejects insecure or +private targets, bounds redirects and retries, and begins with an empty exception list. diff --git a/dune b/dune new file mode 100644 index 0000000..6ac61e8 --- /dev/null +++ b/dune @@ -0,0 +1 @@ +(dirs :standard \ site) diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 0000000..b4524da --- /dev/null +++ b/lychee.toml @@ -0,0 +1,21 @@ +# External links are network-sensitive and run only in the External links workflow. +verbose = "info" +format = "detailed" +no_progress = true + +threads = 2 +max_concurrency = 4 +host_concurrency = 2 +max_redirects = 5 +max_retries = 2 +timeout = 20 +retry_wait_time = 2 + +scheme = ["https"] +require_https = true +insecure = false +exclude_all_private = true +include_mail = false + +# Exceptions must match exact reviewed URLs and explain why the target cannot be checked. +exclude = [] diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..2ec47dd --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,58 @@ +site_name: Trading Engine +site_description: Deterministic execution, replay contracts, and OCaml API reference +site_url: https://fallblu.github.io/trading-engine/ +repo_url: https://github.com/fallblu/trading-engine +repo_name: fallblu/trading-engine +edit_uri: edit/develop/ +docs_dir: _build/documentation-source +site_dir: site +strict: true +theme: + name: material + features: + - content.code.copy + - navigation.footer + - navigation.sections + - navigation.top + - search.highlight +nav: + - Home: index.md + - Project: + - Architecture: docs/architecture.md + - Execution model: docs/execution-model.md + - Scenario and journal: docs/scenario.md + - Diagnostics: docs/diagnostics.md + - Persistra integration: docs/persistra.md + - Contracts: + - Conformance corpus: contracts/conformance/README.md + - Scenario and journal: + - Current 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 v3: contracts/strategy/v3/README.md + - Historical v2: contracts/strategy/v2/README.md + - Historical v1: contracts/strategy/v1/README.md + - API reference: docs/api-reference.md + - Engineering: + - Continuous integration: docs/continuous-integration.md + - OCaml coverage: docs/coverage.md + - Performance: docs/performance.md + - Reducer property testing: docs/reducer-property-testing.md + - Protocol fuzzing: docs/fuzzing.md + - Documentation platform: docs/documentation-platform.md + - Contributing: CONTRIBUTING.md + - Support: SUPPORT.md + - Changelog: CHANGELOG.md +plugins: + - search +markdown_extensions: + - admonition + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.superfences + - tables + - toc: + permalink: true diff --git a/requirements/docs.in b/requirements/docs.in new file mode 100644 index 0000000..e079232 --- /dev/null +++ b/requirements/docs.in @@ -0,0 +1,2 @@ +mkdocs>=1.6.1,<2 +mkdocs-material>=9.7.6,<10 diff --git a/requirements/docs.lock b/requirements/docs.lock new file mode 100644 index 0000000..1b05185 --- /dev/null +++ b/requirements/docs.lock @@ -0,0 +1,75 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --python-version 3.12 --no-python-downloads requirements/docs.in --output-file requirements/docs.lock +babel==2.18.0 + # via mkdocs-material +backrefs==8.0 + # via mkdocs-material +certifi==2026.7.22 + # via requests +charset-normalizer==3.5.1 + # via requests +click==8.4.2 + # via mkdocs +colorama==0.4.6 + # via mkdocs-material +ghp-import==2.1.0 + # via mkdocs +idna==3.19 + # via requests +jinja2==3.1.6 + # via + # mkdocs + # mkdocs-material +markdown==3.10.3 + # via + # mkdocs + # mkdocs-material + # pymdown-extensions +markupsafe==3.0.3 + # via + # jinja2 + # mkdocs +mergedeep==1.3.4 + # via + # mkdocs + # mkdocs-get-deps +mkdocs==1.6.1 + # via + # -r requirements/docs.in + # mkdocs-material +mkdocs-get-deps==0.2.2 + # via mkdocs +mkdocs-material==9.7.7 + # via -r requirements/docs.in +mkdocs-material-extensions==1.3.1 + # via mkdocs-material +packaging==26.3 + # via mkdocs +paginate==0.5.7 + # via mkdocs-material +pathspec==1.1.1 + # via mkdocs +platformdirs==4.11.3 + # via mkdocs-get-deps +pygments==2.21.0 + # via mkdocs-material +pymdown-extensions==11.0.1 + # via mkdocs-material +python-dateutil==2.9.0.post0 + # via ghp-import +pyyaml==6.0.3 + # via + # mkdocs + # mkdocs-get-deps + # pymdown-extensions + # pyyaml-env-tag +pyyaml-env-tag==1.1 + # via mkdocs +requests==2.34.2 + # via mkdocs-material +six==1.17.0 + # via python-dateutil +urllib3==2.7.0 + # via requests +watchdog==6.0.0 + # via mkdocs diff --git a/scripts/bootstrap-documentation-environment b/scripts/bootstrap-documentation-environment new file mode 100755 index 0000000..7cf8985 --- /dev/null +++ b/scripts/bootstrap-documentation-environment @@ -0,0 +1,36 @@ +#!/bin/sh + +set -eu + +repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +documentation_environment="$repository_root/.venv-docs" +documentation_lock="$repository_root/requirements/docs.lock" + +if [ ! -f "$repository_root/_opam/.opam-switch/switch-config" ]; then + printf '%s\n' "error: run 'make bootstrap' before bootstrapping documentation" >&2 + exit 1 +fi + +printf '%s\n' "Installing locked OCaml documentation dependencies..." +opam install "$repository_root" \ + --deps-only \ + --locked \ + --with-doc \ + --require-checksums \ + --switch "$repository_root" \ + --yes + +if [ ! -x "$documentation_environment/bin/python" ]; then + printf '%s\n' "Creating the repository-local documentation environment..." + uv venv \ + --python "$(command -v python3)" \ + --no-python-downloads \ + "$documentation_environment" +fi + +printf '%s\n' "Installing locked documentation site dependencies..." +uv pip sync \ + --python "$documentation_environment/bin/python" \ + --no-python-downloads \ + --strict \ + "$documentation_lock" diff --git a/scripts/build-documentation-site b/scripts/build-documentation-site new file mode 100755 index 0000000..ae11de0 --- /dev/null +++ b/scripts/build-documentation-site @@ -0,0 +1,35 @@ +#!/bin/sh + +set -eu + +repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +documentation_source="$repository_root/_build/documentation-source" +documentation_site="$repository_root/site" +generated_api="$repository_root/_build/default/_doc/_html" + +rm -rf -- "$documentation_source" "$documentation_site" +mkdir -p "$documentation_source" + +sed 's#(.github/SUPPORT.md)#(SUPPORT.md)#g' \ + "$repository_root/README.md" >"$documentation_source/index.md" +cp "$repository_root/CONTRIBUTING.md" "$documentation_source/CONTRIBUTING.md" +cp "$repository_root/CHANGELOG.md" "$documentation_source/CHANGELOG.md" +sed \ + -e 's#(../README.md)#(index.md)#g' \ + -e 's#(../docs/#(docs/#g' \ + "$repository_root/.github/SUPPORT.md" >"$documentation_source/SUPPORT.md" +cp -R "$repository_root/docs" "$documentation_source/docs" +cp -R "$repository_root/contracts" "$documentation_source/contracts" + +opam exec --switch "$repository_root" -- dune build @doc +cp -R "$generated_api" "$documentation_source/api" + +sed 's##[Browse the generated `Trading_engine` module index](../api/trading_engine/Trading_engine/index.html).#' \ + "$repository_root/docs/api-reference.md" \ + >"$documentation_source/docs/api-reference.md" + +"$repository_root/.venv-docs/bin/mkdocs" build \ + --strict \ + --config-file "$repository_root/mkdocs.yml" +"$repository_root/.venv-docs/bin/python" \ + "$repository_root/scripts/check-documentation.py" site diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py new file mode 100644 index 0000000..f3ef6f0 --- /dev/null +++ b/scripts/check-documentation.py @@ -0,0 +1,201 @@ +"""Validate documentation sources and generated site topology.""" + +from __future__ import annotations + +import filecmp +import re +import sys +from html.parser import HTMLParser +from pathlib import Path +from urllib.parse import unquote, urlsplit + + +REPOSITORY_ROOT = Path(__file__).resolve().parent.parent +LINK = re.compile(r"(? None: + super().__init__() + self.links: list[str] = [] + + def handle_starttag( + self, tag: str, attrs: list[tuple[str, str | None]] + ) -> None: + if tag != "a": + return + for name, value in attrs: + if name == "href" and value is not None: + self.links.append(value) + + +def source_markdown_files(root: Path = REPOSITORY_ROOT) -> tuple[Path, ...]: + """Return every Markdown source that belongs to the published site.""" + fixed = ( + root / "README.md", + root / "CONTRIBUTING.md", + root / "CHANGELOG.md", + root / ".github" / "SUPPORT.md", + ) + discovered = tuple(sorted((root / "docs").rglob("*.md"))) + tuple( + sorted((root / "contracts").rglob("README.md")) + ) + return fixed + discovered + + +def markdown_link_failures(path: Path, root: Path = REPOSITORY_ROOT) -> list[str]: + """Return actionable failures for repository-relative Markdown links.""" + failures: list[str] = [] + for target in LINK.findall(path.read_text(encoding="utf-8")): + parsed = urlsplit(target) + if parsed.scheme == "http": + failures.append(f"{path}: insecure external link {target}") + continue + if parsed.scheme or parsed.netloc or target.startswith("mailto:"): + continue + clean = unquote(parsed.path) + if not clean: + continue + if path == root / "docs" / "api-reference.md" and clean.startswith("../api/"): + continue + resolved = (path.parent / clean).resolve() + if not resolved.is_file(): + failures.append(f"{path}: missing link target {target}") + return failures + + +def source_failures(root: Path = REPOSITORY_ROOT) -> list[str]: + """Validate source topology, navigation, and local links.""" + failures: list[str] = [] + config = (root / "mkdocs.yml").read_text(encoding="utf-8") + for relative in REQUIRED_NAVIGATION: + if relative not in config: + failures.append(f"mkdocs.yml: navigation is missing {relative}") + for path in source_markdown_files(root): + if not path.is_file(): + failures.append(f"missing documentation source: {path}") + continue + failures.extend(markdown_link_failures(path, root)) + api_page = root / "docs" / "api-reference.md" + if "" not in api_page.read_text(encoding="utf-8"): + failures.append("docs/api-reference.md: generated API marker is missing") + return failures + + +def _site_target(site: Path, page: Path, href: str) -> Path | None: + parsed = urlsplit(href) + if parsed.scheme or parsed.netloc or href.startswith(("mailto:", "javascript:")): + return None + clean = unquote(parsed.path) + if not clean: + return None + if clean.startswith("/trading-engine/"): + target = site / clean.removeprefix("/trading-engine/") + elif clean.startswith("/"): + return site / "__invalid_absolute_path__" + else: + target = (page.parent / clean).resolve() + if clean.endswith("/") or not target.suffix: + target /= "index.html" + return target + + +def generated_link_failures(site: Path) -> list[str]: + """Return broken local links from generated HTML pages.""" + failures: list[str] = [] + for page in sorted(site.rglob("*.html")): + parser = _AnchorParser() + parser.feed(page.read_text(encoding="utf-8")) + for href in parser.links: + target = _site_target(site, page, href) + if target is not None and not target.is_file(): + failures.append(f"{page.relative_to(site)}: broken generated link {href}") + return failures + + +def site_failures(root: Path = REPOSITORY_ROOT) -> list[str]: + """Validate generated pages, public modules, and exact contract assets.""" + site = root / "site" + failures: list[str] = [] + required_pages = ( + site / "index.html", + site / "docs" / "architecture" / "index.html", + site / "docs" / "execution-model" / "index.html", + site / "docs" / "scenario" / "index.html", + site / "docs" / "api-reference" / "index.html", + site / "contracts" / "v4" / "index.html", + site / "contracts" / "v3" / "index.html", + site / "contracts" / "v2" / "index.html", + site / "contracts" / "v1" / "index.html", + site / "api" / "trading_engine" / "Trading_engine" / "index.html", + ) + for page in required_pages: + if not page.is_file(): + failures.append(f"generated documentation is missing {page.relative_to(site)}") + + api_root = site / "api" / "trading_engine" / "Trading_engine" + for interface in sorted((root / "lib").glob("*.mli")): + if interface.stem in PUBLIC_MODULE_EXCLUSIONS: + continue + module = interface.stem.capitalize() + page = api_root / module / "index.html" + if not page.is_file(): + failures.append(f"generated API is missing public module {module}") + + for source in sorted((root / "contracts").rglob("*")): + if not source.is_file() or source.suffix not in {".json", ".jsonl"}: + continue + published = site / source.relative_to(root) + if not published.is_file(): + failures.append(f"published contracts are missing {source.relative_to(root)}") + elif not filecmp.cmp(source, published, shallow=False): + failures.append(f"published contract differs from {source.relative_to(root)}") + + failures.extend(generated_link_failures(site)) + return failures + + +def main(argv: list[str]) -> int: + if len(argv) != 2 or argv[1] not in {"source", "site"}: + print("usage: check-documentation.py source|site", file=sys.stderr) + return 2 + failures = source_failures() if argv[1] == "source" else site_failures() + if failures: + print("\n".join(failures), file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/test/dune b/test/dune index b95ed06..04840be 100644 --- a/test/dune +++ b/test/dune @@ -153,6 +153,12 @@ (action (run python3 %{dep:test_development_environment.py}))) +(rule + (alias runtest) + (deps test_documentation.py ../scripts/check-documentation.py) + (action + (run python3 %{dep:test_documentation.py}))) + (rule (alias runtest) (deps diff --git a/test/test_documentation.py b/test/test_documentation.py new file mode 100644 index 0000000..84cc9e4 --- /dev/null +++ b/test/test_documentation.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import importlib.util +import pathlib +import tempfile +import unittest + + +REPOSITORY_ROOT = pathlib.Path(__file__).resolve().parent.parent +MODULE_PATH = REPOSITORY_ROOT / "scripts" / "check-documentation.py" +SPEC = importlib.util.spec_from_file_location("check_documentation", MODULE_PATH) +if SPEC is None or SPEC.loader is None: + raise RuntimeError(f"cannot load {MODULE_PATH}") +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class DocumentationCheckTest(unittest.TestCase): + def test_reports_missing_and_insecure_links(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + page = root / "page.md" + page.write_text( + "[missing](missing.md) [insecure](http://example.com) " + "[secure](https://example.com)\n", + encoding="utf-8", + ) + + self.assertEqual( + MODULE.markdown_link_failures(page, root), + [ + f"{page}: missing link target missing.md", + f"{page}: insecure external link http://example.com", + ], + ) + + def test_resolves_generated_directory_links(self) -> None: + with tempfile.TemporaryDirectory() as directory: + site = pathlib.Path(directory) + page = site / "guide" / "index.html" + target = site / "api" / "index.html" + page.parent.mkdir() + target.parent.mkdir() + page.write_text('API', encoding="utf-8") + target.write_text("API", encoding="utf-8") + + self.assertEqual(MODULE.generated_link_failures(site), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_repository_metadata.py b/test/test_repository_metadata.py index 00a3d7b..fc3f352 100644 --- a/test/test_repository_metadata.py +++ b/test/test_repository_metadata.py @@ -22,7 +22,7 @@ def test_repository_profile_is_specific_and_bounded(self) -> None: "Deterministic event-driven OCaml execution engine with versioned replay contracts " "and causal audit journals", ) - self.assertEqual(profile["homepage"], "https://github.com/fallblu/trading-engine#readme") + self.assertEqual(profile["homepage"], "https://fallblu.github.io/trading-engine/") self.assertEqual( profile["topics"], [ diff --git a/trading_engine.opam b/trading_engine.opam index 392a9f0..218ff84 100644 --- a/trading_engine.opam +++ b/trading_engine.opam @@ -9,7 +9,7 @@ an append-only audit journal. maintainer: "James Mallette " authors: ["James Mallette "] license: "MIT" -homepage: "https://github.com/fallblu/trading-engine" +homepage: "https://fallblu.github.io/trading-engine/" bug-reports: "https://github.com/fallblu/trading-engine/issues" build: [ ["dune" "subst"] {dev} diff --git a/trading_engine.opam.locked b/trading_engine.opam.locked index 71703a4..54dbfb9 100644 --- a/trading_engine.opam.locked +++ b/trading_engine.opam.locked @@ -94,5 +94,5 @@ build: [ ["dune" "build" "-p" name "-j" jobs] ] license: "MIT" -homepage: "https://github.com/fallblu/trading-engine" +homepage: "https://fallblu.github.io/trading-engine/" bug-reports: "https://github.com/fallblu/trading-engine/issues" From 414e28abc2ced4d6b46b2d6d2343687f81fe1d6c Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 02:58:14 -0400 Subject: [PATCH 32/57] build: define reproducible release artifacts --- .github/workflows/release-candidate.yml | 129 ++++++ .gitignore | 1 + CHANGELOG.md | 2 + CONTRIBUTING.md | 4 + Makefile | 8 +- README.md | 1 + docs/release-artifacts.md | 66 ++++ mkdocs.yml | 1 + scripts/build-release-artifacts | 112 ++++++ scripts/check-documentation.py | 1 + scripts/check-release-artifacts | 38 ++ scripts/release_artifacts.py | 499 ++++++++++++++++++++++++ test/dune | 6 + test/test_release_artifacts.py | 87 +++++ 14 files changed, 954 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-candidate.yml create mode 100644 docs/release-artifacts.md create mode 100755 scripts/build-release-artifacts create mode 100755 scripts/check-release-artifacts create mode 100644 scripts/release_artifacts.py create mode 100644 test/test_release_artifacts.py diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml new file mode 100644 index 0000000..5f9a4c8 --- /dev/null +++ b/.github/workflows/release-candidate.yml @@ -0,0 +1,129 @@ +name: Release candidate + +on: + pull_request: + paths: + - .github/workflows/release-candidate.yml + - bin/** + - contracts/** + - docs/** + - lib/** + - requirements/** + - scripts/bootstrap-development-environment + - scripts/bootstrap-documentation-environment + - scripts/build-documentation-site + - scripts/build-release-artifacts + - scripts/check-release-artifacts + - scripts/release_artifacts.py + - CHANGELOG.md + - LICENSE + - Makefile + - README.md + - dune + - dune-project + - mkdocs.yml + - trading_engine.opam + - trading_engine.opam.locked + push: + branches: + - develop + paths: + - .github/workflows/release-candidate.yml + - bin/** + - contracts/** + - docs/** + - lib/** + - requirements/** + - scripts/bootstrap-development-environment + - scripts/bootstrap-documentation-environment + - scripts/build-documentation-site + - scripts/build-release-artifacts + - scripts/check-release-artifacts + - scripts/release_artifacts.py + - CHANGELOG.md + - LICENSE + - Makefile + - README.md + - dune + - dune-project + - mkdocs.yml + - trading_engine.opam + - trading_engine.opam.locked + workflow_dispatch: + inputs: + version: + description: Existing exact tag version without the v prefix + required: true + type: string + +permissions: + contents: read + +concurrency: + group: release-candidate-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + candidate: + name: release-candidate + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + RELEASE_VERSION: ${{ inputs.version || '' }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: Validate manual tag boundary + if: github.event_name == 'workflow_dispatch' + run: test "$GITHUB_REF" = "refs/tags/v$RELEASE_VERSION" + - uses: ocaml/setup-ocaml@605a7e998e76e035b82c14d618a6e1010732c4ce # v3.7.1 + with: + ocaml-compiler: "5.5.0" + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + cache-dependency-glob: requirements/*.lock + python-version: "3.12" + - run: make bootstrap + - run: make release-check VERSION="$RELEASE_VERSION" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-candidate-${{ github.sha }} + path: release + if-no-files-found: error + retention-days: 14 + + attest: + name: release-attestations + if: github.event_name == 'workflow_dispatch' + needs: candidate + runs-on: ubuntu-latest + environment: release + permissions: + artifact-metadata: write + attestations: write + contents: read + id-token: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-candidate-${{ github.sha }} + path: release + - id: provenance + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-checksums: release/SUBJECTS.sha256 + - id: sbom + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-checksums: release/SUBJECTS.sha256 + sbom-path: release/sbom.spdx.json + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-attestations-${{ github.sha }} + path: | + ${{ steps.provenance.outputs.bundle-path }} + ${{ steps.sbom.outputs.bundle-path }} + if-no-files-found: error + retention-days: 14 diff --git a/.gitignore b/.gitignore index bff82e9..940ca49 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ /.venv-schema/ /.venv-docs/ /site/ +/release/ /.direnv/ /.envrc *.install diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a07b03..aac0415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ cells, with safe concurrency cancellation and documented required versus informational gates. - Publish one strict documentation site for architecture, versioned contracts, and generated OCaml APIs, with offline topology checks and bounded external-link validation. +- Define a reproducible release-candidate artifact set with install verification, checksums, SPDX + inventory, SLSA provenance, and a manual tag-only signing boundary. ## 1.0.0 — 2026-08-21 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8bbd88e..89452db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,10 @@ documentation tools in `.venv-docs`, stages versioned contracts without modifyin the public OCaml interfaces, and validates the complete output. See the [documentation platform](docs/documentation-platform.md) for publication and link-checking policy. +Run `make release-check` only from a clean tracked revision to reproduce the complete candidate +artifact set twice and verify its install. This never tags or publishes. See +[release artifacts and provenance](docs/release-artifacts.md) for the human approval boundary. + The gate formats a copy check, builds every target, and runs all tests. Keep commits small, coherent, and working. Use subject-only conventional commit messages such as `feat: implement deterministic order matching`. diff --git a/Makefile b/Makefile index 4822855..b847ff4 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ export PATH := $(CURDIR)/.venv-schema/bin:$(PATH) -.PHONY: bootstrap environment-check build test metadata-check docs-bootstrap docs-source-check docs-check docs-build determinism-check dependency-band-check coverage benchmark-smoke benchmark fuzz-smoke fuzz fmt-check check +.PHONY: bootstrap environment-check build test metadata-check docs-bootstrap docs-source-check docs-check docs-build release-build release-check determinism-check dependency-band-check coverage benchmark-smoke benchmark fuzz-smoke fuzz fmt-check check FUZZ_SEED ?= 20260821 FUZZ_CASES ?= 10000 @@ -32,6 +32,12 @@ docs-check: docs-source-check docs-build: docs-bootstrap docs-check @./scripts/build-documentation-site +release-build: environment-check + @./scripts/build-release-artifacts "$(CURDIR)/release" "$(VERSION)" + +release-check: environment-check + @VERSION="$(VERSION)" ./scripts/check-release-artifacts + determinism-check: build @./scripts/check-deterministic-journals diff --git a/README.md b/README.md index b0fa402..d40d90d 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,7 @@ do not provide reducer snapshots or restart recovery. - [OCaml coverage](docs/coverage.md) - [Continuous integration and portability matrix](docs/continuous-integration.md) - [Documentation platform and generated API](docs/documentation-platform.md) +- [Release artifacts and provenance](docs/release-artifacts.md) - [Performance](docs/performance.md) - [Reducer property testing](docs/reducer-property-testing.md) - [Protocol fuzzing](docs/fuzzing.md) diff --git a/docs/release-artifacts.md b/docs/release-artifacts.md new file mode 100644 index 0000000..ebbb338 --- /dev/null +++ b/docs/release-artifacts.md @@ -0,0 +1,66 @@ +# Release artifacts and provenance + +This repository can build and verify a release candidate, but it does not automatically choose a +version, create or push a tag, publish an opam package, or create a GitHub release. Each of those +actions requires separate human approval. + +## Artifact set + +An approved version produces these distributable subjects: + +| Artifact | Intended contents | +| --- | --- | +| `trading-engine-VERSION-linux-x86_64.tar.gz` | Installed CLI, OCaml library, package metadata, versioned contracts, fixtures, and license documents | +| `trading-engine-VERSION-source.tar.gz` | Every Git-tracked source file at the exact revision | +| `trading-engine-VERSION-contracts.tar.gz` | Conformance data plus every versioned schema, fixture, and contract README | +| `trading-engine-VERSION-documentation.tar.gz` | Offline strict site with project guides, contract pages and assets, and generated OCaml API pages | +| `trading-engine-VERSION.opam` | Exact checked-in opam package definition | + +The candidate also contains `release-manifest.json`, `SUBJECTS.sha256`, `SHA256SUMS`, an SPDX 2.3 +SBOM, and a deterministic in-toto statement with a SLSA v1 provenance predicate. The SBOM covers +the project and every locked OCaml and Python dependency used to build the artifact set. The +provenance binds subject hashes to the Git revision, lockfile hashes, target, version, and source +date epoch. + +The Linux archive is the supported prebuilt target. Other systems install through the source and +opam artifacts until an equally strict native target is added and independently reproduced. + +## Deterministic build + +Run `make release-check` from a clean tracked revision. The check derives `SOURCE_DATE_EPOCH` from +that commit, performs two clean builds, normalizes archive ownership and timestamps, suppresses +gzip timestamps, and compares the complete candidate directories byte for byte. It then validates +archive topology and metadata, every checksum, SPDX structure, provenance subjects, exact opam +bytes and lint result, and the installed CLI's reported version. + +Generated files are written to ignored `release/`. A candidate is disposable evidence; it is not a +release. The `Release candidate` workflow repeats the check on a fresh Ubuntu runner for relevant +pull requests and `develop` changes and retains the candidate for 14 days. + +## Approval, signing, and publication + +The following is a human-controlled release procedure, not an automated promise: + +1. Approve a version change separately, update public version references, and pass all repository + and cross-repository checks. +2. Review the exact release commit, create an approved signed `vVERSION` tag, and push that tag. +3. Configure required reviewers on the `release` GitHub environment. Manually dispatch the + `Release candidate` workflow from the exact tag and enter the matching version. The workflow + rejects branches and mismatched versions. +4. Approve the environment deployment. GitHub OIDC then obtains short-lived Sigstore certificates + and records signed build-provenance and SPDX SBOM attestations for the subjects. No long-lived + signing key is stored in the repository. +5. Download the candidate and attestation bundles. Verify `sha256sum -c SHA256SUMS`, then verify + each distributable with `gh attestation verify ARTIFACT --repo fallblu/trading-engine`. +6. If an additional offline signature is required, a human signer reviews the hashes and runs + `cosign sign-blob --yes --bundle SHA256SUMS.sigstore.json SHA256SUMS`; a second person verifies + the bundle before publication. +7. Create a draft GitHub release with the existing signed tag and the complete candidate set. + Review downloaded assets and attestations again, then explicitly publish the draft. Never + regenerate or replace artifacts under an existing version. +8. Submit the exact opam file and source checksum to `opam-repository` in a separate reviewed pull + request. Documentation remains versioned inside the release artifact even though the latest + project documentation also lives on GitHub Pages. + +The manual workflow uploads only short-lived Actions artifacts and attestations. It does not create +a tag, GitHub release, release commit, package publication, or version bump. diff --git a/mkdocs.yml b/mkdocs.yml index 2ec47dd..f261414 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,6 +42,7 @@ nav: - Reducer property testing: docs/reducer-property-testing.md - Protocol fuzzing: docs/fuzzing.md - Documentation platform: docs/documentation-platform.md + - Release artifacts: docs/release-artifacts.md - Contributing: CONTRIBUTING.md - Support: SUPPORT.md - Changelog: CHANGELOG.md diff --git a/scripts/build-release-artifacts b/scripts/build-release-artifacts new file mode 100755 index 0000000..b105b56 --- /dev/null +++ b/scripts/build-release-artifacts @@ -0,0 +1,112 @@ +#!/bin/sh + +set -eu + +repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +output=${1:-"$repository_root/release"} +requested_version=${2:-} + +fail() { + printf '%s\n' "error: $1" >&2 + exit 1 +} + +case "$output" in + "$repository_root/release" | /tmp/trading-engine-release-check.*/first | /tmp/trading-engine-release-check.*/second) ;; + *) fail "release output must be the repository release directory or checker workspace" ;; +esac + +command -v git >/dev/null 2>&1 || fail "git is required" +command -v gzip >/dev/null 2>&1 || fail "gzip is required" +command -v tar >/dev/null 2>&1 || fail "tar is required" +tar --version | grep -Fq "GNU tar" || fail "GNU tar is required" + +case "$(uname -s)-$(uname -m)" in + Linux-x86_64) target=linux-x86_64 ;; + *) fail "the checked release target is Linux x86_64" ;; +esac + +version=$(sed -n 's/^(version \([^)]*\))$/\1/p' "$repository_root/dune-project") +[ -n "$version" ] || fail "dune-project version is missing" +if [ -n "$requested_version" ] && [ "$requested_version" != "$version" ]; then + fail "requested version $requested_version differs from dune-project $version" +fi +case "$version" in + *[!0-9A-Za-z.+~-]* | "") fail "version contains unsupported characters" ;; +esac + +if [ "${RELEASE_ALLOW_DIRTY:-0}" != 1 ]; then + git -C "$repository_root" diff --quiet || fail "tracked worktree changes are not releasable" + git -C "$repository_root" diff --cached --quiet || fail "staged changes are not releasable" +fi + +revision=$(git -C "$repository_root" rev-parse HEAD) +epoch=${SOURCE_DATE_EPOCH:-$(git -C "$repository_root" show -s --format=%ct HEAD)} +case "$epoch" in + *[!0-9]* | "") fail "SOURCE_DATE_EPOCH must be a nonnegative integer" ;; +esac +export SOURCE_DATE_EPOCH=$epoch + +package="trading-engine-$version" +workspace=$(mktemp -d /tmp/trading-engine-release.XXXXXX) +trap 'rm -rf -- "$workspace"' EXIT HUP INT TERM + +rm -rf -- "$output" +mkdir -p "$output" + +make -C "$repository_root" docs-build +opam exec --switch "$repository_root" -- dune build --root "$repository_root" @install + +install_root="$repository_root/_build/release-install" +rm -rf -- "$install_root" +opam exec --switch "$repository_root" -- dune install \ + --root "$repository_root" \ + --prefix "$install_root/$package" + +contracts_root="$workspace/contracts/$package" +documentation_root="$workspace/documentation/$package" +mkdir -p "$contracts_root" "$documentation_root" +cp -R "$repository_root/contracts" "$contracts_root/contracts" +cp -R "$repository_root/site/." "$documentation_root/" + +normalize_tree() { + find "$1" -exec touch -h -d "@$epoch" {} + +} + +create_archive() { + archive_root=$1 + archive_name=$2 + normalize_tree "$archive_root" + tar \ + --sort=name \ + --format=pax \ + --mtime="@$epoch" \ + --owner=0 \ + --group=0 \ + --numeric-owner \ + --pax-option=delete=atime,delete=ctime \ + -C "$archive_root" \ + -cf - \ + "$package" | gzip -n -9 >"$output/$archive_name" +} + +create_archive "$install_root" "$package-$target.tar.gz" +create_archive "$workspace/contracts" "$package-contracts.tar.gz" +create_archive "$workspace/documentation" "$package-documentation.tar.gz" + +git -C "$repository_root" archive \ + --format=tar \ + --prefix="$package/" \ + HEAD | gzip -n -9 >"$output/$package-source.tar.gz" +cp "$repository_root/trading_engine.opam" "$output/$package.opam" + +python3 "$repository_root/scripts/release_artifacts.py" generate \ + --root "$repository_root" \ + --output "$output" \ + --version "$version" \ + --target "$target" \ + --revision "$revision" \ + --epoch "$epoch" + +printf '%s\n' "Release candidate: $version ($target, $revision)" +find "$output" -maxdepth 1 -type f -printf '%f\n' | sort diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index f3ef6f0..1f8c786 100644 --- a/scripts/check-documentation.py +++ b/scripts/check-documentation.py @@ -39,6 +39,7 @@ "docs/reducer-property-testing.md", "docs/fuzzing.md", "docs/documentation-platform.md", + "docs/release-artifacts.md", "CONTRIBUTING.md", "SUPPORT.md", "CHANGELOG.md", diff --git a/scripts/check-release-artifacts b/scripts/check-release-artifacts new file mode 100755 index 0000000..c47bcc0 --- /dev/null +++ b/scripts/check-release-artifacts @@ -0,0 +1,38 @@ +#!/bin/sh + +set -eu + +repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +version=${VERSION:-} +target=linux-x86_64 +revision=$(git -C "$repository_root" rev-parse HEAD) +epoch=$(git -C "$repository_root" show -s --format=%ct HEAD) +workspace=$(mktemp -d /tmp/trading-engine-release-check.XXXXXX) +trap 'rm -rf -- "$workspace"' EXIT HUP INT TERM + +export SOURCE_DATE_EPOCH=$epoch + +opam lint "$repository_root/trading_engine.opam" +rm -rf -- "$repository_root/_build" +"$repository_root/scripts/build-release-artifacts" "$workspace/first" "$version" +rm -rf -- "$repository_root/_build" +"$repository_root/scripts/build-release-artifacts" "$workspace/second" "$version" + +diff -r --no-dereference "$workspace/first" "$workspace/second" + +if [ -z "$version" ]; then + version=$(sed -n 's/^(version \([^)]*\))$/\1/p' "$repository_root/dune-project") +fi +python3 "$repository_root/scripts/release_artifacts.py" verify \ + --root "$repository_root" \ + --output "$workspace/first" \ + --version "$version" \ + --target "$target" \ + --revision "$revision" \ + --epoch "$epoch" + +release_output="$repository_root/release" +rm -rf -- "$release_output" +mkdir -p "$release_output" +cp -R "$workspace/first/." "$release_output/" +printf '%s\n' "Release artifacts reproduced exactly and installation passed." diff --git a/scripts/release_artifacts.py b/scripts/release_artifacts.py new file mode 100644 index 0000000..880b10e --- /dev/null +++ b/scripts/release_artifacts.py @@ -0,0 +1,499 @@ +"""Generate and verify deterministic release-candidate metadata.""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +import re +import subprocess +import tarfile +import tempfile +from pathlib import Path, PurePosixPath +from urllib.parse import quote + + +OPAM_DEPENDENCY = re.compile( + r'^\s*"(?P[^"]+)"\s+\{=\s+"(?P[^"]+)"' +) +PYTHON_DEPENDENCY = re.compile( + r"^(?P[A-Za-z0-9_.-]+)==(?P[^\s;]+)$" +) + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def artifact_names(version: str, target: str) -> dict[str, str]: + stem = f"trading-engine-{version}" + return { + "binary": f"{stem}-{target}.tar.gz", + "source": f"{stem}-source.tar.gz", + "contracts": f"{stem}-contracts.tar.gz", + "documentation": f"{stem}-documentation.tar.gz", + "opam": f"{stem}.opam", + } + + +def parse_opam_dependencies(path: Path) -> list[tuple[str, str, str]]: + dependencies = [] + for line in path.read_text(encoding="utf-8").splitlines(): + match = OPAM_DEPENDENCY.match(line) + if match: + dependencies.append( + ("opam", match.group("name"), match.group("version")) + ) + return dependencies + + +def parse_python_dependencies(path: Path) -> list[tuple[str, str, str]]: + dependencies = [] + for line in path.read_text(encoding="utf-8").splitlines(): + match = PYTHON_DEPENDENCY.match(line) + if match: + dependencies.append( + ("pypi", match.group("name").lower(), match.group("version")) + ) + return dependencies + + +def dependency_inventory(root: Path) -> list[tuple[str, str, str]]: + dependencies = parse_opam_dependencies(root / "trading_engine.opam.locked") + dependencies.extend(parse_python_dependencies(root / "requirements/docs.lock")) + dependencies.extend(parse_python_dependencies(root / "requirements/schema.lock")) + return sorted(set(dependencies)) + + +def spdx_id(ecosystem: str, name: str, version: str) -> str: + value = re.sub(r"[^A-Za-z0-9.-]", "-", f"{ecosystem}-{name}-{version}") + return f"SPDXRef-Package-{value}" + + +def generate_sbom( + root: Path, + version: str, + target: str, + revision: str, + epoch: int, +) -> dict[str, object]: + project_id = "SPDXRef-Package-trading-engine" + packages: list[dict[str, object]] = [ + { + "SPDXID": project_id, + "name": "trading-engine", + "versionInfo": version, + "downloadLocation": "NOASSERTION", + "filesAnalyzed": False, + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "copyrightText": "NOASSERTION", + "primaryPackagePurpose": "APPLICATION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": f"pkg:opam/trading_engine@{quote(version)}", + } + ], + } + ] + relationships: list[dict[str, str]] = [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": project_id, + } + ] + for ecosystem, name, dependency_version in dependency_inventory(root): + dependency_id = spdx_id(ecosystem, name, dependency_version) + packages.append( + { + "SPDXID": dependency_id, + "name": name, + "versionInfo": dependency_version, + "downloadLocation": "NOASSERTION", + "filesAnalyzed": False, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION", + "primaryPackagePurpose": "LIBRARY", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": ( + f"pkg:{ecosystem}/{quote(name)}@{quote(dependency_version)}" + ), + } + ], + } + ) + if ecosystem == "opam": + relationships.append( + { + "spdxElementId": project_id, + "relationshipType": "DEPENDS_ON", + "relatedSpdxElement": dependency_id, + } + ) + else: + relationships.append( + { + "spdxElementId": dependency_id, + "relationshipType": "BUILD_DEPENDENCY_OF", + "relatedSpdxElement": project_id, + } + ) + + created = dt.datetime.fromtimestamp(epoch, tz=dt.timezone.utc).isoformat() + return { + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": f"trading-engine-{version}-{target}", + "documentNamespace": ( + "https://github.com/fallblu/trading-engine/releases/sbom/" + f"{revision}/{target}" + ), + "creationInfo": { + "created": created.replace("+00:00", "Z"), + "creators": ["Tool: scripts/release_artifacts.py"], + "licenseListVersion": "3.27.0", + }, + "packages": packages, + "relationships": relationships, + } + + +def write_json(path: Path, value: object) -> None: + path.write_text( + json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + + +def checksum_lines(directory: Path, names: list[str]) -> str: + return "".join(f"{sha256(directory / name)} {name}\n" for name in sorted(names)) + + +def generate_metadata( + root: Path, + output: Path, + version: str, + target: str, + revision: str, + epoch: int, +) -> None: + names = artifact_names(version, target) + missing = [name for name in names.values() if not (output / name).is_file()] + if missing: + raise ValueError(f"release subjects are missing: {', '.join(missing)}") + + artifacts = [ + { + "name": names["binary"], + "role": "installable binary and OCaml package", + "contents": ["bin", "lib", "share/trading_engine/contracts", "doc"], + }, + { + "name": names["source"], + "role": "Git source", + "contents": ["tracked repository files at sourceRevision"], + }, + { + "name": names["contracts"], + "role": "versioned contracts", + "contents": ["contract READMEs", "schemas", "fixtures", "conformance"], + }, + { + "name": names["documentation"], + "role": "offline documentation site", + "contents": ["project guides", "versioned contracts", "generated OCaml API"], + }, + { + "name": names["opam"], + "role": "opam package definition", + "contents": ["trading_engine.opam"], + }, + ] + subject_names = sorted(names.values()) + subject_digests = {name: sha256(output / name) for name in subject_names} + manifest = { + "schemaVersion": 1, + "project": "fallblu/trading-engine", + "version": version, + "target": target, + "sourceRevision": revision, + "sourceDateEpoch": epoch, + "artifacts": [ + {**artifact, "sha256": subject_digests[artifact["name"]]} + for artifact in artifacts + ], + } + write_json(output / "release-manifest.json", manifest) + write_json( + output / "sbom.spdx.json", + generate_sbom(root, version, target, revision, epoch), + ) + + statement = { + "_type": "https://in-toto.io/Statement/v1", + "subject": [ + {"name": name, "digest": {"sha256": subject_digests[name]}} + for name in subject_names + ], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": ( + "https://github.com/fallblu/trading-engine/blob/develop/" + "docs/release-artifacts.md#deterministic-build" + ), + "externalParameters": {"version": version, "target": target}, + "internalParameters": {"sourceDateEpoch": epoch}, + "resolvedDependencies": [ + { + "uri": "git+https://github.com/fallblu/trading-engine", + "digest": {"gitCommit": revision}, + }, + *[ + { + "uri": path, + "digest": {"sha256": sha256(root / path)}, + } + for path in ( + "trading_engine.opam.locked", + "requirements/docs.lock", + "requirements/schema.lock", + ) + ], + ], + }, + "runDetails": { + "builder": { + "id": ( + "https://github.com/fallblu/trading-engine/" + ".github/workflows/release-candidate.yml" + ) + } + }, + }, + } + (output / "provenance.intoto.jsonl").write_text( + json.dumps(statement, sort_keys=True, separators=(",", ":")) + "\n", + encoding="utf-8", + ) + (output / "SUBJECTS.sha256").write_text( + checksum_lines(output, subject_names), encoding="utf-8" + ) + checksum_names = subject_names + [ + "SUBJECTS.sha256", + "provenance.intoto.jsonl", + "release-manifest.json", + "sbom.spdx.json", + ] + (output / "SHA256SUMS").write_text( + checksum_lines(output, checksum_names), encoding="utf-8" + ) + + +def parse_checksums(path: Path) -> dict[str, str]: + checksums: dict[str, str] = {} + for line in path.read_text(encoding="utf-8").splitlines(): + digest, separator, name = line.partition(" ") + if not separator or not re.fullmatch(r"[0-9a-f]{64}", digest): + raise ValueError(f"invalid checksum line in {path.name}: {line}") + if name in checksums: + raise ValueError(f"duplicate checksum subject in {path.name}: {name}") + checksums[name] = digest + return checksums + + +def verify_checksums(directory: Path, filename: str) -> dict[str, str]: + checksums = parse_checksums(directory / filename) + for name, expected in checksums.items(): + path = directory / name + if not path.is_file(): + raise ValueError(f"{filename} references missing file: {name}") + actual = sha256(path) + if actual != expected: + raise ValueError(f"{filename} digest mismatch for {name}") + return checksums + + +def verify_archive( + path: Path, prefix: str, required: tuple[str, ...], epoch: int +) -> None: + with tarfile.open(path, "r:gz") as archive: + names = set() + for member in archive.getmembers(): + pure = PurePosixPath(member.name) + if pure.is_absolute() or ".." in pure.parts: + raise ValueError(f"unsafe archive member in {path.name}: {member.name}") + if member.uid != 0 or member.gid != 0 or member.mtime != epoch: + raise ValueError(f"nondeterministic archive metadata in {path.name}: {member.name}") + names.add(member.name.rstrip("/")) + for relative in required: + expected = f"{prefix}/{relative}".rstrip("/") + if expected not in names: + raise ValueError(f"{path.name} is missing {expected}") + + +def verify_release( + root: Path, output: Path, version: str, target: str, revision: str, epoch: int +) -> None: + names = artifact_names(version, target) + expected_files = set(names.values()) | { + "SHA256SUMS", + "SUBJECTS.sha256", + "provenance.intoto.jsonl", + "release-manifest.json", + "sbom.spdx.json", + } + actual_files = {path.name for path in output.iterdir() if path.is_file()} + if actual_files != expected_files: + raise ValueError( + f"release file set differs: expected {sorted(expected_files)}, " + f"found {sorted(actual_files)}" + ) + + subjects = verify_checksums(output, "SUBJECTS.sha256") + if set(subjects) != set(names.values()): + raise ValueError("SUBJECTS.sha256 does not describe the distributable set") + checksums = verify_checksums(output, "SHA256SUMS") + if set(checksums) != expected_files - {"SHA256SUMS"}: + raise ValueError("SHA256SUMS does not describe every release file") + + package = f"trading-engine-{version}" + verify_archive( + output / names["binary"], + package, + ( + "bin/trading-engine", + "lib/trading_engine/opam", + "share/trading_engine/contracts/v4/scenario.schema.json", + "share/trading_engine/contracts/v4/fixtures/demo.scenario.json", + "doc/trading_engine/README.md", + ), + epoch, + ) + verify_archive( + output / names["source"], + package, + ( + "trading_engine.opam", + "contracts/v1/scenario.schema.json", + "contracts/v4/fixtures/demo.scenario.json", + "docs/architecture.md", + ".github/workflows/release-candidate.yml", + ), + epoch, + ) + verify_archive( + output / names["contracts"], + package, + ( + "contracts/conformance/manifest.json", + "contracts/v1/scenario.schema.json", + "contracts/v4/fixtures/demo.scenario.json", + "contracts/strategy/v3/message.schema.json", + ), + epoch, + ) + verify_archive( + output / names["documentation"], + package, + ( + "index.html", + "docs/architecture/index.html", + "contracts/v1/index.html", + "contracts/v4/scenario.schema.json", + "api/trading_engine/Trading_engine/index.html", + ), + epoch, + ) + + if (output / names["opam"]).read_bytes() != (root / "trading_engine.opam").read_bytes(): + raise ValueError("opam artifact differs from trading_engine.opam") + + manifest = json.loads((output / "release-manifest.json").read_text(encoding="utf-8")) + if ( + manifest.get("version") != version + or manifest.get("target") != target + or manifest.get("sourceRevision") != revision + or manifest.get("sourceDateEpoch") != epoch + ): + raise ValueError("release manifest identity differs from the requested build") + + sbom = json.loads((output / "sbom.spdx.json").read_text(encoding="utf-8")) + if sbom.get("spdxVersion") != "SPDX-2.3": + raise ValueError("SBOM is not SPDX 2.3") + if not any( + package_entry.get("name") == "trading-engine" + and package_entry.get("versionInfo") == version + for package_entry in sbom.get("packages", []) + ): + raise ValueError("SBOM does not describe the project package") + + statement = json.loads( + (output / "provenance.intoto.jsonl").read_text(encoding="utf-8") + ) + if ( + statement.get("_type") != "https://in-toto.io/Statement/v1" + or statement.get("predicateType") != "https://slsa.dev/provenance/v1" + ): + raise ValueError("provenance is not an in-toto SLSA v1 statement") + provenance_subjects = { + subject["name"]: subject["digest"]["sha256"] + for subject in statement.get("subject", []) + } + if provenance_subjects != subjects: + raise ValueError("provenance subjects differ from SUBJECTS.sha256") + + with tempfile.TemporaryDirectory() as directory: + destination = Path(directory) + with tarfile.open(output / names["binary"], "r:gz") as archive: + archive.extractall(destination, filter="data") + executable = destination / package / "bin" / "trading-engine" + result = subprocess.run( + [executable, "--capabilities"], + check=True, + capture_output=True, + text=True, + ) + capabilities = json.loads(result.stdout) + if capabilities.get("engine_version") != version: + raise ValueError("installed binary reports a different version") + + +def main() -> None: + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest="command", required=True) + for command in ("generate", "verify"): + child = subparsers.add_parser(command) + child.add_argument("--root", type=Path, required=True) + child.add_argument("--output", type=Path, required=True) + child.add_argument("--version", required=True) + child.add_argument("--target", required=True) + child.add_argument("--revision", required=True) + child.add_argument("--epoch", type=int, required=True) + args = parser.parse_args() + root = args.root.resolve() + output = args.output.resolve() + if args.command == "generate": + generate_metadata( + root, output, args.version, args.target, args.revision, args.epoch + ) + else: + verify_release( + root, output, args.version, args.target, args.revision, args.epoch + ) + + +if __name__ == "__main__": + main() diff --git a/test/dune b/test/dune index 04840be..3f6f228 100644 --- a/test/dune +++ b/test/dune @@ -159,6 +159,12 @@ (action (run python3 %{dep:test_documentation.py}))) +(rule + (alias runtest) + (deps test_release_artifacts.py ../scripts/release_artifacts.py) + (action + (run python3 %{dep:test_release_artifacts.py}))) + (rule (alias runtest) (deps diff --git a/test/test_release_artifacts.py b/test/test_release_artifacts.py new file mode 100644 index 0000000..b098992 --- /dev/null +++ b/test/test_release_artifacts.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import importlib.util +import json +import pathlib +import tempfile +import unittest + + +REPOSITORY_ROOT = pathlib.Path(__file__).resolve().parent.parent +MODULE_PATH = REPOSITORY_ROOT / "scripts" / "release_artifacts.py" +SPEC = importlib.util.spec_from_file_location("release_artifacts", MODULE_PATH) +if SPEC is None or SPEC.loader is None: + raise RuntimeError(f"cannot load {MODULE_PATH}") +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class ReleaseArtifactsTest(unittest.TestCase): + def create_root(self, directory: pathlib.Path) -> pathlib.Path: + root = directory / "repository" + (root / "requirements").mkdir(parents=True) + (root / "trading_engine.opam.locked").write_text( + 'depends: [\n "ocaml" {= "5.5.0"}\n]\n', encoding="utf-8" + ) + (root / "requirements" / "docs.lock").write_text( + "mkdocs==1.6.1\n", encoding="utf-8" + ) + (root / "requirements" / "schema.lock").write_text( + "jsonschema==4.26.0\n", encoding="utf-8" + ) + return root + + def test_generates_deterministic_spdx_and_provenance(self) -> None: + with tempfile.TemporaryDirectory() as directory_name: + directory = pathlib.Path(directory_name) + root = self.create_root(directory) + output = directory / "release" + output.mkdir() + names = MODULE.artifact_names("1.2.3", "linux-x86_64") + for index, name in enumerate(names.values()): + (output / name).write_bytes(f"artifact-{index}".encode()) + + arguments = (root, output, "1.2.3", "linux-x86_64", "a" * 40, 1234) + MODULE.generate_metadata(*arguments) + first = {path.name: path.read_bytes() for path in output.iterdir()} + MODULE.generate_metadata(*arguments) + second = {path.name: path.read_bytes() for path in output.iterdir()} + + self.assertEqual(first, second) + self.assertEqual( + set(MODULE.verify_checksums(output, "SUBJECTS.sha256")), + set(names.values()), + ) + sbom = json.loads((output / "sbom.spdx.json").read_text()) + self.assertEqual(sbom["spdxVersion"], "SPDX-2.3") + self.assertEqual( + {package["name"] for package in sbom["packages"]}, + {"trading-engine", "ocaml", "mkdocs", "jsonschema"}, + ) + provenance = json.loads( + (output / "provenance.intoto.jsonl").read_text() + ) + self.assertEqual( + provenance["predicateType"], "https://slsa.dev/provenance/v1" + ) + + def test_rejects_duplicate_and_mismatched_checksums(self) -> None: + with tempfile.TemporaryDirectory() as directory_name: + directory = pathlib.Path(directory_name) + artifact = directory / "artifact" + artifact.write_text("contents", encoding="utf-8") + digest = MODULE.sha256(artifact) + checksums = directory / "SHA256SUMS" + checksums.write_text( + f"{digest} artifact\n{digest} artifact\n", encoding="utf-8" + ) + with self.assertRaisesRegex(ValueError, "duplicate checksum"): + MODULE.verify_checksums(directory, "SHA256SUMS") + + checksums.write_text(f"{'0' * 64} artifact\n", encoding="utf-8") + with self.assertRaisesRegex(ValueError, "digest mismatch"): + MODULE.verify_checksums(directory, "SHA256SUMS") + + +if __name__ == "__main__": + unittest.main() From 041c9746ca71b25aaf282c0fdd33f3e60b878674 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 03:29:32 -0400 Subject: [PATCH 33/57] ci: establish security baseline --- .github/ISSUE_TEMPLATE/config.yml | 3 ++ .github/SECURITY.md | 52 ++++++++++++++++++++ .github/SUPPORT.md | 3 ++ .github/dependabot.yml | 35 ++++++++++++++ .github/workflows/codeql.yml | 48 +++++++++++++++++++ .github/workflows/dependency-review.yml | 32 +++++++++++++ .github/workflows/docs.yml | 2 + .github/workflows/external-links.yml | 5 +- .github/workflows/release-candidate.yml | 2 + CHANGELOG.md | 2 + CONTRIBUTING.md | 2 + README.md | 2 + docs/security-maintenance.md | 63 +++++++++++++++++++++++++ mkdocs.yml | 2 + scripts/build-documentation-site | 9 +++- scripts/check-documentation.py | 5 ++ 16 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 .github/SECURITY.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 docs/security-maintenance.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 22fc544..4941f9c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,8 @@ blank_issues_enabled: false contact_links: + - name: Security policy and private reporting + url: https://github.com/fallblu/trading-engine/blob/develop/.github/SECURITY.md + about: Review supported versions and report suspected vulnerabilities privately. - name: Support and usage guidance url: https://github.com/fallblu/trading-engine/blob/develop/.github/SUPPORT.md about: Review supported scope, public boundaries, and safe issue-reporting guidance. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..1f7064f --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,52 @@ +# Security policy + +## Supported versions + +Trading Engine provides security fixes for the latest patch release in the current release line. + +| Release line | Supported | +| --- | --- | +| Latest 1.0.x patch | Yes | +| Earlier releases | No | + +The `develop` branch contains unreleased work and is not a supported release. A fix is staged there +or on a hotfix branch according to the repository's release workflow. This table is updated when a +new release line becomes supported. + +## Report a vulnerability privately + +Use [GitHub private vulnerability reporting](https://github.com/fallblu/trading-engine/security/advisories/new) +to report a suspected vulnerability. Do not open a public issue for an undisclosed vulnerability. + +A useful report includes the affected version or commit, security impact, trigger conditions, a +minimal sanitized reproduction, operating-system and dependency versions, and any disclosure +constraints. Do not include credentials, customer data, proprietary strategies, account details, +or licensed market data. Use synthetic inputs or describe the behavior when a safe reproduction +cannot be shared. + +The maintainer aims to acknowledge a report within three business days and provide an initial +assessment within seven business days. Remediation timing depends on severity, exploitability, and +release risk. These targets are goals, not guarantees. Keep the report private while it is being +assessed and fixed. The maintainer and reporter will coordinate public disclosure after a fix or +mitigation is available. The project does not currently offer a bug bounty. + +## Security boundaries + +An external strategy is an arbitrary executable, not a sandboxed plugin. The engine starts it +directly without a shell, but the child inherits the engine process's operating-system identity, +environment, filesystem access, network access, and standard error. Run only trusted strategies or +isolate them with an operating-system account, container, or sandbox that supplies the minimum +environment and permissions. Do not put secrets in strategy arguments, scenarios, or engine logs. + +Committed fixtures must contain only synthetic or redistributable data. Never add provider +credentials, customer account data, proprietary strategies, or licensed market data. Sanitize any +reproduction before sharing it in an issue, pull request, test, journal, or transcript. + +Journals and strategy transcripts can contain market events, orders, positions, diagnostics, and +a bounded prefix of a rejected strategy response. Store them according to the sensitivity of their +inputs and review them before sharing. Artifact hashes and release attestations provide integrity +and provenance; they do not encrypt data, enforce access control, or prove that an artifact is safe +to execute. + +Repository dependency and analysis controls are described in the +[security maintenance guide](../docs/security-maintenance.md). diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index 955e8e7..98f7bac 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -4,6 +4,9 @@ Start with the [README](../README.md) for setup, supported scope, and command ex [architecture](../docs/architecture.md), [scenario contract](../docs/scenario.md), and [Persistra integration guide](../docs/persistra.md) describe the public boundaries in detail. +Report a suspected vulnerability through the private channel in the +[security policy](SECURITY.md), not through a public issue. + Use the structured issue forms for reproducible bugs, feature proposals, versioned contract changes, and cross-repository compatibility failures. Search existing issues first. Include the engine version, relevant contract versions, exact commands, sanitized inputs, and the smallest diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2af0ef7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,35 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "America/New_York" + target-branch: "develop" + open-pull-requests-limit: 5 + groups: + github-actions: + patterns: + - "*" + update-types: + - "minor" + - "patch" + + - package-ecosystem: "pip" + directory: "/requirements" + schedule: + interval: "weekly" + day: "monday" + time: "10:00" + timezone: "America/New_York" + target-branch: "develop" + open-pull-requests-limit: 5 + groups: + python-tooling: + patterns: + - "*" + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..e3f3ba8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,48 @@ +name: CodeQL + +on: + push: + branches: + - develop + - main + pull_request: + branches: + - develop + - main + schedule: + - cron: "17 13 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + analyze: + name: Analyze ${{ matrix.language }} + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: + - actions + - python + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + with: + languages: ${{ matrix.language }} + build-mode: none + queries: security-extended + - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..3dbe194 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,32 @@ +name: Dependency review + +on: + pull_request: + branches: + - develop + - main + +permissions: + contents: read + +concurrency: + group: dependency-review-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + dependency-review: + name: Review dependency changes + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: moderate + fail-on-scopes: runtime, development, unknown + vulnerability-check: true + license-check: false + comment-summary-in-pr: never + show-openssf-scorecard: false diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index afb38b6..8a62cdf 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - .github/workflows/docs.yml + - .github/SECURITY.md - .github/SUPPORT.md - contracts/** - docs/** @@ -27,6 +28,7 @@ on: - develop paths: - .github/workflows/docs.yml + - .github/SECURITY.md - .github/SUPPORT.md - contracts/** - docs/** diff --git a/.github/workflows/external-links.yml b/.github/workflows/external-links.yml index 2b2abc6..e48646a 100644 --- a/.github/workflows/external-links.yml +++ b/.github/workflows/external-links.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - .github/workflows/external-links.yml + - .github/SECURITY.md - .github/SUPPORT.md - contracts/**/*.md - docs/**/*.md @@ -16,6 +17,7 @@ on: - develop paths: - .github/workflows/external-links.yml + - .github/SECURITY.md - .github/SUPPORT.md - contracts/**/*.md - docs/**/*.md @@ -47,7 +49,8 @@ jobs: with: args: >- --config lychee.toml --verbose --no-progress - 'README.md' 'CONTRIBUTING.md' 'CHANGELOG.md' '.github/SUPPORT.md' + 'README.md' 'CONTRIBUTING.md' 'CHANGELOG.md' + '.github/SECURITY.md' '.github/SUPPORT.md' 'docs/**/*.md' 'contracts/**/README.md' checkbox: false fail: true diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 5f9a4c8..86870a7 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - .github/workflows/release-candidate.yml + - .github/SECURITY.md - bin/** - contracts/** - docs/** @@ -29,6 +30,7 @@ on: - develop paths: - .github/workflows/release-candidate.yml + - .github/SECURITY.md - bin/** - contracts/** - docs/** diff --git a/CHANGELOG.md b/CHANGELOG.md index aac0415..47d3fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Establish a security baseline with private reporting guidance, grouped dependency proposals, + dependency review, and CodeQL analysis for workflows and Python tooling. - Add structured issue and pull-request intake, reviewed planning-label and repository metadata, explicit compatibility guarantees, a pinned required Persistra baseline, and a manual nonrequired latest-head signal. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89452db..7aa58fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,8 @@ coherent, and working. Use subject-only conventional commit messages such as `feat: implement deterministic order matching`. Do not add secrets, provider credentials, or customer account data to fixtures or journals. +Report suspected vulnerabilities through the private channel in the +[security policy](.github/SECURITY.md), not through a public issue. ## Intake and planning metadata diff --git a/README.md b/README.md index d40d90d..b8a428b 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,8 @@ do not provide reducer snapshots or restart recovery. ## Architecture and contracts - [Support and issue guidance](.github/SUPPORT.md) +- [Security policy](.github/SECURITY.md) +- [Security maintenance](docs/security-maintenance.md) - [Contributing](CONTRIBUTING.md) - [Architecture](docs/architecture.md) - [Diagnostic contract](docs/diagnostics.md) diff --git a/docs/security-maintenance.md b/docs/security-maintenance.md new file mode 100644 index 0000000..7d9a491 --- /dev/null +++ b/docs/security-maintenance.md @@ -0,0 +1,63 @@ +# Security maintenance + +Trading Engine combines GitHub security settings, bounded dependency proposals, static analysis, +and repository verification. These controls identify changes for a maintainer to assess. They do +not replace the complete gate, threat analysis, or the human-controlled release process. + +Report an undisclosed vulnerability through the private channel in the +[security policy](https://github.com/fallblu/trading-engine/security/policy). + +## Dependency updates + +Dependabot checks GitHub Actions and the Python documentation and schema tools every Monday. Patch +and minor updates are grouped by ecosystem. Major updates remain separate so their compatibility +impact is visible. At most five version-update pull requests per ecosystem remain open at once. + +Version-update pull requests target `develop`. GitHub always targets Dependabot security-update +pull requests at the repository's default branch, which is `main`; `target-branch` cannot change +that behavior. Treat such a pull request as a security warning and hotfix input. Do not merge it +directly as an ordinary feature change. Reproduce the dependency and lockfile change through the +documented hotfix or `develop` integration flow, then use the human-controlled release process. + +Review each Python input and generated lockfile together. Run the complete repository gate after a +change to `requirements/docs.in`, `requirements/docs.lock`, `requirements/schema.in`, or +`requirements/schema.lock`. + +Dependabot does not support opam manifests. Update `trading_engine.opam` and +`trading_engine.opam.locked` together through a reviewed pull request. The exact locked gate and +the required lowest and highest dependency-band cells must pass. The bands resolve the declared +opam bounds independently, so they catch compatibility errors that one lock cannot. + +Dependabot configuration lives on `develop` until the next human release carries it to `main`, +where GitHub reads `.github/dependabot.yml`. Repository vulnerability alerts and automatic +security-fix proposals are enabled independently through GitHub security settings. + +## Static and dependency analysis + +CodeQL analyzes GitHub Actions workflows and Python build tooling on pull requests and pushes to +`develop` and `main`, on a weekly schedule, and when started manually. Both languages use the +`security-extended` query suite and `none` build mode. The workflow checks out source without +credentials and does not build or execute repository code. + +CodeQL does not provide an OCaml extractor. The CodeQL check therefore makes no static-analysis +claim about the reducer, protocol parsers, external process supervisor, or artifact writer. OCaml +assurance comes from compiler warnings, formatting, deterministic tests, schema conformance, +property tests, fixed fuzz corpora, coverage, and dependency-band checks. These are verification +controls, not a substitute for OCaml security review. + +Dependency review runs on pull requests to `develop` and `main`. It rejects newly introduced +dependencies represented in GitHub's dependency graph when they have vulnerabilities of moderate +severity or higher. It does not execute pull-request code. GitHub does not natively resolve the +opam lock for this check, so the opam review and CI gates remain required. + +## Findings and suppressions + +Investigate each CodeQL or dependency-review finding against the affected path and supported +dependency range. Prefer a code fix, dependency update, or constraint change. Record the evidence +and affected versions in the pull request or a linked issue. + +Do not add broad query exclusions or advisory allowlists. A narrow suppression requires maintainer +review, a linked tracking issue, a reason such as confirmed false positive or unreachable test +code, and a condition for removal. Use GitHub's finding dismissal controls for CodeQL so the reason +and reviewer remain auditable. Any future dependency-review advisory exception must name one GHSA +and follow the same review rules. Re-run the affected workflow after a fix or suppression change. diff --git a/mkdocs.yml b/mkdocs.yml index f261414..3e1333a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,6 +23,7 @@ nav: - Scenario and journal: docs/scenario.md - Diagnostics: docs/diagnostics.md - Persistra integration: docs/persistra.md + - Security policy: SECURITY.md - Contracts: - Conformance corpus: contracts/conformance/README.md - Scenario and journal: @@ -43,6 +44,7 @@ nav: - Protocol fuzzing: docs/fuzzing.md - Documentation platform: docs/documentation-platform.md - Release artifacts: docs/release-artifacts.md + - Security maintenance: docs/security-maintenance.md - Contributing: CONTRIBUTING.md - Support: SUPPORT.md - Changelog: CHANGELOG.md diff --git a/scripts/build-documentation-site b/scripts/build-documentation-site index ae11de0..d444b7e 100755 --- a/scripts/build-documentation-site +++ b/scripts/build-documentation-site @@ -10,10 +10,15 @@ generated_api="$repository_root/_build/default/_doc/_html" rm -rf -- "$documentation_source" "$documentation_site" mkdir -p "$documentation_source" -sed 's#(.github/SUPPORT.md)#(SUPPORT.md)#g' \ +sed \ + -e 's#(.github/SUPPORT.md)#(SUPPORT.md)#g' \ + -e 's#(.github/SECURITY.md)#(SECURITY.md)#g' \ "$repository_root/README.md" >"$documentation_source/index.md" -cp "$repository_root/CONTRIBUTING.md" "$documentation_source/CONTRIBUTING.md" +sed 's#(.github/SECURITY.md)#(SECURITY.md)#g' \ + "$repository_root/CONTRIBUTING.md" >"$documentation_source/CONTRIBUTING.md" cp "$repository_root/CHANGELOG.md" "$documentation_source/CHANGELOG.md" +sed 's#(../docs/#(docs/#g' \ + "$repository_root/.github/SECURITY.md" >"$documentation_source/SECURITY.md" sed \ -e 's#(../README.md)#(index.md)#g' \ -e 's#(../docs/#(docs/#g' \ diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index 1f8c786..fc87f8b 100644 --- a/scripts/check-documentation.py +++ b/scripts/check-documentation.py @@ -24,6 +24,7 @@ "docs/scenario.md", "docs/diagnostics.md", "docs/persistra.md", + "SECURITY.md", "contracts/conformance/README.md", "contracts/v4/README.md", "contracts/v3/README.md", @@ -40,6 +41,7 @@ "docs/fuzzing.md", "docs/documentation-platform.md", "docs/release-artifacts.md", + "docs/security-maintenance.md", "CONTRIBUTING.md", "SUPPORT.md", "CHANGELOG.md", @@ -67,6 +69,7 @@ def source_markdown_files(root: Path = REPOSITORY_ROOT) -> tuple[Path, ...]: root / "README.md", root / "CONTRIBUTING.md", root / "CHANGELOG.md", + root / ".github" / "SECURITY.md", root / ".github" / "SUPPORT.md", ) discovered = tuple(sorted((root / "docs").rglob("*.md"))) + tuple( @@ -154,6 +157,8 @@ def site_failures(root: Path = REPOSITORY_ROOT) -> list[str]: site / "docs" / "architecture" / "index.html", site / "docs" / "execution-model" / "index.html", site / "docs" / "scenario" / "index.html", + site / "docs" / "security-maintenance" / "index.html", + site / "SECURITY" / "index.html", site / "docs" / "api-reference" / "index.html", site / "contracts" / "v4" / "index.html", site / "contracts" / "v3" / "index.html", From 5ae5e777fe11e1f58b7e8b198c0c96143debdba8 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 04:13:41 -0400 Subject: [PATCH 34/57] chore: enforce repository governance --- .github/branch-protection.json | 25 +++++++++++++++++++++++++ .github/repository.json | 6 +++++- CHANGELOG.md | 2 ++ CONTRIBUTING.md | 13 +++++++++++++ docs/repository-governance.md | 25 +++++++++++++++++++++++++ mkdocs.yml | 1 + scripts/check-documentation.py | 2 ++ test/test_repository_metadata.py | 29 +++++++++++++++++++++++++++++ 8 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 .github/branch-protection.json create mode 100644 docs/repository-governance.md diff --git a/.github/branch-protection.json b/.github/branch-protection.json new file mode 100644 index 0000000..7c21939 --- /dev/null +++ b/.github/branch-protection.json @@ -0,0 +1,25 @@ +{ + "branches": { + "main": { + "required_status_checks": { + "strict": true, + "contexts": ["check", "persistra-compatibility"] + }, + "enforce_admins": true, + "required_pull_request_reviews": { + "dismiss_stale_reviews": false, + "require_code_owner_reviews": false, + "required_approving_review_count": 0, + "require_last_push_approval": false + }, + "restrictions": null, + "required_conversation_resolution": true, + "required_linear_history": true, + "allow_force_pushes": false, + "allow_deletions": false, + "block_creations": false, + "lock_branch": false, + "allow_fork_syncing": false + } + } +} diff --git a/.github/repository.json b/.github/repository.json index d465d7f..b6b6ac1 100644 --- a/.github/repository.json +++ b/.github/repository.json @@ -10,5 +10,9 @@ "ocaml", "quantitative-finance", "trading-engine" - ] + ], + "allow_merge_commit": false, + "allow_rebase_merge": true, + "allow_squash_merge": false, + "delete_branch_on_merge": true } diff --git a/CHANGELOG.md b/CHANGELOG.md index 47d3fe1..0d12ef6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Protect `main` with required integration checks and a no-bypass review policy, and make rebase + merging the only supported repository merge mode. - Establish a security baseline with private reporting guidance, grouped dependency proposals, dependency review, and CodeQL analysis for workflows and Python tooling. - Add structured issue and pull-request intake, reviewed planning-label and repository metadata, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7aa58fa..151fd04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,6 +53,19 @@ The gate formats a copy check, builds every target, and runs all tests. Keep com coherent, and working. Use subject-only conventional commit messages such as `feat: implement deterministic order matching`. +## Git workflow + +Create feature branches from `develop` and open pull requests back into `develop`. Use +rebase-and-merge so every coherent commit remains visible; do not use squash or merge commits. +GitHub deletes merged head branches automatically, so verify that the branch is gone afterward. + +Promotion to `main` also uses a pull request and rebase merge. The protected branch requires a head +that is current with `main`, resolved review conversations, and successful `check` and +`persistra-compatibility` jobs. It blocks force pushes and branch deletion and applies to +administrators without a bypass. The rule requires no approval while the repository has one +maintainer, avoiding a self-review deadlock. See [Repository governance](docs/repository-governance.md) +for the complete policy. + Do not add secrets, provider credentials, or customer account data to fixtures or journals. Report suspected vulnerabilities through the private channel in the [security policy](.github/SECURITY.md), not through a public issue. diff --git a/docs/repository-governance.md b/docs/repository-governance.md new file mode 100644 index 0000000..1f39577 --- /dev/null +++ b/docs/repository-governance.md @@ -0,0 +1,25 @@ +# Repository governance + +GitHub settings enforce the integration workflow for the release branch. The reviewed source of +truth lives in `.github/branch-protection.json` and `.github/repository.json`. + +## Main branch protection + +Every change to `main` requires a pull request whose head is current with `main`. The pull request +must resolve all review conversations and pass both checks produced by the mainline workflow: + +- `check`, which runs the complete locked repository gate; +- `persistra-compatibility`, which validates the paired cross-repository baseline. + +The protection blocks force pushes and branch deletion, requires linear history, and applies to +administrators without a bypass. Trading Engine currently has one maintainer, so the rule requires +zero approving reviews. Requiring approval would make self-authored changes impossible to land. +Stale-approval dismissal, code-owner review, and last-push approval are disabled. Revisit that +choice when a second regular reviewer is available. + +## Merge behavior + +Rebase merging is the only supported GitHub merge mode. Merge commits and squash merging are +disabled so coherent commits remain individually visible on a linear history. GitHub deletes +merged head branches automatically. These repository-wide merge settings apply to feature pull +requests into `develop` and promotion pull requests into `main`. diff --git a/mkdocs.yml b/mkdocs.yml index 3e1333a..e8d9f5d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -45,6 +45,7 @@ nav: - Documentation platform: docs/documentation-platform.md - Release artifacts: docs/release-artifacts.md - Security maintenance: docs/security-maintenance.md + - Repository governance: docs/repository-governance.md - Contributing: CONTRIBUTING.md - Support: SUPPORT.md - Changelog: CHANGELOG.md diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index fc87f8b..5da208e 100644 --- a/scripts/check-documentation.py +++ b/scripts/check-documentation.py @@ -42,6 +42,7 @@ "docs/documentation-platform.md", "docs/release-artifacts.md", "docs/security-maintenance.md", + "docs/repository-governance.md", "CONTRIBUTING.md", "SUPPORT.md", "CHANGELOG.md", @@ -158,6 +159,7 @@ def site_failures(root: Path = REPOSITORY_ROOT) -> list[str]: site / "docs" / "execution-model" / "index.html", site / "docs" / "scenario" / "index.html", site / "docs" / "security-maintenance" / "index.html", + site / "docs" / "repository-governance" / "index.html", site / "SECURITY" / "index.html", site / "docs" / "api-reference" / "index.html", site / "contracts" / "v4" / "index.html", diff --git a/test/test_repository_metadata.py b/test/test_repository_metadata.py index fc3f352..3b76053 100644 --- a/test/test_repository_metadata.py +++ b/test/test_repository_metadata.py @@ -37,6 +37,35 @@ def test_repository_profile_is_specific_and_bounded(self) -> None: ], ) self.assertEqual(len(profile["topics"]), len(set(profile["topics"]))) + self.assertFalse(profile["allow_merge_commit"]) + self.assertTrue(profile["allow_rebase_merge"]) + self.assertFalse(profile["allow_squash_merge"]) + self.assertTrue(profile["delete_branch_on_merge"]) + + def test_main_branch_protection_matches_integration_policy(self) -> None: + manifest = json.loads((GITHUB / "branch-protection.json").read_text(encoding="utf-8")) + + self.assertEqual(set(manifest["branches"]), {"main"}) + policy = manifest["branches"]["main"] + self.assertEqual( + policy["required_status_checks"], + {"strict": True, "contexts": ["check", "persistra-compatibility"]}, + ) + self.assertEqual( + policy["required_pull_request_reviews"], + { + "dismiss_stale_reviews": False, + "require_code_owner_reviews": False, + "required_approving_review_count": 0, + "require_last_push_approval": False, + }, + ) + self.assertTrue(policy["enforce_admins"]) + self.assertTrue(policy["required_conversation_resolution"]) + self.assertTrue(policy["required_linear_history"]) + self.assertFalse(policy["allow_force_pushes"]) + self.assertFalse(policy["allow_deletions"]) + self.assertIsNone(policy["restrictions"]) def test_label_manifest_covers_stable_planning_dimensions(self) -> None: labels = json.loads((GITHUB / "labels.json").read_text(encoding="utf-8")) From 9abe11afff702a88af98a2e5b6dfff940f1e1695 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 09:58:40 -0400 Subject: [PATCH 35/57] refactor: publish structured diagnostic contract --- contracts/conformance/manifest.json | 9 +++ contracts/diagnostic/v1/README.md | 12 +++ .../diagnostic/v1/diagnostic.schema.json | 78 +++++++++++++++++++ contracts/diagnostic/v1/dune | 8 ++ .../v1/fixtures/strategy-protocol.json | 15 ++++ contracts/strategy/v3/README.md | 5 +- contracts/strategy/v3/transcript.schema.json | 51 +++--------- docs/diagnostics.md | 4 + mkdocs.yml | 2 + test/dune | 15 ++++ test/validate_diagnostic_schema.py | 53 +++++++++++++ test/validate_strategy_schema.py | 20 +++-- 12 files changed, 224 insertions(+), 48 deletions(-) create mode 100644 contracts/diagnostic/v1/README.md create mode 100644 contracts/diagnostic/v1/diagnostic.schema.json create mode 100644 contracts/diagnostic/v1/dune create mode 100644 contracts/diagnostic/v1/fixtures/strategy-protocol.json create mode 100644 test/validate_diagnostic_schema.py diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index 40fe4e4..f906762 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -117,6 +117,15 @@ {"path": "v4/fixtures/fill-clipped.journal.jsonl", "format": "jsonl"} ] }, + { + "name": "diagnostic-v1", + "schema": "diagnostic/v1/diagnostic.schema.json", + "version_field": "diagnostic_version", + "version": "1", + "sources": [ + {"path": "diagnostic/v1/fixtures/strategy-protocol.json", "format": "json"} + ] + }, { "name": "strategy-message-v1", "schema": "strategy/v1/message.schema.json", diff --git a/contracts/diagnostic/v1/README.md b/contracts/diagnostic/v1/README.md new file mode 100644 index 0000000..7c62f8b --- /dev/null +++ b/contracts/diagnostic/v1/README.md @@ -0,0 +1,12 @@ +# Diagnostic contract v1 + +This directory defines the stable JSON emitted on standard error when the CLI uses +`--diagnostic-format json`. Validate each complete document against +[`diagnostic.schema.json`](diagnostic.schema.json). + +The `code` and typed `context` fields are the machine contract. Treat `message`, cause messages, +and human rendering as explanatory text. Unknown context is omitted. Diagnostics never retain an +input record, strategy response, or unrelated payload value. + +Adding a code or optional context field is compatible within version 1. Removing a code, changing +a field type, or changing a code's meaning requires a new diagnostic contract version. diff --git a/contracts/diagnostic/v1/diagnostic.schema.json b/contracts/diagnostic/v1/diagnostic.schema.json new file mode 100644 index 0000000..d420e0a --- /dev/null +++ b/contracts/diagnostic/v1/diagnostic.schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/diagnostic/v1/diagnostic.schema.json", + "title": "Trading Engine diagnostic contract v1", + "description": "One stable machine-readable process or file boundary failure.", + "type": "object", + "additionalProperties": false, + "required": ["diagnostic_version", "code", "phase", "message", "context", "cause"], + "properties": { + "diagnostic_version": { "const": "1" }, + "code": { + "enum": [ + "cli.invalid_arguments", + "input.io", + "scenario.invalid_json", + "scenario.invalid", + "scenario.unsupported_contract", + "scenario_stream.invalid", + "scenario_stream.changed", + "resource.limit", + "replay.failed", + "reducer.failed", + "strategy.invalid_configuration", + "strategy.protocol", + "strategy.timeout", + "strategy.process", + "strategy.exit", + "artifact.exists", + "artifact.io", + "artifact.state" + ] + }, + "phase": { + "enum": ["cli", "input", "validation", "replay", "reducer", "strategy", "artifact"] + }, + "message": { "type": "string", "minLength": 1 }, + "context": { "$ref": "#/$defs/context" }, + "cause": { + "oneOf": [ + { "type": "null" }, + { "$ref": "#/$defs/cause" } + ] + } + }, + "$defs": { + "canonicalSequence": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "context": { + "type": "object", + "additionalProperties": false, + "properties": { + "json_path": { "type": "string", "minLength": 1 }, + "line": { "type": "integer", "minimum": 1 }, + "sequence": { "$ref": "#/$defs/canonicalSequence" }, + "event_id": { "type": "string", "minLength": 1 }, + "order_id": { "type": "string", "minLength": 1 }, + "causation_ids": { + "type": "array", + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + } + } + }, + "cause": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "message"], + "properties": { + "kind": { "type": "string", "minLength": 1 }, + "message": { "type": "string" }, + "operation": { "type": "string", "minLength": 1 }, + "target": { "type": "string" } + } + } + } +} diff --git a/contracts/diagnostic/v1/dune b/contracts/diagnostic/v1/dune new file mode 100644 index 0000000..47b3c59 --- /dev/null +++ b/contracts/diagnostic/v1/dune @@ -0,0 +1,8 @@ +(install + (section share) + (package trading_engine) + (files + (diagnostic.schema.json as contracts/diagnostic/v1/diagnostic.schema.json) + (fixtures/strategy-protocol.json + as + contracts/diagnostic/v1/fixtures/strategy-protocol.json))) diff --git a/contracts/diagnostic/v1/fixtures/strategy-protocol.json b/contracts/diagnostic/v1/fixtures/strategy-protocol.json new file mode 100644 index 0000000..064227b --- /dev/null +++ b/contracts/diagnostic/v1/fixtures/strategy-protocol.json @@ -0,0 +1,15 @@ +{ + "diagnostic_version": "1", + "code": "strategy.protocol", + "phase": "strategy", + "message": "strategy initialization: invalid strategy response JSON", + "context": { + "json_path": "$", + "line": 1, + "sequence": "1", + "event_id": "diagnostic-demo-event-000000000001", + "order_id": "diagnostic-demo-order-000000000001", + "causation_ids": ["diagnostic-demo-event-000000000000"] + }, + "cause": null +} diff --git a/contracts/strategy/v3/README.md b/contracts/strategy/v3/README.md index a831086..ccaad1b 100644 --- a/contracts/strategy/v3/README.md +++ b/contracts/strategy/v3/README.md @@ -32,7 +32,10 @@ External replay requires an empty batch schedule and empty streamed intent batch 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 includes the structured rejection diagnostic and at most the first 256 raw response bytes encoded +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. diff --git a/contracts/strategy/v3/transcript.schema.json b/contracts/strategy/v3/transcript.schema.json index a3cb381..0ecb931 100644 --- a/contracts/strategy/v3/transcript.schema.json +++ b/contracts/strategy/v3/transcript.schema.json @@ -48,48 +48,17 @@ } }, "diagnostic": { - "type": "object", - "additionalProperties": false, - "required": ["diagnostic_version", "code", "phase", "message", "context", "cause"], - "properties": { - "diagnostic_version": { "const": "1" }, - "code": { "enum": ["strategy.protocol", "resource.limit"] }, - "phase": { "const": "strategy" }, - "message": { "type": "string", "minLength": 1 }, - "context": { "$ref": "#/$defs/diagnosticContext" }, - "cause": { - "oneOf": [ - { "type": "null" }, - { "$ref": "#/$defs/diagnosticCause" } - ] - } - } - }, - "diagnosticContext": { - "type": "object", - "additionalProperties": false, - "properties": { - "json_path": { "type": "string" }, - "line": { "type": "integer" }, - "sequence": { "$ref": "#/$defs/canonicalSequence" }, - "event_id": { "type": "string" }, - "order_id": { "type": "string" }, - "causation_ids": { - "type": "array", - "items": { "type": "string" } + "allOf": [ + { + "$ref": "https://github.com/fallblu/trading-engine/contracts/diagnostic/v1/diagnostic.schema.json" + }, + { + "properties": { + "code": { "enum": ["strategy.protocol", "resource.limit"] }, + "phase": { "const": "strategy" } + } } - } - }, - "diagnosticCause": { - "type": "object", - "additionalProperties": false, - "required": ["kind", "message"], - "properties": { - "kind": { "type": "string", "minLength": 1 }, - "message": { "type": "string" }, - "operation": { "type": "string" }, - "target": { "type": "string" } - } + ] }, "evidence": { "type": "object", diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 6a4537c..dad9b59 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -4,6 +4,10 @@ Process and file boundaries return diagnostic contract version `1`. The CLI prin `message` by default. Pass `--diagnostic-format json` to write one machine-readable diagnostic to standard error. The process exits with status 123 for either format. +The versioned [diagnostic JSON Schema](../contracts/diagnostic/v1/diagnostic.schema.json) is the +authoritative structural contract. The adjacent fixture demonstrates every optional context +field. Strategy rejection records reference this schema instead of copying its field definitions. + Every JSON diagnostic contains: | Field | Type | Meaning | diff --git a/mkdocs.yml b/mkdocs.yml index e8d9f5d..8e91701 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,8 @@ nav: - Security policy: SECURITY.md - Contracts: - Conformance corpus: contracts/conformance/README.md + - Diagnostics: + - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - Current v4: contracts/v4/README.md - Transitional v3: contracts/v3/README.md diff --git a/test/dune b/test/dune index 3f6f228..4d5caaf 100644 --- a/test/dune +++ b/test/dune @@ -132,6 +132,7 @@ validate_strategy_schema.py ../contracts/v3/scenario.schema.json ../contracts/v3/journal.schema.json + ../contracts/diagnostic/v1/diagnostic.schema.json ../contracts/strategy/v3/message.schema.json ../contracts/strategy/v3/transcript.schema.json ../contracts/strategy/v3/fixtures/external.strategy.jsonl) @@ -141,10 +142,24 @@ %{dep:validate_strategy_schema.py} %{dep:../contracts/v3/scenario.schema.json} %{dep:../contracts/v3/journal.schema.json} + %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} %{dep:../contracts/strategy/v3/message.schema.json} %{dep:../contracts/strategy/v3/transcript.schema.json} %{dep:../contracts/strategy/v3/fixtures/external.strategy.jsonl}))) +(rule + (alias runtest) + (deps + validate_diagnostic_schema.py + ../contracts/diagnostic/v1/diagnostic.schema.json + ../contracts/diagnostic/v1/fixtures/strategy-protocol.json) + (action + (run + python3 + %{dep:validate_diagnostic_schema.py} + %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} + %{dep:../contracts/diagnostic/v1/fixtures/strategy-protocol.json}))) + (rule (alias runtest) (deps diff --git a/test/validate_diagnostic_schema.py b/test/validate_diagnostic_schema.py new file mode 100644 index 0000000..d3d6dab --- /dev/null +++ b/test/validate_diagnostic_schema.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Validate the stable diagnostic schema and canonical fixture.""" + +from __future__ import annotations + +import copy +import json +import sys +from pathlib import Path + +from jsonschema import Draft202012Validator +from jsonschema.exceptions import ValidationError + + +def expect_invalid(validator: Draft202012Validator, instance: object) -> None: + try: + validator.validate(instance) + except ValidationError: + return + raise AssertionError("invalid diagnostic unexpectedly passed") + + +def main() -> None: + if len(sys.argv) != 3: + raise SystemExit( + "usage: validate_diagnostic_schema.py DIAGNOSTIC_SCHEMA FIXTURE" + ) + schema_path, fixture_path = map(Path, sys.argv[1:]) + schema = json.loads(schema_path.read_text(encoding="utf-8")) + fixture = json.loads(fixture_path.read_text(encoding="utf-8")) + Draft202012Validator.check_schema(schema) + validator = Draft202012Validator(schema) + validator.validate(fixture) + + unknown_code = copy.deepcopy(fixture) + unknown_code["code"] = "strategy.other" + expect_invalid(validator, unknown_code) + prose_sequence = copy.deepcopy(fixture) + prose_sequence["context"]["sequence"] = "01" + expect_invalid(validator, prose_sequence) + duplicate_cause = copy.deepcopy(fixture) + duplicate_cause["context"]["causation_ids"] *= 2 + expect_invalid(validator, duplicate_cause) + exposed_payload = copy.deepcopy(fixture) + exposed_payload["payload"] = {"secret": True} + expect_invalid(validator, exposed_payload) + incomplete_cause = copy.deepcopy(fixture) + incomplete_cause["cause"] = {"kind": "system_error"} + expect_invalid(validator, incomplete_cause) + + +if __name__ == "__main__": + main() diff --git a/test/validate_strategy_schema.py b/test/validate_strategy_schema.py index ffd6848..f419b22 100644 --- a/test/validate_strategy_schema.py +++ b/test/validate_strategy_schema.py @@ -34,15 +34,23 @@ def expect_invalid(validator: Draft202012Validator, instance: object) -> None: def main() -> None: - if len(sys.argv) != 6: + if len(sys.argv) != 7: raise SystemExit( "usage: validate_strategy_schema.py SCENARIO_SCHEMA JOURNAL_SCHEMA " - "MESSAGE_SCHEMA TRANSCRIPT_SCHEMA TRANSCRIPT" + "DIAGNOSTIC_SCHEMA MESSAGE_SCHEMA TRANSCRIPT_SCHEMA TRANSCRIPT" ) - scenario_path, journal_path, message_path, transcript_path, fixture_path = map( - Path, sys.argv[1:] - ) - schemas = [load(path) for path in (scenario_path, journal_path, message_path)] + ( + scenario_path, + journal_path, + diagnostic_path, + message_path, + transcript_path, + fixture_path, + ) = map(Path, sys.argv[1:]) + schemas = [ + load(path) + for path in (scenario_path, journal_path, diagnostic_path, message_path) + ] transcript_schema = load(transcript_path) for schema in [*schemas, transcript_schema]: Draft202012Validator.check_schema(schema) From 4a586dcefce831d309677c3ed8697bec09c3f1ae Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 10:22:13 -0400 Subject: [PATCH 36/57] feat: add explicit venue calendars --- CHANGELOG.md | 2 + README.md | 14 +- bench/benchmark_batch_schedule.py | 2 +- bench/benchmark_replay.py | 26 +- contracts/conformance/cases.json | 44 +-- contracts/conformance/manifest.json | 29 ++ contracts/v5/README.md | 25 ++ contracts/v5/dune | 16 + contracts/v5/fixtures/demo.journal.jsonl | 20 ++ contracts/v5/fixtures/demo.scenario.json | 163 +++++++++ contracts/v5/fixtures/demo.scenario.jsonl | 6 + .../v5/fixtures/fill-clipped.journal.jsonl | 10 + .../v5/fixtures/fill-clipped.scenario.json | 110 ++++++ contracts/v5/journal.schema.json | 177 ++++++++++ contracts/v5/scenario-stream.schema.json | 76 ++++ contracts/v5/scenario.schema.json | 330 ++++++++++++++++++ docs/api-reference.md | 2 +- docs/continuous-integration.md | 2 +- docs/persistra.md | 8 +- docs/scenario.md | 30 +- lib/contract.ml | 7 +- lib/contract.mli | 1 + lib/engine.ml | 2 +- lib/id.ml | 2 + lib/id.mli | 2 + lib/scenario.ml | 110 +++++- lib/scenario.mli | 2 + lib/scenario_shape.ml | 81 +++-- lib/scenario_shape.mli | 6 +- lib/scenario_validation.ml | 41 ++- lib/scenario_validation.mli | 2 + lib/venue_calendar.ml | 140 ++++++++ lib/venue_calendar.mli | 59 ++++ mkdocs.yml | 3 +- scripts/check-deterministic-journals | 12 +- scripts/check-documentation.py | 1 + scripts/release_artifacts.py | 10 +- test/cli.t | 24 +- test/dune | 70 ++-- test/test_engine.ml | 1 + test/test_reducer.ml | 17 +- test/test_scenario.ml | 18 +- test/test_venue_calendar.ml | 166 +++++++++ 43 files changed, 1707 insertions(+), 162 deletions(-) create mode 100644 contracts/v5/README.md create mode 100644 contracts/v5/dune create mode 100644 contracts/v5/fixtures/demo.journal.jsonl create mode 100644 contracts/v5/fixtures/demo.scenario.json create mode 100644 contracts/v5/fixtures/demo.scenario.jsonl create mode 100644 contracts/v5/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v5/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v5/journal.schema.json create mode 100644 contracts/v5/scenario-stream.schema.json create mode 100644 contracts/v5/scenario.schema.json create mode 100644 lib/venue_calendar.ml create mode 100644 lib/venue_calendar.mli create mode 100644 test/test_venue_calendar.ml diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d12ef6..9a120f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add contract v5 venue calendars with explicit venue and calendar identities, regular and + extended trading phases, holidays, early closes, and reducer-independent clock resolution. - Protect `main` with required integration checks and a no-bypass review policy, and make rebase merging the only supported repository merge mode. - Establish a security baseline with private reporting guidance, grouped dependency proposals, diff --git a/README.md b/README.md index b8a428b..364df16 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v4/fixtures/demo.scenario.json \ + --input contracts/v5/fixtures/demo.scenario.json \ --validate-only ``` @@ -92,7 +92,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v4/fixtures/demo.scenario.json \ + --input contracts/v5/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -100,7 +100,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v4/fixtures/demo.scenario.jsonl \ + --input contracts/v5/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -216,12 +216,12 @@ 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 v4 and conformance fixtures](contracts/v4/README.md) +- [Current contract v5 and conformance fixtures](contracts/v5/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v4/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v4/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v4/journal.schema.json) +- [Scenario JSON Schema](contracts/v5/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v5/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v5/journal.schema.json) - [External 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) diff --git a/bench/benchmark_batch_schedule.py b/bench/benchmark_batch_schedule.py index e550ba5..0d30361 100644 --- a/bench/benchmark_batch_schedule.py +++ b/bench/benchmark_batch_schedule.py @@ -16,7 +16,7 @@ ROOT = Path(__file__).resolve().parents[1] DEFAULT_EXECUTABLE = ROOT / "_build/default/bin/main.exe" -FIXTURE = ROOT / "contracts/v4/fixtures/demo.scenario.json" +FIXTURE = ROOT / "contracts/v5/fixtures/demo.scenario.json" def timestamp(value: datetime) -> str: diff --git a/bench/benchmark_replay.py b/bench/benchmark_replay.py index acdda0d..13abca8 100644 --- a/bench/benchmark_replay.py +++ b/bench/benchmark_replay.py @@ -23,7 +23,7 @@ ROOT = Path(__file__).resolve().parents[1] DEFAULT_EXECUTABLE = ROOT / "_build/default/bin/main.exe" DEFAULT_BASELINE = ROOT / "bench/baselines/linux-x86_64.json" -FIXTURE = ROOT / "contracts/v4/fixtures/demo.scenario.json" +FIXTURE = ROOT / "contracts/v5/fixtures/demo.scenario.json" STRATEGY = ROOT / "bench/latency_strategy.py" SUMMARY_PATTERN = re.compile( r"\baudits=(?P[0-9]+).*\bactive=(?P[0-9]+)" @@ -164,6 +164,29 @@ def build_scenario(case: BenchmarkCase) -> dict[str, object]: }, "run_id": f"benchmark-{case.name}", "instruments": instruments, + "venue_calendars": [ + { + "calendar_id": "benchmark-venue-calendar", + "calendar_version": "1", + "venue_id": "BENCHMARK", + "instrument_ids": [ + instrument["instrument_id"] for instrument in instruments + ], + "sessions": [ + { + "session_date": "2026-02-01", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-01T00:00:00Z", + "closes_at": "2026-02-02T00:00:00Z", + } + ], + } + ], + } + ], "risk": { "max_order_quantity": "1000000", "max_long_position": "1000000", @@ -200,6 +223,7 @@ def stream_records(document: dict[str, object]) -> list[dict[str, object]]: "base_currency", "initial_cash", "instruments", + "venue_calendars", "risk", "execution", "max_internal_events", diff --git a/contracts/conformance/cases.json b/contracts/conformance/cases.json index bfc9fbd..022a2eb 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -2,10 +2,10 @@ "format_version": "1", "cases": [ { - "name": "scenario-v4-valid", - "artifact": "scenario-v4", + "name": "scenario-v5-valid", + "artifact": "scenario-v5", "kind": "scenario", - "source": "v4/fixtures/demo.scenario.json", + "source": "v5/fixtures/demo.scenario.json", "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", @@ -23,9 +23,9 @@ }, { "name": "scenario-missing-version", - "artifact": "scenario-v4", + "artifact": "scenario-v5", "kind": "scenario", - "source": "v4/fixtures/demo.scenario.json", + "source": "v5/fixtures/demo.scenario.json", "mutations": [{"op": "remove", "path": ["contract_version"]}], "schema_expectation": "reject", "runtime_expectation": "reject", @@ -33,9 +33,9 @@ }, { "name": "scenario-unknown-field", - "artifact": "scenario-v4", + "artifact": "scenario-v5", "kind": "scenario", - "source": "v4/fixtures/demo.scenario.json", + "source": "v5/fixtures/demo.scenario.json", "mutations": [{"op": "add", "path": ["unexpected_contract_field"], "value": true}], "schema_expectation": "reject", "runtime_expectation": "reject", @@ -43,9 +43,9 @@ }, { "name": "scenario-invalid-scalar-type", - "artifact": "scenario-v4", + "artifact": "scenario-v5", "kind": "scenario", - "source": "v4/fixtures/demo.scenario.json", + "source": "v5/fixtures/demo.scenario.json", "mutations": [{"op": "replace", "path": ["initial_cash", 0, "amount"], "value": 10000}], "schema_expectation": "reject", "runtime_expectation": "reject", @@ -53,9 +53,9 @@ }, { "name": "scenario-duplicate-instrument", - "artifact": "scenario-v4", + "artifact": "scenario-v5", "kind": "scenario", - "source": "v4/fixtures/demo.scenario.json", + "source": "v5/fixtures/demo.scenario.json", "mutations": [{"op": "append_copy", "path": ["instruments"], "index": 0}], "schema_expectation": "accept", "runtime_expectation": "reject", @@ -63,19 +63,19 @@ }, { "name": "scenario-overlapping-slices", - "artifact": "scenario-v4", + "artifact": "scenario-v5", "kind": "scenario", - "source": "v4/fixtures/demo.scenario.json", + "source": "v5/fixtures/demo.scenario.json", "mutations": [{"op": "replace", "path": ["slices", 1, "start_at"], "value": "2026-01-02T20:00:00Z"}], "schema_expectation": "accept", "runtime_expectation": "reject", "rule": "semantic" }, { - "name": "scenario-stream-v4-valid", - "artifact": "scenario-stream-v4", + "name": "scenario-stream-v5-valid", + "artifact": "scenario-stream-v5", "kind": "scenario_stream", - "source": "v4/fixtures/demo.scenario.jsonl", + "source": "v5/fixtures/demo.scenario.jsonl", "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", @@ -93,9 +93,9 @@ }, { "name": "scenario-stream-missing-version", - "artifact": "scenario-stream-v4", + "artifact": "scenario-stream-v5", "kind": "scenario_stream", - "source": "v4/fixtures/demo.scenario.jsonl", + "source": "v5/fixtures/demo.scenario.jsonl", "record": 1, "mutations": [{"op": "remove", "path": ["contract_version"]}], "schema_expectation": "reject", @@ -104,9 +104,9 @@ }, { "name": "scenario-stream-unknown-field", - "artifact": "scenario-stream-v4", + "artifact": "scenario-stream-v5", "kind": "scenario_stream", - "source": "v4/fixtures/demo.scenario.jsonl", + "source": "v5/fixtures/demo.scenario.jsonl", "record": 2, "mutations": [{"op": "add", "path": ["unexpected_contract_field"], "value": true}], "schema_expectation": "reject", @@ -115,9 +115,9 @@ }, { "name": "scenario-stream-out-of-order-sequence", - "artifact": "scenario-stream-v4", + "artifact": "scenario-stream-v5", "kind": "scenario_stream", - "source": "v4/fixtures/demo.scenario.jsonl", + "source": "v5/fixtures/demo.scenario.jsonl", "record": 2, "mutations": [{"op": "replace", "path": ["scenario_sequence"], "value": "3"}], "schema_expectation": "accept", diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index f906762..084369c 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -117,6 +117,35 @@ {"path": "v4/fixtures/fill-clipped.journal.jsonl", "format": "jsonl"} ] }, + { + "name": "scenario-v5", + "schema": "v5/scenario.schema.json", + "version_field": "contract_version", + "version": "5", + "sources": [ + {"path": "v5/fixtures/demo.scenario.json", "format": "json"}, + {"path": "v5/fixtures/fill-clipped.scenario.json", "format": "json"} + ] + }, + { + "name": "scenario-stream-v5", + "schema": "v5/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "5", + "sources": [ + {"path": "v5/fixtures/demo.scenario.jsonl", "format": "jsonl"} + ] + }, + { + "name": "journal-v5", + "schema": "v5/journal.schema.json", + "version_field": "contract_version", + "version": "5", + "sources": [ + {"path": "v5/fixtures/demo.journal.jsonl", "format": "jsonl"}, + {"path": "v5/fixtures/fill-clipped.journal.jsonl", "format": "jsonl"} + ] + }, { "name": "diagnostic-v1", "schema": "diagnostic/v1/diagnostic.schema.json", diff --git a/contracts/v5/README.md b/contracts/v5/README.md new file mode 100644 index 0000000..3f2818a --- /dev/null +++ b/contracts/v5/README.md @@ -0,0 +1,25 @@ +# Trading Engine contract v5 + +This directory is the authoritative v5 process and file contract shared by Trading Engine and its +clients. Versions 4 and 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 5 adds explicit immutable venue-calendar snapshots. Each calendar has stable venue and +calendar identities, a calendar contract version, explicit instrument membership, and ordered +date policies. A date is either a holiday or an open session named as regular or early-close. +Open sessions contain absolute timestamp intervals for configured premarket, opening-auction, +regular, closing-auction, and postmarket phases. + +Calendar producers resolve local civil time, time-zone database versions, daylight-saving rules, +and clock changes before creating a scenario. The reducer receives only absolute instants. Missing +date policies are errors and must never be inferred from weekdays or adjacent sessions. Runtime +validation also rejects duplicate calendar identities, overlapping instrument membership, missing +instrument coverage, unordered or overlapping phases, holidays with phases, and open sessions +without a regular phase. + +Every v5 scenario, stream record, and journal record carries `"contract_version": "5"`. diff --git a/contracts/v5/dune b/contracts/v5/dune new file mode 100644 index 0000000..6e47d5e --- /dev/null +++ b/contracts/v5/dune @@ -0,0 +1,16 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v5/journal.schema.json) + (scenario-stream.schema.json as contracts/v5/scenario-stream.schema.json) + (scenario.schema.json as contracts/v5/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v5/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v5/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl as contracts/v5/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v5/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v5/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v5/fixtures/demo.journal.jsonl b/contracts/v5/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..1418046 --- /dev/null +++ b/contracts/v5/fixtures/demo.journal.jsonl @@ -0,0 +1,20 @@ +{"contract_version":"5","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":"4ec24403fa1f8725edcc399c608ad0bbaca5c17981e32c8e44f7347a4dd65b85","execution_model":"completed_bar_v1"}} +{"contract_version":"5","engine_sequence":"2","event_id":"demo-event-000000000002","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":[]}} +{"contract_version":"5","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.615","reference_price":"104"}]}} +{"contract_version":"5","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} +{"contract_version":"5","engine_sequence":"5","event_id":"demo-event-000000000005","causation_ids":["demo-event-000000000002","demo-event-000000000003"],"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":"9.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","updated_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"5","engine_sequence":"6","event_id":"demo-event-000000000006","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":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"10000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"0","mark":"104","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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"10000","maintenance_excess":"10000","margin_call":false}}} +{"contract_version":"5","engine_sequence":"7","event_id":"demo-event-000000000007","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} +{"contract_version":"5","engine_sequence":"8","event_id":"demo-event-000000000008","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} +{"contract_version":"5","engine_sequence":"9","event_id":"demo-event-000000000009","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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":"9.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","updated_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"5","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000003","demo-event-000000000007"],"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":"3.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000010","updated_event_id":"demo-event-000000000010","created_sequence":"10","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"5","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"642","long_market_value":"642","short_market_value":"0","gross_exposure":"642","cost_basis":"618.868","realized_pnl":"0","unrealized_pnl":"23.132","equity":"10023.132","dividend_pnl":"0","execution_fees":"0.868","borrow_fees":"0","total_fees":"0.868","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"6","mark":"107","fx_rate":"1","market_value":"642","base_market_value":"642","cost_basis":"618.868","base_cost_basis":"618.868","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"23.132","base_unrealized_pnl":"23.132","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0.868","base_execution_fees":"0.868","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0.868","base_total_fees":"0.868"}],"margin":{"initial_requirement":"321","maintenance_requirement":"160.5","initial_excess":"9702.132","maintenance_excess":"9862.632","margin_call":false}}} +{"contract_version":"5","engine_sequence":"12","event_id":"demo-event-000000000012","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":[]}} +{"contract_version":"5","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000010","demo-event-000000000012"],"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":"3.615","price":"107","notional":"386.805","fee":"0.636805","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} +{"contract_version":"5","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000012"],"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":"5","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.115","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000015","updated_event_id":"demo-event-000000000015","created_sequence":"15","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"5","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000012"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"8993.690195","net_market_value":"1009.575","long_market_value":"1009.575","short_market_value":"0","gross_exposure":"1009.575","cost_basis":"1006.309805","realized_pnl":"0","unrealized_pnl":"3.265195","equity":"10003.265195","dividend_pnl":"0","execution_fees":"1.504805","borrow_fees":"0","total_fees":"1.504805","cash_balances":[{"currency":"USD","amount":"8993.690195","fx_rate":"1","base_value":"8993.690195"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"9.615","mark":"105","fx_rate":"1","market_value":"1009.575","base_market_value":"1009.575","cost_basis":"1006.309805","base_cost_basis":"1006.309805","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"3.265195","base_unrealized_pnl":"3.265195","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"1.504805","base_execution_fees":"1.504805","borrow_fees":"0","base_borrow_fees":"0","total_fees":"1.504805","base_total_fees":"1.504805"}],"margin":{"initial_requirement":"504.7875","maintenance_requirement":"252.39375","initial_excess":"9498.477695","maintenance_excess":"9750.871445","margin_call":false}}} +{"contract_version":"5","engine_sequence":"17","event_id":"demo-event-000000000017","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":[]}} +{"contract_version":"5","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"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.115","price":"105","notional":"747.075","fee":"0.997075","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} +{"contract_version":"5","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}}} +{"contract_version":"5","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"4ec24403fa1f8725edcc399c608ad0bbaca5c17981e32c8e44f7347a4dd65b85","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v5/fixtures/demo.scenario.json b/contracts/v5/fixtures/demo.scenario.json new file mode 100644 index 0000000..307a95d --- /dev/null +++ b/contracts/v5/fixtures/demo.scenario.json @@ -0,0 +1,163 @@ +{ + "contract_version": "5", + "metadata": { + "producer": "trading-engine-demo", + "purpose": "deterministic conformance fixture" + }, + "run_id": "demo", + "base_currency": "USD", + "initial_cash": [ + { "currency": "USD", "amount": "10000" } + ], + "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_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_gross_exposure": "1000000", + "max_leverage": "2", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "short_borrow_bps": 100 + }, + "execution": { + "model": "completed_bar_v1", + "participation_bps": 5000, + "fixed_fee": "0.25", + "fee_bps": 10 + }, + "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": [] + }, + { + "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": "12" } + ], + "fx_rates": [{ "currency": "USD", "rate": "1" }], + "corporate_actions": [] + }, + { + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v5/fixtures/demo.scenario.jsonl b/contracts/v5/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..ae4227d --- /dev/null +++ b/contracts/v5/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"5","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"run_id":"demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"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_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_gross_exposure":"1000000","max_leverage":"2","initial_margin_bps":5000,"maintenance_margin_bps":2500,"short_borrow_bps":100},"execution":{"model":"completed_bar_v1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10},"max_internal_events":1000}} +{"contract_version":"5","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"}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"5","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"5","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"}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"5","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"}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"5","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v5/fixtures/fill-clipped.journal.jsonl b/contracts/v5/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..255d5a9 --- /dev/null +++ b/contracts/v5/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,10 @@ +{"contract_version":"5","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":"d734b4ea2364d6fb00fc5366f04035e14c188c30b84672c1d4278bba930eefea","execution_model":"completed_bar_v1"}} +{"contract_version":"5","engine_sequence":"2","event_id":"fill-clipped-event-000000000002","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":[]}} +{"contract_version":"5","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":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000003","updated_event_id":"fill-clipped-event-000000000003","created_sequence":"3","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"5","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false}}} +{"contract_version":"5","engine_sequence":"5","event_id":"fill-clipped-event-000000000005","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":[]}} +{"contract_version":"5","engine_sequence":"6","event_id":"fill-clipped-event-000000000006","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"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":"5","price":"100"}} +{"contract_version":"5","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"fill-clipped-fill-000000000001","order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"5","price":"100","notional":"500","fee":"10","executed_at":"2026-02-03T14:30:00.000000Z","slice_sequence":"2"}} +{"contract_version":"5","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"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","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000003","updated_event_id":"fill-clipped-event-000000000003","created_sequence":"3","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"5","filled_notional":"500","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"5","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}}} +{"contract_version":"5","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000009"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"d734b4ea2364d6fb00fc5366f04035e14c188c30b84672c1d4278bba930eefea","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v5/fixtures/fill-clipped.scenario.json b/contracts/v5/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..41e404b --- /dev/null +++ b/contracts/v5/fixtures/fill-clipped.scenario.json @@ -0,0 +1,110 @@ +{ + "contract_version": "5", + "metadata": { + "producer": "trading-engine", + "purpose": "fill clipping conformance fixture" + }, + "run_id": "fill-clipped", + "base_currency": "USD", + "initial_cash": [ + { "currency": "USD", "amount": "550" } + ], + "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_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "short_borrow_bps": 100 + }, + "execution": { + "model": "completed_bar_v1", + "participation_bps": 10000, + "fixed_fee": "10", + "fee_bps": 0 + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "market", + "limit_price": 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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v5/journal.schema.json b/contracts/v5/journal.schema.json new file mode 100644 index 0000000..ca161ad --- /dev/null +++ b/contracts/v5/journal.schema.json @@ -0,0 +1,177 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v5/journal.schema.json", + "title": "Trading Engine v5 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": "5" }, + "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", "market_slice_received", "target_portfolio_requested", "order_accepted", "order_rejected", "order_cancelled", "split_applied", "cash_dividend_applied", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] + }, + "payload": { "type": "object" } + }, + "allOf": [ + { "if": { "properties": { "event_type": { "const": "run_started" } } }, "then": { "properties": { "payload": { "$ref": "#/$defs/runStarted" } } } }, + { "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"] } } }, "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } + }, + "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" } } } + ] + }, + "marketSlice": { + "type": "object", "additionalProperties": false, + "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], + "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" } } + } + }, + "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", "limit_price", "origin", "created_event_id", "updated_event_id", "created_sequence", "created_at", "eligible_after_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"] }, "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, "origin": { "enum": ["direct", "target_rebalance", "margin_liquidation"] }, "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" }, "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", "margin_call"] } } + }, + "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" } } + }, + "orderAdjusted": { + "type": "object", "additionalProperties": false, "required": ["order", "action_id"], + "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } + }, + "fill": { + "type": "object", "additionalProperties": false, + "required": ["fill_id", "order_id", "instrument_id", "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", "slice_sequence"], + "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/unsignedDecimal" }, "executed_at": { "$ref": "#/$defs/timestamp" }, "slice_sequence": { "$ref": "#/$defs/sequence" } } + }, + "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 } } + }, + "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" } } } + ] + }, + "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" } } + }, + "cashAttribution": { + "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], + "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } + }, + "positionAttribution": { + "type": "object", "additionalProperties": false, + "required": ["instrument_id", "quote_currency", "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"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, "quote_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "base_execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_total_fees": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "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" } } + }, + "valuation": { + "type": "object", "additionalProperties": false, + "required": ["base_currency", "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", "total_fees", "cash_balances", "positions", "margin"], + "properties": { + "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "cash_balances": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashAttribution" } }, "positions": { "type": "array", "items": { "$ref": "#/$defs/positionAttribution" } }, "margin": { "$ref": "#/$defs/margin" } + } + }, + "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": { "const": "completed_bar_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/v5/scenario-stream.schema.json b/contracts/v5/scenario-stream.schema.json new file mode 100644 index 0000000..87d151a --- /dev/null +++ b/contracts/v5/scenario-stream.schema.json @@ -0,0 +1,76 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v5/scenario-stream.schema.json", + "title": "Trading Engine v5 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": "5" }, + "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": "5" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/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": "5" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/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_cash", "instruments", "venue_calendars", "risk", "execution", "max_internal_events"], + "properties": { + "metadata": { "type": "object" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/cashBalance" } }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/execution" }, + "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/v5/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v5/scenario.schema.json b/contracts/v5/scenario.schema.json new file mode 100644 index 0000000..1e934dc --- /dev/null +++ b/contracts/v5/scenario.schema.json @@ -0,0 +1,330 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json", + "title": "Trading Engine v5 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_cash", "instruments", "venue_calendars", "risk", "execution", "max_internal_events", "schedule", "slices"], + "properties": { + "contract_version": { "const": "5" }, + "metadata": { "type": "object" }, + "run_id": { "$ref": "#/$defs/identifier" }, + "base_currency": { "$ref": "#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/cashBalance" } + }, + "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" }, + "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": { + "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/unsignedDecimal" } + } + }, + "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_order_quantity", "max_long_position", "max_short_position", "max_gross_exposure", "max_leverage", "initial_margin_bps", "maintenance_margin_bps", "short_borrow_bps"], + "properties": { + "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, + "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "execution": { + "type": "object", + "additionalProperties": false, + "required": ["model", "participation_bps", "fixed_fee", "fee_bps"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, + "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "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", "limit_price"], + "properties": { + "type": { "const": "submit_order" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "order_kind": { "enum": ["market", "limit"] }, + "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] } + } + }, + "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"], + "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" } } + } + }, + "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" } + } + } + ] + } + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index 525c90e..8544219 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/v4/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v5/README.md) remain authoritative for process boundaries. diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index e9da298..22dacea 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -16,7 +16,7 @@ range declared by the package rather than pretending to be reproducible locks. A required Ubuntu cell means the declared support bounds or the implementation must change. The macOS cell is an early portability signal while Ubuntu remains the supported build platform. -Every runtime cell replays the v3 demo, v4 demo, and v4 risk-limited fill scenarios under `TZ=UTC` +Every runtime cell replays the v3 demo, v5 demo, and v5 risk-limited fill scenarios under `TZ=UTC` and the C locale. It compares the resulting journal files byte for byte with their 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. diff --git a/docs/persistra.md b/docs/persistra.md index 9e77188..fdecb17 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -53,9 +53,9 @@ 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 also advertises current contract v4 while retaining 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. +structural checks. The engine also advertises current contract v5 while retaining v4 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 v3](../contracts/strategy/v3/README.md). Persistra's host turns protocol @@ -78,7 +78,7 @@ 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 v4 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v5 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, diff --git a/docs/scenario.md b/docs/scenario.md index e01f12f..c31a9cc 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 v4 demo](../contracts/v4/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v4/scenario.schema.json) provides structural validation. +Use [the v5 demo](../contracts/v5/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v5/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/v4/scenario-stream.schema.json) validates each line, -and [the v4 stream fixture](../contracts/v4/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v5/scenario-stream.schema.json) validates each line, +and [the v5 stream fixture](../contracts/v5/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,12 +42,13 @@ retains current account, order, target, and latest-bar state required by executi | Field | Meaning | |---|---| -| `contract_version` | Required string identifying this file contract; v4 is `"4"` | +| `contract_version` | Required string identifying this file contract; v5 is `"5"` | | `metadata` | Required arbitrary JSON object preserved for provenance and ignored by execution | | `run_id` | Stable identity used in generated IDs | | `base_currency` | Reporting currency used for aggregate risk and valuation | | `initial_cash` | One explicit nonnegative balance for every scenario currency | | `instruments` | Approved executable-instrument catalog, at most 4,096 entries | +| `venue_calendars` | Immutable venue/session policies covering every configured instrument | | `risk` | Signed position, exposure, leverage, margin, and borrow policy | | `execution` | Capacity and fee configuration | | `max_internal_events` | Positive reducer feedback cap, at most 100,000 | @@ -72,6 +73,21 @@ sizes are positive exact values with at most six decimal places. Quote currencie `base_currency`; `initial_cash` contains every distinct quote currency plus the base currency exactly once. +## Venue calendars + +Contract v5 requires every instrument to belong to exactly one explicit venue calendar. A calendar +is identified by `venue_id`, `calendar_id`, and `calendar_version`; version 1 is the only supported +calendar payload. Its `sessions` are unique and ordered by `session_date`, and each date declares +one policy: `regular`, `early_close`, or `holiday`. Holidays have no phases. Open sessions must +contain a `regular` phase and may also contain `premarket`, `opening_auction`, `closing_auction`, +and `postmarket` phases in market order. Phase intervals cannot overlap. + +All phase boundaries are absolute RFC 3339 timestamps. Scenario producers, not the reducer, resolve +venue-local civil times, time-zone database versions, daylight-saving changes, and clock effects. +Calendar lookup rejects a date without an explicit policy; it never infers weekends, holidays, or +hours from adjacent entries. This makes future DAY expiry, auction eligibility, settlement, and +daily-bar publication policies depend on versioned input rather than ambient system state. + Risk contains positive `max_order_quantity`, `max_long_position`, `max_short_position`, `max_gross_exposure`, and `max_leverage` values, initial and maintenance margin basis points, and annualized `short_borrow_bps`. Initial margin cannot be below maintenance margin, and each @@ -80,7 +96,7 @@ exposure must satisfy every applicable limit; exposure-reducing orders remain ad Execution contains: -- `model`, the compiled execution module selected by contract name; v4 supports +- `model`, the compiled execution module selected by contract name; v5 supports `completed_bar_v1` - `participation_bps`, from 0 through 10,000 - `fixed_fee`, a nonnegative money string @@ -178,7 +194,7 @@ than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v4/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v5/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/contract.ml b/lib/contract.ml index d943247..35530f1 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,6 +1,7 @@ -let version = "4" -let previous_version = "3" -let supported_versions = [ version; previous_version ] +let version = "5" +let previous_version = "4" +let legacy_journal_version = "3" +let supported_versions = [ version; previous_version; legacy_journal_version ] let is_supported version = List.mem version supported_versions let strategy_protocol_version = "3" let engine_version = "1.0.0" diff --git a/lib/contract.mli b/lib/contract.mli index 4565459..fe9b1b3 100644 --- a/lib/contract.mli +++ b/lib/contract.mli @@ -2,6 +2,7 @@ val version : string val previous_version : string +val legacy_journal_version : string val supported_versions : string list val is_supported : string -> bool val strategy_protocol_version : string diff --git a/lib/engine.ml b/lib/engine.ml index 3b8c12d..6e512de 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -1079,7 +1079,7 @@ module Interactive = struct | Some limit -> if String.equal reduction.state.config.contract_version - Contract.previous_version + Contract.legacy_journal_version then emit reduction (Audit.Margin_limited diff --git a/lib/id.ml b/lib/id.ml index 5248232..2f1c652 100644 --- a/lib/id.ml +++ b/lib/id.ml @@ -48,3 +48,5 @@ module Fill = Make () module Strategy = Make () module Event = Make () module Corporate_action = Make () +module Venue = Make () +module Venue_calendar = Make () diff --git a/lib/id.mli b/lib/id.mli index 67f0f05..f8c76b6 100644 --- a/lib/id.mli +++ b/lib/id.mli @@ -21,3 +21,5 @@ module Fill : S module Strategy : S module Event : S module Corporate_action : S +module Venue : S +module Venue_calendar : S diff --git a/lib/scenario.ml b/lib/scenario.ml index 4ab64de..90ccdfc 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -5,6 +5,7 @@ type t = { base_currency : string; initial_cash : (string * Scalar.Money.t) list; instruments : Instrument.t list; + venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; @@ -20,6 +21,7 @@ type stream_header = { base_currency : string; initial_cash : (string * Scalar.Money.t) list; instruments : Instrument.t list; + venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; @@ -212,6 +214,70 @@ let parse_timestamp ~name json = let* value = string ~name json in Codec.ptime_of_string value +let parse_venue_phase json = + let* fields = + object_fields ~name:"venue phase" + ~expected:[ "phase"; "opens_at"; "closes_at" ] + json + in + let* kind_json = field fields "phase" in + let* kind_name = string ~name:"venue phase" kind_json in + let* kind = Venue_calendar.phase_kind_of_string kind_name in + let* opens_json = field fields "opens_at" in + let* opens_at = parse_timestamp ~name:"venue phase opens_at" opens_json in + let* closes_json = field fields "closes_at" in + let* closes_at = parse_timestamp ~name:"venue phase closes_at" closes_json in + Venue_calendar.create_phase ~kind ~opens_at ~closes_at + +let parse_venue_session json = + let* fields = + object_fields ~name:"venue session policy" + ~expected:[ "session_date"; "policy"; "phases" ] + json + in + let* date_json = field fields "session_date" in + let* session_date = string ~name:"session_date" date_json in + let* policy_json = field fields "policy" in + let* policy_name = string ~name:"session policy" policy_json in + let* kind = Venue_calendar.session_kind_of_string policy_name in + let* phases_json = field fields "phases" in + let* phases_json = list ~name:"venue phases" phases_json in + let* phases = map_list parse_venue_phase phases_json in + Venue_calendar.create_session ~session_date ~kind ~phases + +let parse_venue_calendar json = + let* fields = + object_fields ~name:"venue calendar" + ~expected: + [ + "calendar_id"; + "calendar_version"; + "venue_id"; + "instrument_ids"; + "sessions"; + ] + json + in + let* id_json = field fields "calendar_id" in + let* id = parse_id Id.Venue_calendar.of_string ~name:"calendar_id" id_json in + let* version_json = field fields "calendar_version" in + let* version = string ~name:"calendar_version" version_json in + let* venue_json = field fields "venue_id" in + let* venue_id = parse_id Id.Venue.of_string ~name:"venue_id" venue_json in + let* instruments_json = field fields "instrument_ids" in + let* instruments_json = + list ~name:"calendar instrument_ids" instruments_json + in + let* instrument_ids = + map_list + (parse_id Id.Instrument.of_string ~name:"calendar instrument_id") + instruments_json + in + let* sessions_json = field fields "sessions" in + let* sessions_json = list ~name:"venue sessions" sessions_json in + let* sessions = map_list parse_venue_session sessions_json in + Venue_calendar.create ~id ~version ~venue_id ~instrument_ids ~sessions + let rec validate_metadata = function | `Assoc fields -> let names = List.map fst fields in @@ -629,13 +695,25 @@ let construct_header ~root ~contract_path ~contract_version let* instruments = map_list_at (child root "instruments") parse_instrument instruments_json in + let* venue_calendars = + match shape.venue_calendars with + | None -> Ok [] + | Some calendars_json -> + let* calendars_json = + list ~name:"venue_calendars" calendars_json + |> at (child root "venue_calendars") + in + map_list_at + (child root "venue_calendars") + parse_venue_calendar calendars_json + in let* max_internal_events = integer ~name:"max_internal_events" shape.max_internal_events |> at (child root "max_internal_events") in let* currencies, catalog = - Scenario_validation.header ~root ~base_currency ~initial_cash ~instruments - ~max_internal_events + Scenario_validation.header ~root ~contract_version ~base_currency + ~initial_cash ~instruments ~venue_calendars ~max_internal_events in let* risk = parse_risk base_currency instruments shape.risk |> at (child root "risk") @@ -651,6 +729,7 @@ let construct_header ~root ~contract_path ~contract_version base_currency; initial_cash; instruments; + venue_calendars; risk; execution_model; execution; @@ -687,6 +766,7 @@ let construct_batch (shape : Scenario_shape.batch) = base_currency = header.base_currency; initial_cash = header.initial_cash; instruments = header.instruments; + venue_calendars = header.venue_calendars; risk = header.risk; execution_model = header.execution_model; execution = header.execution; @@ -700,15 +780,24 @@ let diagnostic code (error : Scenario_shape.error) = error.message let of_yojson json = - let code = + let supplied_version = match json with - | `Assoc fields -> ( - match List.assoc_opt "contract_version" fields with - | Some (`String supplied) when not (Contract.is_supported supplied) -> - Diagnostic.Scenario_unsupported_contract - | _ -> Diagnostic.Scenario_invalid) - | _ -> Diagnostic.Scenario_invalid + | `Assoc fields -> List.assoc_opt "contract_version" fields + | _ -> None in + let* () = + match supplied_version with + | Some (`String supplied) when not (Contract.is_supported supplied) -> + Error + (Diagnostic.make ~code:Diagnostic.Scenario_unsupported_contract + ~phase:Diagnostic.Validation ~json_path:"$.contract_version" + (Printf.sprintf + "unsupported scenario contract_version %S (expected one of %s)" + supplied + (String.concat ", " Contract.supported_versions))) + | _ -> Ok () + in + let code = Diagnostic.Scenario_invalid in let* () = check_batch_limits json in let* shape = Scenario_shape.batch json |> Result.map_error (diagnostic code) @@ -740,7 +829,8 @@ let stream_header_of_yojson ~contract_version json = in let* () = check_stream_header_limits json in let* shape = - Scenario_shape.stream_header json |> Result.map_error (diagnostic code) + Scenario_shape.stream_header ~contract_version json + |> Result.map_error (diagnostic code) in construct_header ~root:"$.payload" ~contract_path:"$.contract_version" ~contract_version shape diff --git a/lib/scenario.mli b/lib/scenario.mli index 95df2db..1fa0f31 100644 --- a/lib/scenario.mli +++ b/lib/scenario.mli @@ -7,6 +7,7 @@ type t = private { base_currency : string; initial_cash : (string * Scalar.Money.t) list; instruments : Instrument.t list; + venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; @@ -22,6 +23,7 @@ type stream_header = private { base_currency : string; initial_cash : (string * Scalar.Money.t) list; instruments : Instrument.t list; + venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index 5890664..7db9bdd 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -6,6 +6,7 @@ type common = { base_currency : Yojson.Safe.t; initial_cash : Yojson.Safe.t; instruments : Yojson.Safe.t; + venue_calendars : Yojson.Safe.t option; risk : Yojson.Safe.t; execution : Yojson.Safe.t; max_internal_events : Yojson.Safe.t; @@ -62,12 +63,17 @@ let field ~root fields name = Error (error ~json_path:(root ^ "." ^ name) ("missing JSON field: " ^ name)) -let common ~root fields = +let common ~root ~contract_version fields = let* metadata = field ~root fields "metadata" in let* run_id = field ~root fields "run_id" in let* base_currency = field ~root fields "base_currency" in let* initial_cash = field ~root fields "initial_cash" in let* instruments = field ~root fields "instruments" in + let venue_calendars = + if String.equal contract_version "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* max_internal_events = field ~root fields "max_internal_events" in @@ -78,6 +84,7 @@ let common ~root fields = base_currency; initial_cash; instruments; + venue_calendars; risk; execution; max_internal_events; @@ -85,48 +92,64 @@ let common ~root fields = let batch json = let root = "$" in + let* preliminary = + match json with + | `Assoc fields -> field ~root fields "contract_version" + | _ -> Error (error ~json_path:root "scenario must be a JSON object") + in + let contract_version = + match preliminary with `String value -> value | _ -> "" + in + let calendar_fields = + if String.equal contract_version "5" then [ "venue_calendars" ] else [] + in let* fields = object_fields ~json_path:root ~name:"scenario" ~expected: - [ - "contract_version"; - "metadata"; - "run_id"; - "base_currency"; - "initial_cash"; - "instruments"; - "risk"; - "execution"; - "max_internal_events"; - "schedule"; - "slices"; - ] + ([ + "contract_version"; + "metadata"; + "run_id"; + "base_currency"; + "initial_cash"; + "instruments"; + "risk"; + "execution"; + "max_internal_events"; + "schedule"; + "slices"; + ] + @ calendar_fields) json in - let* contract_version = field ~root fields "contract_version" in - let* common = common ~root fields in + let* contract_version_json = field ~root fields "contract_version" in + let* common = common ~root ~contract_version fields in let* schedule = field ~root fields "schedule" in let* slices = field ~root fields "slices" in - Ok { contract_version; common; schedule; slices } + Ok { contract_version = contract_version_json; common; schedule; slices } -let stream_header json = +let stream_header ~contract_version json = let root = "$.payload" in + let calendar_fields = + if String.equal contract_version "5" then [ "venue_calendars" ] else [] + in let* fields = object_fields ~json_path:root ~name:"scenario stream header payload" ~expected: - [ - "metadata"; - "run_id"; - "base_currency"; - "initial_cash"; - "instruments"; - "risk"; - "execution"; - "max_internal_events"; - ] + ([ + "metadata"; + "run_id"; + "base_currency"; + "initial_cash"; + "instruments"; + "risk"; + "execution"; + "max_internal_events"; + ] + @ calendar_fields) json in - common ~root fields + common ~root ~contract_version fields let stream_item json = let root = "$.payload" in diff --git a/lib/scenario_shape.mli b/lib/scenario_shape.mli index 01b5554..b750cbc 100644 --- a/lib/scenario_shape.mli +++ b/lib/scenario_shape.mli @@ -8,6 +8,7 @@ type common = { base_currency : Yojson.Safe.t; initial_cash : Yojson.Safe.t; instruments : Yojson.Safe.t; + venue_calendars : Yojson.Safe.t option; risk : Yojson.Safe.t; execution : Yojson.Safe.t; max_internal_events : Yojson.Safe.t; @@ -24,5 +25,8 @@ type stream_item = { market_slice : Yojson.Safe.t; intents : Yojson.Safe.t } val error : json_path:string -> string -> error val batch : Yojson.Safe.t -> (batch, error) result -val stream_header : Yojson.Safe.t -> (common, error) result + +val stream_header : + contract_version:string -> Yojson.Safe.t -> (common, error) result + val stream_item : Yojson.Safe.t -> (stream_item, error) result diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 231f9c0..de0ba22 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -13,8 +13,40 @@ let at json_path result = let child root field = root ^ "." ^ field -let header ~root ~base_currency ~initial_cash ~instruments ~max_internal_events - = +let validate_venue_calendars ~root catalog venue_calendars = + let ids = + List.map (fun calendar -> calendar.Venue_calendar.id) venue_calendars + in + let unique_ids = List.sort_uniq Id.Venue_calendar.compare ids in + if List.length ids <> List.length unique_ids then + fail + ~json_path:(child root "venue_calendars") + "venue calendar IDs must be unique" + else + let coverage, overlap = + List.fold_left + (fun (covered, overlap) calendar -> + let members = calendar.Venue_calendar.instrument_ids in + ( Id.Instrument.Set.union covered members, + overlap + || not + (Id.Instrument.Set.is_empty + (Id.Instrument.Set.inter covered members)) )) + (Id.Instrument.Set.empty, false) + venue_calendars + in + if overlap then + fail + ~json_path:(child root "venue_calendars") + "each instrument must reference exactly one venue calendar" + else if not (Id.Instrument.Set.equal coverage catalog) then + fail + ~json_path:(child root "venue_calendars") + "venue calendars must cover every configured instrument exactly once" + else Ok () + +let header ~root ~contract_version ~base_currency ~initial_cash ~instruments + ~venue_calendars ~max_internal_events = let* () = Account.create ~base_currency ~initial_cash |> Result.map (fun _ -> ()) @@ -31,6 +63,11 @@ let header ~root ~base_currency ~initial_cash ~instruments ~max_internal_events if Id.Instrument.Set.cardinal catalog <> List.length instruments then fail ~json_path:(child root "instruments") "instrument IDs must be unique" else + let* () = + if String.equal contract_version "5" then + validate_venue_calendars ~root catalog venue_calendars + else Ok () + in let currencies = base_currency :: List.map diff --git a/lib/scenario_validation.mli b/lib/scenario_validation.mli index 8b7f3ec..fba700a 100644 --- a/lib/scenario_validation.mli +++ b/lib/scenario_validation.mli @@ -2,9 +2,11 @@ val header : root:string -> + contract_version:string -> base_currency:string -> initial_cash:(string * Scalar.Money.t) list -> instruments:Instrument.t list -> + venue_calendars:Venue_calendar.t list -> max_internal_events:int -> (string list * Id.Instrument.Set.t, Scenario_shape.error) result diff --git a/lib/venue_calendar.ml b/lib/venue_calendar.ml new file mode 100644 index 0000000..4992c3d --- /dev/null +++ b/lib/venue_calendar.ml @@ -0,0 +1,140 @@ +type phase_kind = + | Premarket + | Opening_auction + | Regular + | Closing_auction + | Postmarket + +type phase = { kind : phase_kind; opens_at : Ptime.t; closes_at : Ptime.t } +type session_kind = Regular_session | Early_close | Holiday + +type session = { + session_date : string; + kind : session_kind; + phases : phase list; +} + +type t = { + id : Id.Venue_calendar.t; + version : string; + venue_id : Id.Venue.t; + instrument_ids : Id.Instrument.Set.t; + sessions : session list; +} + +let ( let* ) result function_ = + match result with Ok value -> function_ value | Error _ as error -> error + +let phase_kind_of_string = function + | "premarket" -> Ok Premarket + | "opening_auction" -> Ok Opening_auction + | "regular" -> Ok Regular + | "closing_auction" -> Ok Closing_auction + | "postmarket" -> Ok Postmarket + | value -> Error (Printf.sprintf "unsupported venue phase %S" value) + +let phase_kind_to_string = function + | Premarket -> "premarket" + | Opening_auction -> "opening_auction" + | Regular -> "regular" + | Closing_auction -> "closing_auction" + | Postmarket -> "postmarket" + +let session_kind_of_string = function + | "regular" -> Ok Regular_session + | "early_close" -> Ok Early_close + | "holiday" -> Ok Holiday + | value -> Error (Printf.sprintf "unsupported session policy %S" value) + +let session_kind_to_string = function + | Regular_session -> "regular" + | Early_close -> "early_close" + | Holiday -> "holiday" + +let phase_rank = function + | Premarket -> 0 + | Opening_auction -> 1 + | Regular -> 2 + | Closing_auction -> 3 + | Postmarket -> 4 + +let create_phase ~kind ~opens_at ~closes_at = + if Ptime.compare opens_at closes_at >= 0 then + Error "venue phase opens_at must precede closes_at" + else Ok { kind; opens_at; closes_at } + +let valid_session_date value = + String.length value = 10 + && value.[4] = '-' + && value.[7] = '-' + && + match Ptime.of_rfc3339 (value ^ "T00:00:00Z") with + | Ok _ -> true + | Error _ -> false + +let validate_phases phases = + let rec loop previous_kind previous_close seen_regular = function + | [] -> + if seen_regular then Ok () + else Error "open session must define a regular phase" + | (phase : phase) :: remaining -> + if + Option.exists + (fun kind -> phase_rank phase.kind <= phase_rank kind) + previous_kind + then Error "venue phases must be unique and in market order" + else if + Option.exists + (fun closes_at -> Ptime.compare phase.opens_at closes_at < 0) + previous_close + then Error "venue phases must not overlap" + else + loop (Some phase.kind) (Some phase.closes_at) + (seen_regular || phase.kind = Regular) + remaining + in + loop None None false phases + +let create_session ~session_date ~kind ~phases = + if not (valid_session_date session_date) then + Error "session_date must be a canonical YYYY-MM-DD date" + else + match kind with + | Holiday -> + if phases = [] then Ok { session_date; kind; phases } + else Error "holiday session policy must not define phases" + | Regular_session | Early_close -> + let* () = validate_phases phases in + Ok { session_date; kind; phases } + +let create ~id ~version ~venue_id ~instrument_ids ~sessions = + if not (String.equal version "1") then + Error (Printf.sprintf "unsupported venue calendar version %S" version) + else if instrument_ids = [] then + Error "venue calendar must reference at least one instrument" + else if sessions = [] then + Error "venue calendar must define at least one session policy" + else + let instrument_set = Id.Instrument.Set.of_list instrument_ids in + if Id.Instrument.Set.cardinal instrument_set <> List.length instrument_ids + then Error "venue calendar instrument IDs must be unique" + else + let dates = List.map (fun session -> session.session_date) sessions in + if List.sort_uniq String.compare dates <> dates then + Error "venue calendar sessions must have unique, increasing dates" + else + Ok { id; version; venue_id; instrument_ids = instrument_set; sessions } + +let session_on calendar ~session_date = + match + List.find_opt + (fun session -> String.equal session.session_date session_date) + calendar.sessions + with + | Some session -> Ok session + | None -> + Error + (Printf.sprintf + "venue calendar %s version %s has no explicit policy for %s" + (Id.Venue_calendar.to_string calendar.id) + calendar.version session_date) diff --git a/lib/venue_calendar.mli b/lib/venue_calendar.mli new file mode 100644 index 0000000..9b9e277 --- /dev/null +++ b/lib/venue_calendar.mli @@ -0,0 +1,59 @@ +(** Immutable venue-local session policies resolved outside the reducer. *) + +type phase_kind = + | Premarket + | Opening_auction + | Regular + | Closing_auction + | Postmarket + +type phase = private { + kind : phase_kind; + opens_at : Ptime.t; + closes_at : Ptime.t; +} + +type session_kind = Regular_session | Early_close | Holiday + +type session = private { + session_date : string; + kind : session_kind; + phases : phase list; +} + +type t = private { + id : Id.Venue_calendar.t; + version : string; + venue_id : Id.Venue.t; + instrument_ids : Id.Instrument.Set.t; + sessions : session list; +} + +val phase_kind_of_string : string -> (phase_kind, string) result +val phase_kind_to_string : phase_kind -> string +val session_kind_of_string : string -> (session_kind, string) result +val session_kind_to_string : session_kind -> string + +val create_phase : + kind:phase_kind -> + opens_at:Ptime.t -> + closes_at:Ptime.t -> + (phase, string) result + +val create_session : + session_date:string -> + kind:session_kind -> + phases:phase list -> + (session, string) result + +val create : + id:Id.Venue_calendar.t -> + version:string -> + venue_id:Id.Venue.t -> + instrument_ids:Id.Instrument.t list -> + sessions:session list -> + (t, string) result + +val session_on : t -> session_date:string -> (session, string) result +(** Return the explicit policy for [session_date]. Missing dates are errors and + are never inferred from weekdays, holidays, or neighboring sessions. *) diff --git a/mkdocs.yml b/mkdocs.yml index 8e91701..5755443 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,7 +29,8 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v4: contracts/v4/README.md + - Current 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 diff --git a/scripts/check-deterministic-journals b/scripts/check-deterministic-journals index 551c703..3d15677 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -43,10 +43,10 @@ compare_journal \ contracts/v3/fixtures/demo.scenario.json \ contracts/v3/fixtures/demo.journal.jsonl compare_journal \ - v4-demo \ - contracts/v4/fixtures/demo.scenario.json \ - contracts/v4/fixtures/demo.journal.jsonl + v5-demo \ + contracts/v5/fixtures/demo.scenario.json \ + contracts/v5/fixtures/demo.journal.jsonl compare_journal \ - v4-fill-clipped \ - contracts/v4/fixtures/fill-clipped.scenario.json \ - contracts/v4/fixtures/fill-clipped.journal.jsonl + v5-fill-clipped \ + contracts/v5/fixtures/fill-clipped.scenario.json \ + contracts/v5/fixtures/fill-clipped.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index 5da208e..80fa81b 100644 --- a/scripts/check-documentation.py +++ b/scripts/check-documentation.py @@ -26,6 +26,7 @@ "docs/persistra.md", "SECURITY.md", "contracts/conformance/README.md", + "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", diff --git a/scripts/release_artifacts.py b/scripts/release_artifacts.py index 880b10e..0bc1b7a 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/v4/scenario.schema.json", - "share/trading_engine/contracts/v4/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v5/scenario.schema.json", + "share/trading_engine/contracts/v5/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/v4/fixtures/demo.scenario.json", + "contracts/v5/fixtures/demo.scenario.json", "docs/architecture.md", ".github/workflows/release-candidate.yml", ), @@ -400,7 +400,7 @@ def verify_release( ( "contracts/conformance/manifest.json", "contracts/v1/scenario.schema.json", - "contracts/v4/fixtures/demo.scenario.json", + "contracts/v5/fixtures/demo.scenario.json", "contracts/strategy/v3/message.schema.json", ), epoch, @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v4/scenario.schema.json", + "contracts/v5/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index 8023904..c19a88e 100644 --- a/test/cli.t +++ b/test/cli.t @@ -2,22 +2,22 @@ 1.0.0 $ ../bin/main.exe --capabilities - {"engine_version":"1.0.0","scenario_contract_versions":["4","3"],"journal_contract_versions":["4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"strategy_protocol_versions":["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":["5","4","3"],"journal_contract_versions":["5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"strategy_protocol_versions":["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/v4/fixtures/demo.scenario.json - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=991890e8c1cc839a0c321a6d30b2ba20a8b588d4135310f43a548fbc929e9fcf + $ ../bin/main.exe --validate-only --input ../contracts/v5/fixtures/demo.scenario.json + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=4ec24403fa1f8725edcc399c608ad0bbaca5c17981e32c8e44f7347a4dd65b85 - $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v4/fixtures/demo.scenario.jsonl - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=6afe9bbda482265cfa24c35167150f02eea1a457aa5025143f3556b8046ae91b + $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v5/fixtures/demo.scenario.jsonl + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=793deb0f4bbf6f4192c283534e031da05a8c02071021f523ef6523763bc1904b - $ ../bin/main.exe --input-format jsonl --input ../contracts/v4/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts + $ ../bin/main.exe --input-format jsonl --input ../contracts/v5/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts run=demo audits=20 orders=3 active=0 filled=2 rejected=0 cash=9739.76812 equity=10004.76812 gross=265 realized=1.419136 unrealized=3.348984 fees=2.50188 journal=streamed.journal.jsonl $ wc -l < streamed.journal.jsonl 20 - $ head -n 5 ../contracts/v4/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl + $ head -n 5 ../contracts/v5/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl $ ../bin/main.exe --validate-only --input-format jsonl --input truncated.scenario.jsonl trading-engine: scenario_end must terminate the scenario stream [123] @@ -32,26 +32,26 @@ 1 scenario_stream.invalid validation 6 6 None - $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v4/fixtures/demo.scenario.json > invalid-tick.json + $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v5/fixtures/demo.scenario.json > invalid-tick.json $ ../bin/main.exe --validate-only --input invalid-tick.json trading-engine: market prices and volumes must align with instrument increments [123] - $ ../bin/main.exe --input ../contracts/v4/fixtures/demo.scenario.json + $ ../bin/main.exe --input ../contracts/v5/fixtures/demo.scenario.json trading-engine: --journal is required unless --validate-only is set [123] - $ ../bin/main.exe --validate-only --input ../contracts/v4/fixtures/demo.scenario.json --journal validation.journal.jsonl + $ ../bin/main.exe --validate-only --input ../contracts/v5/fixtures/demo.scenario.json --journal validation.journal.jsonl trading-engine: --journal cannot be used with --validate-only [123] $ test ! -e validation.journal.jsonl - $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v4/fixtures/demo.scenario.json + $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v5/fixtures/demo.scenario.json trading-engine: --durable-artifacts cannot be used with --validate-only [123] - $ ../bin/main.exe --input ../contracts/v4/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 + $ ../bin/main.exe --input ../contracts/v5/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 trading-engine: --strategy-arg, --strategy-timeout, and --strategy-transcript require --strategy-executable [123] $ test ! -e ignored.journal.jsonl diff --git a/test/dune b/test/dune index 4d5caaf..012fa40 100644 --- a/test/dune +++ b/test/dune @@ -12,17 +12,19 @@ test_strategy_protocol test_contract_conformance test_boundary_failures + test_venue_calendar test_scenario test_engine) (deps - ../contracts/v4/fixtures/demo.journal.jsonl + ../contracts/v5/fixtures/demo.journal.jsonl + ../contracts/v5/fixtures/demo.scenario.json + ../contracts/v5/fixtures/demo.scenario.jsonl + ../contracts/v5/fixtures/fill-clipped.journal.jsonl + ../contracts/v5/fixtures/fill-clipped.scenario.json + ../contracts/v5/journal.schema.json + ../contracts/v5/scenario-stream.schema.json + ../contracts/v5/scenario.schema.json ../contracts/v4/fixtures/demo.scenario.json - ../contracts/v4/fixtures/demo.scenario.jsonl - ../contracts/v4/fixtures/fill-clipped.journal.jsonl - ../contracts/v4/fixtures/fill-clipped.scenario.json - ../contracts/v4/journal.schema.json - ../contracts/v4/scenario-stream.schema.json - ../contracts/v4/scenario.schema.json ../contracts/v3/fixtures/demo.journal.jsonl ../contracts/v3/fixtures/demo.scenario.json ../contracts/v3/fixtures/demo.scenario.jsonl @@ -60,50 +62,50 @@ ../contracts/strategy/v3/fixtures/external.scenario.json ../contracts/strategy/v3/fixtures/external.scenario.jsonl ../contracts/strategy/v3/fixtures/external.strategy.jsonl - ../contracts/v4/fixtures/demo.scenario.json - ../contracts/v4/fixtures/demo.scenario.jsonl)) + ../contracts/v5/fixtures/demo.scenario.json + ../contracts/v5/fixtures/demo.scenario.jsonl)) (rule (alias runtest) (deps validate_schemas.py - ../contracts/v4/fixtures/demo.journal.jsonl - ../contracts/v4/fixtures/demo.scenario.json - ../contracts/v4/fixtures/demo.scenario.jsonl - ../contracts/v4/journal.schema.json - ../contracts/v4/scenario-stream.schema.json - ../contracts/v4/scenario.schema.json) + ../contracts/v5/fixtures/demo.journal.jsonl + ../contracts/v5/fixtures/demo.scenario.json + ../contracts/v5/fixtures/demo.scenario.jsonl + ../contracts/v5/journal.schema.json + ../contracts/v5/scenario-stream.schema.json + ../contracts/v5/scenario.schema.json) (action (run python3 %{dep:validate_schemas.py} - %{dep:../contracts/v4/scenario.schema.json} - %{dep:../contracts/v4/scenario-stream.schema.json} - %{dep:../contracts/v4/journal.schema.json} - %{dep:../contracts/v4/fixtures/demo.scenario.json} - %{dep:../contracts/v4/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v4/fixtures/demo.journal.jsonl}))) + %{dep:../contracts/v5/scenario.schema.json} + %{dep:../contracts/v5/scenario-stream.schema.json} + %{dep:../contracts/v5/journal.schema.json} + %{dep:../contracts/v5/fixtures/demo.scenario.json} + %{dep:../contracts/v5/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v5/fixtures/demo.journal.jsonl}))) (rule (alias runtest) (deps validate_schemas.py - ../contracts/v4/fixtures/fill-clipped.journal.jsonl - ../contracts/v4/fixtures/fill-clipped.scenario.json - ../contracts/v4/fixtures/demo.scenario.jsonl - ../contracts/v4/journal.schema.json - ../contracts/v4/scenario-stream.schema.json - ../contracts/v4/scenario.schema.json) + ../contracts/v5/fixtures/fill-clipped.journal.jsonl + ../contracts/v5/fixtures/fill-clipped.scenario.json + ../contracts/v5/fixtures/demo.scenario.jsonl + ../contracts/v5/journal.schema.json + ../contracts/v5/scenario-stream.schema.json + ../contracts/v5/scenario.schema.json) (action (run python3 %{dep:validate_schemas.py} - %{dep:../contracts/v4/scenario.schema.json} - %{dep:../contracts/v4/scenario-stream.schema.json} - %{dep:../contracts/v4/journal.schema.json} - %{dep:../contracts/v4/fixtures/fill-clipped.scenario.json} - %{dep:../contracts/v4/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v4/fixtures/fill-clipped.journal.jsonl}))) + %{dep:../contracts/v5/scenario.schema.json} + %{dep:../contracts/v5/scenario-stream.schema.json} + %{dep:../contracts/v5/journal.schema.json} + %{dep:../contracts/v5/fixtures/fill-clipped.scenario.json} + %{dep:../contracts/v5/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v5/fixtures/fill-clipped.journal.jsonl}))) (rule (alias runtest) @@ -193,6 +195,6 @@ (deps test_benchmark_replay.py ../bench/benchmark_replay.py - ../contracts/v4/fixtures/demo.scenario.json) + ../contracts/v5/fixtures/demo.scenario.json) (action (run python3 %{dep:test_benchmark_replay.py}))) diff --git a/test/test_engine.ml b/test/test_engine.ml index e10d1de..38d6a8c 100644 --- a/test/test_engine.ml +++ b/test/test_engine.ml @@ -11,5 +11,6 @@ let () = ("strategy-protocol", Test_strategy_protocol.tests); ("contract-conformance", Test_contract_conformance.tests); ("boundary-failures", Test_boundary_failures.tests); + ("venue-calendar", Test_venue_calendar.tests); ("scenario", Test_scenario.tests); ] diff --git a/test/test_reducer.ml b/test/test_reducer.ml index 3bb45fa..4532e35 100644 --- a/test/test_reducer.ml +++ b/test/test_reducer.ml @@ -186,10 +186,10 @@ let superseding_target_replaces_retry () = (event_names events))) | _ -> Alcotest.fail "expected one replacement order" -let fill_limit_clips_buy_to_lots () = +let fill_limit_clips_buy_to_lots ?(contract_version = T.Contract.version) () = let constrained = risk ~max_leverage:"1" () in let state = - runner ~initial_cash:"550" ~risk:constrained + runner ~contract_version ~initial_cash:"550" ~risk:constrained ~execution:(execution ~fixed_fee:"10" ()) [ (1L, [ target "10" ]) ] in @@ -215,6 +215,8 @@ let fill_limit_clips_buy_to_lots () = String.equal (T.Audit.event_name audit.T.Audit.event) "fill_clipped") events in + Alcotest.(check string) + "fill-clipped contract version" contract_version limited.contract_version; match limited.event with | T.Audit.Fill_clipped { @@ -234,11 +236,14 @@ let fill_limit_clips_buy_to_lots () = (T.Scalar.Ratio.to_decimal_string threshold) | _ -> Alcotest.fail "expected leverage clipping audit" +let v4_replays_keep_the_fill_clipped_record () = + fill_limit_clips_buy_to_lots ~contract_version:T.Contract.previous_version () + let v3_replays_keep_the_legacy_clipping_record () = let constrained = risk ~max_leverage:"1" () in let state = - runner ~contract_version:T.Contract.previous_version ~initial_cash:"550" - ~risk:constrained + runner ~contract_version:T.Contract.legacy_journal_version + ~initial_cash:"550" ~risk:constrained ~execution:(execution ~fixed_fee:"10" ()) [ (1L, [ target "10" ]) ] in @@ -260,7 +265,7 @@ let v3_replays_keep_the_legacy_clipping_record () = events in Alcotest.(check string) - "legacy journal version" T.Contract.previous_version + "legacy journal version" T.Contract.legacy_journal_version limited.contract_version; match limited.event with | T.Audit.Margin_limited { requested_quantity; permitted_quantity; _ } -> @@ -799,6 +804,8 @@ let tests = superseding_target_replaces_retry; Alcotest.test_case "fill clipping identifies leverage" `Quick fill_limit_clips_buy_to_lots; + Alcotest.test_case "v4 keeps fill clipping records" `Quick + v4_replays_keep_the_fill_clipped_record; Alcotest.test_case "v3 keeps legacy clipping records" `Quick v3_replays_keep_the_legacy_clipping_record; Alcotest.test_case "invalid fill candidates fail" `Quick diff --git a/test/test_scenario.ml b/test/test_scenario.ml index ee12a60..940a253 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/v4/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v5/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/v4/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v5/fixtures/demo.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -125,9 +125,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v4/scenario.schema.json"; - check_schema "../contracts/v4/scenario-stream.schema.json"; - check_schema "../contracts/v4/journal.schema.json" + check_schema "../contracts/v5/scenario.schema.json"; + check_schema "../contracts/v5/scenario-stream.schema.json"; + check_schema "../contracts/v5/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -189,7 +189,7 @@ 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 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 5, 4, 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -730,7 +730,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v4/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v5/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -758,7 +758,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v4/fixtures/fill-clipped.scenario.json" In_channel.input_all + "../contracts/v5/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in let result = @@ -771,7 +771,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v4/fixtures/fill-clipped.journal.jsonl" In_channel.input_all + "../contracts/v5/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual diff --git a/test/test_venue_calendar.ml b/test/test_venue_calendar.ml new file mode 100644 index 0000000..2b8b03b --- /dev/null +++ b/test/test_venue_calendar.ml @@ -0,0 +1,166 @@ +open Test_support +module T = Trading_engine + +let phase kind opens_at closes_at = + T.Venue_calendar.create_phase ~kind ~opens_at:(timestamp opens_at) + ~closes_at:(timestamp closes_at) + |> ok + +let explicit_session_policies () = + let regular = + T.Venue_calendar.create_session ~session_date:"2026-01-02" + ~kind:T.Venue_calendar.Regular_session + ~phases: + [ + phase T.Venue_calendar.Premarket "2026-01-02T09:00:00Z" + "2026-01-02T14:25:00Z"; + phase T.Venue_calendar.Opening_auction "2026-01-02T14:25:00Z" + "2026-01-02T14:30:00Z"; + phase T.Venue_calendar.Regular "2026-01-02T14:30:00Z" + "2026-01-02T20:55:00Z"; + phase T.Venue_calendar.Closing_auction "2026-01-02T20:55:00Z" + "2026-01-02T21:00:00Z"; + phase T.Venue_calendar.Postmarket "2026-01-02T21:00:00Z" + "2026-01-03T01:00:00Z"; + ] + |> ok + in + let holiday = + T.Venue_calendar.create_session ~session_date:"2026-01-03" + ~kind:T.Venue_calendar.Holiday ~phases:[] + |> ok + in + let early_close = + T.Venue_calendar.create_session ~session_date:"2026-01-05" + ~kind:T.Venue_calendar.Early_close + ~phases: + [ + phase T.Venue_calendar.Regular "2026-01-05T14:30:00Z" + "2026-01-05T18:00:00Z"; + ] + |> ok + in + let calendar = + T.Venue_calendar.create + ~id:(T.Id.Venue_calendar.of_string_exn "xnas-2026") + ~version:"1" + ~venue_id:(T.Id.Venue.of_string_exn "XNAS") + ~instrument_ids:[ instrument_id "demo-equity-acme" ] + ~sessions:[ regular; holiday; early_close ] + |> ok + in + let selected = + T.Venue_calendar.session_on calendar ~session_date:"2026-01-05" |> ok + in + Alcotest.(check string) + "early-close policy" "early_close" + (T.Venue_calendar.session_kind_to_string selected.kind); + let missing = + T.Venue_calendar.session_on calendar ~session_date:"2026-01-04" |> error + in + Alcotest.(check string) + "missing policy is not inferred" + "venue calendar xnas-2026 version 1 has no explicit policy for 2026-01-04" + missing + +let ambiguous_phase_policies_are_rejected () = + let regular = + phase T.Venue_calendar.Regular "2026-01-02T14:30:00Z" "2026-01-02T21:00:00Z" + in + let overlapping = + phase T.Venue_calendar.Postmarket "2026-01-02T20:00:00Z" + "2026-01-03T01:00:00Z" + in + Alcotest.(check bool) + "overlap rejected" true + (Result.is_error + (T.Venue_calendar.create_session ~session_date:"2026-01-02" + ~kind:T.Venue_calendar.Regular_session + ~phases:[ regular; overlapping ])); + Alcotest.(check bool) + "missing regular phase rejected" true + (Result.is_error + (T.Venue_calendar.create_session ~session_date:"2026-01-02" + ~kind:T.Venue_calendar.Regular_session + ~phases: + [ + phase T.Venue_calendar.Premarket "2026-01-02T09:00:00Z" + "2026-01-02T14:00:00Z"; + ])); + Alcotest.(check bool) + "holiday phases rejected" true + (Result.is_error + (T.Venue_calendar.create_session ~session_date:"2026-01-02" + ~kind:T.Venue_calendar.Holiday ~phases:[ regular ])) + +let scenario_contract_requires_calendar_coverage () = + let document = + Yojson.Safe.from_file "../contracts/v5/fixtures/demo.scenario.json" + in + let scenario = T.Scenario.of_yojson document |> ok in + Alcotest.(check int) + "calendar retained" 1 + (List.length scenario.venue_calendars); + let missing = + match document with + | `Assoc fields -> + `Assoc + (List.filter + (fun (name, _) -> not (String.equal name "venue_calendars")) + fields) + | json -> json + in + let missing = T.Scenario.of_yojson missing |> error in + Alcotest.(check (option string)) + "missing calendar path" (Some "$") missing.context.json_path; + let wrong_member = + match document with + | `Assoc fields -> + `Assoc + (List.map + (fun (name, value) -> + if not (String.equal name "venue_calendars") then (name, value) + else + let calendars = + match value with + | `List [ `Assoc calendar ] -> + `List + [ + `Assoc + (List.map + (fun (field, value) -> + if String.equal field "instrument_ids" then + (field, `List [ `String "unknown" ]) + else (field, value)) + calendar); + ] + | _ -> Alcotest.fail "fixture calendar shape changed" + in + (name, calendars)) + fields) + | _ -> Alcotest.fail "fixture scenario must be an object" + in + let uncovered = T.Scenario.of_yojson wrong_member |> error in + Alcotest.(check (option string)) + "coverage path" (Some "$.venue_calendars") uncovered.context.json_path + +let v4_remains_a_calendar_free_compatibility_contract () = + let scenario = + T.Scenario.read_file "../contracts/v4/fixtures/demo.scenario.json" |> ok + in + Alcotest.(check string) "v4 retained" "4" scenario.contract_version; + Alcotest.(check int) + "no inferred calendars" 0 + (List.length scenario.venue_calendars) + +let tests = + [ + Alcotest.test_case "explicit policies and missing dates" `Quick + explicit_session_policies; + Alcotest.test_case "ambiguous phases rejected" `Quick + ambiguous_phase_policies_are_rejected; + Alcotest.test_case "scenario calendar coverage" `Quick + scenario_contract_requires_calendar_coverage; + Alcotest.test_case "v4 compatibility does not infer calendars" `Quick + v4_remains_a_calendar_free_compatibility_contract; + ] From 6c75eafb60a40c72b0110b030bb544528476d7cd Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 10:37:27 -0400 Subject: [PATCH 37/57] feat: version execution model configuration --- CHANGELOG.md | 2 + README.md | 3 +- bench/benchmark_replay.py | 9 ++- contracts/conformance/cases.json | 10 +++ contracts/v5/README.md | 5 ++ contracts/v5/fixtures/demo.journal.jsonl | 4 +- contracts/v5/fixtures/demo.scenario.json | 9 ++- contracts/v5/fixtures/demo.scenario.jsonl | 2 +- .../v5/fixtures/fill-clipped.journal.jsonl | 4 +- .../v5/fixtures/fill-clipped.scenario.json | 9 ++- contracts/v5/scenario.schema.json | 11 ++- docs/execution-model.md | 28 ++++++- docs/scenario.md | 10 ++- lib/contract.ml | 1 + lib/execution_model.ml | 61 +++++++++++++++ lib/execution_model.mli | 13 ++++ lib/scenario.ml | 74 ++++++++++++++++--- test/cli.t | 6 +- test/test_diagnostic.ml | 42 +++++++++++ test/test_scenario.ml | 33 ++++++++- test/validate_schemas.py | 16 +++- 21 files changed, 317 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a120f5..746d0eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Publish strict versioned configuration and machine-readable capabilities for each compiled + execution model. - Add contract v5 venue calendars with explicit venue and calendar identities, regular and extended trading phases, holidays, early closes, and reducer-independent clock resolution. - Protect `main` with required integration checks and a no-bypass review policy, and make rebase diff --git a/README.md b/README.md index 364df16..008492c 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,8 @@ scenario slices and scheduled or external intents - Signed average-cost accounting, realized and unrealized P&L, and equity reconciliation - Per-currency cash and per-instrument quantity, mark, value, basis, P&L, and fee attribution - Deterministic event IDs, ordered causal references, and order-creation attribution -- Contract-selected compiled execution modules; v3 currently exposes `completed_bar_v1` +- Contract-selected compiled execution modules with versioned model-owned configuration and + capability descriptors; v5 currently exposes `completed_bar_v1` - 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 diff --git a/bench/benchmark_replay.py b/bench/benchmark_replay.py index 13abca8..67283d0 100644 --- a/bench/benchmark_replay.py +++ b/bench/benchmark_replay.py @@ -199,9 +199,12 @@ def build_scenario(case: BenchmarkCase) -> dict[str, object]: }, "execution": { "model": "completed_bar_v1", - "participation_bps": 10000, - "fixed_fee": "0", - "fee_bps": 0, + "configuration": { + "version": "1", + "participation_bps": 10000, + "fixed_fee": "0", + "fee_bps": 0, + }, }, "max_internal_events": max(1000, case.active_order_count * 4 + 16), "schedule": schedule, diff --git a/contracts/conformance/cases.json b/contracts/conformance/cases.json index 022a2eb..cf27a1d 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -51,6 +51,16 @@ "runtime_expectation": "reject", "rule": "structural" }, + { + "name": "scenario-unsupported-execution-configuration", + "artifact": "scenario-v5", + "kind": "scenario", + "source": "v5/fixtures/demo.scenario.json", + "mutations": [{"op": "replace", "path": ["execution", "configuration", "version"], "value": "2"}], + "schema_expectation": "reject", + "runtime_expectation": "reject", + "rule": "structural" + }, { "name": "scenario-duplicate-instrument", "artifact": "scenario-v5", diff --git a/contracts/v5/README.md b/contracts/v5/README.md index 3f2818a..5495e2e 100644 --- a/contracts/v5/README.md +++ b/contracts/v5/README.md @@ -23,3 +23,8 @@ instrument coverage, unordered or overlapping phases, holidays with phases, and without a regular phase. Every v5 scenario, stream record, and journal record carries `"contract_version": "5"`. + +The v5 `execution` object also namespaces strict configuration beneath the stable model name. +`completed_bar_v1` configuration version `"1"` requires participation basis points, fixed fee, +and fee basis points. Runtime capabilities describe its required fields, supported market and limit +orders, completed-OHLCV data requirement, and numeric limits. diff --git a/contracts/v5/fixtures/demo.journal.jsonl b/contracts/v5/fixtures/demo.journal.jsonl index 1418046..7900887 100644 --- a/contracts/v5/fixtures/demo.journal.jsonl +++ b/contracts/v5/fixtures/demo.journal.jsonl @@ -1,4 +1,4 @@ -{"contract_version":"5","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":"4ec24403fa1f8725edcc399c608ad0bbaca5c17981e32c8e44f7347a4dd65b85","execution_model":"completed_bar_v1"}} +{"contract_version":"5","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":"b800732e40c20c06605c6a1352d3482a3f41fc7ae4b07594860a1c3f153a655c","execution_model":"completed_bar_v1"}} {"contract_version":"5","engine_sequence":"2","event_id":"demo-event-000000000002","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":[]}} {"contract_version":"5","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.615","reference_price":"104"}]}} {"contract_version":"5","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} @@ -17,4 +17,4 @@ {"contract_version":"5","engine_sequence":"17","event_id":"demo-event-000000000017","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":[]}} {"contract_version":"5","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"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.115","price":"105","notional":"747.075","fee":"0.997075","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} {"contract_version":"5","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}}} -{"contract_version":"5","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"4ec24403fa1f8725edcc399c608ad0bbaca5c17981e32c8e44f7347a4dd65b85","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} +{"contract_version":"5","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"b800732e40c20c06605c6a1352d3482a3f41fc7ae4b07594860a1c3f153a655c","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v5/fixtures/demo.scenario.json b/contracts/v5/fixtures/demo.scenario.json index 307a95d..1ba8e32 100644 --- a/contracts/v5/fixtures/demo.scenario.json +++ b/contracts/v5/fixtures/demo.scenario.json @@ -80,9 +80,12 @@ }, "execution": { "model": "completed_bar_v1", - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 + "configuration": { + "version": "1", + "participation_bps": 5000, + "fixed_fee": "0.25", + "fee_bps": 10 + } }, "max_internal_events": 1000, "schedule": [ diff --git a/contracts/v5/fixtures/demo.scenario.jsonl b/contracts/v5/fixtures/demo.scenario.jsonl index ae4227d..7a9733a 100644 --- a/contracts/v5/fixtures/demo.scenario.jsonl +++ b/contracts/v5/fixtures/demo.scenario.jsonl @@ -1,4 +1,4 @@ -{"contract_version":"5","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"run_id":"demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"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_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_gross_exposure":"1000000","max_leverage":"2","initial_margin_bps":5000,"maintenance_margin_bps":2500,"short_borrow_bps":100},"execution":{"model":"completed_bar_v1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10},"max_internal_events":1000}} +{"contract_version":"5","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"run_id":"demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"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_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_gross_exposure":"1000000","max_leverage":"2","initial_margin_bps":5000,"maintenance_margin_bps":2500,"short_borrow_bps":100},"execution":{"model":"completed_bar_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000}} {"contract_version":"5","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"}},"record_type":"market_slice","scenario_sequence":"2"} {"contract_version":"5","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} {"contract_version":"5","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"}},"record_type":"market_slice","scenario_sequence":"4"} diff --git a/contracts/v5/fixtures/fill-clipped.journal.jsonl b/contracts/v5/fixtures/fill-clipped.journal.jsonl index 255d5a9..3734bbb 100644 --- a/contracts/v5/fixtures/fill-clipped.journal.jsonl +++ b/contracts/v5/fixtures/fill-clipped.journal.jsonl @@ -1,4 +1,4 @@ -{"contract_version":"5","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":"d734b4ea2364d6fb00fc5366f04035e14c188c30b84672c1d4278bba930eefea","execution_model":"completed_bar_v1"}} +{"contract_version":"5","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":"3a66be243ea4ae30e5554c7e0dbdc091bd7dd7c5003f4faf726f04b0fbdaeed6","execution_model":"completed_bar_v1"}} {"contract_version":"5","engine_sequence":"2","event_id":"fill-clipped-event-000000000002","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":[]}} {"contract_version":"5","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":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000003","updated_event_id":"fill-clipped-event-000000000003","created_sequence":"3","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} {"contract_version":"5","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false}}} @@ -7,4 +7,4 @@ {"contract_version":"5","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"fill-clipped-fill-000000000001","order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"5","price":"100","notional":"500","fee":"10","executed_at":"2026-02-03T14:30:00.000000Z","slice_sequence":"2"}} {"contract_version":"5","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"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","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000003","updated_event_id":"fill-clipped-event-000000000003","created_sequence":"3","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"5","filled_notional":"500","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} {"contract_version":"5","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}}} -{"contract_version":"5","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000009"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"d734b4ea2364d6fb00fc5366f04035e14c188c30b84672c1d4278bba930eefea","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} +{"contract_version":"5","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000009"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"3a66be243ea4ae30e5554c7e0dbdc091bd7dd7c5003f4faf726f04b0fbdaeed6","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v5/fixtures/fill-clipped.scenario.json b/contracts/v5/fixtures/fill-clipped.scenario.json index 41e404b..b213a8b 100644 --- a/contracts/v5/fixtures/fill-clipped.scenario.json +++ b/contracts/v5/fixtures/fill-clipped.scenario.json @@ -61,9 +61,12 @@ }, "execution": { "model": "completed_bar_v1", - "participation_bps": 10000, - "fixed_fee": "10", - "fee_bps": 0 + "configuration": { + "version": "1", + "participation_bps": 10000, + "fixed_fee": "10", + "fee_bps": 0 + } }, "max_internal_events": 1000, "schedule": [ diff --git a/contracts/v5/scenario.schema.json b/contracts/v5/scenario.schema.json index 1e934dc..f9696da 100644 --- a/contracts/v5/scenario.schema.json +++ b/contracts/v5/scenario.schema.json @@ -162,9 +162,18 @@ "execution": { "type": "object", "additionalProperties": false, - "required": ["model", "participation_bps", "fixed_fee", "fee_bps"], + "required": ["model", "configuration"], "properties": { "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + "completedBarV1Configuration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fixed_fee", "fee_bps"], + "properties": { + "version": { "const": "1" }, "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } diff --git a/docs/execution-model.md b/docs/execution-model.md index 4a5cb48..2e3a435 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,10 +1,36 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v3 advertises and accepts `completed_bar_v1`; embedders can inject another module through +Contract v5 advertises and accepts `completed_bar_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 v5 envelope separates selection from +model-specific parameters: + +```json +{ + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "1", + "participation_bps": 5000, + "fixed_fee": "0.25", + "fee_bps": 10 + } + } +} +``` + +The model and configuration version are validated before replay. Unknown models, unsupported +model/version pairs, missing fields, and fields from another model are rejected. Contracts v3 and +v4 retain their frozen flat execution object. + +`--capabilities` preserves the `execution_models` name list and publishes one deterministic +descriptor per model under `execution_model_contracts`: supported scenario and configuration +versions, required fields, order types, market-data requirements, and numeric limits. Clients can +therefore reject incompatible scenarios without guessing from a shared execution object. + 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. diff --git a/docs/scenario.md b/docs/scenario.md index c31a9cc..e1a9d19 100644 --- a/docs/scenario.md +++ b/docs/scenario.md @@ -94,14 +94,18 @@ annualized `short_borrow_bps`. Initial margin cannot be below maintenance margin quantity limit must cover at least one lot for every instrument. Orders that increase gross exposure must satisfy every applicable limit; exposure-reducing orders remain admissible. -Execution contains: +Contract v5 execution contains a stable `model` and a model-owned `configuration`. For +`completed_bar_v1`, configuration version `"1"` contains: -- `model`, the compiled execution module selected by contract name; v5 supports - `completed_bar_v1` +- `version`, the strict model-configuration contract version - `participation_bps`, from 0 through 10,000 - `fixed_fee`, a nonnegative money string - `fee_bps`, from 0 through 10,000 +The engine advertises each model's scenario and configuration versions, required fields, supported +order types, data requirements, and limits through `--capabilities.execution_model_contracts`. The +v3 and v4 scenario contracts preserve their flat execution object unchanged. + ## Schedule and intents Schedule entries are positive, strictly increasing, and anchored to existing slices: diff --git a/lib/contract.ml b/lib/contract.ml index 35530f1..6903702 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -16,6 +16,7 @@ let capabilities_to_yojson () = ("scenario_formats", strings [ "json"; "jsonl" ]); ("journal_formats", strings [ "jsonl" ]); ("execution_models", strings Execution_model.supported); + ("execution_model_contracts", Execution_model.capabilities_to_yojson ()); ("strategy_protocol_versions", strings [ strategy_protocol_version ]); ("resource_limits", Resource_limits.to_yojson ()); ] diff --git a/lib/execution_model.ml b/lib/execution_model.ml index 1a1b148..ca432d1 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -11,6 +11,15 @@ end type t = (module S) +type configuration_contract = { + version : string; + scenario_contract_versions : string list; + required_fields : string list; + supported_order_types : string list; + data_requirements : string list; + limits : Yojson.Safe.t; +} + module Completed_bar_v1 = struct let name = "completed_bar_v1" let start_slice = Execution.start_slice @@ -21,6 +30,58 @@ let name (module Model : S) = Model.name let builtins : t list = [ (module Completed_bar_v1) ] let supported = List.map name builtins +let completed_bar_v1_contract = + { + version = "1"; + scenario_contract_versions = [ "5"; "4"; "3" ]; + required_fields = [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ]; + supported_order_types = [ "market"; "limit" ]; + data_requirements = [ "completed_ohlcv_bars" ]; + limits = + `Assoc + [ + ( "participation_bps", + `Assoc [ ("minimum", `Int 0); ("maximum", `Int 10_000) ] ); + ("fee_bps", `Assoc [ ("minimum", `Int 0); ("maximum", `Int 10_000) ]); + ( "fixed_fee", + `Assoc [ ("minimum", `String "0"); ("unit", `String "money") ] ); + ]; + } + +let configuration_contract model = + match name model with + | "completed_bar_v1" -> completed_bar_v1_contract + | unsupported -> + invalid_arg + (Printf.sprintf "execution model %S has no configuration contract" + unsupported) + +let supports_configuration model version = + String.equal (configuration_contract model).version version + +let supports_contract model version = + List.mem version (configuration_contract model).scenario_contract_versions + +let strings values = `List (List.map (fun value -> `String value) values) + +let capabilities_to_yojson () = + `List + (List.map + (fun model -> + let contract = configuration_contract model in + `Assoc + [ + ("name", `String (name model)); + ("configuration_versions", strings [ contract.version ]); + ( "scenario_contract_versions", + strings contract.scenario_contract_versions ); + ("required_fields", strings contract.required_fields); + ("supported_order_types", strings contract.supported_order_types); + ("data_requirements", strings contract.data_requirements); + ("limits", contract.limits); + ]) + builtins) + let find requested = match List.find_opt (fun model -> String.equal requested (name model)) builtins diff --git a/lib/execution_model.mli b/lib/execution_model.mli index 06336f8..e346b9f 100644 --- a/lib/execution_model.mli +++ b/lib/execution_model.mli @@ -19,10 +19,23 @@ end type t +type configuration_contract = private { + version : string; + scenario_contract_versions : string list; + required_fields : string list; + supported_order_types : string list; + data_requirements : string list; + limits : Yojson.Safe.t; +} + val of_module : (module S) -> t val name : t -> string val find : string -> (t, string) result val supported : string list +val configuration_contract : t -> configuration_contract +val supports_configuration : t -> string -> bool +val supports_contract : t -> string -> bool +val capabilities_to_yojson : unit -> Yojson.Safe.t val start_slice : t -> diff --git a/lib/scenario.ml b/lib/scenario.ml index 90ccdfc..f86e2c0 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -363,7 +363,18 @@ let parse_risk base_currency instruments json = ~max_short_position ~max_gross_exposure ~max_leverage ~initial_margin_bps ~maintenance_margin_bps ~short_borrow_bps -let parse_execution json = +let parse_execution_values fields = + let* participation_json = field fields "participation_bps" in + let* participation_bps = + integer ~name:"participation_bps" participation_json + in + let* fixed_json = field fields "fixed_fee" in + let* fixed_fee = parse_money ~name:"fixed_fee" fixed_json in + let* fee_json = field fields "fee_bps" in + let* fee_bps = integer ~name:"fee_bps" fee_json in + Execution.create ~participation_bps ~fixed_fee ~fee_bps + +let parse_legacy_execution ~contract_version json = let* fields = object_fields ~name:"execution" ~expected:[ "model"; "participation_bps"; "fixed_fee"; "fee_bps" ] @@ -372,17 +383,59 @@ let parse_execution json = let* model_json = field fields "model" in let* model_name = string ~name:"execution model" model_json in let* execution_model = Execution_model.find model_name in - let* participation_json = field fields "participation_bps" in - let* participation_bps = - integer ~name:"participation_bps" participation_json + let* () = + if Execution_model.supports_contract execution_model contract_version then + Ok () + else + Error + (Printf.sprintf + "execution model %S does not support scenario contract %S" model_name + contract_version) in - let* fixed_json = field fields "fixed_fee" in - let* fixed_fee = parse_money ~name:"fixed_fee" fixed_json in - let* fee_json = field fields "fee_bps" in - let* fee_bps = integer ~name:"fee_bps" fee_json in - let* execution = Execution.create ~participation_bps ~fixed_fee ~fee_bps in + let* execution = parse_execution_values fields in Ok (execution_model, execution) +let parse_versioned_execution ~contract_version json = + let* fields = + object_fields ~name:"execution" ~expected:[ "model"; "configuration" ] json + in + let* model_json = field fields "model" in + let* model_name = string ~name:"execution model" model_json in + let* execution_model = Execution_model.find model_name in + let* () = + if Execution_model.supports_contract execution_model contract_version then + Ok () + else + Error + (Printf.sprintf + "execution model %S does not support scenario contract %S" model_name + contract_version) + in + let* configuration_json = field fields "configuration" in + let expected = + (Execution_model.configuration_contract execution_model).required_fields + in + let* configuration = + object_fields + ~name:(model_name ^ " execution configuration") + ~expected configuration_json + in + let* version_json = field configuration "version" in + let* version = string ~name:"execution configuration version" version_json in + if not (Execution_model.supports_configuration execution_model version) then + Error + (Printf.sprintf + "unsupported execution configuration version %S for model %S" version + model_name) + else + let* execution = parse_execution_values configuration in + Ok (execution_model, execution) + +let parse_execution ~contract_version json = + if String.equal contract_version "5" then + parse_versioned_execution ~contract_version json + else parse_legacy_execution ~contract_version json + let parse_side json = let* value = string ~name:"side" json in match value with @@ -719,7 +772,8 @@ let construct_header ~root ~contract_path ~contract_version parse_risk base_currency instruments shape.risk |> at (child root "risk") in let* execution_model, execution = - parse_execution shape.execution |> at (child root "execution") + parse_execution ~contract_version shape.execution + |> at (child root "execution") in let header : stream_header = { diff --git a/test/cli.t b/test/cli.t index c19a88e..4ef8b6d 100644 --- a/test/cli.t +++ b/test/cli.t @@ -2,13 +2,13 @@ 1.0.0 $ ../bin/main.exe --capabilities - {"engine_version":"1.0.0","scenario_contract_versions":["5","4","3"],"journal_contract_versions":["5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"strategy_protocol_versions":["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":["5","4","3"],"journal_contract_versions":["5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["1"],"scenario_contract_versions":["5","4","3"],"required_fields":["version","participation_bps","fixed_fee","fee_bps"],"supported_order_types":["market","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":["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/v5/fixtures/demo.scenario.json - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=4ec24403fa1f8725edcc399c608ad0bbaca5c17981e32c8e44f7347a4dd65b85 + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=b800732e40c20c06605c6a1352d3482a3f41fc7ae4b07594860a1c3f153a655c $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v5/fixtures/demo.scenario.jsonl - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=793deb0f4bbf6f4192c283534e031da05a8c02071021f523ef6523763bc1904b + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=ba51f0f956d90fcaf57ed905e58d89fbc707d050f54ba1334b0a8d0dbb32856d $ ../bin/main.exe --input-format jsonl --input ../contracts/v5/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts run=demo audits=20 orders=3 active=0 filled=2 rejected=0 diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 9441e4a..5465e15 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -90,6 +90,46 @@ let capabilities_publish_versioned_resource_limits () = "resource diagnostic code" "resource.limit" (T.Diagnostic.code_to_string T.Diagnostic.Resource_limit) +let capabilities_describe_execution_contracts () = + let model = + match + T.Contract.capabilities_to_yojson () |> field "execution_model_contracts" + with + | `List [ model ] -> model + | _ -> Alcotest.fail "expected one execution-model capability" + in + Alcotest.(check string) + "stable model name" "completed_bar_v1" + (match field "name" model with + | `String value -> value + | _ -> Alcotest.fail "expected execution-model name"); + let strings name = + match field name model with + | `List values -> + List.map + (function + | `String value -> value + | _ -> Alcotest.fail (name ^ " must contain strings")) + values + | _ -> Alcotest.fail (name ^ " must be an array") + in + Alcotest.(check (list string)) + "configuration versions" [ "1" ] + (strings "configuration_versions"); + Alcotest.(check (list string)) + "scenario contracts" [ "5"; "4"; "3" ] + (strings "scenario_contract_versions"); + Alcotest.(check (list string)) + "required fields" + [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ] + (strings "required_fields"); + Alcotest.(check (list string)) + "order types" [ "market"; "limit" ] + (strings "supported_order_types"); + Alcotest.(check (list string)) + "market data" [ "completed_ohlcv_bars" ] + (strings "data_requirements") + let tests = [ Alcotest.test_case "renders stable machine context" `Quick @@ -98,4 +138,6 @@ let tests = preserves_sanitized_exception; Alcotest.test_case "versioned resource capabilities" `Quick capabilities_publish_versioned_resource_limits; + Alcotest.test_case "execution-model capabilities" `Quick + capabilities_describe_execution_contracts; ] diff --git a/test/test_scenario.ml b/test/test_scenario.ml index 940a253..b62d39d 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -623,7 +623,38 @@ let execution_model_is_required_and_supported () = in Alcotest.(check string) "unsupported model diagnosed" "unsupported execution model \"future_model\"" - (T.Scenario.of_yojson unsupported |> diagnostic_message) + (T.Scenario.of_yojson unsupported |> diagnostic_message); + let change_configuration change = + change_execution (map_field "configuration" change) + in + let missing_version = + change_configuration (function + | `Assoc fields -> + `Assoc + (List.filter + (fun (name, _) -> not (String.equal name "version")) + fields) + | _ -> Alcotest.fail "configuration must be an object") + in + Alcotest.(check bool) + "configuration version required" true + (Result.is_error (T.Scenario.of_yojson missing_version)); + let unsupported_version = + change_configuration (change_field "version" (`String "2")) + in + Alcotest.(check string) + "unsupported model/version diagnosed" + "unsupported execution configuration version \"2\" for model \ + \"completed_bar_v1\"" + (T.Scenario.of_yojson unsupported_version |> diagnostic_message); + let extra_configuration = + change_configuration (function + | `Assoc fields -> `Assoc (("future_parameter", `Int 1) :: fields) + | _ -> Alcotest.fail "configuration must be an object") + in + Alcotest.(check bool) + "model configuration is strict" true + (Result.is_error (T.Scenario.of_yojson extra_configuration)) let deterministic_replay () = let scenario = demo () in diff --git a/test/validate_schemas.py b/test/validate_schemas.py index b7a83d0..6d59042 100644 --- a/test/validate_schemas.py +++ b/test/validate_schemas.py @@ -115,7 +115,21 @@ def main() -> None: unsupported_execution_model = copy.deepcopy(scenario) unsupported_execution_model["execution"]["model"] = "future_model" expect_invalid(scenario_validator, unsupported_execution_model) - if contract_version in {"3", "4"}: + if contract_version == "5": + missing_configuration_version = copy.deepcopy(scenario) + del missing_configuration_version["execution"]["configuration"][ + "version" + ] + expect_invalid(scenario_validator, missing_configuration_version) + unsupported_configuration_version = copy.deepcopy(scenario) + unsupported_configuration_version["execution"]["configuration"][ + "version" + ] = "2" + expect_invalid(scenario_validator, unsupported_configuration_version) + unknown_configuration_field = copy.deepcopy(scenario) + unknown_configuration_field["execution"]["configuration"]["future"] = True + expect_invalid(scenario_validator, unknown_configuration_field) + if contract_version in {"3", "4", "5"}: excessive_feedback_cap = copy.deepcopy(scenario) excessive_feedback_cap["max_internal_events"] = 100001 expect_invalid(scenario_validator, excessive_feedback_cap) From e09e4ef1af2028d42ef0f65df0f47de14833ec8b Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 11:08:59 -0400 Subject: [PATCH 38/57] feat: support explicit initial portfolio state --- CHANGELOG.md | 3 + README.md | 26 +- bench/benchmark_replay.py | 10 +- bench/latency_strategy.py | 2 +- contracts/conformance/cases.json | 86 +++- contracts/conformance/manifest.json | 49 +++ contracts/strategy/v4/README.md | 51 +++ contracts/strategy/v4/dune | 15 + .../v4/fixtures/external.scenario.json | 196 ++++++++++ .../v4/fixtures/external.scenario.jsonl | 4 + .../v4/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v4/message.schema.json | 290 ++++++++++++++ contracts/strategy/v4/transcript.schema.json | 82 ++++ contracts/v6/README.md | 31 ++ contracts/v6/dune | 16 + contracts/v6/fixtures/demo.journal.jsonl | 22 ++ contracts/v6/fixtures/demo.scenario.json | 294 ++++++++++++++ contracts/v6/fixtures/demo.scenario.jsonl | 6 + .../v6/fixtures/fill-clipped.journal.jsonl | 12 + .../v6/fixtures/fill-clipped.scenario.json | 164 ++++++++ contracts/v6/journal.schema.json | 186 +++++++++ contracts/v6/scenario-stream.schema.json | 76 ++++ contracts/v6/scenario.schema.json | 369 ++++++++++++++++++ docs/api-reference.md | 2 +- docs/architecture.md | 2 +- docs/continuous-integration.md | 11 +- docs/execution-model.md | 6 +- docs/persistra.md | 10 +- docs/scenario.md | 44 ++- lib/account.ml | 30 ++ lib/account.mli | 1 + lib/audit.ml | 2 + lib/audit.mli | 1 + lib/codec.ml | 46 +++ lib/codec.mli | 1 + lib/contract.ml | 13 +- lib/engine.ml | 209 ++++++---- lib/engine.mli | 15 + lib/execution_model.ml | 2 +- lib/external_replay.ml | 17 +- lib/initial_portfolio.ml | 120 ++++++ lib/initial_portfolio.mli | 42 ++ lib/replay.ml | 20 +- lib/scenario.ml | 125 +++++- lib/scenario.mli | 2 + lib/scenario_shape.ml | 28 +- lib/scenario_shape.mli | 2 +- lib/scenario_validation.ml | 88 ++++- lib/scenario_validation.mli | 9 + lib/strategy_protocol.ml | 15 +- lib/strategy_protocol.mli | 1 + mkdocs.yml | 6 +- scripts/check-documentation.py | 2 + scripts/release_artifacts.py | 12 +- test/cli.t | 58 +-- test/dune | 96 ++--- test/fake_strategy.py | 2 +- test/test_boundary_failures.ml | 1 + test/test_diagnostic.ml | 2 +- test/test_scenario.ml | 162 ++++++-- test/test_strategy_protocol.ml | 11 +- test/validate_schemas.py | 9 +- 62 files changed, 2940 insertions(+), 289 deletions(-) create mode 100644 contracts/strategy/v4/README.md create mode 100644 contracts/strategy/v4/dune create mode 100644 contracts/strategy/v4/fixtures/external.scenario.json create mode 100644 contracts/strategy/v4/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v4/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v4/message.schema.json create mode 100644 contracts/strategy/v4/transcript.schema.json create mode 100644 contracts/v6/README.md create mode 100644 contracts/v6/dune create mode 100644 contracts/v6/fixtures/demo.journal.jsonl create mode 100644 contracts/v6/fixtures/demo.scenario.json create mode 100644 contracts/v6/fixtures/demo.scenario.jsonl create mode 100644 contracts/v6/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v6/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v6/journal.schema.json create mode 100644 contracts/v6/scenario-stream.schema.json create mode 100644 contracts/v6/scenario.schema.json create mode 100644 lib/initial_portfolio.ml create mode 100644 lib/initial_portfolio.mli diff --git a/CHANGELOG.md b/CHANGELOG.md index 746d0eb..4c380a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add contract v6 explicit initial portfolio snapshots with signed cash and positions, accounting + history, initial marks and FX, strict risk validation, initial-state auditing, and strategy + protocol v4 initialization. - Publish strict versioned configuration and machine-readable capabilities for each compiled execution model. - Add contract v5 venue calendars with explicit venue and calendar identities, regular and diff --git a/README.md b/README.md index 008492c..209aef3 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,14 @@ scenario slices and scheduled or external intents - Risk-aware fractional-lot clipping with structured `fill_clipped` reasons and thresholds - Fixed and notional fees with explicit rounding - Explicit multi-currency cash ledgers and complete per-slice FX marks in a base currency +- Explicit signed initial portfolios with cost basis, P&L and fee history, marks, and FX state - Split and cash-dividend processing before matching, including target and order adjustment - Short borrow accrual, maintenance-margin calls, and deterministic liquidation orders - Signed average-cost accounting, realized and unrealized P&L, and equity reconciliation - Per-currency cash and per-instrument quantity, mark, value, basis, P&L, and fee attribution - Deterministic event IDs, ordered causal references, and order-creation attribution - Contract-selected compiled execution modules with versioned model-owned configuration and - capability descriptors; v5 currently exposes `completed_bar_v1` + capability descriptors; v6 currently exposes `completed_bar_v1` - 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 @@ -85,7 +86,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v5/fixtures/demo.scenario.json \ + --input contracts/v6/fixtures/demo.scenario.json \ --validate-only ``` @@ -93,7 +94,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v5/fixtures/demo.scenario.json \ + --input contracts/v6/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -101,7 +102,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v5/fixtures/demo.scenario.jsonl \ + --input contracts/v6/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -110,7 +111,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v3/fixtures/external.scenario.json \ + --input contracts/strategy/v4/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -217,18 +218,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 v5 and conformance fixtures](contracts/v5/README.md) +- [Current contract v6 and conformance fixtures](contracts/v6/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v5/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v5/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v5/journal.schema.json) -- [External strategy protocol v3](contracts/strategy/v3/README.md) +- [Scenario JSON Schema](contracts/v6/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v6/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v6/journal.schema.json) +- [External strategy protocol v4](contracts/strategy/v4/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/v3/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v3/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v4/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v4/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/bench/benchmark_replay.py b/bench/benchmark_replay.py index 67283d0..ab1389c 100644 --- a/bench/benchmark_replay.py +++ b/bench/benchmark_replay.py @@ -23,7 +23,7 @@ ROOT = Path(__file__).resolve().parents[1] DEFAULT_EXECUTABLE = ROOT / "_build/default/bin/main.exe" DEFAULT_BASELINE = ROOT / "bench/baselines/linux-x86_64.json" -FIXTURE = ROOT / "contracts/v5/fixtures/demo.scenario.json" +FIXTURE = ROOT / "contracts/v6/fixtures/demo.scenario.json" STRATEGY = ROOT / "bench/latency_strategy.py" SUMMARY_PATTERN = re.compile( r"\baudits=(?P[0-9]+).*\bactive=(?P[0-9]+)" @@ -163,6 +163,12 @@ def build_scenario(case: BenchmarkCase) -> dict[str, object]: "benchmark_case": case.name, }, "run_id": f"benchmark-{case.name}", + "initial_portfolio": { + "cash": [{"currency": "USD", "amount": "10000"}], + "positions": [], + "marks": [], + "fx_rates": [{"currency": "USD", "rate": "1"}], + }, "instruments": instruments, "venue_calendars": [ { @@ -224,7 +230,7 @@ def stream_records(document: dict[str, object]) -> list[dict[str, object]]: "metadata", "run_id", "base_currency", - "initial_cash", + "initial_portfolio", "instruments", "venue_calendars", "risk", diff --git a/bench/latency_strategy.py b/bench/latency_strategy.py index 8f4ff06..ffdf458 100644 --- a/bench/latency_strategy.py +++ b/bench/latency_strategy.py @@ -36,7 +36,7 @@ print( json.dumps( { - "strategy_protocol_version": "3", + "strategy_protocol_version": "4", "strategy_sequence": request["strategy_sequence"], "message_type": response_type, "payload": payload, diff --git a/contracts/conformance/cases.json b/contracts/conformance/cases.json index cf27a1d..0bc2690 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -1,6 +1,16 @@ { "format_version": "1", "cases": [ + { + "name": "scenario-v6-valid", + "artifact": "scenario-v6", + "kind": "scenario", + "source": "v6/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, { "name": "scenario-v5-valid", "artifact": "scenario-v5", @@ -91,6 +101,16 @@ "runtime_expectation": "accept", "rule": "structural" }, + { + "name": "scenario-stream-v6-valid", + "artifact": "scenario-stream-v6", + "kind": "scenario_stream", + "source": "v6/fixtures/demo.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, { "name": "scenario-stream-v3-valid", "artifact": "scenario-stream-v3", @@ -136,9 +156,9 @@ }, { "name": "strategy-ready-valid", - "artifact": "strategy-message-v3", + "artifact": "strategy-message-v4", "kind": "strategy_response", - "source": "strategy/v3/fixtures/external.strategy.jsonl", + "source": "strategy/v4/fixtures/external.strategy.jsonl", "record": 2, "extract": ["message"], "expected_sequence": "1", @@ -149,9 +169,9 @@ }, { "name": "strategy-intents-valid", - "artifact": "strategy-message-v3", + "artifact": "strategy-message-v4", "kind": "strategy_response", - "source": "strategy/v3/fixtures/external.strategy.jsonl", + "source": "strategy/v4/fixtures/external.strategy.jsonl", "record": 4, "extract": ["message"], "expected_sequence": "2", @@ -162,9 +182,9 @@ }, { "name": "strategy-stopped-valid", - "artifact": "strategy-message-v3", + "artifact": "strategy-message-v4", "kind": "strategy_response", - "source": "strategy/v3/fixtures/external.strategy.jsonl", + "source": "strategy/v4/fixtures/external.strategy.jsonl", "record": 14, "extract": ["message"], "expected_sequence": "7", @@ -175,9 +195,9 @@ }, { "name": "strategy-error-valid", - "artifact": "strategy-message-v3", + "artifact": "strategy-message-v4", "kind": "strategy_response", - "source": "strategy/v3/fixtures/external.strategy.jsonl", + "source": "strategy/v4/fixtures/external.strategy.jsonl", "record": 14, "extract": ["message"], "expected_sequence": "7", @@ -191,9 +211,9 @@ }, { "name": "strategy-missing-version", - "artifact": "strategy-message-v3", + "artifact": "strategy-message-v4", "kind": "strategy_response", - "source": "strategy/v3/fixtures/external.strategy.jsonl", + "source": "strategy/v4/fixtures/external.strategy.jsonl", "record": 2, "extract": ["message"], "expected_sequence": "1", @@ -204,9 +224,9 @@ }, { "name": "strategy-unknown-field", - "artifact": "strategy-message-v3", + "artifact": "strategy-message-v4", "kind": "strategy_response", - "source": "strategy/v3/fixtures/external.strategy.jsonl", + "source": "strategy/v4/fixtures/external.strategy.jsonl", "record": 2, "extract": ["message"], "expected_sequence": "1", @@ -217,9 +237,9 @@ }, { "name": "strategy-wrong-sequence", - "artifact": "strategy-message-v3", + "artifact": "strategy-message-v4", "kind": "strategy_response", - "source": "strategy/v3/fixtures/external.strategy.jsonl", + "source": "strategy/v4/fixtures/external.strategy.jsonl", "record": 2, "extract": ["message"], "expected_sequence": "2", @@ -254,6 +274,18 @@ ], "schema_expectation": "accept" }, + { + "name": "strategy-v3-error-branch", + "artifact": "strategy-message-v3", + "source": "strategy/v3/fixtures/external.strategy.jsonl", + "record": 14, + "extract": ["message"], + "mutations": [ + {"op": "replace", "path": ["message_type"], "value": "error"}, + {"op": "replace", "path": ["payload"], "value": {"message": "intentional conformance error"}} + ], + "schema_expectation": "accept" + }, { "name": "strategy-v3-rejected-response-branch", "artifact": "strategy-transcript-v3", @@ -279,6 +311,32 @@ }, "mutations": [], "schema_expectation": "accept" + }, + { + "name": "strategy-v4-rejected-response-branch", + "artifact": "strategy-transcript-v4", + "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 084369c..6ef1a69 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -146,6 +146,37 @@ {"path": "v5/fixtures/fill-clipped.journal.jsonl", "format": "jsonl"} ] }, + { + "name": "scenario-v6", + "schema": "v6/scenario.schema.json", + "version_field": "contract_version", + "version": "6", + "sources": [ + {"path": "v6/fixtures/demo.scenario.json", "format": "json"}, + {"path": "v6/fixtures/fill-clipped.scenario.json", "format": "json"}, + {"path": "strategy/v4/fixtures/external.scenario.json", "format": "json"} + ] + }, + { + "name": "scenario-stream-v6", + "schema": "v6/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "6", + "sources": [ + {"path": "v6/fixtures/demo.scenario.jsonl", "format": "jsonl"}, + {"path": "strategy/v4/fixtures/external.scenario.jsonl", "format": "jsonl"} + ] + }, + { + "name": "journal-v6", + "schema": "v6/journal.schema.json", + "version_field": "contract_version", + "version": "6", + "sources": [ + {"path": "v6/fixtures/demo.journal.jsonl", "format": "jsonl"}, + {"path": "v6/fixtures/fill-clipped.journal.jsonl", "format": "jsonl"} + ] + }, { "name": "diagnostic-v1", "schema": "diagnostic/v1/diagnostic.schema.json", @@ -208,6 +239,24 @@ "sources": [ {"path": "strategy/v3/fixtures/external.strategy.jsonl", "format": "jsonl"} ] + }, + { + "name": "strategy-message-v4", + "schema": "strategy/v4/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "4", + "sources": [ + {"path": "strategy/v4/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"]} + ] + }, + { + "name": "strategy-transcript-v4", + "schema": "strategy/v4/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "4", + "sources": [ + {"path": "strategy/v4/fixtures/external.strategy.jsonl", "format": "jsonl"} + ] } ] } diff --git a/contracts/strategy/v4/README.md b/contracts/strategy/v4/README.md new file mode 100644 index 0000000..e718cba --- /dev/null +++ b/contracts/strategy/v4/README.md @@ -0,0 +1,51 @@ +# External strategy protocol v4 + +Version 4 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`. + +Every message repeats `strategy_protocol_version: "4"` 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, 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 v6 and includes the exact `initial_portfolio` +snapshot alongside the legacy cash projection. It also carries the nested, versioned execution +configuration, so a strategy can reject incompatible 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, fills, order updates, and rejected intents. Response +intents use the scenario v6 intent shapes. + +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/v4/dune b/contracts/strategy/v4/dune new file mode 100644 index 0000000..b1b6030 --- /dev/null +++ b/contracts/strategy/v4/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v4/message.schema.json) + (transcript.schema.json as contracts/strategy/v4/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v4/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v4/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v4/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v4/fixtures/external.scenario.json b/contracts/strategy/v4/fixtures/external.scenario.json new file mode 100644 index 0000000..3a0e07b --- /dev/null +++ b/contracts/strategy/v4/fixtures/external.scenario.json @@ -0,0 +1,196 @@ +{ + "contract_version": "6", + "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_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_gross_exposure": "1000000", + "max_leverage": "2", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "short_borrow_bps": 0 + }, + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "1", + "participation_bps": 5000, + "fixed_fee": "0.25", + "fee_bps": 10 + } + }, + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/strategy/v4/fixtures/external.scenario.jsonl b/contracts/strategy/v4/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..2048f33 --- /dev/null +++ b/contracts/strategy/v4/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"6","payload":{"metadata":{"producer":"strategy-protocol-fixture"},"run_id":"external-demo","base_currency":"USD","initial_portfolio":{"cash":[{"amount":"10000","currency":"USD"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"1","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"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":{"initial_margin_bps":5000,"maintenance_margin_bps":2500,"max_gross_exposure":"1000000","max_leverage":"2","max_long_position":"1000","max_order_quantity":"1000","max_short_position":"1000","short_borrow_bps":0},"execution":{"model":"completed_bar_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000},"record_type":"scenario_header","scenario_sequence":"1"} +{"contract_version":"6","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"}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"6","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"}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"6","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v4/fixtures/external.strategy.jsonl b/contracts/strategy/v4/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..fd78fc4 --- /dev/null +++ b/contracts/strategy/v4/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"4","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"6","scenario_sha256":"1faa37778736b022309ffc290658161f9e97bb6f7492c7083045922998f237f5","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"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"risk":{"max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_gross_exposure":"1000000","max_leverage":"2","initial_margin_bps":5000,"maintenance_margin_bps":2500,"short_borrow_bps":0},"execution":{"model":"completed_bar_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"metadata":{"producer":"strategy-protocol-fixture"}}}} +{"strategy_protocol_version":"4","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"4","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0"}]},"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":[]}}}}} +{"strategy_protocol_version":"4","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","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":"4","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0"}]},"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","limit_price":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","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","limit_price":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","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} +{"strategy_protocol_version":"4","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"4","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}]},"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"}}}}} +{"strategy_protocol_version":"4","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"4","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}]},"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","limit_price":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","filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} +{"strategy_protocol_version":"4","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"4","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}]},"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":[]}}}}} +{"strategy_protocol_version":"4","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"4","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"4","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v4/message.schema.json b/contracts/strategy/v4/message.schema.json new file mode 100644 index 0000000..cb3c259 --- /dev/null +++ b/contracts/strategy/v4/message.schema.json @@ -0,0 +1,290 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v4/message.schema.json", + "title": "Trading Engine external strategy protocol v4 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": "4" }, + "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", "risk", "execution", "metadata"], + "properties": { + "engine_version": { "type": "string", "minLength": 1 }, + "scenario_contract_version": { "const": "6" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/instrument" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/execution" }, + "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/v6/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/v6/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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"], + "properties": { + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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/v6/journal.schema.json#/$defs/cashAttribution" } + }, + "positions": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/markedPosition" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/signedDecimal" } + ] + }, + "markedPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], + "properties": { + "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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/v6/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/v6/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/v6/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/v6/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/v4/transcript.schema.json b/contracts/strategy/v4/transcript.schema.json new file mode 100644 index 0000000..0602db3 --- /dev/null +++ b/contracts/strategy/v4/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/v4/transcript.schema.json", + "title": "Trading Engine external strategy protocol v4 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": "4" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v4/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/v6/README.md b/contracts/v6/README.md new file mode 100644 index 0000000..ff1bd28 --- /dev/null +++ b/contracts/v6/README.md @@ -0,0 +1,31 @@ +# Trading Engine contract v6 + +This directory is the authoritative v6 process and file contract shared by Trading Engine and its +clients. Versions 5, 4, and 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 6 replaces cash-only initialization with an explicit portfolio snapshot. It carries signed +cash, signed positions, native cost basis, realized and dividend P&L histories, execution and +borrow fee histories, position marks, and currency-to-base FX marks. Historical attribution is +point-in-time state and is not applied to cash again. + +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; position limits; aggregate exposure and +leverage limits; and initial margin. Signed cash is valid. A successful v6 run emits `initial_state` +immediately after `run_started`, followed by a reconciled initial `valuation`, before market data. + +Version 6 retains the immutable venue-calendar and model-owned execution-configuration envelopes +introduced by v5. + +Every v6 scenario, stream record, and journal record carries `"contract_version": "6"`. + +The v6 `execution` object namespaces strict configuration beneath the stable model name. +`completed_bar_v1` configuration version `"1"` requires participation basis points, fixed fee, +and fee basis points. Runtime capabilities describe its required fields, supported market and limit +orders, completed-OHLCV data requirement, and numeric limits. diff --git a/contracts/v6/dune b/contracts/v6/dune new file mode 100644 index 0000000..1bf8fd9 --- /dev/null +++ b/contracts/v6/dune @@ -0,0 +1,16 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v6/journal.schema.json) + (scenario-stream.schema.json as contracts/v6/scenario-stream.schema.json) + (scenario.schema.json as contracts/v6/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v6/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v6/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl as contracts/v6/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v6/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v6/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v6/fixtures/demo.journal.jsonl b/contracts/v6/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..c9cca94 --- /dev/null +++ b/contracts/v6/fixtures/demo.journal.jsonl @@ -0,0 +1,22 @@ +{"contract_version":"6","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":"c98534261412acebf4b49d87619dc7c951538581c2a7dd05f315eb64d761cec2","execution_model":"completed_bar_v1"}} +{"contract_version":"6","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false}}}} +{"contract_version":"6","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false}}} +{"contract_version":"6","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":[]}} +{"contract_version":"6","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} +{"contract_version":"6","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":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} +{"contract_version":"6","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004","demo-event-000000000005"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000007","updated_event_id":"demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"6","engine_sequence":"8","event_id":"demo-event-000000000008","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","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"14","equity":"10104","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"}],"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"}],"margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052","maintenance_excess":"10078","margin_call":false}}} +{"contract_version":"6","engine_sequence":"9","event_id":"demo-event-000000000009","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} +{"contract_version":"6","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} +{"contract_version":"6","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000007","updated_event_id":"demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"6","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000005","demo-event-000000000009"],"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.715","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000012","updated_event_id":"demo-event-000000000012","created_sequence":"12","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"6","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000009"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5","unrealized_pnl":"40.132","equity":"10130.132","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618"}],"margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.632","maintenance_excess":"9942.882","margin_call":false}}} +{"contract_version":"6","engine_sequence":"14","event_id":"demo-event-000000000014","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":[]}} +{"contract_version":"6","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.715","price":"107","notional":"290.505","fee":"0.540505","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} +{"contract_version":"6","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000014"],"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":"6","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":["demo-event-000000000014","demo-event-000000000016"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000017","updated_event_id":"demo-event-000000000017","created_sequence":"17","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"6","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000014"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.086495","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5","unrealized_pnl":"20.161495","equity":"10110.161495","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.086495","fx_rate":"1","base_value":"9090.086495"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505"}],"margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.123995","maintenance_excess":"9855.142745","margin_call":false}}} +{"contract_version":"6","engine_sequence":"19","event_id":"demo-event-000000000019","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":[]}} +{"contract_version":"6","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000017","demo-event-000000000019"],"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":"105","notional":"757.575","fee":"1.007575","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} +{"contract_version":"6","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false}}} +{"contract_version":"6","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"c98534261412acebf4b49d87619dc7c951538581c2a7dd05f315eb64d761cec2","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false}},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v6/fixtures/demo.scenario.json b/contracts/v6/fixtures/demo.scenario.json new file mode 100644 index 0000000..5b4303c --- /dev/null +++ b/contracts/v6/fixtures/demo.scenario.json @@ -0,0 +1,294 @@ +{ + "contract_version": "6", + "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_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_gross_exposure": "1000000", + "max_leverage": "2", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "short_borrow_bps": 100 + }, + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "1", + "participation_bps": 5000, + "fixed_fee": "0.25", + "fee_bps": 10 + } + }, + "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": [] + }, + { + "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": "12" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [] + }, + { + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v6/fixtures/demo.scenario.jsonl b/contracts/v6/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..712bef4 --- /dev/null +++ b/contracts/v6/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"6","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_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_gross_exposure":"1000000","max_leverage":"2","initial_margin_bps":5000,"maintenance_margin_bps":2500,"short_borrow_bps":100},"execution":{"model":"completed_bar_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000}} +{"contract_version":"6","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"}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"6","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"6","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"}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"6","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"}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"6","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v6/fixtures/fill-clipped.journal.jsonl b/contracts/v6/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..14bd262 --- /dev/null +++ b/contracts/v6/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,12 @@ +{"contract_version":"6","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":"0ba5499f954080438421e7bf439a0258214392ab6baab7d69015d8c622d5c04c","execution_model":"completed_bar_v1"}} +{"contract_version":"6","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false}}}} +{"contract_version":"6","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false}}} +{"contract_version":"6","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":[]}} +{"contract_version":"6","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":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"6","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":"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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false}}} +{"contract_version":"6","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","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":[]}} +{"contract_version":"6","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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":"5","price":"100"}} +{"contract_version":"6","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"fill-clipped-fill-000000000001","order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"5","price":"100","notional":"500","fee":"10","executed_at":"2026-02-03T14:30:00.000000Z","slice_sequence":"2"}} +{"contract_version":"6","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"5","filled_notional":"500","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"6","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000007"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}}} +{"contract_version":"6","engine_sequence":"12","event_id":"fill-clipped-event-000000000012","causation_ids":["fill-clipped-event-000000000011"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"0ba5499f954080438421e7bf439a0258214392ab6baab7d69015d8c622d5c04c","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v6/fixtures/fill-clipped.scenario.json b/contracts/v6/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..b72cefa --- /dev/null +++ b/contracts/v6/fixtures/fill-clipped.scenario.json @@ -0,0 +1,164 @@ +{ + "contract_version": "6", + "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_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "short_borrow_bps": 100 + }, + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "1", + "participation_bps": 10000, + "fixed_fee": "10", + "fee_bps": 0 + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "market", + "limit_price": 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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v6/journal.schema.json b/contracts/v6/journal.schema.json new file mode 100644 index 0000000..0203411 --- /dev/null +++ b/contracts/v6/journal.schema.json @@ -0,0 +1,186 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v6/journal.schema.json", + "title": "Trading Engine v6 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": "6" }, + "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_cancelled", "split_applied", "cash_dividend_applied", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] + }, + "payload": { "type": "object" } + }, + "allOf": [ + { "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"] } } }, "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } + }, + "initialState": { + "type": "object", "additionalProperties": false, + "required": ["portfolio", "valuation"], + "properties": { + "portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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" } } } + ] + }, + "marketSlice": { + "type": "object", "additionalProperties": false, + "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], + "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" } } + } + }, + "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", "limit_price", "origin", "created_event_id", "updated_event_id", "created_sequence", "created_at", "eligible_after_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"] }, "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, "origin": { "enum": ["direct", "target_rebalance", "margin_liquidation"] }, "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" }, "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", "margin_call"] } } + }, + "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" } } + }, + "orderAdjusted": { + "type": "object", "additionalProperties": false, "required": ["order", "action_id"], + "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } + }, + "fill": { + "type": "object", "additionalProperties": false, + "required": ["fill_id", "order_id", "instrument_id", "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", "slice_sequence"], + "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/unsignedDecimal" }, "executed_at": { "$ref": "#/$defs/timestamp" }, "slice_sequence": { "$ref": "#/$defs/sequence" } } + }, + "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 } } + }, + "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" } } } + ] + }, + "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" } } + }, + "cashAttribution": { + "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], + "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } + }, + "positionAttribution": { + "type": "object", "additionalProperties": false, + "required": ["instrument_id", "quote_currency", "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"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, "quote_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "base_execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_total_fees": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "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" } } + }, + "valuation": { + "type": "object", "additionalProperties": false, + "required": ["base_currency", "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", "total_fees", "cash_balances", "positions", "margin"], + "properties": { + "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "cash_balances": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashAttribution" } }, "positions": { "type": "array", "items": { "$ref": "#/$defs/positionAttribution" } }, "margin": { "$ref": "#/$defs/margin" } + } + }, + "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": { "const": "completed_bar_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/v6/scenario-stream.schema.json b/contracts/v6/scenario-stream.schema.json new file mode 100644 index 0000000..f70b604 --- /dev/null +++ b/contracts/v6/scenario-stream.schema.json @@ -0,0 +1,76 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v6/scenario-stream.schema.json", + "title": "Trading Engine v6 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": "6" }, + "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": "6" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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": "6" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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", "max_internal_events"], + "properties": { + "metadata": { "type": "object" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/execution" }, + "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/v6/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v6/scenario.schema.json b/contracts/v6/scenario.schema.json new file mode 100644 index 0000000..d5e75ea --- /dev/null +++ b/contracts/v6/scenario.schema.json @@ -0,0 +1,369 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json", + "title": "Trading Engine v6 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", "max_internal_events", "schedule", "slices"], + "properties": { + "contract_version": { "const": "6" }, + "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" }, + "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": { + "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_order_quantity", "max_long_position", "max_short_position", "max_gross_exposure", "max_leverage", "initial_margin_bps", "maintenance_margin_bps", "short_borrow_bps"], + "properties": { + "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, + "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "execution": { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + "completedBarV1Configuration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fixed_fee", "fee_bps"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, + "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "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", "limit_price"], + "properties": { + "type": { "const": "submit_order" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "order_kind": { "enum": ["market", "limit"] }, + "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] } + } + }, + "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"], + "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" } } + } + }, + "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" } + } + } + ] + } + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index 8544219..40e5de8 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/v5/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v6/README.md) remain authoritative for process boundaries. diff --git a/docs/architecture.md b/docs/architecture.md index c08b42f..b1b1bbf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -123,7 +123,7 @@ Running the same scenario bytes produces byte-identical audit lines. `Engine.Interactive` stops at each strategy request and exposes the immutable context and event. Its `resume` transition accepts typed intents and continues the same pure reducer. The scripted runner invokes an in-process callback at that boundary. The external runner serializes it through -protocol v3. Reducer state never contains a process, clock, pipe, timeout, or file handle. +protocol v4. Reducer state never contains a process, clock, pipe, timeout, or file handle. Each strategy callback carries an account valuation built at that reducer boundary. All callbacks for a slice use its receipt time, completed bars, and FX vector. A callback response is reduced diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 22dacea..711e17b 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -16,10 +16,13 @@ range declared by the package rather than pretending to be reproducible locks. A required Ubuntu cell means the declared support bounds or the implementation must change. The macOS cell is an early portability signal while Ubuntu remains the supported build platform. -Every runtime cell replays the v3 demo, v5 demo, and v5 risk-limited fill scenarios under `TZ=UTC` -and the C locale. It compares the resulting journal files byte for byte with their 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. +Every runtime cell replays the frozen v3 demo, v5 demo, and v5 risk-limited fill scenarios under +`TZ=UTC` and the C locale. It compares the resulting journal files byte for byte with their +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 v6 batch, stream, journal, and +strategy-v4 fixtures, including the initial portfolio and its reconciled first valuation. Coverage runs once in the exact locked Ubuntu environment. The required Persistra job also runs once against its full pinned commit; it is not repeated across dependency or operating-system diff --git a/docs/execution-model.md b/docs/execution-model.md index 2e3a435..ff8995c 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,11 +1,11 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v5 advertises and accepts `completed_bar_v1`; embedders can inject another module through +Contract v6 advertises and accepts `completed_bar_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 v5 envelope separates selection from +Each compiled model owns a strict configuration contract. The v6 envelope separates selection from model-specific parameters: ```json @@ -24,7 +24,7 @@ model-specific parameters: The model and configuration version are validated before replay. Unknown models, unsupported model/version pairs, missing fields, and fields from another model are rejected. Contracts v3 and -v4 retain their frozen flat execution object. +v4 retain their frozen flat execution object; v5 retains its frozen configured envelope. `--capabilities` preserves the `execution_models` name list and publishes one deterministic descriptor per model under `execution_model_contracts`: supported scenario and configuration diff --git a/docs/persistra.md b/docs/persistra.md index fdecb17..1b5e696 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -17,7 +17,7 @@ The JSON scenario carries: - One explicit executable-instrument catalog - Signed position, exposure, leverage, margin, borrow, participation, and fee policies - Strictly increasing synchronized market slices with complete FX marks and corporate actions -- Explicit initial cash ledgers for every base or quote currency +- Explicit signed initial cash and positions with accounting history, marks, and FX state - Optional scheduled full-portfolio signed weight or fractional quantity targets - Optional direct orders, cancellations, and metrics @@ -53,12 +53,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 also advertises current contract v5 while retaining v4 and exact v3 +structural checks. The engine advertises current contract v6 while retaining v5, v4, 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 v3](../contracts/strategy/v3/README.md). Persistra's host turns protocol +[strategy protocol v4](../contracts/strategy/v4/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, @@ -78,14 +78,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 v5 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v6 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 v3; a host must complete its exact initialization, event, + external boundary is strategy v4; 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 e1a9d19..d54d166 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 v5 demo](../contracts/v5/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v5/scenario.schema.json) provides structural validation. +Use [the v6 demo](../contracts/v6/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v6/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/v5/scenario-stream.schema.json) validates each line, -and [the v5 stream fixture](../contracts/v5/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v6/scenario-stream.schema.json) validates each line, +and [the v6 stream fixture](../contracts/v6/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,11 +42,11 @@ retains current account, order, target, and latest-bar state required by executi | Field | Meaning | |---|---| -| `contract_version` | Required string identifying this file contract; v5 is `"5"` | +| `contract_version` | Required string identifying this file contract; v6 is `"6"` | | `metadata` | Required arbitrary JSON object preserved for provenance and ignored by execution | | `run_id` | Stable identity used in generated IDs | | `base_currency` | Reporting currency used for aggregate risk and valuation | -| `initial_cash` | One explicit nonnegative balance for every scenario currency | +| `initial_portfolio` | Signed cash and positions with accounting history, marks, and FX state | | `instruments` | Approved executable-instrument catalog, at most 4,096 entries | | `venue_calendars` | Immutable venue/session policies covering every configured instrument | | `risk` | Signed position, exposure, leverage, margin, and borrow policy | @@ -70,12 +70,26 @@ record without a line feed and drains an oversized record without retaining byte Each instrument contains `instrument_id`, `symbol`, `quote_currency`, `tick_size`, and `lot_size`. Identifiers and labels are nonempty and contain no whitespace or control characters. Tick and lot sizes are positive exact values with at most six decimal places. Quote currencies may differ from -`base_currency`; `initial_cash` contains every distinct quote currency plus the base currency -exactly once. +`base_currency`; `initial_portfolio.cash` contains every distinct quote currency plus the base +currency exactly once. + +## Initial portfolio + +The v6 `initial_portfolio` contains `cash`, `positions`, `marks`, and `fx_rates`. Cash is signed and +has exact scenario-currency coverage. Each nonzero signed position names a catalog instrument and +records signed `quantity` and `cost_basis`, signed `realized_pnl` and `dividend_pnl`, and +nonnegative `execution_fees` and `borrow_fees`. Basis has the same sign as quantity. These P&L and +fee values are point-in-time histories; importing them does not apply them to cash again. + +Position quantities align to instrument lots and respect long and short limits. Marks cover the +position set exactly, are positive, and align to instrument ticks. FX rates cover every scenario +currency exactly and the base rate is one. Before replay, the engine constructs the account, +reconciles its valuation, and enforces gross exposure, leverage, and initial margin. It accepts +negative cash when the complete marked account remains valid under the configured risk policy. ## Venue calendars -Contract v5 requires every instrument to belong to exactly one explicit venue calendar. A calendar +Contract v6 requires every instrument to belong to exactly one explicit venue calendar. A calendar is identified by `venue_id`, `calendar_id`, and `calendar_version`; version 1 is the only supported calendar payload. Its `sessions` are unique and ordered by `session_date`, and each date declares one policy: `regular`, `early_close`, or `holiday`. Holidays have no phases. Open sessions must @@ -94,7 +108,7 @@ annualized `short_borrow_bps`. Initial margin cannot be below maintenance margin quantity limit must cover at least one lot for every instrument. Orders that increase gross exposure must satisfy every applicable limit; exposure-reducing orders remain admissible. -Contract v5 execution contains a stable `model` and a model-owned `configuration`. For +Contract v6 execution contains a stable `model` and a model-owned `configuration`. For `completed_bar_v1`, configuration version `"1"` contains: - `version`, the strict model-configuration contract version @@ -104,7 +118,7 @@ Contract v5 execution contains a stable `model` and a model-owned `configuration The engine advertises each model's scenario and configuration versions, required fields, supported order types, data requirements, and limits through `--capabilities.execution_model_contracts`. The -v3 and v4 scenario contracts preserve their flat execution object unchanged. +v3 and v4 scenario contracts preserve their flat execution object unchanged; v5 remains frozen. ## Schedule and intents @@ -198,14 +212,16 @@ than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v5/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v6/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 so a journal remains self-describing when it is streamed or split. -The first record is `run_started` with `scenario_sha256` and the selected execution model. The CLI -hashes the exact batch document or stream bytes it parses. +The first record is `run_started` with `scenario_sha256` and the selected execution model. In v6, +`initial_state` then records the imported portfolio and reconciled valuation, followed by an +initial `valuation`; both precede the first market slice. The CLI hashes the exact batch document +or stream bytes it parses. `market_slice_received` contains the complete normalized slice. Portfolio requests record their basis, original weight when applicable, computed quantity, and sizing reference price. Orders use `eligible_after_slice_sequence`; fills use `slice_sequence`. `fill_clipped` records the proposed diff --git a/lib/account.ml b/lib/account.ml index 9b18274..d63f652 100644 --- a/lib/account.ml +++ b/lib/account.ml @@ -115,6 +115,36 @@ let create ~base_currency ~initial_cash = positions = Id.Instrument.Map.empty; } +let of_initial_portfolio (initial : Initial_portfolio.t) = + let cash = + List.fold_left + (fun balances (currency, amount) -> + Currency_map.add currency amount balances) + Currency_map.empty initial.cash + in + let positions = + List.fold_left + (fun positions (value : Initial_portfolio.position) -> + Id.Instrument.Map.add value.instrument_id + { + quantity = value.quantity; + cost_basis = value.cost_basis; + realized_pnl = value.realized_pnl; + dividend_pnl = value.dividend_pnl; + execution_fees = value.execution_fees; + borrow_fees = value.borrow_fees; + } + positions) + Id.Instrument.Map.empty initial.positions + in + Ok + { + base_currency = initial.base_currency; + initial_cash = cash; + cash; + positions; + } + let base_currency (state : t) = state.base_currency let initial_cash (state : t) = Currency_map.bindings state.initial_cash let cash_balances (state : t) = Currency_map.bindings state.cash diff --git a/lib/account.mli b/lib/account.mli index a0845a7..d581d4a 100644 --- a/lib/account.mli +++ b/lib/account.mli @@ -67,6 +67,7 @@ val create : initial_cash:(string * Scalar.Money.t) list -> (t, string) result +val of_initial_portfolio : Initial_portfolio.t -> (t, string) result val base_currency : t -> string val initial_cash : t -> (string * Scalar.Money.t) list val cash_balances : t -> (string * Scalar.Money.t) list diff --git a/lib/audit.ml b/lib/audit.ml index 532b8e6..0615c7f 100644 --- a/lib/audit.ml +++ b/lib/audit.ml @@ -25,6 +25,7 @@ type valuation = { account : Account.valuation; margin : Risk.margin_snapshot } type event = | Run_started of { scenario_sha256 : string; execution_model : string } + | Initial_state of { portfolio : Initial_portfolio.t; valuation : valuation } | Market_slice_received of Market_slice.t | Target_portfolio_requested of { basis : target_basis; @@ -120,6 +121,7 @@ let target_basis_to_string = function let event_name = function | Run_started _ -> "run_started" + | Initial_state _ -> "initial_state" | Market_slice_received _ -> "market_slice_received" | Target_portfolio_requested _ -> "target_portfolio_requested" | Order_accepted _ -> "order_accepted" diff --git a/lib/audit.mli b/lib/audit.mli index 154ad0e..e48b7fe 100644 --- a/lib/audit.mli +++ b/lib/audit.mli @@ -27,6 +27,7 @@ type valuation = { account : Account.valuation; margin : Risk.margin_snapshot } type event = | Run_started of { scenario_sha256 : string; execution_model : string } + | Initial_state of { portfolio : Initial_portfolio.t; valuation : valuation } | Market_slice_received of Market_slice.t | Target_portfolio_requested of { basis : target_basis; diff --git a/lib/codec.ml b/lib/codec.ml index 8be46ea..02c9c8c 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -210,6 +210,46 @@ let fill_to_yojson fill = ("slice_sequence", int64 fill.slice_sequence); ] +let initial_position_to_yojson (position : Initial_portfolio.position) = + `Assoc + [ + ("instrument_id", instrument_id position.instrument_id); + ("quantity", quantity position.quantity); + ("cost_basis", money position.cost_basis); + ("realized_pnl", money position.realized_pnl); + ("dividend_pnl", money position.dividend_pnl); + ("execution_fees", money position.execution_fees); + ("borrow_fees", money position.borrow_fees); + ] + +let initial_portfolio_to_yojson (portfolio : Initial_portfolio.t) = + let cash = + List.map + (fun (currency, amount) -> + `Assoc [ ("currency", string currency); ("amount", money amount) ]) + portfolio.cash + in + let marks = + List.map + (fun (id, value) -> + `Assoc [ ("instrument_id", instrument_id id); ("price", price value) ]) + portfolio.marks + in + let fx_rates = + List.map + (fun (currency, rate) -> + `Assoc [ ("currency", string currency); ("rate", price rate) ]) + portfolio.fx_rates + in + `Assoc + [ + ("cash", `List cash); + ( "positions", + `List (List.map initial_position_to_yojson portfolio.positions) ); + ("marks", `List marks); + ("fx_rates", `List fx_rates); + ] + let position_attribution_to_yojson position = `Assoc [ @@ -311,6 +351,12 @@ let payload_to_yojson = function ("scenario_sha256", string scenario_sha256); ("execution_model", string execution_model); ] + | Audit.Initial_state { portfolio; valuation } -> + `Assoc + [ + ("portfolio", initial_portfolio_to_yojson portfolio); + ("valuation", valuation_to_yojson valuation); + ] | Audit.Market_slice_received market_slice -> market_slice_to_yojson market_slice | Audit.Target_portfolio_requested { basis; targets } -> diff --git a/lib/codec.mli b/lib/codec.mli index aee2617..8c36bc7 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -6,5 +6,6 @@ val bar_to_yojson : Bar.t -> Yojson.Safe.t val market_slice_to_yojson : Market_slice.t -> Yojson.Safe.t val order_to_yojson : Order.t -> Yojson.Safe.t val fill_to_yojson : Fill.t -> Yojson.Safe.t +val initial_portfolio_to_yojson : Initial_portfolio.t -> Yojson.Safe.t val audit_to_yojson : Audit.t -> Yojson.Safe.t val audit_to_string : Audit.t -> string diff --git a/lib/contract.ml b/lib/contract.ml index 6903702..2b16f3d 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,9 +1,12 @@ -let version = "5" -let previous_version = "4" -let legacy_journal_version = "3" -let supported_versions = [ version; previous_version; legacy_journal_version ] +let version = "6" +let previous_version = "5" +let legacy_journal_version = "4" + +let supported_versions = + [ version; previous_version; legacy_journal_version; "3" ] + let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "3" +let strategy_protocol_version = "4" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) diff --git a/lib/engine.ml b/lib/engine.ml index 6e512de..4d232a3 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -52,7 +52,9 @@ module Interactive = struct last_slice_end : Ptime.t option; last_received_at : Ptime.t option; latest_bars : Bar.t Id.Instrument.Map.t; + latest_marks : Scalar.Price.t Id.Instrument.Map.t; latest_fx_rates : (string * Scalar.Price.t) list; + initial_portfolio : Initial_portfolio.t option; applied_action_ids : Id.Corporate_action.Set.t; desired_targets : desired_targets option; liquidation_pending : bool; @@ -97,17 +99,44 @@ module Interactive = struct processed : int; } + let create_state ~run_id ~scenario_sha256 ~config ~account ~latest_marks + ~latest_fx_rates ~initial_portfolio = + Ok + { + run_id; + scenario_sha256; + config; + engine_sequence = 0L; + next_order_number = 1L; + next_fill_number = 1L; + last_slice_sequence = None; + last_slice_end = None; + last_received_at = None; + latest_bars = Id.Instrument.Map.empty; + latest_marks; + latest_fx_rates; + initial_portfolio; + applied_action_ids = Id.Corporate_action.Set.empty; + desired_targets = None; + liquidation_pending = false; + account; + oms = Oms.empty; + started = false; + completed = false; + } + + let expected_currencies config = + Risk.base_currency config.risk + :: List.map + (fun instrument -> instrument.Instrument.quote_currency) + (Risk.instruments config.risk) + |> List.sort_uniq String.compare + let create ~run_id ~scenario_sha256 ~config ~initial_cash = if not (valid_sha256 scenario_sha256) then Error "scenario SHA-256 must contain 64 lowercase hexadecimal characters" else - let expected_currencies = - Risk.base_currency config.risk - :: List.map - (fun instrument -> instrument.Instrument.quote_currency) - (Risk.instruments config.risk) - |> List.sort_uniq String.compare - in + let expected_currencies = expected_currencies config in let supplied_currencies = List.map fst initial_cash |> List.sort_uniq String.compare in @@ -124,28 +153,43 @@ module Interactive = struct let base_rate = Scalar.Price.of_decimal_string "1" |> Result.get_ok in - Ok - { - run_id; - scenario_sha256; - config; - engine_sequence = 0L; - next_order_number = 1L; - next_fill_number = 1L; - last_slice_sequence = None; - last_slice_end = None; - last_received_at = None; - latest_bars = Id.Instrument.Map.empty; - latest_fx_rates = - [ (Risk.base_currency config.risk, base_rate) ]; - applied_action_ids = Id.Corporate_action.Set.empty; - desired_targets = None; - liquidation_pending = false; - account; - oms = Oms.empty; - started = false; - completed = false; - } + create_state ~run_id ~scenario_sha256 ~config ~account + ~latest_marks:Id.Instrument.Map.empty + ~latest_fx_rates:[ (Risk.base_currency config.risk, base_rate) ] + ~initial_portfolio:None + + let create_with_portfolio ~run_id ~scenario_sha256 ~config ~initial_portfolio + = + if not (valid_sha256 scenario_sha256) then + Error "scenario SHA-256 must contain 64 lowercase hexadecimal characters" + else if + not + (String.equal initial_portfolio.Initial_portfolio.base_currency + (Risk.base_currency config.risk)) + then Error "initial portfolio base currency differs from risk configuration" + else + let supplied = + List.map fst initial_portfolio.cash |> List.sort_uniq String.compare + in + if supplied <> expected_currencies config then + Error "initial cash must contain every configured currency exactly once" + else + let* account = Account.of_initial_portfolio initial_portfolio in + let latest_marks = + List.fold_left + (fun marks (instrument_id, price) -> + Id.Instrument.Map.add instrument_id price marks) + Id.Instrument.Map.empty initial_portfolio.marks + in + let* valuation = + Account.value account + ~instruments:(Risk.instruments config.risk) + ~marks:initial_portfolio.marks ~fx_rates:initial_portfolio.fx_rates + in + let* () = Risk.check_initial config.risk valuation in + create_state ~run_id ~scenario_sha256 ~config ~account ~latest_marks + ~latest_fx_rates:initial_portfolio.fx_rates + ~initial_portfolio:(Some initial_portfolio) let account state = state.account let oms state = state.oms @@ -182,20 +226,42 @@ module Interactive = struct let emit reduction event = emit_with_id reduction event |> Result.map fst + let value state = + Account.value state.account + ~instruments:(Risk.instruments state.config.risk) + ~marks:(Id.Instrument.Map.bindings state.latest_marks) + ~fx_rates:state.latest_fx_rates + let ensure_started reduction = if reduction.state.started then Ok reduction else let state = { reduction.state with started = true } in - emit_with_id - (with_causes { reduction with state } []) - (Audit.Run_started - { - scenario_sha256 = reduction.state.scenario_sha256; - execution_model = - Execution_model.name reduction.state.config.execution_model; - }) - |> Result.map (fun (reduction, event_id) -> - with_causes reduction [ event_id ]) + let* reduction, event_id = + emit_with_id + (with_causes { reduction with state } []) + (Audit.Run_started + { + scenario_sha256 = reduction.state.scenario_sha256; + execution_model = + Execution_model.name reduction.state.config.execution_model; + }) + in + let reduction = with_causes reduction [ event_id ] in + match reduction.state.initial_portfolio with + | None -> Ok reduction + | Some portfolio -> + let* account = value reduction.state in + let* margin = + Risk.margin_snapshot reduction.state.config.risk account + in + let valuation = Audit.{ account; margin } in + let* reduction, initial_event_id = + emit_with_id reduction + (Audit.Initial_state { portfolio; valuation }) + in + emit + (with_causes reduction [ initial_event_id ]) + (Audit.Valuation valuation) let enqueue reduction items = { reduction with pending = Pending_queue.enqueue reduction.pending items } @@ -203,16 +269,6 @@ module Interactive = struct let prepend reduction items = { reduction with pending = Pending_queue.prepend reduction.pending items } - let value state = - let marks = - Id.Instrument.Map.bindings state.latest_bars - |> List.map (fun (instrument_id, bar) -> - (instrument_id, bar.Bar.close_price)) - in - Account.value state.account - ~instruments:(Risk.instruments state.config.risk) - ~marks ~fx_rates:state.latest_fx_rates - let strategy_context state now = let latest_bars = Id.Instrument.Map.bindings state.latest_bars |> List.map snd @@ -279,9 +335,7 @@ module Interactive = struct in match let marks = - Id.Instrument.Map.bindings reduction.state.latest_bars - |> List.map (fun (instrument_id, bar) -> - (instrument_id, bar.Bar.close_price)) + Id.Instrument.Map.bindings reduction.state.latest_marks in Risk.check reduction.state.config.risk ~account:reduction.state.account ~oms:reduction.state.oms ~marks @@ -679,16 +733,7 @@ module Interactive = struct > 0 then Error "target gross weight exceeds maximum leverage" else - let* valuation = - let marks = - Id.Instrument.Map.bindings state.latest_bars - |> List.map (fun (instrument_id, bar) -> - (instrument_id, bar.Bar.close_price)) - in - Account.value state.account - ~instruments:(Risk.instruments state.config.risk) - ~marks ~fx_rates:state.latest_fx_rates - in + let* valuation = value state in let add result (target : Strategy.weight_target) = let* desired, requested = result in match @@ -1333,19 +1378,11 @@ module Interactive = struct (fun bars bar -> Id.Instrument.Map.add bar.Bar.instrument_id bar bars) state.latest_bars market_slice.bars in - let state = - { - state with - last_slice_sequence = Some market_slice.slice_sequence; - last_slice_end = Some market_slice.end_at; - last_received_at = Some market_slice.received_at; - latest_fx_rates = - List.map - (fun mark -> (mark.Market_slice.currency, mark.Market_slice.rate)) - market_slice.fx_rates; - latest_bars; - applied_action_ids; - } + let latest_marks = + List.fold_left + (fun marks bar -> + Id.Instrument.Map.add bar.Bar.instrument_id bar.close_price marks) + state.latest_marks market_slice.bars in let reduction = { @@ -1360,6 +1397,22 @@ module Interactive = struct } in let* reduction = ensure_started reduction in + let state = + { + reduction.state with + last_slice_sequence = Some market_slice.slice_sequence; + last_slice_end = Some market_slice.end_at; + last_received_at = Some market_slice.received_at; + latest_fx_rates = + List.map + (fun mark -> (mark.Market_slice.currency, mark.Market_slice.rate)) + market_slice.fx_rates; + latest_bars; + latest_marks; + applied_action_ids; + } + in + let reduction = { reduction with state } in let* reduction, slice_event_id = emit_with_id (with_causes reduction []) (Audit.Market_slice_received market_slice) @@ -1564,6 +1617,12 @@ module Make (Strategy_impl : Strategy.S) = struct Interactive.create ~run_id ~scenario_sha256 ~config ~initial_cash |> Result.map (fun engine -> { engine; strategy_state }) + let create_with_portfolio ~run_id ~scenario_sha256 ~config ~initial_portfolio + ~strategy_state = + Interactive.create_with_portfolio ~run_id ~scenario_sha256 ~config + ~initial_portfolio + |> Result.map (fun engine -> { engine; strategy_state }) + let account state = Interactive.account state.engine let oms state = Interactive.oms state.engine diff --git a/lib/engine.mli b/lib/engine.mli index 21228a6..72b3835 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -21,6 +21,13 @@ module Interactive : sig initial_cash:(string * Scalar.Money.t) list -> (t, string) result + val create_with_portfolio : + run_id:Id.Run.t -> + scenario_sha256:string -> + config:config -> + initial_portfolio:Initial_portfolio.t -> + (t, string) result + val account : t -> Account.t val oms : t -> Oms.t val latest_bar : t -> Id.Instrument.t -> Bar.t option @@ -42,6 +49,14 @@ module Make (Strategy_impl : Strategy.S) : sig strategy_state:Strategy_impl.state -> (t, string) result + val create_with_portfolio : + run_id:Id.Run.t -> + scenario_sha256:string -> + config:config -> + initial_portfolio:Initial_portfolio.t -> + strategy_state:Strategy_impl.state -> + (t, string) result + val account : t -> Account.t val oms : t -> Oms.t val latest_bar : t -> Id.Instrument.t -> Bar.t option diff --git a/lib/execution_model.ml b/lib/execution_model.ml index ca432d1..e3ac8eb 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -33,7 +33,7 @@ let supported = List.map name builtins let completed_bar_v1_contract = { version = "1"; - scenario_contract_versions = [ "5"; "4"; "3" ]; + scenario_contract_versions = [ "6"; "5"; "4"; "3" ]; required_fields = [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ]; supported_order_types = [ "market"; "limit" ]; data_requirements = [ "completed_ohlcv_bars" ]; diff --git a/lib/external_replay.ml b/lib/external_replay.ml index 583f248..130c2ba 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -63,6 +63,7 @@ let initialization_of_scenario ~scenario_sha256 (scenario : Scenario.t) = run_id = scenario.run_id; base_currency = scenario.base_currency; initial_cash = scenario.initial_cash; + initial_portfolio = scenario.initial_portfolio; instruments = scenario.instruments; risk = scenario.risk; execution_model = scenario.execution_model; @@ -79,6 +80,7 @@ let initialization_of_header ~scenario_sha256 (header : Scenario.stream_header) run_id = header.run_id; base_currency = header.base_currency; initial_cash = header.initial_cash; + initial_portfolio = header.initial_portfolio; instruments = header.instruments; risk = header.risk; execution_model = header.execution_model; @@ -86,13 +88,21 @@ let initialization_of_header ~scenario_sha256 (header : Scenario.stream_header) } let create_runner ~contract_version ~run_id ~scenario_sha256 ~risk - ~execution_model ~execution ~max_internal_events ~initial_cash = + ~execution_model ~execution ~max_internal_events ~initial_cash + ~initial_portfolio = let* config = Engine.config ~contract_version ~risk ~execution_model ~execution ~max_internal_events |> reducer_result in - Runner.create ~run_id ~scenario_sha256 ~config ~initial_cash |> reducer_result + match initial_portfolio with + | None -> + Runner.create ~run_id ~scenario_sha256 ~config ~initial_cash + |> reducer_result + | Some initial_portfolio -> + Runner.create_with_portfolio ~run_id ~scenario_sha256 ~config + ~initial_portfolio + |> reducer_result let append_events journal events = match journal with @@ -170,6 +180,7 @@ let run ?(durability = Artifact_writer.Buffered) ~env ~scenario_sha256 ~execution_model:scenario.execution_model ~execution:scenario.execution ~max_internal_events:scenario.max_internal_events ~initial_cash:scenario.initial_cash + ~initial_portfolio:scenario.initial_portfolio in let* journal, transcript = create_artifacts ~durability ~journal_path ~transcript_path @@ -224,6 +235,7 @@ let validate_stream_pass ~scenario_sha256 channel = ~execution_model:header.execution_model ~execution:header.execution ~max_internal_events:header.max_internal_events ~initial_cash:header.initial_cash + ~initial_portfolio:header.initial_portfolio in Ok (runner, initialization_of_header ~scenario_sha256 header, 0L)) ~step:(fun (runner, initialization, slice_count) item -> @@ -252,6 +264,7 @@ let replay_stream_pass ~scenario_sha256 ~journal ~session channel = ~execution_model:header.execution_model ~execution:header.execution ~max_internal_events:header.max_internal_events ~initial_cash:header.initial_cash + ~initial_portfolio:header.initial_portfolio in Ok { runner; journal = Some journal; audit_count = 0L }) ~step:(fun state item -> diff --git a/lib/initial_portfolio.ml b/lib/initial_portfolio.ml new file mode 100644 index 0000000..5bf986d --- /dev/null +++ b/lib/initial_portfolio.ml @@ -0,0 +1,120 @@ +type position = { + instrument_id : Id.Instrument.t; + quantity : Scalar.Quantity.t; + cost_basis : Scalar.Money.t; + realized_pnl : Scalar.Money.t; + dividend_pnl : Scalar.Money.t; + execution_fees : Scalar.Money.t; + borrow_fees : Scalar.Money.t; +} + +type t = { + base_currency : string; + cash : (string * Scalar.Money.t) list; + positions : position list; + marks : (Id.Instrument.t * Scalar.Price.t) list; + fx_rates : (string * Scalar.Price.t) list; +} + +let ( let* ) result function_ = + match result with Ok value -> function_ value | Error _ as error -> error + +let valid_currency value = + String.length value > 0 + && String.for_all + (fun character -> + let code = Char.code character in + code >= 0x21 && code <> 0x7f) + value + +let position ~instrument_id ~quantity ~cost_basis ~realized_pnl ~dividend_pnl + ~execution_fees ~borrow_fees = + if Scalar.Quantity.is_zero quantity then + Error "initial position quantity must be nonzero" + else if + Scalar.Quantity.is_positive quantity + <> (Scalar.Money.compare cost_basis Scalar.Money.zero > 0) + then Error "initial position cost basis must have the same sign as quantity" + else if Scalar.Money.compare execution_fees Scalar.Money.zero < 0 then + Error "initial execution fees must be nonnegative" + else if Scalar.Money.compare borrow_fees Scalar.Money.zero < 0 then + Error "initial borrow fees must be nonnegative" + else + Ok + { + instrument_id; + quantity; + cost_basis; + realized_pnl; + dividend_pnl; + execution_fees; + borrow_fees; + } + +let unique compare values = + List.length values = List.length (List.sort_uniq compare values) + +let create ~base_currency ~cash ~positions ~marks ~fx_rates = + if not (valid_currency base_currency) then + Error "base currency must not be empty or contain whitespace" + else if cash = [] then Error "initial cash must contain at least one currency" + else if not (List.for_all (fun (currency, _) -> valid_currency currency) cash) + then Error "cash currency must not be empty or contain whitespace" + else if not (unique String.compare (List.map fst cash)) then + Error "initial cash currencies must be unique" + else if not (List.mem_assoc base_currency cash) then + Error "initial cash must include the base currency" + else + let position_ids = List.map (fun value -> value.instrument_id) positions in + let mark_ids = List.map fst marks in + let fx_currencies = List.map fst fx_rates in + if not (unique Id.Instrument.compare position_ids) then + Error "initial position instrument IDs must be unique" + else if not (unique Id.Instrument.compare mark_ids) then + Error "initial mark instrument IDs must be unique" + else if + List.sort Id.Instrument.compare position_ids + <> List.sort Id.Instrument.compare mark_ids + then Error "initial marks must cover every initial position exactly once" + else if not (unique String.compare fx_currencies) then + Error "initial FX currencies must be unique" + else if + List.sort String.compare (List.map fst cash) + <> List.sort String.compare fx_currencies + then Error "initial FX rates must cover every cash currency exactly once" + else + match List.assoc_opt base_currency fx_rates with + | None -> Error "initial FX rates must include the base currency" + | Some rate -> + let one = Scalar.Price.of_decimal_string "1" |> Result.get_ok in + if Scalar.Price.compare rate one <> 0 then + Error "initial base-currency FX rate must equal one" + else + Ok + { + base_currency; + cash = + List.sort + (fun (left, _) (right, _) -> String.compare left right) + cash; + positions = + List.sort + (fun left right -> + Id.Instrument.compare left.instrument_id + right.instrument_id) + positions; + marks = + List.sort + (fun (left, _) (right, _) -> + Id.Instrument.compare left right) + marks; + fx_rates = + List.sort + (fun (left, _) (right, _) -> String.compare left right) + fx_rates; + } + +let cash_only ~base_currency ~cash = + let one = Scalar.Price.of_decimal_string "1" |> Result.get_ok in + let fx_rates = List.map (fun (currency, _) -> (currency, one)) cash in + create ~base_currency ~cash ~positions:[] ~marks:[] ~fx_rates diff --git a/lib/initial_portfolio.mli b/lib/initial_portfolio.mli new file mode 100644 index 0000000..2143ec8 --- /dev/null +++ b/lib/initial_portfolio.mli @@ -0,0 +1,42 @@ +(** Immutable point-in-time portfolio state used to start a replay. *) + +type position = private { + instrument_id : Id.Instrument.t; + quantity : Scalar.Quantity.t; + cost_basis : Scalar.Money.t; + realized_pnl : Scalar.Money.t; + dividend_pnl : Scalar.Money.t; + execution_fees : Scalar.Money.t; + borrow_fees : Scalar.Money.t; +} + +type t = private { + base_currency : string; + cash : (string * Scalar.Money.t) list; + positions : position list; + marks : (Id.Instrument.t * Scalar.Price.t) list; + fx_rates : (string * Scalar.Price.t) list; +} + +val position : + instrument_id:Id.Instrument.t -> + quantity:Scalar.Quantity.t -> + cost_basis:Scalar.Money.t -> + realized_pnl:Scalar.Money.t -> + dividend_pnl:Scalar.Money.t -> + execution_fees:Scalar.Money.t -> + borrow_fees:Scalar.Money.t -> + (position, string) result + +val create : + base_currency:string -> + cash:(string * Scalar.Money.t) list -> + positions:position list -> + marks:(Id.Instrument.t * Scalar.Price.t) list -> + fx_rates:(string * Scalar.Price.t) list -> + (t, string) result + +val cash_only : + base_currency:string -> + cash:(string * Scalar.Money.t) list -> + (t, string) result diff --git a/lib/replay.ml b/lib/replay.ml index 37e5991..d914d89 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -81,8 +81,13 @@ let run ~scenario_sha256 ?journal_path ?(durability = Artifact_writer.Buffered) |> reducer_result in let* initial = - Runner.create ~run_id:scenario.run_id ~scenario_sha256 ~config - ~initial_cash:scenario.initial_cash ~strategy_state + (match scenario.initial_portfolio with + | None -> + Runner.create ~run_id:scenario.run_id ~scenario_sha256 ~config + ~initial_cash:scenario.initial_cash ~strategy_state + | Some initial_portfolio -> + Runner.create_with_portfolio ~run_id:scenario.run_id ~scenario_sha256 + ~config ~initial_portfolio ~strategy_state) |> reducer_result in let journal_result = @@ -157,8 +162,15 @@ let run_stream_pass ~scenario_sha256 ~journal channel = | Error _ as error -> error | Ok config -> ( match - Runner.create ~run_id:header.run_id ~scenario_sha256 ~config - ~initial_cash:header.initial_cash ~strategy_state + (match header.initial_portfolio with + | None -> + Runner.create ~run_id:header.run_id ~scenario_sha256 + ~config ~initial_cash:header.initial_cash + ~strategy_state + | Some initial_portfolio -> + Runner.create_with_portfolio ~run_id:header.run_id + ~scenario_sha256 ~config ~initial_portfolio + ~strategy_state) |> reducer_result with | Error _ as error -> error diff --git a/lib/scenario.ml b/lib/scenario.ml index f86e2c0..79d58a2 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -4,6 +4,7 @@ type t = { run_id : Id.Run.t; base_currency : string; initial_cash : (string * Scalar.Money.t) list; + initial_portfolio : Initial_portfolio.t option; instruments : Instrument.t list; venue_calendars : Venue_calendar.t list; risk : Risk.t; @@ -20,6 +21,7 @@ type stream_header = { run_id : Id.Run.t; base_currency : string; initial_cash : (string * Scalar.Money.t) list; + initial_portfolio : Initial_portfolio.t option; instruments : Instrument.t list; venue_calendars : Venue_calendar.t list; risk : Risk.t; @@ -432,7 +434,7 @@ let parse_versioned_execution ~contract_version json = Ok (execution_model, execution) let parse_execution ~contract_version json = - if String.equal contract_version "5" then + if List.mem contract_version [ "6"; "5" ] then parse_versioned_execution ~contract_version json else parse_legacy_execution ~contract_version json @@ -622,6 +624,92 @@ let parse_fx_mark json = let* rate = parse_price ~name:"FX rate" rate_json in Market_slice.fx_mark ~currency ~rate +let parse_initial_position json = + let* fields = + object_fields ~name:"initial position" + ~expected: + [ + "instrument_id"; + "quantity"; + "cost_basis"; + "realized_pnl"; + "dividend_pnl"; + "execution_fees"; + "borrow_fees"; + ] + json + in + let* instrument_json = field fields "instrument_id" in + let* instrument_id = + parse_id Id.Instrument.of_string ~name:"instrument_id" instrument_json + in + let* quantity_json = field fields "quantity" in + let* quantity = + parse_quantity ~name:"initial position quantity" quantity_json + in + let* basis_json = field fields "cost_basis" in + let* cost_basis = + parse_money ~name:"initial position cost_basis" basis_json + in + let* realized_json = field fields "realized_pnl" in + let* realized_pnl = + parse_money ~name:"initial position realized_pnl" realized_json + in + let* dividend_json = field fields "dividend_pnl" in + let* dividend_pnl = + parse_money ~name:"initial position dividend_pnl" dividend_json + in + let* execution_json = field fields "execution_fees" in + let* execution_fees = + parse_money ~name:"initial position execution_fees" execution_json + in + let* borrow_json = field fields "borrow_fees" in + let* borrow_fees = + parse_money ~name:"initial position borrow_fees" borrow_json + in + Initial_portfolio.position ~instrument_id ~quantity ~cost_basis ~realized_pnl + ~dividend_pnl ~execution_fees ~borrow_fees + +let parse_initial_mark json = + let* fields = + object_fields ~name:"initial mark" + ~expected:[ "instrument_id"; "price" ] + json + in + let* instrument_json = field fields "instrument_id" in + let* instrument_id = + parse_id Id.Instrument.of_string ~name:"instrument_id" instrument_json + in + let* price_json = field fields "price" in + let* price = parse_price ~name:"initial mark price" price_json in + Ok (instrument_id, price) + +let parse_initial_fx_rate json = + let* mark = parse_fx_mark json in + Ok (mark.Market_slice.currency, mark.rate) + +let parse_initial_portfolio ~base_currency json = + let* fields = + object_fields ~name:"initial portfolio" + ~expected:[ "cash"; "positions"; "marks"; "fx_rates" ] + json + in + let* cash_json = field fields "cash" in + let* cash_json = list ~name:"initial portfolio cash" cash_json in + let* cash = map_list parse_cash_balance cash_json in + let* positions_json = field fields "positions" in + let* positions_json = + list ~name:"initial portfolio positions" positions_json + in + let* positions = map_list parse_initial_position positions_json in + let* marks_json = field fields "marks" in + let* marks_json = list ~name:"initial portfolio marks" marks_json in + let* marks = map_list parse_initial_mark marks_json in + let* fx_json = field fields "fx_rates" in + let* fx_json = list ~name:"initial portfolio FX rates" fx_json in + let* fx_rates = map_list parse_initial_fx_rate fx_json in + Initial_portfolio.create ~base_currency ~cash ~positions ~marks ~fx_rates + let parse_corporate_action json = let* fields = match json with @@ -732,14 +820,24 @@ let construct_header ~root ~contract_path ~contract_version string ~name:"base_currency" shape.base_currency |> at (child root "base_currency") in - let* initial_cash_json = - list ~name:"initial_cash" shape.initial_cash - |> at (child root "initial_cash") - in - let* initial_cash = - map_list_at - (child root "initial_cash") - parse_cash_balance initial_cash_json + let* initial_cash, initial_portfolio = + if String.equal contract_version "6" then + let* portfolio = + parse_initial_portfolio ~base_currency shape.initial_state + |> at (child root "initial_portfolio") + in + Ok (portfolio.Initial_portfolio.cash, Some portfolio) + else + let* initial_cash_json = + list ~name:"initial_cash" shape.initial_state + |> at (child root "initial_cash") + in + let* initial_cash = + map_list_at + (child root "initial_cash") + parse_cash_balance initial_cash_json + in + Ok (initial_cash, None) in let* instruments_json = list ~name:"instruments" shape.instruments @@ -771,6 +869,13 @@ let construct_header ~root ~contract_path ~contract_version let* risk = parse_risk base_currency instruments shape.risk |> at (child root "risk") in + let* () = + match initial_portfolio with + | None -> Ok () + | Some portfolio -> + Scenario_validation.initial_portfolio ~root ~currencies ~catalog + ~instruments ~risk portfolio + in let* execution_model, execution = parse_execution ~contract_version shape.execution |> at (child root "execution") @@ -782,6 +887,7 @@ let construct_header ~root ~contract_path ~contract_version run_id; base_currency; initial_cash; + initial_portfolio; instruments; venue_calendars; risk; @@ -819,6 +925,7 @@ let construct_batch (shape : Scenario_shape.batch) = run_id = header.run_id; base_currency = header.base_currency; initial_cash = header.initial_cash; + initial_portfolio = header.initial_portfolio; instruments = header.instruments; venue_calendars = header.venue_calendars; risk = header.risk; diff --git a/lib/scenario.mli b/lib/scenario.mli index 1fa0f31..5b6953c 100644 --- a/lib/scenario.mli +++ b/lib/scenario.mli @@ -6,6 +6,7 @@ type t = private { run_id : Id.Run.t; base_currency : string; initial_cash : (string * Scalar.Money.t) list; + initial_portfolio : Initial_portfolio.t option; instruments : Instrument.t list; venue_calendars : Venue_calendar.t list; risk : Risk.t; @@ -22,6 +23,7 @@ type stream_header = private { run_id : Id.Run.t; base_currency : string; initial_cash : (string * Scalar.Money.t) list; + initial_portfolio : Initial_portfolio.t option; instruments : Instrument.t list; venue_calendars : Venue_calendar.t list; risk : Risk.t; diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index 7db9bdd..b1a5190 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -4,7 +4,7 @@ type common = { metadata : Yojson.Safe.t; run_id : Yojson.Safe.t; base_currency : Yojson.Safe.t; - initial_cash : Yojson.Safe.t; + initial_state : Yojson.Safe.t; instruments : Yojson.Safe.t; venue_calendars : Yojson.Safe.t option; risk : Yojson.Safe.t; @@ -67,10 +67,14 @@ let common ~root ~contract_version fields = let* metadata = field ~root fields "metadata" in let* run_id = field ~root fields "run_id" in let* base_currency = field ~root fields "base_currency" in - let* initial_cash = field ~root fields "initial_cash" in + let initial_field = + if String.equal contract_version "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 String.equal contract_version "5" then + if List.mem contract_version [ "6"; "5" ] then List.assoc_opt "venue_calendars" fields else None in @@ -82,7 +86,7 @@ let common ~root ~contract_version fields = metadata; run_id; base_currency; - initial_cash; + initial_state; instruments; venue_calendars; risk; @@ -101,7 +105,11 @@ let batch json = match preliminary with `String value -> value | _ -> "" in let calendar_fields = - if String.equal contract_version "5" then [ "venue_calendars" ] else [] + if List.mem contract_version [ "6"; "5" ] then [ "venue_calendars" ] else [] + in + let initial_field = + if String.equal contract_version "6" then "initial_portfolio" + else "initial_cash" in let* fields = object_fields ~json_path:root ~name:"scenario" @@ -111,7 +119,7 @@ let batch json = "metadata"; "run_id"; "base_currency"; - "initial_cash"; + initial_field; "instruments"; "risk"; "execution"; @@ -131,7 +139,11 @@ let batch json = let stream_header ~contract_version json = let root = "$.payload" in let calendar_fields = - if String.equal contract_version "5" then [ "venue_calendars" ] else [] + if List.mem contract_version [ "6"; "5" ] then [ "venue_calendars" ] else [] + in + let initial_field = + if String.equal contract_version "6" then "initial_portfolio" + else "initial_cash" in let* fields = object_fields ~json_path:root ~name:"scenario stream header payload" @@ -140,7 +152,7 @@ let stream_header ~contract_version json = "metadata"; "run_id"; "base_currency"; - "initial_cash"; + initial_field; "instruments"; "risk"; "execution"; diff --git a/lib/scenario_shape.mli b/lib/scenario_shape.mli index b750cbc..bdbd719 100644 --- a/lib/scenario_shape.mli +++ b/lib/scenario_shape.mli @@ -6,7 +6,7 @@ type common = { metadata : Yojson.Safe.t; run_id : Yojson.Safe.t; base_currency : Yojson.Safe.t; - initial_cash : Yojson.Safe.t; + initial_state : Yojson.Safe.t; instruments : Yojson.Safe.t; venue_calendars : Yojson.Safe.t option; risk : Yojson.Safe.t; diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index de0ba22..9ba25b9 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -48,9 +48,11 @@ let validate_venue_calendars ~root catalog venue_calendars = let header ~root ~contract_version ~base_currency ~initial_cash ~instruments ~venue_calendars ~max_internal_events = let* () = - Account.create ~base_currency ~initial_cash - |> Result.map (fun _ -> ()) - |> at (child root "initial_cash") + if String.equal contract_version "6" then Ok () + else + Account.create ~base_currency ~initial_cash + |> Result.map (fun _ -> ()) + |> at (child root "initial_cash") in if instruments = [] then fail ~json_path:(child root "instruments") @@ -64,7 +66,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path:(child root "instruments") "instrument IDs must be unique" else let* () = - if String.equal contract_version "5" then + if List.mem contract_version [ "6"; "5" ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -80,8 +82,12 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments in if cash_currencies <> currencies then fail - ~json_path:(child root "initial_cash") - "initial_cash must contain every scenario currency exactly once" + ~json_path: + (child root + (if String.equal contract_version "6" then + "initial_portfolio.cash" + else "initial_cash")) + "initial cash must contain every scenario currency exactly once" else if max_internal_events <= 0 then fail ~json_path:(child root "max_internal_events") @@ -93,6 +99,76 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments max_internal_events Resource_limits.internal_events) else Ok (currencies, catalog) +let initial_portfolio ~root ~currencies ~catalog ~instruments ~risk initial = + let path = child root "initial_portfolio" in + let cash_currencies = List.map fst initial.Initial_portfolio.cash in + let fx_currencies = List.map fst initial.fx_rates in + let expected_currencies = List.sort String.compare currencies in + if List.sort String.compare cash_currencies <> expected_currencies then + fail ~json_path:(child path "cash") + "initial cash must contain every scenario currency exactly once" + else if List.sort String.compare fx_currencies <> expected_currencies then + fail ~json_path:(child path "fx_rates") + "initial FX rates must contain every scenario currency exactly once" + else + let instrument_map = + List.fold_left + (fun map instrument -> + Id.Instrument.Map.add instrument.Instrument.id instrument map) + Id.Instrument.Map.empty instruments + in + let* () = + List.fold_left + (fun result (position : Initial_portfolio.position) -> + let* () = result in + if not (Id.Instrument.Set.mem position.instrument_id catalog) then + fail ~json_path:(child path "positions") + "initial position refers to an unknown instrument" + else + match + Id.Instrument.Map.find_opt position.instrument_id instrument_map + with + | None -> assert false + | Some instrument -> + if + not + (Scalar.Quantity.is_multiple position.quantity + ~lot:instrument.Instrument.lot_size) + then + fail ~json_path:(child path "positions") + "initial position quantity is not aligned to its \ + instrument lot" + else + Risk.check_position risk position.quantity + |> at (child path "positions")) + (Ok ()) initial.positions + in + let* () = + List.fold_left + (fun result (instrument_id, mark) -> + let* () = result in + if not (Id.Instrument.Set.mem instrument_id catalog) then + fail ~json_path:(child path "marks") + "initial mark refers to an unknown instrument" + else + match Id.Instrument.Map.find_opt instrument_id instrument_map with + | None -> assert false + | Some instrument -> + if Scalar.Price.is_multiple mark ~tick:instrument.tick_size then + Ok () + else + fail ~json_path:(child path "marks") + "initial mark is not aligned to its instrument tick size") + (Ok ()) initial.marks + in + let* account = Account.of_initial_portfolio initial |> at path in + let* valuation = + Account.value account ~instruments ~marks:initial.marks + ~fx_rates:initial.fx_rates + |> at path + in + Risk.check_initial risk valuation |> at path + let changes_orders = function | Strategy.Target_weights _ | Strategy.Target_quantities _ | Strategy.Submit_order _ | Strategy.Cancel_order _ -> diff --git a/lib/scenario_validation.mli b/lib/scenario_validation.mli index fba700a..3582d2d 100644 --- a/lib/scenario_validation.mli +++ b/lib/scenario_validation.mli @@ -10,6 +10,15 @@ val header : max_internal_events:int -> (string list * Id.Instrument.Set.t, Scenario_shape.error) result +val initial_portfolio : + root:string -> + currencies:string list -> + catalog:Id.Instrument.Set.t -> + instruments:Instrument.t list -> + risk:Risk.t -> + Initial_portfolio.t -> + (unit, Scenario_shape.error) result + val batch : root:string -> base_currency:string -> diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index 9db5baf..c40e5d6 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -8,6 +8,7 @@ type initialization = { run_id : Id.Run.t; base_currency : string; initial_cash : (string * Scalar.Money.t) list; + initial_portfolio : Initial_portfolio.t option; instruments : Instrument.t list; risk : Risk.t; execution_model : Execution_model.t; @@ -76,9 +77,14 @@ let execution_to_yojson model execution = `Assoc [ ("model", string (Execution_model.name model)); - ("participation_bps", `Int (Execution.participation_bps execution)); - ("fixed_fee", money (Execution.fixed_fee execution)); - ("fee_bps", `Int (Execution.fee_bps execution)); + ( "configuration", + `Assoc + [ + ("version", string "1"); + ("participation_bps", `Int (Execution.participation_bps execution)); + ("fixed_fee", money (Execution.fixed_fee execution)); + ("fee_bps", `Int (Execution.fee_bps execution)); + ] ); ] let initialize_message ~sequence:message_sequence initialization = @@ -103,6 +109,9 @@ let initialize_message ~sequence:message_sequence initialization = ("run_id", string (Id.Run.to_string initialization.run_id)); ("base_currency", string initialization.base_currency); ("initial_cash", `List (List.map cash_balance_to_yojson initial_cash)); + ( "initial_portfolio", + Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson + initialization.initial_portfolio ); ("instruments", `List (List.map instrument_to_yojson instruments)); ("risk", risk_to_yojson initialization.risk); ( "execution", diff --git a/lib/strategy_protocol.mli b/lib/strategy_protocol.mli index 0bca4a4..ce8b0dc 100644 --- a/lib/strategy_protocol.mli +++ b/lib/strategy_protocol.mli @@ -10,6 +10,7 @@ type initialization = { run_id : Id.Run.t; base_currency : string; initial_cash : (string * Scalar.Money.t) list; + initial_portfolio : Initial_portfolio.t option; instruments : Instrument.t list; risk : Risk.t; execution_model : Execution_model.t; diff --git a/mkdocs.yml b/mkdocs.yml index 5755443..1e70145 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,13 +29,15 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v5: contracts/v5/README.md + - Current v6: contracts/v6/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 v3: contracts/strategy/v3/README.md + - Current v4: contracts/strategy/v4/README.md + - Historical v3: contracts/strategy/v3/README.md - Historical v2: contracts/strategy/v2/README.md - Historical v1: contracts/strategy/v1/README.md - API reference: docs/api-reference.md diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index 80fa81b..00be5ee 100644 --- a/scripts/check-documentation.py +++ b/scripts/check-documentation.py @@ -26,11 +26,13 @@ "docs/persistra.md", "SECURITY.md", "contracts/conformance/README.md", + "contracts/v6/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", + "contracts/strategy/v4/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 0bc1b7a..af53d34 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/v5/scenario.schema.json", - "share/trading_engine/contracts/v5/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v6/scenario.schema.json", + "share/trading_engine/contracts/v6/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/v5/fixtures/demo.scenario.json", + "contracts/v6/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/v5/fixtures/demo.scenario.json", - "contracts/strategy/v3/message.schema.json", + "contracts/v6/fixtures/demo.scenario.json", + "contracts/strategy/v4/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v5/scenario.schema.json", + "contracts/v6/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index 4ef8b6d..eb7ac90 100644 --- a/test/cli.t +++ b/test/cli.t @@ -2,22 +2,22 @@ 1.0.0 $ ../bin/main.exe --capabilities - {"engine_version":"1.0.0","scenario_contract_versions":["5","4","3"],"journal_contract_versions":["5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["1"],"scenario_contract_versions":["5","4","3"],"required_fields":["version","participation_bps","fixed_fee","fee_bps"],"supported_order_types":["market","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":["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":["6","5","4","3"],"journal_contract_versions":["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":["1"],"scenario_contract_versions":["6","5","4","3"],"required_fields":["version","participation_bps","fixed_fee","fee_bps"],"supported_order_types":["market","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":["4"],"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/v5/fixtures/demo.scenario.json - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=b800732e40c20c06605c6a1352d3482a3f41fc7ae4b07594860a1c3f153a655c + $ ../bin/main.exe --validate-only --input ../contracts/v6/fixtures/demo.scenario.json + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=c98534261412acebf4b49d87619dc7c951538581c2a7dd05f315eb64d761cec2 - $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v5/fixtures/demo.scenario.jsonl - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=ba51f0f956d90fcaf57ed905e58d89fbc707d050f54ba1334b0a8d0dbb32856d + $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v6/fixtures/demo.scenario.jsonl + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=b0859d248708402801a1155a68026ca8b799b19a425223fa3d614b7fa6224253 - $ ../bin/main.exe --input-format jsonl --input ../contracts/v5/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts - run=demo audits=20 orders=3 active=0 filled=2 rejected=0 - cash=9739.76812 equity=10004.76812 gross=265 realized=1.419136 unrealized=3.348984 fees=2.50188 + $ ../bin/main.exe --input-format jsonl --input ../contracts/v6/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts + run=demo audits=22 orders=3 active=0 filled=2 rejected=0 + cash=9846.65392 equity=10111.65392 gross=265 realized=18.965682 unrealized=7.688238 fees=3.16608 journal=streamed.journal.jsonl $ wc -l < streamed.journal.jsonl - 20 + 22 - $ head -n 5 ../contracts/v5/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl + $ head -n 5 ../contracts/v6/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl $ ../bin/main.exe --validate-only --input-format jsonl --input truncated.scenario.jsonl trading-engine: scenario_end must terminate the scenario stream [123] @@ -32,45 +32,45 @@ 1 scenario_stream.invalid validation 6 6 None - $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v5/fixtures/demo.scenario.json > invalid-tick.json + $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v6/fixtures/demo.scenario.json > invalid-tick.json $ ../bin/main.exe --validate-only --input invalid-tick.json trading-engine: market prices and volumes must align with instrument increments [123] - $ ../bin/main.exe --input ../contracts/v5/fixtures/demo.scenario.json + $ ../bin/main.exe --input ../contracts/v6/fixtures/demo.scenario.json trading-engine: --journal is required unless --validate-only is set [123] - $ ../bin/main.exe --validate-only --input ../contracts/v5/fixtures/demo.scenario.json --journal validation.journal.jsonl + $ ../bin/main.exe --validate-only --input ../contracts/v6/fixtures/demo.scenario.json --journal validation.journal.jsonl trading-engine: --journal cannot be used with --validate-only [123] $ test ! -e validation.journal.jsonl - $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v5/fixtures/demo.scenario.json + $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v6/fixtures/demo.scenario.json trading-engine: --durable-artifacts cannot be used with --validate-only [123] - $ ../bin/main.exe --input ../contracts/v5/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 + $ ../bin/main.exe --input ../contracts/v6/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 trading-engine: --strategy-arg, --strategy-timeout, and --strategy-transcript require --strategy-executable [123] $ test ! -e ignored.journal.jsonl $ mkdir external - $ ../bin/main.exe --input ../contracts/strategy/v3/fixtures/external.scenario.json --journal external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external/run.strategy.jsonl --strategy-timeout 5 --durable-artifacts - run=external-demo audits=10 orders=1 active=0 filled=1 rejected=0 - cash=9794 equity=10008 gross=214 realized=0 unrealized=8 fees=0 + $ ../bin/main.exe --input ../contracts/strategy/v4/fixtures/external.scenario.json --journal external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external/run.strategy.jsonl --strategy-timeout 5 --durable-artifacts + run=external-demo audits=12 orders=1 active=0 filled=1 rejected=0 + cash=9793.544 equity=10007.544 gross=214 realized=0 unrealized=7.544 fees=0.456 journal=external/run.journal.jsonl strategy_transcript=external/run.strategy.jsonl $ python3 -c 'from pathlib import Path; print(len(Path("external/run.journal.jsonl").read_text().splitlines()), len(Path("external/run.strategy.jsonl").read_text().splitlines()))' - 10 14 - $ diff -u ../contracts/strategy/v3/fixtures/external.strategy.jsonl external/run.strategy.jsonl + 12 14 + $ diff -u ../contracts/strategy/v4/fixtures/external.strategy.jsonl external/run.strategy.jsonl $ mkdir callback-ordering - $ ../bin/main.exe --input ../contracts/strategy/v3/fixtures/external.scenario.json --journal callback-ordering/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg cancel-next --strategy-transcript callback-ordering/run.strategy.jsonl --strategy-timeout 5 - run=external-demo audits=10 orders=2 active=0 filled=1 rejected=0 - cash=9897 equity=10004 gross=107 realized=0 unrealized=4 fees=0 + $ ../bin/main.exe --input ../contracts/strategy/v4/fixtures/external.scenario.json --journal callback-ordering/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg cancel-next --strategy-transcript callback-ordering/run.strategy.jsonl --strategy-timeout 5 + run=external-demo audits=12 orders=2 active=0 filled=1 rejected=0 + cash=9896.647 equity=10003.647 gross=107 realized=0 unrealized=3.647 fees=0.353 journal=callback-ordering/run.journal.jsonl strategy_transcript=callback-ordering/run.strategy.jsonl $ python3 -c 'import json; journal=[json.loads(line) for line in open("callback-ordering/run.journal.jsonl")]; transcript=[json.loads(line) for line in open("callback-ordering/run.strategy.jsonl")]; events=[record["event_type"] for record in journal]; cancellations=[record["payload"]["reason"] for record in journal if record["event_type"] == "order_cancelled"]; requests=[record["message"]["payload"] for record in transcript if record["direction"] == "engine_to_strategy" and record["message"]["message_type"] == "event"]; fill=next(request for request in requests if request["event"]["type"] == "fill_received"); following=requests[requests.index(fill) + 1]; print(events.count("fill_applied"), cancellations, events.count("intent_rejected")); print(len(fill["context"]["working_orders"]), len(following["context"]["working_orders"])); print(fill["context"]["latest_bars"][0]["close"], fill["context"]["portfolio"]["positions"][0]["mark"])' @@ -79,7 +79,7 @@ 107 107 $ mkdir failed-external - $ ../bin/main.exe --input ../contracts/strategy/v3/fixtures/external.scenario.json --journal failed-external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg stall --strategy-transcript failed-external/run.strategy.jsonl --strategy-timeout 0.01 + $ ../bin/main.exe --input ../contracts/strategy/v4/fixtures/external.scenario.json --journal failed-external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg stall --strategy-transcript failed-external/run.strategy.jsonl --strategy-timeout 0.01 trading-engine: strategy initialization: external strategy timed out [123] $ test ! -e failed-external/run.journal.jsonl @@ -92,7 +92,7 @@ > expected="$2" > directory="fault-$mode" > mkdir "$directory" - > output=$(../bin/main.exe --input ../contracts/strategy/v3/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 5 2>&1) + > output=$(../bin/main.exe --input ../contracts/strategy/v4/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 5 2>&1) > status=$? > test "$status" -eq 123 || return 1 > case "$output" in *"$expected"*) ;; *) return 1 ;; esac @@ -169,7 +169,7 @@ > directory="process-tree-$mode" > mkdir "$directory" > pid_path="$directory/grandchild.pid" - > output=$(../bin/main.exe --input ../contracts/strategy/v3/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-arg "$pid_path" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 0.2 2>&1) + > output=$(../bin/main.exe --input ../contracts/strategy/v4/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-arg "$pid_path" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 0.2 2>&1) > status=$? > test "$status" -eq 123 || return 1 > case "$output" in *"$expected"*) ;; *) return 1 ;; esac @@ -198,8 +198,8 @@ grandchild-malformed: process tree reaped $ mkdir external-stream - $ ../bin/main.exe --input-format jsonl --input ../contracts/strategy/v3/fixtures/external.scenario.jsonl --journal external-stream/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-stream/run.strategy.jsonl --strategy-timeout 5 - run=external-demo audits=10 orders=1 active=0 filled=1 rejected=0 - cash=9794 equity=10008 gross=214 realized=0 unrealized=8 fees=0 + $ ../bin/main.exe --input-format jsonl --input ../contracts/strategy/v4/fixtures/external.scenario.jsonl --journal external-stream/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-stream/run.strategy.jsonl --strategy-timeout 5 + run=external-demo audits=12 orders=1 active=0 filled=1 rejected=0 + cash=9793.544 equity=10007.544 gross=214 realized=0 unrealized=7.544 fees=0.456 journal=external-stream/run.journal.jsonl strategy_transcript=external-stream/run.strategy.jsonl diff --git a/test/dune b/test/dune index 012fa40..f322ca3 100644 --- a/test/dune +++ b/test/dune @@ -16,20 +16,22 @@ test_scenario test_engine) (deps - ../contracts/v5/fixtures/demo.journal.jsonl + ../contracts/v6/fixtures/demo.journal.jsonl + ../contracts/v6/fixtures/demo.scenario.json + ../contracts/v6/fixtures/demo.scenario.jsonl + ../contracts/v6/fixtures/fill-clipped.journal.jsonl + ../contracts/v6/fixtures/fill-clipped.scenario.json + ../contracts/v6/journal.schema.json + ../contracts/v6/scenario-stream.schema.json + ../contracts/v6/scenario.schema.json ../contracts/v5/fixtures/demo.scenario.json ../contracts/v5/fixtures/demo.scenario.jsonl - ../contracts/v5/fixtures/fill-clipped.journal.jsonl - ../contracts/v5/fixtures/fill-clipped.scenario.json - ../contracts/v5/journal.schema.json - ../contracts/v5/scenario-stream.schema.json - ../contracts/v5/scenario.schema.json ../contracts/v4/fixtures/demo.scenario.json ../contracts/v3/fixtures/demo.journal.jsonl ../contracts/v3/fixtures/demo.scenario.json ../contracts/v3/fixtures/demo.scenario.jsonl ../contracts/conformance/cases.json - ../contracts/strategy/v3/fixtures/external.strategy.jsonl + ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries trading_engine @@ -59,53 +61,53 @@ (deps ../bin/main.exe fake_strategy.py - ../contracts/strategy/v3/fixtures/external.scenario.json - ../contracts/strategy/v3/fixtures/external.scenario.jsonl - ../contracts/strategy/v3/fixtures/external.strategy.jsonl - ../contracts/v5/fixtures/demo.scenario.json - ../contracts/v5/fixtures/demo.scenario.jsonl)) + ../contracts/strategy/v4/fixtures/external.scenario.json + ../contracts/strategy/v4/fixtures/external.scenario.jsonl + ../contracts/strategy/v4/fixtures/external.strategy.jsonl + ../contracts/v6/fixtures/demo.scenario.json + ../contracts/v6/fixtures/demo.scenario.jsonl)) (rule (alias runtest) (deps validate_schemas.py - ../contracts/v5/fixtures/demo.journal.jsonl - ../contracts/v5/fixtures/demo.scenario.json - ../contracts/v5/fixtures/demo.scenario.jsonl - ../contracts/v5/journal.schema.json - ../contracts/v5/scenario-stream.schema.json - ../contracts/v5/scenario.schema.json) + ../contracts/v6/fixtures/demo.journal.jsonl + ../contracts/v6/fixtures/demo.scenario.json + ../contracts/v6/fixtures/demo.scenario.jsonl + ../contracts/v6/journal.schema.json + ../contracts/v6/scenario-stream.schema.json + ../contracts/v6/scenario.schema.json) (action (run python3 %{dep:validate_schemas.py} - %{dep:../contracts/v5/scenario.schema.json} - %{dep:../contracts/v5/scenario-stream.schema.json} - %{dep:../contracts/v5/journal.schema.json} - %{dep:../contracts/v5/fixtures/demo.scenario.json} - %{dep:../contracts/v5/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v5/fixtures/demo.journal.jsonl}))) + %{dep:../contracts/v6/scenario.schema.json} + %{dep:../contracts/v6/scenario-stream.schema.json} + %{dep:../contracts/v6/journal.schema.json} + %{dep:../contracts/v6/fixtures/demo.scenario.json} + %{dep:../contracts/v6/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v6/fixtures/demo.journal.jsonl}))) (rule (alias runtest) (deps validate_schemas.py - ../contracts/v5/fixtures/fill-clipped.journal.jsonl - ../contracts/v5/fixtures/fill-clipped.scenario.json - ../contracts/v5/fixtures/demo.scenario.jsonl - ../contracts/v5/journal.schema.json - ../contracts/v5/scenario-stream.schema.json - ../contracts/v5/scenario.schema.json) + ../contracts/v6/fixtures/fill-clipped.journal.jsonl + ../contracts/v6/fixtures/fill-clipped.scenario.json + ../contracts/v6/fixtures/demo.scenario.jsonl + ../contracts/v6/journal.schema.json + ../contracts/v6/scenario-stream.schema.json + ../contracts/v6/scenario.schema.json) (action (run python3 %{dep:validate_schemas.py} - %{dep:../contracts/v5/scenario.schema.json} - %{dep:../contracts/v5/scenario-stream.schema.json} - %{dep:../contracts/v5/journal.schema.json} - %{dep:../contracts/v5/fixtures/fill-clipped.scenario.json} - %{dep:../contracts/v5/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v5/fixtures/fill-clipped.journal.jsonl}))) + %{dep:../contracts/v6/scenario.schema.json} + %{dep:../contracts/v6/scenario-stream.schema.json} + %{dep:../contracts/v6/journal.schema.json} + %{dep:../contracts/v6/fixtures/fill-clipped.scenario.json} + %{dep:../contracts/v6/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v6/fixtures/fill-clipped.journal.jsonl}))) (rule (alias runtest) @@ -132,22 +134,22 @@ (alias runtest) (deps validate_strategy_schema.py - ../contracts/v3/scenario.schema.json - ../contracts/v3/journal.schema.json + ../contracts/v6/scenario.schema.json + ../contracts/v6/journal.schema.json ../contracts/diagnostic/v1/diagnostic.schema.json - ../contracts/strategy/v3/message.schema.json - ../contracts/strategy/v3/transcript.schema.json - ../contracts/strategy/v3/fixtures/external.strategy.jsonl) + ../contracts/strategy/v4/message.schema.json + ../contracts/strategy/v4/transcript.schema.json + ../contracts/strategy/v4/fixtures/external.strategy.jsonl) (action (run python3 %{dep:validate_strategy_schema.py} - %{dep:../contracts/v3/scenario.schema.json} - %{dep:../contracts/v3/journal.schema.json} + %{dep:../contracts/v6/scenario.schema.json} + %{dep:../contracts/v6/journal.schema.json} %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} - %{dep:../contracts/strategy/v3/message.schema.json} - %{dep:../contracts/strategy/v3/transcript.schema.json} - %{dep:../contracts/strategy/v3/fixtures/external.strategy.jsonl}))) + %{dep:../contracts/strategy/v4/message.schema.json} + %{dep:../contracts/strategy/v4/transcript.schema.json} + %{dep:../contracts/strategy/v4/fixtures/external.strategy.jsonl}))) (rule (alias runtest) @@ -195,6 +197,6 @@ (deps test_benchmark_replay.py ../bench/benchmark_replay.py - ../contracts/v5/fixtures/demo.scenario.json) + ../contracts/v6/fixtures/demo.scenario.json) (action (run python3 %{dep:test_benchmark_replay.py}))) diff --git a/test/fake_strategy.py b/test/fake_strategy.py index b230dbb..0302937 100755 --- a/test/fake_strategy.py +++ b/test/fake_strategy.py @@ -121,7 +121,7 @@ def response(request: dict[str, object]) -> dict[str, object]: response_type = "error" payload = {"message": "unsupported request"} return { - "strategy_protocol_version": "3", + "strategy_protocol_version": "4", "strategy_sequence": sequence, "message_type": response_type, "payload": payload, diff --git a/test/test_boundary_failures.ml b/test/test_boundary_failures.ml index fe0f431..b8ce96a 100644 --- a/test/test_boundary_failures.ml +++ b/test/test_boundary_failures.ml @@ -324,6 +324,7 @@ let initialization () = run_id = run_id "boundary-failure"; base_currency = "USD"; initial_cash = [ ("USD", money "10000") ]; + initial_portfolio = None; instruments = [ instrument ]; risk = risk ~instruments:[ instrument ] (); execution_model = T.Execution_model.find "completed_bar_v1" |> ok; diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 5465e15..00643f5 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -117,7 +117,7 @@ let capabilities_describe_execution_contracts () = "configuration versions" [ "1" ] (strings "configuration_versions"); Alcotest.(check (list string)) - "scenario contracts" [ "5"; "4"; "3" ] + "scenario contracts" [ "6"; "5"; "4"; "3" ] (strings "scenario_contract_versions"); Alcotest.(check (list string)) "required fields" diff --git a/test/test_scenario.ml b/test/test_scenario.ml index b62d39d..10c88fb 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/v5/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v6/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/v5/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v6/fixtures/demo.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -125,9 +125,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v5/scenario.schema.json"; - check_schema "../contracts/v5/scenario-stream.schema.json"; - check_schema "../contracts/v5/journal.schema.json" + check_schema "../contracts/v6/scenario.schema.json"; + check_schema "../contracts/v6/scenario-stream.schema.json"; + check_schema "../contracts/v6/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -189,7 +189,7 @@ 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 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 6, 5, 4, 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -200,7 +200,7 @@ let contract_version_is_required_and_supported () = let duplicate_fields_are_rejected () = let changed = - map_root (fun fields -> ("initial_cash", `String "0") :: fields) + map_root (fun fields -> ("initial_portfolio", `String "0") :: fields) in let message = T.Scenario.of_yojson changed |> diagnostic_message in Alcotest.(check bool) @@ -589,7 +589,31 @@ let portfolio_targets_are_total_and_aligned () = map_root (fun fields -> List.map (fun (name, value) -> - if String.equal name "initial_cash" then (name, `String "10000.0") + if String.equal name "initial_portfolio" then + match value with + | `Assoc portfolio_fields -> + let changed = + List.map + (fun (field, field_value) -> + if String.equal field "cash" then + match field_value with + | `List (`Assoc cash_fields :: rest) -> + let cash = + `Assoc + (List.map + (fun (cash_field, cash_value) -> + if String.equal cash_field "amount" then + (cash_field, `String "10000.0") + else (cash_field, cash_value)) + cash_fields) + in + (field, `List (cash :: rest)) + | _ -> (field, field_value) + else (field, field_value)) + portfolio_fields + in + (name, `Assoc changed) + | _ -> (name, value) else (name, value)) fields) in @@ -597,6 +621,92 @@ let portfolio_targets_are_total_and_aligned () = "noncanonical scalar rejected" true (Result.is_error (T.Scenario.of_yojson noncanonical)) +let update_initial_portfolio field change = + map_root (fun fields -> + List.map + (fun (name, value) -> + if String.equal name "initial_portfolio" then + match value with + | `Assoc portfolio_fields -> + ( name, + `Assoc + (List.map + (fun (key, item) -> + if String.equal key field then (key, change item) + else (key, item)) + portfolio_fields) ) + | _ -> (name, value) + else (name, value)) + fields) + +let update_first_object_field field value = function + | `List (`Assoc fields :: rest) -> + `List + (`Assoc + (List.map + (fun (name, current) -> + if String.equal name field then (name, value) + else (name, current)) + fields) + :: rest) + | _ -> Alcotest.fail "expected a nonempty object array" + +let initial_portfolio_validation () = + let signed_cash = + update_initial_portfolio "cash" + (update_first_object_field "amount" (`String "-1")) + in + Alcotest.(check bool) + "signed cash accepted" true + (Result.is_ok (T.Scenario.of_yojson signed_cash)); + let wrong_basis = + update_initial_portfolio "positions" + (update_first_object_field "cost_basis" (`String "-90")) + in + Alcotest.(check bool) + "basis sign rejected" true + (Result.is_error (T.Scenario.of_yojson wrong_basis)); + let off_lot = + update_initial_portfolio "positions" + (update_first_object_field "quantity" (`String "0.0005")) + in + Alcotest.(check bool) + "off-lot holding rejected" true + (Result.is_error (T.Scenario.of_yojson off_lot)); + let missing_mark = update_initial_portfolio "marks" (fun _ -> `List []) in + Alcotest.(check bool) + "missing initial mark rejected" true + (Result.is_error (T.Scenario.of_yojson missing_mark)); + let insufficient_margin = + update_initial_portfolio "cash" + (update_first_object_field "amount" (`String "-100")) + in + Alcotest.(check bool) + "initial margin enforced" true + (Result.is_error (T.Scenario.of_yojson insufficient_margin)) + +let initial_portfolio_is_audited_and_reconciled () = + let result = T.Replay.run ~scenario_sha256:(demo_hash ()) (demo ()) |> ok in + match result.audits with + | _started :: initial :: first_valuation :: _ -> ( + match (initial.event, first_valuation.event) with + | ( T.Audit.Initial_state { portfolio; valuation = initial_valuation }, + T.Audit.Valuation first_valuation ) -> + Alcotest.(check int) "one holding" 1 (List.length portfolio.positions); + Alcotest.check money_testable "initial equity" (money "10100") + initial_valuation.account.equity; + Alcotest.check money_testable "first valuation reconciles" + initial_valuation.account.equity first_valuation.account.equity; + let position = List.hd initial_valuation.account.positions in + Alcotest.check money_testable "native basis" (money "90") + position.cost_basis; + Alcotest.check money_testable "realized attribution" (money "5") + position.realized_pnl; + Alcotest.check money_testable "historical fees" (money "0.75") + position.total_fees + | _ -> Alcotest.fail "expected initial_state followed by valuation") + | _ -> Alcotest.fail "expected initial audit records" + let execution_model_is_required_and_supported () = let change_execution change = map_root (fun fields -> @@ -665,7 +775,7 @@ let deterministic_replay () = Alcotest.(check (list string)) "byte-identical event encoding" (encode first) (encode second); Alcotest.(check int) - "one valuation per slice" 4 + "initial valuation plus one per slice" 5 (List.length (List.filter (fun audit -> @@ -711,24 +821,24 @@ let audit_ids_are_deterministic_and_causal () = in Alcotest.(check (list string)) "external slice has no engine cause" [] - (cause_strings (event 7L)); + (cause_strings (event 9L)); Alcotest.(check (list string)) "target order cites slice and target request" - [ "demo-event-000000000002"; "demo-event-000000000003" ] - (cause_strings (event 5L)); + [ "demo-event-000000000004"; "demo-event-000000000005" ] + (cause_strings (event 7L)); Alcotest.(check (list string)) "fill cites order creation and executable slice" - [ "demo-event-000000000005"; "demo-event-000000000007" ] - (cause_strings (event 8L)); + [ "demo-event-000000000007"; "demo-event-000000000009" ] + (cause_strings (event 10L)); Alcotest.(check (list string)) "completion cites terminal valuation" - [ "demo-event-000000000019" ] - (cause_strings (event 20L)); - match (event 5L).event with + [ "demo-event-000000000021" ] + (cause_strings (event 22L)); + match (event 7L).event with | T.Audit.Order_accepted order -> Alcotest.(check string) "order snapshot retains creation event" - (T.Id.Event.to_string (event 5L).event_id) + (T.Id.Event.to_string (event 7L).event_id) (T.Id.Event.to_string order.created_event_id) | _ -> Alcotest.fail "expected accepted order" @@ -761,7 +871,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v5/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v6/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -789,7 +899,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v5/fixtures/fill-clipped.scenario.json" In_channel.input_all + "../contracts/v6/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in let result = @@ -802,7 +912,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v5/fixtures/fill-clipped.journal.jsonl" In_channel.input_all + "../contracts/v6/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual @@ -901,8 +1011,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) "twenty audits" 20L result.audit_count; - Alcotest.check money_testable "same equity" (money "10004.76812") + Alcotest.(check int64) "twenty-two audits" 22L result.audit_count; + Alcotest.check money_testable "same equity" (money "10111.65392") result.valuation.equity; Alcotest.(check string) "stream and batch journals agree" expected @@ -1080,7 +1190,7 @@ let large_stream_replay_does_not_retain_audit_history () = "all slices consumed" (Int64.of_int slice_count) result.slice_count; Alcotest.(check int64) "events counted without an audit list" - (Int64.of_int ((2 * slice_count) + 2)) + (Int64.of_int ((2 * slice_count) + 4)) result.audit_count; Alcotest.(check int) "no orders accumulated" 0 (List.length result.orders)) @@ -1112,6 +1222,10 @@ let tests = market_slice_timeline_is_non_overlapping; Alcotest.test_case "portfolio target validation" `Quick portfolio_targets_are_total_and_aligned; + Alcotest.test_case "initial portfolio validation" `Quick + initial_portfolio_validation; + Alcotest.test_case "initial portfolio audit reconciliation" `Quick + initial_portfolio_is_audited_and_reconciled; Alcotest.test_case "execution model required and supported" `Quick execution_model_is_required_and_supported; Alcotest.test_case "deterministic replay" `Quick deterministic_replay; diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index 8861c5b..0f5e7b0 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -11,6 +11,7 @@ let initialization () = run_id = run_id "test-run"; base_currency = "USD"; initial_cash = [ ("USD", money "10000") ]; + initial_portfolio = None; instruments = [ instrument ]; risk = risk ~instruments:[ instrument ] (); execution_model = T.Execution_model.find "completed_bar_v1" |> ok; @@ -26,7 +27,7 @@ let initialize_message_is_complete () = T.Strategy_protocol.initialize_message ~sequence:1L (initialization ()) in Alcotest.(check string) - "protocol version" "3" + "protocol version" "4" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -134,7 +135,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "3"); + ("strategy_protocol_version", `String "4"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); @@ -192,8 +193,8 @@ let responses_are_strict_and_typed () = let duplicate = `Assoc [ - ("strategy_protocol_version", `String "3"); - ("strategy_protocol_version", `String "3"); + ("strategy_protocol_version", `String "4"); + ("strategy_protocol_version", `String "4"); ("strategy_sequence", `String "3"); ("message_type", `String "stopped"); ("payload", `Assoc []); @@ -220,7 +221,7 @@ let responses_are_strict_and_typed () = let unknown_field = `Assoc [ - ("strategy_protocol_version", `String "3"); + ("strategy_protocol_version", `String "4"); ("strategy_sequence", `String "3"); ("message_type", `String "stopped"); ("payload", `Assoc []); diff --git a/test/validate_schemas.py b/test/validate_schemas.py index 6d59042..827d76e 100644 --- a/test/validate_schemas.py +++ b/test/validate_schemas.py @@ -69,6 +69,7 @@ def main() -> None: journal_validator = Draft202012Validator( journal_schema, format_checker=Draft202012Validator.FORMAT_CHECKER, + registry=registry, ) stream_validator = Draft202012Validator( stream_schema, @@ -115,7 +116,7 @@ def main() -> None: unsupported_execution_model = copy.deepcopy(scenario) unsupported_execution_model["execution"]["model"] = "future_model" expect_invalid(scenario_validator, unsupported_execution_model) - if contract_version == "5": + if contract_version in {"5", "6"}: missing_configuration_version = copy.deepcopy(scenario) del missing_configuration_version["execution"]["configuration"][ "version" @@ -129,7 +130,7 @@ def main() -> None: unknown_configuration_field = copy.deepcopy(scenario) unknown_configuration_field["execution"]["configuration"]["future"] = True expect_invalid(scenario_validator, unknown_configuration_field) - if contract_version in {"3", "4", "5"}: + if contract_version in {"3", "4", "5", "6"}: excessive_feedback_cap = copy.deepcopy(scenario) excessive_feedback_cap["max_internal_events"] = 100001 expect_invalid(scenario_validator, excessive_feedback_cap) @@ -149,7 +150,7 @@ def main() -> None: malformed_stream_slice = copy.deepcopy(stream_records[1]) malformed_stream_slice["payload"]["market_slice"]["unexpected"] = True expect_invalid(stream_validator, malformed_stream_slice) - if contract_version in {"3", "4"}: + if contract_version in {"3", "4", "5", "6"}: excessive_stream_catalog = copy.deepcopy(stream_records[0]) excessive_stream_catalog["payload"]["instruments"] = ( [stream_records[0]["payload"]["instruments"][0]] * 4097 @@ -162,6 +163,8 @@ def main() -> None: noncanonical = copy.deepcopy(scenario) if contract_version in {"3", "4"}: noncanonical["initial_cash"][0]["amount"] = "10000.0" + elif contract_version == "6": + noncanonical["initial_portfolio"]["cash"][0]["amount"] = "10000.0" else: noncanonical["initial_cash"] = "10000.0" expect_invalid(scenario_validator, noncanonical) From 6c548fd9808f315dc3f39b4f96ea167367a028bc Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 11:16:33 -0400 Subject: [PATCH 39/57] fix: preserve legacy strategy compatibility --- contracts/strategy/v4/README.md | 2 + lib/contract.ml | 9 ++- lib/contract.mli | 1 + lib/strategy_process.ml | 11 ++- lib/strategy_protocol.ml | 137 +++++++++++++++++++++----------- lib/strategy_protocol.mli | 16 +++- test/cli.t | 2 +- test/test_strategy_protocol.ml | 32 ++++++++ 8 files changed, 154 insertions(+), 56 deletions(-) diff --git a/contracts/strategy/v4/README.md b/contracts/strategy/v4/README.md index e718cba..1883476 100644 --- a/contracts/strategy/v4/README.md +++ b/contracts/strategy/v4/README.md @@ -3,6 +3,8 @@ Version 4 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 v3 remains available for legacy scenario contracts and retains its frozen message and +transcript shapes. Every message repeats `strategy_protocol_version: "4"` and a positive canonical `strategy_sequence`. A response must repeat the sequence of its request. Only one request is diff --git a/lib/contract.ml b/lib/contract.ml index 2b16f3d..4bf9255 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,12 +1,13 @@ let version = "6" let previous_version = "5" -let legacy_journal_version = "4" +let legacy_journal_version = "3" let supported_versions = - [ version; previous_version; legacy_journal_version; "3" ] + [ version; previous_version; "4"; legacy_journal_version ] let is_supported version = List.mem version supported_versions let strategy_protocol_version = "4" +let previous_strategy_protocol_version = "3" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -20,7 +21,9 @@ let capabilities_to_yojson () = ("journal_formats", strings [ "jsonl" ]); ("execution_models", strings Execution_model.supported); ("execution_model_contracts", Execution_model.capabilities_to_yojson ()); - ("strategy_protocol_versions", strings [ strategy_protocol_version ]); + ( "strategy_protocol_versions", + strings + [ strategy_protocol_version; previous_strategy_protocol_version ] ); ("resource_limits", Resource_limits.to_yojson ()); ] diff --git a/lib/contract.mli b/lib/contract.mli index fe9b1b3..967a91a 100644 --- a/lib/contract.mli +++ b/lib/contract.mli @@ -6,6 +6,7 @@ val legacy_journal_version : string val supported_versions : string list val is_supported : string -> bool val strategy_protocol_version : string +val previous_strategy_protocol_version : string val engine_version : string val capabilities_to_yojson : unit -> Yojson.Safe.t val capabilities_to_string : unit -> string diff --git a/lib/strategy_process.ml b/lib/strategy_process.ml index a6fe339..a4ae59f 100644 --- a/lib/strategy_process.ml +++ b/lib/strategy_process.ml @@ -7,6 +7,7 @@ type t = { transcript : Strategy_transcript.t; effects : Boundary_effects.t; timeout : float; + protocol_version : string; mutable next_sequence : int64; } @@ -258,7 +259,8 @@ let exchange session ~stage ~expected_sequence request = in let* response = response in match - Strategy_protocol.response_of_string ~expected_sequence response + Strategy_protocol.response_of_string + ~protocol_version:session.protocol_version ~expected_sequence response |> Result.map_error (Diagnostic.annotate ~sequence:expected_sequence ~json_path:"$") with @@ -297,7 +299,8 @@ let on_event session context event = let* sequence = next_sequence session in let* response = exchange_at session ~stage:"strategy event" ~sequence (fun ~sequence -> - Strategy_protocol.event_message ~sequence context event) + Strategy_protocol.event_message + ~protocol_version:session.protocol_version ~sequence context event) in match response with | Strategy_protocol.Intents intents -> Ok intents @@ -314,7 +317,8 @@ let shutdown session = let* sequence = next_sequence session in let* response = exchange_at session ~stage:"strategy shutdown" ~sequence - Strategy_protocol.shutdown_message + (Strategy_protocol.shutdown_message_for + ~protocol_version:session.protocol_version) in match response with | Strategy_protocol.Stopped -> Ok () @@ -457,6 +461,7 @@ let run_session ~effects ~env ~command ~executable ~timeout ~transcript transcript; effects; timeout; + protocol_version = Strategy_protocol.protocol_version initialization; next_sequence = 1L; } in diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index c40e5d6..74163de 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -38,10 +38,10 @@ let ratio value = string (Scalar.Ratio.to_decimal_string value) let timestamp value = string (Codec.ptime_to_string value) let instrument_id value = string (Id.Instrument.to_string value) -let message ~sequence:message_sequence ~message_type payload = +let message ~protocol_version ~sequence:message_sequence ~message_type payload = `Assoc [ - ("strategy_protocol_version", string version); + ("strategy_protocol_version", string protocol_version); ("strategy_sequence", sequence message_sequence); ("message_type", string message_type); ("payload", payload); @@ -73,21 +73,36 @@ let risk_to_yojson risk = ("short_borrow_bps", `Int (Risk.short_borrow_bps risk)); ] -let execution_to_yojson model execution = - `Assoc - [ - ("model", string (Execution_model.name model)); - ( "configuration", - `Assoc - [ - ("version", string "1"); - ("participation_bps", `Int (Execution.participation_bps execution)); - ("fixed_fee", money (Execution.fixed_fee execution)); - ("fee_bps", `Int (Execution.fee_bps execution)); - ] ); - ] +let execution_to_yojson ~protocol_version model execution = + if String.equal protocol_version version then + `Assoc + [ + ("model", string (Execution_model.name model)); + ( "configuration", + `Assoc + [ + ("version", string "1"); + ("participation_bps", `Int (Execution.participation_bps execution)); + ("fixed_fee", money (Execution.fixed_fee execution)); + ("fee_bps", `Int (Execution.fee_bps execution)); + ] ); + ] + else + `Assoc + [ + ("model", string (Execution_model.name model)); + ("participation_bps", `Int (Execution.participation_bps execution)); + ("fixed_fee", money (Execution.fixed_fee execution)); + ("fee_bps", `Int (Execution.fee_bps execution)); + ] + +let protocol_version initialization = + if String.equal initialization.scenario_contract_version Contract.version then + version + else Contract.previous_strategy_protocol_version let initialize_message ~sequence:message_sequence initialization = + let protocol_version = protocol_version initialization in let instruments = List.sort (fun left right -> @@ -99,26 +114,39 @@ let initialize_message ~sequence:message_sequence initialization = (fun (left, _) (right, _) -> String.compare left right) initialization.initial_cash in - message ~sequence:message_sequence ~message_type:"initialize" - (`Assoc - [ - ("engine_version", string Contract.engine_version); - ( "scenario_contract_version", - string initialization.scenario_contract_version ); - ("scenario_sha256", string initialization.scenario_sha256); - ("run_id", string (Id.Run.to_string initialization.run_id)); - ("base_currency", string initialization.base_currency); - ("initial_cash", `List (List.map cash_balance_to_yojson initial_cash)); - ( "initial_portfolio", - Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson - initialization.initial_portfolio ); - ("instruments", `List (List.map instrument_to_yojson instruments)); - ("risk", risk_to_yojson initialization.risk); - ( "execution", - execution_to_yojson initialization.execution_model - initialization.execution ); - ("metadata", initialization.metadata); - ]) + let fields = + [ + ("engine_version", string Contract.engine_version); + ( "scenario_contract_version", + string initialization.scenario_contract_version ); + ("scenario_sha256", string initialization.scenario_sha256); + ("run_id", string (Id.Run.to_string initialization.run_id)); + ("base_currency", string initialization.base_currency); + ("initial_cash", `List (List.map cash_balance_to_yojson initial_cash)); + ("instruments", `List (List.map instrument_to_yojson instruments)); + ("risk", risk_to_yojson initialization.risk); + ( "execution", + execution_to_yojson ~protocol_version initialization.execution_model + initialization.execution ); + ("metadata", initialization.metadata); + ] + in + let fields = + if String.equal protocol_version version then + let initial_portfolio = + Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson + initialization.initial_portfolio + in + List.concat + [ + List.take 6 fields; + [ ("initial_portfolio", initial_portfolio) ]; + List.drop 6 fields; + ] + else fields + in + message ~protocol_version ~sequence:message_sequence + ~message_type:"initialize" (`Assoc fields) let cash_attribution_to_yojson (balance : Account.cash_attribution) = `Assoc @@ -210,15 +238,20 @@ let event_to_yojson = function | Strategy.Intent_rejected reason -> `Assoc [ ("type", string "intent_rejected"); ("reason", string reason) ] -let event_message ~sequence:message_sequence context event = - message ~sequence:message_sequence ~message_type:"event" +let event_message ?(protocol_version = version) ~sequence:message_sequence + context event = + message ~protocol_version ~sequence:message_sequence ~message_type:"event" (`Assoc [ ("context", context_to_yojson context); ("event", event_to_yojson event); ]) -let shutdown_message ~sequence:message_sequence = - message ~sequence:message_sequence ~message_type:"shutdown" (`Assoc []) +let shutdown_message_for ~protocol_version ~sequence:message_sequence = + message ~protocol_version ~sequence:message_sequence ~message_type:"shutdown" + (`Assoc []) + +let shutdown_message ~sequence = + shutdown_message_for ~protocol_version:version ~sequence let object_fields ~name ~expected = function | `Assoc fields -> @@ -295,7 +328,7 @@ let parse_stopped_payload json = let* _ = object_fields ~name:"strategy stopped payload" ~expected:[] json in Ok Stopped -let response_of_yojson_result ~expected_sequence json = +let response_of_yojson_result ~protocol_version ~expected_sequence json = let* fields = object_fields ~name:"strategy response" ~expected: @@ -311,7 +344,7 @@ let response_of_yojson_result ~expected_sequence json = let* supplied_version = required_string ~name:"strategy_protocol_version" version_json in - if not (String.equal supplied_version version) then + if not (String.equal supplied_version protocol_version) then Error ("unsupported strategy protocol version: " ^ supplied_version) else let* sequence_json = field fields "strategy_sequence" in @@ -338,12 +371,13 @@ let response_of_yojson_result ~expected_sequence json = |> Result.map (fun message -> Failed message) | value -> Error ("unsupported strategy response type: " ^ value) -let response_of_yojson ~expected_sequence json = +let response_of_yojson ?(protocol_version = version) ~expected_sequence json = let json_path = match json with | `Assoc fields -> ( match List.assoc_opt "strategy_protocol_version" fields with - | Some (`String supplied) when not (String.equal supplied version) -> + | Some (`String supplied) + when not (String.equal supplied protocol_version) -> "$.strategy_protocol_version" | _ -> ( match List.assoc_opt "strategy_sequence" fields with @@ -375,13 +409,14 @@ let response_of_yojson ~expected_sequence json = (Printf.sprintf "intent count is %d; limit is %d" observed Resource_limits.intents_per_batch)) | _ -> - response_of_yojson_result ~expected_sequence json + response_of_yojson_result ~protocol_version ~expected_sequence json |> Result.map_error (fun message -> Diagnostic.make ~code:Diagnostic.Strategy_protocol ~phase:Diagnostic.Strategy ~sequence:expected_sequence ~json_path message) -let response_of_string ~expected_sequence document = +let response_of_string ?(protocol_version = version) ~expected_sequence document + = if String.length document > max_message_bytes then Error (Diagnostic.make ~code:Diagnostic.Resource_limit @@ -391,7 +426,7 @@ let response_of_string ~expected_sequence document = else try let json = Yojson.Safe.from_string document in - response_of_yojson ~expected_sequence json + response_of_yojson ~protocol_version ~expected_sequence json |> Result.map (fun response -> (response, json)) with Yojson.Json_error message as exception_ -> Error @@ -405,9 +440,17 @@ let direction_to_string = function | Strategy_to_engine -> "strategy_to_engine" let transcript_record ~transcript_sequence ~direction ~message = + let protocol_version = + match message with + | `Assoc fields -> ( + match List.assoc_opt "strategy_protocol_version" fields with + | Some (`String value) -> value + | _ -> version) + | _ -> version + in `Assoc [ - ("strategy_protocol_version", string version); + ("strategy_protocol_version", string protocol_version); ("transcript_sequence", sequence transcript_sequence); ("direction", string (direction_to_string direction)); ("message", message); diff --git a/lib/strategy_protocol.mli b/lib/strategy_protocol.mli index ce8b0dc..936911f 100644 --- a/lib/strategy_protocol.mli +++ b/lib/strategy_protocol.mli @@ -27,17 +27,29 @@ type response = type direction = Engine_to_strategy | Strategy_to_engine +val protocol_version : initialization -> string val initialize_message : sequence:int64 -> initialization -> Yojson.Safe.t val event_message : - sequence:int64 -> Strategy.context -> Strategy.event -> Yojson.Safe.t + ?protocol_version:string -> + sequence:int64 -> + Strategy.context -> + Strategy.event -> + Yojson.Safe.t + +val shutdown_message_for : + protocol_version:string -> sequence:int64 -> Yojson.Safe.t val shutdown_message : sequence:int64 -> Yojson.Safe.t val response_of_yojson : - expected_sequence:int64 -> Yojson.Safe.t -> (response, Diagnostic.t) result + ?protocol_version:string -> + expected_sequence:int64 -> + Yojson.Safe.t -> + (response, Diagnostic.t) result val response_of_string : + ?protocol_version:string -> expected_sequence:int64 -> string -> (response * Yojson.Safe.t, Diagnostic.t) result diff --git a/test/cli.t b/test/cli.t index eb7ac90..58e5748 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":["6","5","4","3"],"journal_contract_versions":["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":["1"],"scenario_contract_versions":["6","5","4","3"],"required_fields":["version","participation_bps","fixed_fee","fee_bps"],"supported_order_types":["market","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":["4"],"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":["6","5","4","3"],"journal_contract_versions":["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":["1"],"scenario_contract_versions":["6","5","4","3"],"required_fields":["version","participation_bps","fixed_fee","fee_bps"],"supported_order_types":["market","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":["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/v6/fixtures/demo.scenario.json valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=c98534261412acebf4b49d87619dc7c951538581c2a7dd05f315eb64d761cec2 diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index 0f5e7b0..990e14f 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -48,6 +48,36 @@ let initialize_message_is_complete () = | `List values -> List.length values | _ -> Alcotest.fail "expected instruments") +let legacy_initialize_message_remains_frozen () = + let initialization = + { + (initialization ()) with + scenario_contract_version = T.Contract.legacy_journal_version; + } + in + let message = + T.Strategy_protocol.initialize_message ~sequence:1L initialization + in + Alcotest.(check string) + "legacy protocol version" "3" + (match field "strategy_protocol_version" message with + | `String value -> value + | _ -> Alcotest.fail "expected version string"); + let payload = field "payload" message in + Alcotest.(check bool) + "no v4 initial portfolio" false + (match payload with + | `Assoc fields -> List.mem_assoc "initial_portfolio" fields + | _ -> Alcotest.fail "expected payload object"); + let execution = field "execution" payload in + Alcotest.(check bool) + "flat v3 execution" true + (match execution with + | `Assoc fields -> + List.mem_assoc "participation_bps" fields + && not (List.mem_assoc "configuration" fields) + | _ -> Alcotest.fail "expected execution object") + let event_message_contains_complete_context () = let account = test_account () in let slice = market_slice 1L in @@ -405,6 +435,8 @@ let tests = [ Alcotest.test_case "initialize message is complete" `Quick initialize_message_is_complete; + Alcotest.test_case "legacy initialize message remains frozen" `Quick + legacy_initialize_message_remains_frozen; Alcotest.test_case "event context is complete" `Quick event_message_contains_complete_context; Alcotest.test_case "nonpositive equity omits weights" `Quick From 6460a6d384511f1873d653dfdb7ab0ee2bb4c85a Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 11:57:52 -0400 Subject: [PATCH 40/57] feat: add instrument and group risk policies --- CHANGELOG.md | 3 + README.md | 24 +- contracts/conformance/cases.json | 455 +++++++- contracts/conformance/manifest.json | 296 ++++- contracts/strategy/v5/README.md | 53 + contracts/strategy/v5/dune | 15 + .../v5/fixtures/external.scenario.json | 204 ++++ .../v5/fixtures/external.scenario.jsonl | 4 + .../v5/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v5/message.schema.json | 294 +++++ contracts/strategy/v5/transcript.schema.json | 82 ++ contracts/v7/README.md | 32 + contracts/v7/dune | 16 + contracts/v7/fixtures/demo.journal.jsonl | 22 + contracts/v7/fixtures/demo.scenario.json | 302 +++++ contracts/v7/fixtures/demo.scenario.jsonl | 6 + .../v7/fixtures/fill-clipped.journal.jsonl | 11 + .../v7/fixtures/fill-clipped.scenario.json | 172 +++ contracts/v7/journal.schema.json | 238 ++++ contracts/v7/scenario-stream.schema.json | 76 ++ contracts/v7/scenario.schema.json | 428 +++++++ docs/api-reference.md | 2 +- docs/continuous-integration.md | 2 +- docs/execution-model.md | 4 +- docs/persistra.md | 6 +- docs/scenario.md | 28 +- lib/codec.ml | 118 +- lib/contract.ml | 13 +- lib/engine.ml | 62 +- lib/execution_model.ml | 2 +- lib/id.ml | 1 + lib/id.mli | 1 + lib/risk.ml | 1021 ++++++++++++++++- lib/risk.mli | 116 ++ lib/scenario.ml | 198 +++- lib/scenario_shape.ml | 14 +- lib/scenario_validation.ml | 33 +- lib/strategy.ml | 7 +- lib/strategy.mli | 3 + lib/strategy_protocol.ml | 143 ++- mkdocs.yml | 4 +- scripts/check-documentation.py | 4 +- scripts/release_artifacts.py | 12 +- test/cli.t | 38 +- test/dune | 96 +- test/fake_strategy.py | 2 +- test/test_contract_conformance.ml | 8 +- test/test_diagnostic.ml | 3 +- test/test_engine.ml | 1 + test/test_risk_groups.ml | 194 ++++ test/test_scenario.ml | 19 +- test/test_strategy_protocol.ml | 18 +- 52 files changed, 4596 insertions(+), 324 deletions(-) create mode 100644 contracts/strategy/v5/README.md create mode 100644 contracts/strategy/v5/dune create mode 100644 contracts/strategy/v5/fixtures/external.scenario.json create mode 100644 contracts/strategy/v5/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v5/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v5/message.schema.json create mode 100644 contracts/strategy/v5/transcript.schema.json create mode 100644 contracts/v7/README.md create mode 100644 contracts/v7/dune create mode 100644 contracts/v7/fixtures/demo.journal.jsonl create mode 100644 contracts/v7/fixtures/demo.scenario.json create mode 100644 contracts/v7/fixtures/demo.scenario.jsonl create mode 100644 contracts/v7/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v7/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v7/journal.schema.json create mode 100644 contracts/v7/scenario-stream.schema.json create mode 100644 contracts/v7/scenario.schema.json create mode 100644 test/test_risk_groups.ml diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c380a4..a21bbe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add contract v7 exact per-instrument risk policies, versioned overlapping exposure groups, + reservation-aware admission and fill clipping, group diagnostics, and strategy protocol v5. + - Add contract v6 explicit initial portfolio snapshots with signed cash and positions, accounting history, initial marks and FX, strict risk validation, initial-state auditing, and strategy protocol v4 initialization. diff --git a/README.md b/README.md index 209aef3..52f98d9 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ scenario slices and scheduled or external intents - Per-currency cash and per-instrument quantity, mark, value, basis, P&L, and fee attribution - Deterministic event IDs, ordered causal references, and order-creation attribution - Contract-selected compiled execution modules with versioned model-owned configuration and - capability descriptors; v6 currently exposes `completed_bar_v1` + capability descriptors; v7 currently exposes `completed_bar_v1` - 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 @@ -86,7 +86,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v6/fixtures/demo.scenario.json \ + --input contracts/v7/fixtures/demo.scenario.json \ --validate-only ``` @@ -94,7 +94,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v6/fixtures/demo.scenario.json \ + --input contracts/v7/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -102,7 +102,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v6/fixtures/demo.scenario.jsonl \ + --input contracts/v7/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -111,7 +111,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v4/fixtures/external.scenario.json \ + --input contracts/strategy/v5/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -218,19 +218,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 v6 and conformance fixtures](contracts/v6/README.md) +- [Current contract v7 and conformance fixtures](contracts/v7/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v6/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v6/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v6/journal.schema.json) -- [External strategy protocol v4](contracts/strategy/v4/README.md) +- [Scenario JSON Schema](contracts/v7/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v7/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v7/journal.schema.json) +- [External strategy protocol v5](contracts/strategy/v5/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/v4/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v4/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v5/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v5/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 0bc2690..8d28ebd 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -1,6 +1,26 @@ { "format_version": "1", "cases": [ + { + "name": "scenario-v7-valid", + "artifact": "scenario-v7", + "kind": "scenario", + "source": "v7/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-stream-v7-valid", + "artifact": "scenario-stream-v7", + "kind": "scenario_stream", + "source": "v7/fixtures/demo.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, { "name": "scenario-v6-valid", "artifact": "scenario-v6", @@ -36,7 +56,14 @@ "artifact": "scenario-v5", "kind": "scenario", "source": "v5/fixtures/demo.scenario.json", - "mutations": [{"op": "remove", "path": ["contract_version"]}], + "mutations": [ + { + "op": "remove", + "path": [ + "contract_version" + ] + } + ], "schema_expectation": "reject", "runtime_expectation": "reject", "rule": "structural" @@ -46,7 +73,15 @@ "artifact": "scenario-v5", "kind": "scenario", "source": "v5/fixtures/demo.scenario.json", - "mutations": [{"op": "add", "path": ["unexpected_contract_field"], "value": true}], + "mutations": [ + { + "op": "add", + "path": [ + "unexpected_contract_field" + ], + "value": true + } + ], "schema_expectation": "reject", "runtime_expectation": "reject", "rule": "structural" @@ -56,7 +91,17 @@ "artifact": "scenario-v5", "kind": "scenario", "source": "v5/fixtures/demo.scenario.json", - "mutations": [{"op": "replace", "path": ["initial_cash", 0, "amount"], "value": 10000}], + "mutations": [ + { + "op": "replace", + "path": [ + "initial_cash", + 0, + "amount" + ], + "value": 10000 + } + ], "schema_expectation": "reject", "runtime_expectation": "reject", "rule": "structural" @@ -66,7 +111,17 @@ "artifact": "scenario-v5", "kind": "scenario", "source": "v5/fixtures/demo.scenario.json", - "mutations": [{"op": "replace", "path": ["execution", "configuration", "version"], "value": "2"}], + "mutations": [ + { + "op": "replace", + "path": [ + "execution", + "configuration", + "version" + ], + "value": "2" + } + ], "schema_expectation": "reject", "runtime_expectation": "reject", "rule": "structural" @@ -76,7 +131,15 @@ "artifact": "scenario-v5", "kind": "scenario", "source": "v5/fixtures/demo.scenario.json", - "mutations": [{"op": "append_copy", "path": ["instruments"], "index": 0}], + "mutations": [ + { + "op": "append_copy", + "path": [ + "instruments" + ], + "index": 0 + } + ], "schema_expectation": "accept", "runtime_expectation": "reject", "rule": "semantic" @@ -86,7 +149,17 @@ "artifact": "scenario-v5", "kind": "scenario", "source": "v5/fixtures/demo.scenario.json", - "mutations": [{"op": "replace", "path": ["slices", 1, "start_at"], "value": "2026-01-02T20:00:00Z"}], + "mutations": [ + { + "op": "replace", + "path": [ + "slices", + 1, + "start_at" + ], + "value": "2026-01-02T20:00:00Z" + } + ], "schema_expectation": "accept", "runtime_expectation": "reject", "rule": "semantic" @@ -127,7 +200,14 @@ "kind": "scenario_stream", "source": "v5/fixtures/demo.scenario.jsonl", "record": 1, - "mutations": [{"op": "remove", "path": ["contract_version"]}], + "mutations": [ + { + "op": "remove", + "path": [ + "contract_version" + ] + } + ], "schema_expectation": "reject", "runtime_expectation": "reject", "rule": "structural" @@ -138,7 +218,15 @@ "kind": "scenario_stream", "source": "v5/fixtures/demo.scenario.jsonl", "record": 2, - "mutations": [{"op": "add", "path": ["unexpected_contract_field"], "value": true}], + "mutations": [ + { + "op": "add", + "path": [ + "unexpected_contract_field" + ], + "value": true + } + ], "schema_expectation": "reject", "runtime_expectation": "reject", "rule": "structural" @@ -149,18 +237,28 @@ "kind": "scenario_stream", "source": "v5/fixtures/demo.scenario.jsonl", "record": 2, - "mutations": [{"op": "replace", "path": ["scenario_sequence"], "value": "3"}], + "mutations": [ + { + "op": "replace", + "path": [ + "scenario_sequence" + ], + "value": "3" + } + ], "schema_expectation": "accept", "runtime_expectation": "reject", "rule": "semantic" }, { "name": "strategy-ready-valid", - "artifact": "strategy-message-v4", + "artifact": "strategy-message-v5", "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", + "source": "strategy/v5/fixtures/external.strategy.jsonl", "record": 2, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "1", "mutations": [], "schema_expectation": "accept", @@ -169,11 +267,13 @@ }, { "name": "strategy-intents-valid", - "artifact": "strategy-message-v4", + "artifact": "strategy-message-v5", "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", + "source": "strategy/v5/fixtures/external.strategy.jsonl", "record": 4, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "2", "mutations": [], "schema_expectation": "accept", @@ -182,11 +282,13 @@ }, { "name": "strategy-stopped-valid", - "artifact": "strategy-message-v4", + "artifact": "strategy-message-v5", "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", + "source": "strategy/v5/fixtures/external.strategy.jsonl", "record": 14, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "7", "mutations": [], "schema_expectation": "accept", @@ -195,15 +297,31 @@ }, { "name": "strategy-error-valid", - "artifact": "strategy-message-v4", + "artifact": "strategy-message-v5", "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", + "source": "strategy/v5/fixtures/external.strategy.jsonl", "record": 14, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "7", "mutations": [ - {"op": "replace", "path": ["message_type"], "value": "error"}, - {"op": "replace", "path": ["payload"], "value": {"message": "intentional conformance error"}} + { + "op": "replace", + "path": [ + "message_type" + ], + "value": "error" + }, + { + "op": "replace", + "path": [ + "payload" + ], + "value": { + "message": "intentional conformance error" + } + } ], "schema_expectation": "accept", "runtime_expectation": "accept", @@ -211,42 +329,207 @@ }, { "name": "strategy-missing-version", - "artifact": "strategy-message-v4", + "artifact": "strategy-message-v5", "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", + "source": "strategy/v5/fixtures/external.strategy.jsonl", "record": 2, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "1", - "mutations": [{"op": "remove", "path": ["strategy_protocol_version"]}], + "mutations": [ + { + "op": "remove", + "path": [ + "strategy_protocol_version" + ] + } + ], "schema_expectation": "reject", "runtime_expectation": "reject", "rule": "structural" }, { "name": "strategy-unknown-field", - "artifact": "strategy-message-v4", + "artifact": "strategy-message-v5", "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", + "source": "strategy/v5/fixtures/external.strategy.jsonl", "record": 2, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "1", - "mutations": [{"op": "add", "path": ["unexpected_contract_field"], "value": true}], + "mutations": [ + { + "op": "add", + "path": [ + "unexpected_contract_field" + ], + "value": true + } + ], "schema_expectation": "reject", "runtime_expectation": "reject", "rule": "structural" }, { "name": "strategy-wrong-sequence", + "artifact": "strategy-message-v5", + "kind": "strategy_response", + "source": "strategy/v5/fixtures/external.strategy.jsonl", + "record": 2, + "extract": [ + "message" + ], + "expected_sequence": "2", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "reject", + "rule": "semantic" + }, + { + "name": "strategy-ready-valid-v4", + "artifact": "strategy-message-v4", + "kind": "strategy_response", + "source": "strategy/v4/fixtures/external.strategy.jsonl", + "record": 2, + "extract": [ + "message" + ], + "expected_sequence": "1", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural", + "protocol_version": "4" + }, + { + "name": "strategy-intents-valid-v4", + "artifact": "strategy-message-v4", + "kind": "strategy_response", + "source": "strategy/v4/fixtures/external.strategy.jsonl", + "record": 4, + "extract": [ + "message" + ], + "expected_sequence": "2", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural", + "protocol_version": "4" + }, + { + "name": "strategy-stopped-valid-v4", + "artifact": "strategy-message-v4", + "kind": "strategy_response", + "source": "strategy/v4/fixtures/external.strategy.jsonl", + "record": 14, + "extract": [ + "message" + ], + "expected_sequence": "7", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural", + "protocol_version": "4" + }, + { + "name": "strategy-error-valid-v4", + "artifact": "strategy-message-v4", + "kind": "strategy_response", + "source": "strategy/v4/fixtures/external.strategy.jsonl", + "record": 14, + "extract": [ + "message" + ], + "expected_sequence": "7", + "mutations": [ + { + "op": "replace", + "path": [ + "message_type" + ], + "value": "error" + }, + { + "op": "replace", + "path": [ + "payload" + ], + "value": { + "message": "intentional conformance error" + } + } + ], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural", + "protocol_version": "4" + }, + { + "name": "strategy-missing-version-v4", + "artifact": "strategy-message-v4", + "kind": "strategy_response", + "source": "strategy/v4/fixtures/external.strategy.jsonl", + "record": 2, + "extract": [ + "message" + ], + "expected_sequence": "1", + "mutations": [ + { + "op": "remove", + "path": [ + "strategy_protocol_version" + ] + } + ], + "schema_expectation": "reject", + "runtime_expectation": "reject", + "rule": "structural", + "protocol_version": "4" + }, + { + "name": "strategy-unknown-field-v4", + "artifact": "strategy-message-v4", + "kind": "strategy_response", + "source": "strategy/v4/fixtures/external.strategy.jsonl", + "record": 2, + "extract": [ + "message" + ], + "expected_sequence": "1", + "mutations": [ + { + "op": "add", + "path": [ + "unexpected_contract_field" + ], + "value": true + } + ], + "schema_expectation": "reject", + "runtime_expectation": "reject", + "rule": "structural", + "protocol_version": "4" + }, + { + "name": "strategy-wrong-sequence-v4", "artifact": "strategy-message-v4", "kind": "strategy_response", "source": "strategy/v4/fixtures/external.strategy.jsonl", "record": 2, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "2", "mutations": [], "schema_expectation": "accept", "runtime_expectation": "reject", - "rule": "semantic" + "rule": "semantic", + "protocol_version": "4" } ], "schema_only_cases": [ @@ -255,10 +538,26 @@ "artifact": "strategy-message-v1", "source": "strategy/v1/fixtures/external.strategy.jsonl", "record": 14, - "extract": ["message"], + "extract": [ + "message" + ], "mutations": [ - {"op": "replace", "path": ["message_type"], "value": "error"}, - {"op": "replace", "path": ["payload"], "value": {"message": "intentional conformance error"}} + { + "op": "replace", + "path": [ + "message_type" + ], + "value": "error" + }, + { + "op": "replace", + "path": [ + "payload" + ], + "value": { + "message": "intentional conformance error" + } + } ], "schema_expectation": "accept" }, @@ -267,10 +566,26 @@ "artifact": "strategy-message-v2", "source": "strategy/v2/fixtures/external.strategy.jsonl", "record": 14, - "extract": ["message"], + "extract": [ + "message" + ], "mutations": [ - {"op": "replace", "path": ["message_type"], "value": "error"}, - {"op": "replace", "path": ["payload"], "value": {"message": "intentional conformance error"}} + { + "op": "replace", + "path": [ + "message_type" + ], + "value": "error" + }, + { + "op": "replace", + "path": [ + "payload" + ], + "value": { + "message": "intentional conformance error" + } + } ], "schema_expectation": "accept" }, @@ -279,10 +594,26 @@ "artifact": "strategy-message-v3", "source": "strategy/v3/fixtures/external.strategy.jsonl", "record": 14, - "extract": ["message"], + "extract": [ + "message" + ], "mutations": [ - {"op": "replace", "path": ["message_type"], "value": "error"}, - {"op": "replace", "path": ["payload"], "value": {"message": "intentional conformance error"}} + { + "op": "replace", + "path": [ + "message_type" + ], + "value": "error" + }, + { + "op": "replace", + "path": [ + "payload" + ], + "value": { + "message": "intentional conformance error" + } + } ], "schema_expectation": "accept" }, @@ -299,7 +630,10 @@ "code": "strategy.protocol", "phase": "strategy", "message": "strategy initialization: invalid strategy response JSON", - "context": {"json_path": "$", "sequence": "1"}, + "context": { + "json_path": "$", + "sequence": "1" + }, "cause": null }, "evidence": { @@ -325,7 +659,10 @@ "code": "strategy.protocol", "phase": "strategy", "message": "strategy initialization: invalid strategy response JSON", - "context": {"json_path": "$", "sequence": "1"}, + "context": { + "json_path": "$", + "sequence": "1" + }, "cause": null }, "evidence": { @@ -337,6 +674,36 @@ }, "mutations": [], "schema_expectation": "accept" + }, + { + "name": "strategy-v5-rejected-response-branch", + "artifact": "strategy-transcript-v5", + "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", + "source": "strategy/v5/fixtures/external.strategy.jsonl" } ] } diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index 6ef1a69..57b1e0a 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -7,7 +7,10 @@ "version_field": "contract_version", "version": "1", "sources": [ - {"path": "v1/fixtures/demo.scenario.json", "format": "json"} + { + "path": "v1/fixtures/demo.scenario.json", + "format": "json" + } ] }, { @@ -16,7 +19,10 @@ "version_field": "contract_version", "version": "1", "sources": [ - {"path": "v1/fixtures/demo.scenario.jsonl", "format": "jsonl"} + { + "path": "v1/fixtures/demo.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -25,7 +31,10 @@ "version_field": "contract_version", "version": "1", "sources": [ - {"path": "v1/fixtures/demo.journal.jsonl", "format": "jsonl"} + { + "path": "v1/fixtures/demo.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -34,7 +43,10 @@ "version_field": "contract_version", "version": "2", "sources": [ - {"path": "v2/fixtures/demo.scenario.json", "format": "json"} + { + "path": "v2/fixtures/demo.scenario.json", + "format": "json" + } ] }, { @@ -43,7 +55,10 @@ "version_field": "contract_version", "version": "2", "sources": [ - {"path": "v2/fixtures/demo.scenario.jsonl", "format": "jsonl"} + { + "path": "v2/fixtures/demo.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -52,7 +67,10 @@ "version_field": "contract_version", "version": "2", "sources": [ - {"path": "v2/fixtures/demo.journal.jsonl", "format": "jsonl"} + { + "path": "v2/fixtures/demo.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -61,10 +79,22 @@ "version_field": "contract_version", "version": "3", "sources": [ - {"path": "v3/fixtures/demo.scenario.json", "format": "json"}, - {"path": "strategy/v1/fixtures/external.scenario.json", "format": "json"}, - {"path": "strategy/v2/fixtures/external.scenario.json", "format": "json"}, - {"path": "strategy/v3/fixtures/external.scenario.json", "format": "json"} + { + "path": "v3/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "strategy/v1/fixtures/external.scenario.json", + "format": "json" + }, + { + "path": "strategy/v2/fixtures/external.scenario.json", + "format": "json" + }, + { + "path": "strategy/v3/fixtures/external.scenario.json", + "format": "json" + } ] }, { @@ -73,10 +103,22 @@ "version_field": "contract_version", "version": "3", "sources": [ - {"path": "v3/fixtures/demo.scenario.jsonl", "format": "jsonl"}, - {"path": "strategy/v1/fixtures/external.scenario.jsonl", "format": "jsonl"}, - {"path": "strategy/v2/fixtures/external.scenario.jsonl", "format": "jsonl"}, - {"path": "strategy/v3/fixtures/external.scenario.jsonl", "format": "jsonl"} + { + "path": "v3/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v1/fixtures/external.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v2/fixtures/external.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v3/fixtures/external.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -85,7 +127,10 @@ "version_field": "contract_version", "version": "3", "sources": [ - {"path": "v3/fixtures/demo.journal.jsonl", "format": "jsonl"} + { + "path": "v3/fixtures/demo.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -94,8 +139,14 @@ "version_field": "contract_version", "version": "4", "sources": [ - {"path": "v4/fixtures/demo.scenario.json", "format": "json"}, - {"path": "v4/fixtures/fill-clipped.scenario.json", "format": "json"} + { + "path": "v4/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v4/fixtures/fill-clipped.scenario.json", + "format": "json" + } ] }, { @@ -104,7 +155,10 @@ "version_field": "contract_version", "version": "4", "sources": [ - {"path": "v4/fixtures/demo.scenario.jsonl", "format": "jsonl"} + { + "path": "v4/fixtures/demo.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -113,8 +167,14 @@ "version_field": "contract_version", "version": "4", "sources": [ - {"path": "v4/fixtures/demo.journal.jsonl", "format": "jsonl"}, - {"path": "v4/fixtures/fill-clipped.journal.jsonl", "format": "jsonl"} + { + "path": "v4/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v4/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -123,8 +183,14 @@ "version_field": "contract_version", "version": "5", "sources": [ - {"path": "v5/fixtures/demo.scenario.json", "format": "json"}, - {"path": "v5/fixtures/fill-clipped.scenario.json", "format": "json"} + { + "path": "v5/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v5/fixtures/fill-clipped.scenario.json", + "format": "json" + } ] }, { @@ -133,7 +199,10 @@ "version_field": "contract_version", "version": "5", "sources": [ - {"path": "v5/fixtures/demo.scenario.jsonl", "format": "jsonl"} + { + "path": "v5/fixtures/demo.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -142,8 +211,14 @@ "version_field": "contract_version", "version": "5", "sources": [ - {"path": "v5/fixtures/demo.journal.jsonl", "format": "jsonl"}, - {"path": "v5/fixtures/fill-clipped.journal.jsonl", "format": "jsonl"} + { + "path": "v5/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v5/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -152,9 +227,18 @@ "version_field": "contract_version", "version": "6", "sources": [ - {"path": "v6/fixtures/demo.scenario.json", "format": "json"}, - {"path": "v6/fixtures/fill-clipped.scenario.json", "format": "json"}, - {"path": "strategy/v4/fixtures/external.scenario.json", "format": "json"} + { + "path": "v6/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v6/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "strategy/v4/fixtures/external.scenario.json", + "format": "json" + } ] }, { @@ -163,8 +247,14 @@ "version_field": "contract_version", "version": "6", "sources": [ - {"path": "v6/fixtures/demo.scenario.jsonl", "format": "jsonl"}, - {"path": "strategy/v4/fixtures/external.scenario.jsonl", "format": "jsonl"} + { + "path": "v6/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v4/fixtures/external.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -173,8 +263,14 @@ "version_field": "contract_version", "version": "6", "sources": [ - {"path": "v6/fixtures/demo.journal.jsonl", "format": "jsonl"}, - {"path": "v6/fixtures/fill-clipped.journal.jsonl", "format": "jsonl"} + { + "path": "v6/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v6/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -183,7 +279,10 @@ "version_field": "diagnostic_version", "version": "1", "sources": [ - {"path": "diagnostic/v1/fixtures/strategy-protocol.json", "format": "json"} + { + "path": "diagnostic/v1/fixtures/strategy-protocol.json", + "format": "json" + } ] }, { @@ -192,7 +291,13 @@ "version_field": "strategy_protocol_version", "version": "1", "sources": [ - {"path": "strategy/v1/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"]} + { + "path": "strategy/v1/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } ] }, { @@ -201,7 +306,10 @@ "version_field": "strategy_protocol_version", "version": "1", "sources": [ - {"path": "strategy/v1/fixtures/external.strategy.jsonl", "format": "jsonl"} + { + "path": "strategy/v1/fixtures/external.strategy.jsonl", + "format": "jsonl" + } ] }, { @@ -210,7 +318,13 @@ "version_field": "strategy_protocol_version", "version": "2", "sources": [ - {"path": "strategy/v2/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"]} + { + "path": "strategy/v2/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } ] }, { @@ -219,7 +333,10 @@ "version_field": "strategy_protocol_version", "version": "2", "sources": [ - {"path": "strategy/v2/fixtures/external.strategy.jsonl", "format": "jsonl"} + { + "path": "strategy/v2/fixtures/external.strategy.jsonl", + "format": "jsonl" + } ] }, { @@ -228,7 +345,13 @@ "version_field": "strategy_protocol_version", "version": "3", "sources": [ - {"path": "strategy/v3/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"]} + { + "path": "strategy/v3/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } ] }, { @@ -237,7 +360,10 @@ "version_field": "strategy_protocol_version", "version": "3", "sources": [ - {"path": "strategy/v3/fixtures/external.strategy.jsonl", "format": "jsonl"} + { + "path": "strategy/v3/fixtures/external.strategy.jsonl", + "format": "jsonl" + } ] }, { @@ -246,7 +372,13 @@ "version_field": "strategy_protocol_version", "version": "4", "sources": [ - {"path": "strategy/v4/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"]} + { + "path": "strategy/v4/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } ] }, { @@ -255,7 +387,89 @@ "version_field": "strategy_protocol_version", "version": "4", "sources": [ - {"path": "strategy/v4/fixtures/external.strategy.jsonl", "format": "jsonl"} + { + "path": "strategy/v4/fixtures/external.strategy.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "scenario-v7", + "schema": "v7/scenario.schema.json", + "version_field": "contract_version", + "version": "7", + "sources": [ + { + "path": "v7/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v7/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "strategy/v5/fixtures/external.scenario.json", + "format": "json" + } + ] + }, + { + "name": "scenario-stream-v7", + "schema": "v7/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "7", + "sources": [ + { + "path": "v7/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v5/fixtures/external.scenario.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "journal-v7", + "schema": "v7/journal.schema.json", + "version_field": "contract_version", + "version": "7", + "sources": [ + { + "path": "v7/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v7/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "strategy-message-v5", + "schema": "strategy/v5/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "5", + "sources": [ + { + "path": "strategy/v5/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } + ] + }, + { + "name": "strategy-transcript-v5", + "schema": "strategy/v5/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "5", + "sources": [ + { + "path": "strategy/v5/fixtures/external.strategy.jsonl", + "format": "jsonl" + } ] } ] diff --git a/contracts/strategy/v5/README.md b/contracts/strategy/v5/README.md new file mode 100644 index 0000000..d5da556 --- /dev/null +++ b/contracts/strategy/v5/README.md @@ -0,0 +1,53 @@ +# External strategy protocol v5 + +Version 5 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`. +Protocols v4 and v3 remain available for legacy scenario contracts and retain their frozen shapes. + +Every message repeats `strategy_protocol_version: "5"` 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 v7 and includes the exact `initial_portfolio` +snapshot alongside the legacy cash projection. It also carries the nested, versioned execution +configuration, so a strategy can reject incompatible 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, fills, order updates, and rejected intents. Response +intents use the scenario v7 intent shapes. + +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/v5/dune b/contracts/strategy/v5/dune new file mode 100644 index 0000000..3f3d97d --- /dev/null +++ b/contracts/strategy/v5/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v5/message.schema.json) + (transcript.schema.json as contracts/strategy/v5/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v5/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v5/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v5/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v5/fixtures/external.scenario.json b/contracts/strategy/v5/fixtures/external.scenario.json new file mode 100644 index 0000000..7a4da01 --- /dev/null +++ b/contracts/strategy/v5/fixtures/external.scenario.json @@ -0,0 +1,204 @@ +{ + "contract_version": "7", + "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": "1", + "participation_bps": 5000, + "fixed_fee": "0.25", + "fee_bps": 10 + } + }, + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/strategy/v5/fixtures/external.scenario.jsonl b/contracts/strategy/v5/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..83383a9 --- /dev/null +++ b/contracts/strategy/v5/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"7","payload":{"metadata":{"producer":"strategy-protocol-fixture"},"run_id":"external-demo","base_currency":"USD","initial_portfolio":{"cash":[{"amount":"10000","currency":"USD"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"1","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"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":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000},"record_type":"scenario_header","scenario_sequence":"1"} +{"contract_version":"7","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"}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"7","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"}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"7","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v5/fixtures/external.strategy.jsonl b/contracts/strategy/v5/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..d24e191 --- /dev/null +++ b/contracts/strategy/v5/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"5","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"7","scenario_sha256":"006cea48630b4cd06e7e3908de78065e931c069cac90ae04d2567f20896d413b","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"}]},"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":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"metadata":{"producer":"strategy-protocol-fixture"}}}} +{"strategy_protocol_version":"5","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"5","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":[]}}}}} +{"strategy_protocol_version":"5","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","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":"5","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0"}],"group_exposures":[]},"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","limit_price":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","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","limit_price":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","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} +{"strategy_protocol_version":"5","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"5","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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"}}}}} +{"strategy_protocol_version":"5","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"5","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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","limit_price":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","filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} +{"strategy_protocol_version":"5","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"5","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":[]}}}}} +{"strategy_protocol_version":"5","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"5","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"5","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v5/message.schema.json b/contracts/strategy/v5/message.schema.json new file mode 100644 index 0000000..1e49290 --- /dev/null +++ b/contracts/strategy/v5/message.schema.json @@ -0,0 +1,294 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v5/message.schema.json", + "title": "Trading Engine external strategy protocol v5 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": "5" }, + "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", "risk", "execution", "metadata"], + "properties": { + "engine_version": { "type": "string", "minLength": 1 }, + "scenario_contract_version": { "const": "7" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/instrument" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/execution" }, + "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/v7/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/v7/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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/v7/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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/v7/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/v7/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/signedDecimal" } + ] + }, + "markedPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], + "properties": { + "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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/v7/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/v7/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/v7/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/v7/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/v5/transcript.schema.json b/contracts/strategy/v5/transcript.schema.json new file mode 100644 index 0000000..cc6fba1 --- /dev/null +++ b/contracts/strategy/v5/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/v5/transcript.schema.json", + "title": "Trading Engine external strategy protocol v5 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": "5" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v5/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/v7/README.md b/contracts/v7/README.md new file mode 100644 index 0000000..1ec393d --- /dev/null +++ b/contracts/v7/README.md @@ -0,0 +1,32 @@ +# Trading Engine contract v7 + +This directory is the authoritative v7 process and file contract shared by Trading Engine and its +clients. Versions 6, 5, 4, and 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 v7 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 v7 scenario, stream record, and journal record carries `"contract_version": "7"`. + +The v7 `execution` object retains the versioned configuration introduced by v5. +`completed_bar_v1` configuration version `"1"` requires participation basis points, fixed fee, +and fee basis points. Runtime capabilities describe its required fields, supported market and limit +orders, completed-OHLCV data requirement, and numeric limits. diff --git a/contracts/v7/dune b/contracts/v7/dune new file mode 100644 index 0000000..4497ccd --- /dev/null +++ b/contracts/v7/dune @@ -0,0 +1,16 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v7/journal.schema.json) + (scenario-stream.schema.json as contracts/v7/scenario-stream.schema.json) + (scenario.schema.json as contracts/v7/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v7/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v7/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl as contracts/v7/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v7/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v7/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v7/fixtures/demo.journal.jsonl b/contracts/v7/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..acf860e --- /dev/null +++ b/contracts/v7/fixtures/demo.journal.jsonl @@ -0,0 +1,22 @@ +{"contract_version":"7","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":"d1991fa67140bff80fcbeb9b04b211d8c9cf4f41d4fba39dcec66d5ef3e5fab9","execution_model":"completed_bar_v1"}} +{"contract_version":"7","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} +{"contract_version":"7","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} +{"contract_version":"7","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":[]}} +{"contract_version":"7","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} +{"contract_version":"7","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":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} +{"contract_version":"7","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004","demo-event-000000000005"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000007","updated_event_id":"demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"7","engine_sequence":"8","event_id":"demo-event-000000000008","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","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"14","equity":"10104","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"}],"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"}],"margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052","maintenance_excess":"10078","margin_call":false},"group_exposures":[]}} +{"contract_version":"7","engine_sequence":"9","event_id":"demo-event-000000000009","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} +{"contract_version":"7","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} +{"contract_version":"7","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000007","updated_event_id":"demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"7","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000005","demo-event-000000000009"],"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.715","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000012","updated_event_id":"demo-event-000000000012","created_sequence":"12","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"7","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000009"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5","unrealized_pnl":"40.132","equity":"10130.132","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618"}],"margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.632","maintenance_excess":"9942.882","margin_call":false},"group_exposures":[]}} +{"contract_version":"7","engine_sequence":"14","event_id":"demo-event-000000000014","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":[]}} +{"contract_version":"7","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.715","price":"107","notional":"290.505","fee":"0.540505","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} +{"contract_version":"7","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000014"],"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":"7","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":["demo-event-000000000014","demo-event-000000000016"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000017","updated_event_id":"demo-event-000000000017","created_sequence":"17","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"7","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000014"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.086495","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5","unrealized_pnl":"20.161495","equity":"10110.161495","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.086495","fx_rate":"1","base_value":"9090.086495"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505"}],"margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.123995","maintenance_excess":"9855.142745","margin_call":false},"group_exposures":[]}} +{"contract_version":"7","engine_sequence":"19","event_id":"demo-event-000000000019","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":[]}} +{"contract_version":"7","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000017","demo-event-000000000019"],"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":"105","notional":"757.575","fee":"1.007575","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} +{"contract_version":"7","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false},"group_exposures":[]}} +{"contract_version":"7","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"d1991fa67140bff80fcbeb9b04b211d8c9cf4f41d4fba39dcec66d5ef3e5fab9","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v7/fixtures/demo.scenario.json b/contracts/v7/fixtures/demo.scenario.json new file mode 100644 index 0000000..7ad99d3 --- /dev/null +++ b/contracts/v7/fixtures/demo.scenario.json @@ -0,0 +1,302 @@ +{ + "contract_version": "7", + "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_v1", + "configuration": { + "version": "1", + "participation_bps": 5000, + "fixed_fee": "0.25", + "fee_bps": 10 + } + }, + "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": [] + }, + { + "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": "12" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [] + }, + { + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v7/fixtures/demo.scenario.jsonl b/contracts/v7/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..6bd7435 --- /dev/null +++ b/contracts/v7/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"7","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_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000}} +{"contract_version":"7","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"}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"7","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"7","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"}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"7","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"}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"7","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v7/fixtures/fill-clipped.journal.jsonl b/contracts/v7/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..678ea43 --- /dev/null +++ b/contracts/v7/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,11 @@ +{"contract_version":"7","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":"61f1319c6667400bec59562d106b580a7607e97fa3f1d837018ac7c4f38cc6bb","execution_model":"completed_bar_v1"}} +{"contract_version":"7","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} +{"contract_version":"7","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"7","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":[]}} +{"contract_version":"7","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":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"7","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":"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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"7","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","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":[]}} +{"contract_version":"7","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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":"7","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"7","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000007"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21: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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"7","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000010"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"61f1319c6667400bec59562d106b580a7607e97fa3f1d837018ac7c4f38cc6bb","execution_model":"completed_bar_v1","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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v7/fixtures/fill-clipped.scenario.json b/contracts/v7/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..159d2e4 --- /dev/null +++ b/contracts/v7/fixtures/fill-clipped.scenario.json @@ -0,0 +1,172 @@ +{ + "contract_version": "7", + "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": "1", + "participation_bps": 10000, + "fixed_fee": "10", + "fee_bps": 0 + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "market", + "limit_price": 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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v7/journal.schema.json b/contracts/v7/journal.schema.json new file mode 100644 index 0000000..808db61 --- /dev/null +++ b/contracts/v7/journal.schema.json @@ -0,0 +1,238 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v7/journal.schema.json", + "title": "Trading Engine v6 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": "7" }, + "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_cancelled", "split_applied", "cash_dividend_applied", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] + }, + "payload": { "type": "object" } + }, + "allOf": [ + { "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"] } } }, "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } + }, + "initialState": { + "type": "object", "additionalProperties": false, + "required": ["portfolio", "valuation"], + "properties": { + "portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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" } } } + ] + }, + "marketSlice": { + "type": "object", "additionalProperties": false, + "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], + "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" } } + } + }, + "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", "limit_price", "origin", "created_event_id", "updated_event_id", "created_sequence", "created_at", "eligible_after_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"] }, "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, "origin": { "enum": ["direct", "target_rebalance", "margin_liquidation"] }, "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" }, "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", "margin_call"] } } + }, + "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" } } + }, + "orderAdjusted": { + "type": "object", "additionalProperties": false, "required": ["order", "action_id"], + "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } + }, + "fill": { + "type": "object", "additionalProperties": false, + "required": ["fill_id", "order_id", "instrument_id", "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", "slice_sequence"], + "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/unsignedDecimal" }, "executed_at": { "$ref": "#/$defs/timestamp" }, "slice_sequence": { "$ref": "#/$defs/sequence" } } + }, + "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" } } + }, + "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": "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_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" } } + }, + "cashAttribution": { + "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], + "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } + }, + "positionAttribution": { + "type": "object", "additionalProperties": false, + "required": ["instrument_id", "quote_currency", "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"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, "quote_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "base_execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_total_fees": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "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", "net_market_value", "long_market_value", "short_market_value", "gross_exposure", "cost_basis", "realized_pnl", "unrealized_pnl", "equity", "dividend_pnl", "execution_fees", "borrow_fees", "total_fees", "cash_balances", "positions", "margin", "group_exposures"], + "properties": { + "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "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" } } + } + }, + "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": { "const": "completed_bar_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/v7/scenario-stream.schema.json b/contracts/v7/scenario-stream.schema.json new file mode 100644 index 0000000..90afc77 --- /dev/null +++ b/contracts/v7/scenario-stream.schema.json @@ -0,0 +1,76 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v7/scenario-stream.schema.json", + "title": "Trading Engine v6 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": "7" }, + "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": "7" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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": "7" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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", "max_internal_events"], + "properties": { + "metadata": { "type": "object" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/execution" }, + "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/v7/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v7/scenario.schema.json b/contracts/v7/scenario.schema.json new file mode 100644 index 0000000..8dbb6ae --- /dev/null +++ b/contracts/v7/scenario.schema.json @@ -0,0 +1,428 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json", + "title": "Trading Engine v6 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", "max_internal_events", "schedule", "slices"], + "properties": { + "contract_version": { "const": "7" }, + "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" }, + "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": { + "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": { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + "completedBarV1Configuration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fixed_fee", "fee_bps"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, + "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "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", "limit_price"], + "properties": { + "type": { "const": "submit_order" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "order_kind": { "enum": ["market", "limit"] }, + "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] } + } + }, + "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"], + "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" } } + } + }, + "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" } + } + } + ] + } + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index 40e5de8..e546ded 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/v6/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v7/README.md) remain authoritative for process boundaries. diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 711e17b..5153d11 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 v6 batch, stream, journal, and +The full test suite additionally validates and replays the current v7 batch, stream, journal, and strategy-v4 fixtures, including the initial portfolio and its 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 ff8995c..c7a2de8 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,11 +1,11 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v6 advertises and accepts `completed_bar_v1`; embedders can inject another module through +Contract v7 advertises and accepts `completed_bar_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 v6 envelope separates selection from +Each compiled model owns a strict configuration contract. The v7 envelope separates selection from model-specific parameters: ```json diff --git a/docs/persistra.md b/docs/persistra.md index 1b5e696..dab8b11 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -53,12 +53,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 v6 while retaining v5, v4, and exact v3 +structural checks. The engine advertises current contract v7 while retaining v6, v5, v4, 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 v4](../contracts/strategy/v4/README.md). Persistra's host turns protocol +[strategy protocol v5](../contracts/strategy/v5/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, @@ -78,7 +78,7 @@ 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 v6 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v7 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, diff --git a/docs/scenario.md b/docs/scenario.md index d54d166..41d70ad 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 v6 demo](../contracts/v6/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v6/scenario.schema.json) provides structural validation. +Use [the v6 demo](../contracts/v7/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v7/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/v6/scenario-stream.schema.json) validates each line, -and [the v6 stream fixture](../contracts/v6/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v7/scenario-stream.schema.json) validates each line, +and [the v6 stream fixture](../contracts/v7/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; v6 is `"6"` | +| `contract_version` | Required string identifying this file contract; v7 is `"7"` | | `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 | @@ -89,7 +89,7 @@ negative cash when the complete marked account remains valid under the configure ## Venue calendars -Contract v6 requires every instrument to belong to exactly one explicit venue calendar. A calendar +Contract v7 requires every instrument to belong to exactly one explicit venue calendar. A calendar is identified by `venue_id`, `calendar_id`, and `calendar_version`; version 1 is the only supported calendar payload. Its `sessions` are unique and ordered by `session_date`, and each date declares one policy: `regular`, `early_close`, or `holiday`. Holidays have no phases. Open sessions must @@ -102,13 +102,15 @@ Calendar lookup rejects a date without an explicit policy; it never infers weeke hours from adjacent entries. This makes future DAY expiry, auction eligibility, settlement, and daily-bar publication policies depend on versioned input rather than ambient system state. -Risk contains positive `max_order_quantity`, `max_long_position`, `max_short_position`, -`max_gross_exposure`, and `max_leverage` values, initial and maintenance margin basis points, and -annualized `short_borrow_bps`. Initial margin cannot be below maintenance margin, and each -quantity limit must cover at least one lot for every instrument. Orders that increase gross -exposure must satisfy every applicable limit; exposure-reducing orders remain admissible. +Risk contains portfolio-wide positive `max_gross_exposure` and `max_leverage`, annualized +`short_borrow_bps`, exactly one `instrument_policies` entry per catalog instrument, and an explicit +`groups` array. Instrument policies define order, long, short, notional, initial-margin, +maintenance-margin, and shorting limits. Groups carry versioned identities and explicit membership; +they may overlap and can constrain gross, long, short, absolute net, and gross-to-equity +concentration exposure. Admission and fill clipping include working-order reservations. Every +applicable group is enforced, with group identity providing deterministic tie ordering. -Contract v6 execution contains a stable `model` and a model-owned `configuration`. For +Contract v7 execution contains a stable `model` and a model-owned `configuration`. For `completed_bar_v1`, configuration version `"1"` contains: - `version`, the strict model-configuration contract version @@ -212,7 +214,7 @@ than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v6/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v7/journal.schema.json) validates each JSON Lines record. Every record contains `contract_version`, `engine_sequence`, deterministic `event_id`, ordered `causation_ids`, `run_id`, `recorded_at`, `event_type`, and an event-specific `payload`. Causal references are unique prior event IDs from the same run. The version is repeated on every record diff --git a/lib/codec.ml b/lib/codec.ml index 02c9c8c..3332d9d 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -105,6 +105,82 @@ let fill_limit_to_yojson = function | Risk.Initial_margin value -> ( "initial_margin", `Assoc [ ("unit", string "basis_points"); ("value", `Int value) ] ) + | Risk.Instrument_maximum_long_position (id, value) -> + ( "instrument_max_long_position", + `Assoc + [ + ("instrument_id", instrument_id id); + ("unit", string "quantity"); + ("value", quantity value); + ] ) + | Risk.Instrument_maximum_short_position (id, value) -> + ( "instrument_max_short_position", + `Assoc + [ + ("instrument_id", instrument_id id); + ("unit", string "quantity"); + ("value", quantity value); + ] ) + | Risk.Instrument_maximum_notional (id, value) -> + ( "instrument_max_notional_exposure", + `Assoc + [ + ("instrument_id", instrument_id id); + ("unit", string "money"); + ("value", money value); + ] ) + | Risk.Instrument_shorting_disabled id -> + ( "instrument_shorting_disabled", + `Assoc [ ("instrument_id", instrument_id id); ("value", `Bool false) ] + ) + | Risk.Instrument_initial_margin (id, value) -> + ( "instrument_initial_margin", + `Assoc + [ + ("instrument_id", instrument_id id); + ("unit", string "basis_points"); + ("value", `Int value); + ] ) + | Risk.Group_maximum_gross (id, value) -> + ( "group_max_gross_exposure", + `Assoc + [ + ("group_id", string (Id.Risk_group.to_string id)); + ("unit", string "money"); + ("value", money value); + ] ) + | Risk.Group_maximum_long (id, value) -> + ( "group_max_long_exposure", + `Assoc + [ + ("group_id", string (Id.Risk_group.to_string id)); + ("unit", string "money"); + ("value", money value); + ] ) + | Risk.Group_maximum_short (id, value) -> + ( "group_max_short_exposure", + `Assoc + [ + ("group_id", string (Id.Risk_group.to_string id)); + ("unit", string "money"); + ("value", money value); + ] ) + | Risk.Group_maximum_absolute_net (id, value) -> + ( "group_max_absolute_net_exposure", + `Assoc + [ + ("group_id", string (Id.Risk_group.to_string id)); + ("unit", string "money"); + ("value", money value); + ] ) + | Risk.Group_maximum_concentration (id, value) -> + ( "group_max_concentration", + `Assoc + [ + ("group_id", string (Id.Risk_group.to_string id)); + ("unit", string "ratio"); + ("value", string (Scalar.Ratio.to_decimal_string value)); + ] ) let bar_to_yojson bar = `Assoc @@ -318,10 +394,34 @@ let margin_to_yojson margin = ("margin_call", `Bool margin.margin_call); ] -let valuation_to_yojson valuation = +let group_exposure_to_yojson (exposure : Risk.group_exposure) = + `Assoc + [ + ("group_id", string (Id.Risk_group.to_string exposure.group_id)); + ("gross_exposure", money exposure.gross_exposure); + ("net_exposure", money exposure.net_exposure); + ("long_exposure", money exposure.long_exposure); + ("short_exposure", money exposure.short_exposure); + ( "concentration", + Option.fold ~none:`Null ~some:weight exposure.concentration ); + ] + +let valuation_to_yojson ~contract_version valuation = match account_valuation_to_yojson valuation.Audit.account with | `Assoc fields -> - `Assoc (fields @ [ ("margin", margin_to_yojson valuation.margin) ]) + let fields = fields @ [ ("margin", margin_to_yojson valuation.margin) ] in + let fields = + if String.equal contract_version Contract.version then + fields + @ [ + ( "group_exposures", + `List + (List.map group_exposure_to_yojson + valuation.margin.Risk.group_exposures) ); + ] + else fields + in + `Assoc fields | _ -> assert false let order_counts_to_yojson counts = @@ -344,7 +444,7 @@ let requested_target_to_yojson target = Option.fold ~none:`Null ~some:price target.reference_price ); ] -let payload_to_yojson = function +let payload_to_yojson ~contract_version = function | Audit.Run_started { scenario_sha256; execution_model } -> `Assoc [ @@ -355,7 +455,7 @@ let payload_to_yojson = function `Assoc [ ("portfolio", initial_portfolio_to_yojson portfolio); - ("valuation", valuation_to_yojson valuation); + ("valuation", valuation_to_yojson ~contract_version valuation); ] | Audit.Market_slice_received market_slice -> market_slice_to_yojson market_slice @@ -458,18 +558,18 @@ let payload_to_yojson = function ("fee", money fee); ] | Audit.Margin_call_triggered valuation | Audit.Margin_restored valuation -> - valuation_to_yojson valuation + valuation_to_yojson ~contract_version valuation | Audit.Intent_rejected reason -> `Assoc [ ("reason", string reason) ] | Audit.Metric_emitted { name; value } -> `Assoc [ ("name", string name); ("value", string value) ] - | Audit.Valuation valuation -> valuation_to_yojson valuation + | Audit.Valuation valuation -> valuation_to_yojson ~contract_version valuation | Audit.Run_completed { scenario_sha256; execution_model; valuation; order_counts } -> `Assoc [ ("scenario_sha256", string scenario_sha256); ("execution_model", string execution_model); - ("valuation", valuation_to_yojson valuation); + ("valuation", valuation_to_yojson ~contract_version valuation); ("order_counts", order_counts_to_yojson order_counts); ] @@ -487,7 +587,9 @@ let audit_to_yojson audit = ("run_id", string (Id.Run.to_string audit.run_id)); ("recorded_at", timestamp audit.recorded_at); ("event_type", string (Audit.event_name audit.event)); - ("payload", payload_to_yojson audit.event); + ( "payload", + payload_to_yojson ~contract_version:audit.contract_version audit.event + ); ] let audit_to_string audit = Yojson.Safe.to_string (audit_to_yojson audit) diff --git a/lib/contract.ml b/lib/contract.ml index 4bf9255..c9da0e3 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,13 +1,13 @@ -let version = "6" -let previous_version = "5" +let version = "7" +let previous_version = "6" let legacy_journal_version = "3" let supported_versions = - [ version; previous_version; "4"; legacy_journal_version ] + [ version; previous_version; "5"; "4"; legacy_journal_version ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "4" -let previous_strategy_protocol_version = "3" +let strategy_protocol_version = "5" +let previous_strategy_protocol_version = "4" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -23,7 +23,8 @@ let capabilities_to_yojson () = ("execution_model_contracts", Execution_model.capabilities_to_yojson ()); ( "strategy_protocol_versions", strings - [ strategy_protocol_version; previous_strategy_protocol_version ] ); + [ strategy_protocol_version; previous_strategy_protocol_version; "3" ] + ); ("resource_limits", Resource_limits.to_yojson ()); ] diff --git a/lib/engine.ml b/lib/engine.ml index 4d232a3..fd929bf 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -274,7 +274,8 @@ module Interactive = struct Id.Instrument.Map.bindings state.latest_bars |> List.map snd in let* valuation = value state in - Strategy.context ~now ~valuation + let* group_exposures = Risk.group_exposures state.config.risk valuation in + Strategy.context ~now ~valuation ~group_exposures ~working_orders:(Oms.active_orders state.oms) ~latest_bars @@ -688,7 +689,10 @@ module Interactive = struct ~lot:instrument.Instrument.lot_size) then Error "target quantity is not aligned to its instrument lot" else - let* () = Risk.check_position state.config.risk target.quantity in + let* () = + Risk.check_position_for state.config.risk target.instrument_id + target.quantity + in Ok ( Id.Instrument.Map.add target.instrument_id target.quantity desired, @@ -748,7 +752,10 @@ module Interactive = struct ~weight:target.weight ~price:bar.close_price ~lot_size:instrument.lot_size in - let* () = Risk.check_position state.config.risk quantity in + let* () = + Risk.check_position_for state.config.risk target.instrument_id + quantity + in Ok ( Id.Instrument.Map.add target.instrument_id quantity desired, Audit. @@ -972,23 +979,34 @@ module Interactive = struct Account.value account ~instruments ~marks ~fx_rates:state.latest_fx_rates in - Ok (fee, after_position, after) + Ok (fee, account, after_position, after) in match prepared with | Error message -> Error (`Invalid message) - | Ok (fee, after_position, after) -> ( - match - Risk.check_post_fill state.config.risk ~before_position - ~after_position ~before ~after - with + | Ok (fee, account, after_position, after) -> ( + let checked = + let* () = + Risk.check_post_fill_for state.config.risk + ~instrument_id:instrument.id ~before_position ~after_position + ~before ~after + in + Risk.check_reserved_fill state.config.risk ~account ~oms:state.oms + ~marks ~fx_rates:state.latest_fx_rates ~order + ~filled_quantity:quantity ~after + in + match checked with | Ok () -> Ok fee | Error (Risk.Limit limit) -> Error (`Limit limit) | Error (Risk.Invalid message) -> Error (`Invalid message)) in let lot_value = Scalar.Quantity.to_micros instrument.lot_size in + let* policy_order_limit = + match Risk.max_order_quantity_for state.config.risk instrument.id with + | Some value -> Ok value + | None -> Error "fill instrument has no risk policy" + in let quantity_limit = - Scalar.Quantity.minimum proposed.quantity - (Risk.max_order_quantity state.config.risk) + Scalar.Quantity.minimum proposed.quantity policy_order_limit in let requested_lots = Int64.div (Scalar.Quantity.to_micros quantity_limit) lot_value @@ -1013,10 +1031,7 @@ module Interactive = struct let* limit = if not clipped then Ok None else if Int64.equal lots requested_lots then - Ok - (Some - (Risk.Maximum_order_quantity - (Risk.max_order_quantity state.config.risk))) + Ok (Some (Risk.Maximum_order_quantity policy_order_limit)) else let next_lots = Int64.succ lots in let next_quantity = @@ -1225,10 +1240,12 @@ module Interactive = struct match Risk.instrument state.config.risk instrument_id with | None -> Error "target refers to an unknown instrument" | Some instrument -> ( - let bounded = - Scalar.Quantity.minimum delta - (Risk.max_order_quantity state.config.risk) + let order_limit = + Risk.max_order_quantity_for state.config.risk instrument_id + |> Option.value + ~default:(Risk.max_order_quantity state.config.risk) in + let bounded = Scalar.Quantity.minimum delta order_limit in match Scalar.Quantity.round_toward_zero_to_multiple bounded ~multiple:instrument.lot_size @@ -1288,10 +1305,13 @@ module Interactive = struct if Scalar.Quantity.is_zero quantity || already_working then Ok reduction else let* absolute = Scalar.Quantity.absolute quantity in - let bounded = - Scalar.Quantity.minimum absolute - (Risk.max_order_quantity reduction.state.config.risk) + let order_limit = + Risk.max_order_quantity_for reduction.state.config.risk + instrument.Instrument.id + |> Option.value + ~default:(Risk.max_order_quantity reduction.state.config.risk) in + let bounded = Scalar.Quantity.minimum absolute order_limit in let* quantity = Scalar.Quantity.round_toward_zero_to_multiple bounded ~multiple:instrument.lot_size diff --git a/lib/execution_model.ml b/lib/execution_model.ml index e3ac8eb..cc1a628 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -33,7 +33,7 @@ let supported = List.map name builtins let completed_bar_v1_contract = { version = "1"; - scenario_contract_versions = [ "6"; "5"; "4"; "3" ]; + scenario_contract_versions = [ "7"; "6"; "5"; "4"; "3" ]; required_fields = [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ]; supported_order_types = [ "market"; "limit" ]; data_requirements = [ "completed_ohlcv_bars" ]; diff --git a/lib/id.ml b/lib/id.ml index 2f1c652..b1eaf0f 100644 --- a/lib/id.ml +++ b/lib/id.ml @@ -50,3 +50,4 @@ module Event = Make () module Corporate_action = Make () module Venue = Make () module Venue_calendar = Make () +module Risk_group = Make () diff --git a/lib/id.mli b/lib/id.mli index f8c76b6..ba0479e 100644 --- a/lib/id.mli +++ b/lib/id.mli @@ -23,3 +23,4 @@ module Event : S module Corporate_action : S module Venue : S module Venue_calendar : S +module Risk_group : S diff --git a/lib/risk.ml b/lib/risk.ml index 7cd9082..225fbd0 100644 --- a/lib/risk.ml +++ b/lib/risk.ml @@ -1,6 +1,9 @@ type t = { + versioned : bool; base_currency : string; instruments : Instrument.t Id.Instrument.Map.t; + instrument_policies : instrument_policy Id.Instrument.Map.t; + groups : group list; max_order_quantity : Scalar.Quantity.t; max_long_position : Scalar.Quantity.t; max_short_position : Scalar.Quantity.t; @@ -11,12 +14,50 @@ type t = { short_borrow_bps : int; } +and instrument_policy = { + instrument_id : Id.Instrument.t; + max_order_quantity : Scalar.Quantity.t; + max_long_position : Scalar.Quantity.t; + max_short_position : Scalar.Quantity.t; + max_notional_exposure : Scalar.Money.t option; + initial_margin_bps : int; + maintenance_margin_bps : int; + shorting_allowed : bool; +} + +and group_kind = Issuer | Sector | Currency | Country | Asset_class | Custom + +and group_limits = { + max_gross_exposure : Scalar.Money.t option; + max_long_exposure : Scalar.Money.t option; + max_short_exposure : Scalar.Money.t option; + max_absolute_net_exposure : Scalar.Money.t option; + max_concentration : Scalar.Ratio.t option; +} + +and group = { + group_id : Id.Risk_group.t; + group_kind : group_kind; + instrument_ids : Id.Instrument.t list; + limits : group_limits; +} + +type group_exposure = { + group_id : Id.Risk_group.t; + gross_exposure : Scalar.Money.t; + net_exposure : Scalar.Money.t; + long_exposure : Scalar.Money.t; + short_exposure : Scalar.Money.t; + concentration : Scalar.Weight.t option; +} + type margin_snapshot = { initial_requirement : Scalar.Money.t; maintenance_requirement : Scalar.Money.t; initial_excess : Scalar.Money.t; maintenance_excess : Scalar.Money.t; margin_call : bool; + group_exposures : group_exposure list; } type fill_limit = @@ -26,6 +67,16 @@ type fill_limit = | Maximum_gross_exposure of Scalar.Money.t | Maximum_leverage of Scalar.Ratio.t | Initial_margin of int + | Instrument_maximum_long_position of Id.Instrument.t * Scalar.Quantity.t + | Instrument_maximum_short_position of Id.Instrument.t * Scalar.Quantity.t + | Instrument_maximum_notional of Id.Instrument.t * Scalar.Money.t + | Instrument_shorting_disabled of Id.Instrument.t + | Instrument_initial_margin of Id.Instrument.t * int + | Group_maximum_gross of Id.Risk_group.t * Scalar.Money.t + | Group_maximum_long of Id.Risk_group.t * Scalar.Money.t + | Group_maximum_short of Id.Risk_group.t * Scalar.Money.t + | Group_maximum_absolute_net of Id.Risk_group.t * Scalar.Money.t + | Group_maximum_concentration of Id.Risk_group.t * Scalar.Ratio.t type fill_check_error = Limit of fill_limit | Invalid of string @@ -40,6 +91,108 @@ let valid_label value = code >= 0x21 && code <> 0x7f) value +let valid_margin ~initial_margin_bps ~maintenance_margin_bps = + if initial_margin_bps <= 0 || initial_margin_bps > 10_000 then + Error "initial margin basis points must be between 1 and 10000" + else if maintenance_margin_bps <= 0 || maintenance_margin_bps > 10_000 then + Error "maintenance margin basis points must be between 1 and 10000" + else if initial_margin_bps < maintenance_margin_bps then + Error "initial margin must not be below maintenance margin" + else Ok () + +let create_instrument_policy ~instrument ~max_order_quantity ~max_long_position + ~max_short_position ~max_notional_exposure ~initial_margin_bps + ~maintenance_margin_bps ~shorting_allowed = + let lot = instrument.Instrument.lot_size in + if not (Scalar.Quantity.is_positive max_order_quantity) then + Error "maximum order quantity must be positive" + else if not (Scalar.Quantity.is_positive max_long_position) then + Error "maximum long position must be positive" + else if not (Scalar.Quantity.is_positive max_short_position) then + Error "maximum short position must be positive" + else if Scalar.Quantity.compare max_order_quantity lot < 0 then + Error "maximum order quantity must cover the instrument lot size" + else if Scalar.Quantity.compare max_long_position lot < 0 then + Error "maximum long position must cover the instrument lot size" + else if Scalar.Quantity.compare max_short_position lot < 0 then + Error "maximum short position must cover the instrument lot size" + else if + Option.exists + (fun value -> Scalar.Money.compare value Scalar.Money.zero <= 0) + max_notional_exposure + then Error "maximum instrument notional exposure must be positive" + else + let* () = valid_margin ~initial_margin_bps ~maintenance_margin_bps in + Ok + { + instrument_id = instrument.id; + max_order_quantity; + max_long_position; + max_short_position; + max_notional_exposure; + initial_margin_bps; + maintenance_margin_bps; + shorting_allowed; + } + +let create_group_limits ~max_gross_exposure ~max_long_exposure + ~max_short_exposure ~max_absolute_net_exposure ~max_concentration = + let positive_money = function + | None -> true + | Some value -> Scalar.Money.compare value Scalar.Money.zero > 0 + in + if + not + (List.for_all positive_money + [ + max_gross_exposure; + max_long_exposure; + max_short_exposure; + max_absolute_net_exposure; + ]) + then Error "group money limits must be positive" + else if + Option.exists + (fun value -> + Scalar.Ratio.compare value Scalar.Ratio.one > 0 + || Scalar.Ratio.to_micros value <= 0L) + max_concentration + then Error "group concentration must be greater than zero and at most one" + else if + List.for_all Option.is_none + [ + max_gross_exposure; + max_long_exposure; + max_short_exposure; + max_absolute_net_exposure; + ] + && Option.is_none max_concentration + then Error "group must configure at least one limit" + else + Ok + { + max_gross_exposure; + max_long_exposure; + max_short_exposure; + max_absolute_net_exposure; + max_concentration; + } + +let create_group ~group_id ~group_kind ~instrument_ids ~limits = + if instrument_ids = [] then Error "risk group must contain an instrument" + else if + List.length instrument_ids + <> List.length (List.sort_uniq Id.Instrument.compare instrument_ids) + then Error "risk group instrument IDs must be unique" + else + Ok + { + group_id; + group_kind; + instrument_ids = List.sort Id.Instrument.compare instrument_ids; + limits; + } + let create ~base_currency ~instruments ~max_order_quantity ~max_long_position ~max_short_position ~max_gross_exposure ~max_leverage ~initial_margin_bps ~maintenance_margin_bps ~short_borrow_bps = @@ -95,10 +248,27 @@ let create ~base_currency ~instruments ~max_order_quantity ~max_long_position let* instruments = List.fold_left add (Ok Id.Instrument.Map.empty) instruments in + let* instrument_policies = + Id.Instrument.Map.bindings instruments + |> List.fold_left + (fun result (_, instrument) -> + let* policies = result in + let* policy = + create_instrument_policy ~instrument ~max_order_quantity + ~max_long_position ~max_short_position + ~max_notional_exposure:None ~initial_margin_bps + ~maintenance_margin_bps ~shorting_allowed:true + in + Ok (Id.Instrument.Map.add instrument.Instrument.id policy policies)) + (Ok Id.Instrument.Map.empty) + in Ok { + versioned = false; base_currency; instruments; + instrument_policies; + groups = []; max_order_quantity; max_long_position; max_short_position; @@ -109,6 +279,79 @@ let create ~base_currency ~instruments ~max_order_quantity ~max_long_position short_borrow_bps; } +let create_v7 ~base_currency ~instruments + ~(instrument_policies : instrument_policy list) ~(groups : group list) + ~max_gross_exposure ~max_leverage ~short_borrow_bps = + if not (valid_label base_currency) then + Error "base currency must not be empty or contain whitespace" + else if instruments = [] then Error "risk must define at least one instrument" + else if Scalar.Money.compare max_gross_exposure Scalar.Money.zero <= 0 then + Error "maximum gross exposure must be positive" + else if short_borrow_bps < 0 || short_borrow_bps > 10_000 then + Error "short borrow basis points must be between 0 and 10000" + else + let add_instrument result instrument = + let* map = result in + if Id.Instrument.Map.mem instrument.Instrument.id map then + Error "instrument IDs must be unique" + else Ok (Id.Instrument.Map.add instrument.id instrument map) + in + let* instrument_map = + List.fold_left add_instrument (Ok Id.Instrument.Map.empty) instruments + in + let add_policy result policy = + let* map = result in + if not (Id.Instrument.Map.mem policy.instrument_id instrument_map) then + Error "instrument policy refers to an unknown instrument" + else if Id.Instrument.Map.mem policy.instrument_id map then + Error "instrument policy IDs must be unique" + else Ok (Id.Instrument.Map.add policy.instrument_id policy map) + in + let* policy_map = + List.fold_left add_policy (Ok Id.Instrument.Map.empty) instrument_policies + in + if + Id.Instrument.Map.cardinal policy_map + <> Id.Instrument.Map.cardinal instrument_map + then Error "risk must define exactly one policy for every instrument" + else + let group_ids = List.map (fun (group : group) -> group.group_id) groups in + if + List.length group_ids + <> List.length (List.sort_uniq Id.Risk_group.compare group_ids) + then Error "risk group IDs must be unique" + else if + List.exists + (fun (group : group) -> + List.exists + (fun instrument_id -> + not (Id.Instrument.Map.mem instrument_id instrument_map)) + group.instrument_ids) + groups + then Error "risk group refers to an unknown instrument" + else + let representative = List.hd instrument_policies in + Ok + { + versioned = true; + base_currency; + instruments = instrument_map; + instrument_policies = policy_map; + groups = + List.sort + (fun (left : group) right -> + Id.Risk_group.compare left.group_id right.group_id) + groups; + max_order_quantity = representative.max_order_quantity; + max_long_position = representative.max_long_position; + max_short_position = representative.max_short_position; + max_gross_exposure; + max_leverage; + initial_margin_bps = representative.initial_margin_bps; + maintenance_margin_bps = representative.maintenance_margin_bps; + short_borrow_bps; + } + let base_currency state = state.base_currency let instruments state = @@ -117,6 +360,13 @@ let instruments state = let instrument state instrument_id = Id.Instrument.Map.find_opt instrument_id state.instruments +let instrument_policies state = + Id.Instrument.Map.bindings state.instrument_policies |> List.map snd + +let instrument_policy state instrument_id = + Id.Instrument.Map.find_opt instrument_id state.instrument_policies + +let groups state = state.groups let max_order_quantity state = state.max_order_quantity let max_long_position state = state.max_long_position let max_short_position state = state.max_short_position @@ -126,21 +376,104 @@ let initial_margin_bps state = state.initial_margin_bps let maintenance_margin_bps state = state.maintenance_margin_bps let short_borrow_bps state = state.short_borrow_bps -let margin_snapshot state valuation = - let* initial_requirement = - Scalar.Money.bps_ceil valuation.Account.gross_exposure - ~bps:state.initial_margin_bps +let max_order_quantity_for state instrument_id = + Option.map + (fun (policy : instrument_policy) -> policy.max_order_quantity) + (instrument_policy state instrument_id) + +let group_exposure_from_positions group ~equity positions = + let member instrument_id = + List.exists (Id.Instrument.equal instrument_id) group.instrument_ids in - let* maintenance_requirement = - Scalar.Money.bps_ceil valuation.gross_exposure - ~bps:state.maintenance_margin_bps + let* net, long, short = + List.fold_left + (fun result (position : Account.position_attribution) -> + let* net, long, short = result in + if not (member position.instrument_id) then Ok (net, long, short) + else + let* net = Scalar.Money.add net position.base_market_value in + if + Scalar.Money.compare position.base_market_value Scalar.Money.zero + >= 0 + then + let* long = Scalar.Money.add long position.base_market_value in + Ok (net, long, short) + else + let* magnitude = Scalar.Money.negate position.base_market_value in + let* short = Scalar.Money.add short magnitude in + Ok (net, long, short)) + (Ok (Scalar.Money.zero, Scalar.Money.zero, Scalar.Money.zero)) + positions + in + let* gross = Scalar.Money.add long short in + let* concentration = + if Scalar.Money.compare equity Scalar.Money.zero <= 0 then Ok None + else Scalar.Money.weight_toward_zero gross ~equity |> Result.map Option.some + in + Ok + { + group_id = group.group_id; + gross_exposure = gross; + net_exposure = net; + long_exposure = long; + short_exposure = short; + concentration; + } + +let group_exposures state valuation = + List.fold_left + (fun result group -> + let* exposures = result in + let* exposure = + group_exposure_from_positions group ~equity:valuation.Account.equity + valuation.positions + in + Ok (exposure :: exposures)) + (Ok []) state.groups + |> Result.map List.rev + +let margin_snapshot state valuation = + let requirements = + if not state.versioned then + let* initial = + Scalar.Money.bps_ceil valuation.Account.gross_exposure + ~bps:state.initial_margin_bps + in + let* maintenance = + Scalar.Money.bps_ceil valuation.gross_exposure + ~bps:state.maintenance_margin_bps + in + Ok (initial, maintenance) + else + List.fold_left + (fun result (position : Account.position_attribution) -> + let* initial, maintenance = result in + let* notional = Scalar.Money.absolute position.base_market_value in + let* policy = + match instrument_policy state position.instrument_id with + | Some policy -> Ok policy + | None -> Error "valuation position has no instrument risk policy" + in + let* item_initial = + Scalar.Money.bps_ceil notional ~bps:policy.initial_margin_bps + in + let* item_maintenance = + Scalar.Money.bps_ceil notional ~bps:policy.maintenance_margin_bps + in + let* initial = Scalar.Money.add initial item_initial in + let* maintenance = Scalar.Money.add maintenance item_maintenance in + Ok (initial, maintenance)) + (Ok (Scalar.Money.zero, Scalar.Money.zero)) + valuation.positions in + let* initial_requirement, maintenance_requirement = requirements in let* initial_excess = Scalar.Money.subtract valuation.equity initial_requirement in let* maintenance_excess = Scalar.Money.subtract valuation.equity maintenance_requirement in + let* group_exposures = group_exposures state valuation in Ok { initial_requirement; @@ -149,6 +482,7 @@ let margin_snapshot state valuation = maintenance_excess; margin_call = Scalar.Money.compare maintenance_excess Scalar.Money.zero < 0; + group_exposures; } let check_initial_values state ~equity ~gross_exposure = @@ -170,8 +504,123 @@ let check_initial_values state ~equity ~gross_exposure = else Ok () let check_initial state valuation = - check_initial_values state ~equity:valuation.Account.equity - ~gross_exposure:valuation.gross_exposure + if not state.versioned then + check_initial_values state ~equity:valuation.Account.equity + ~gross_exposure:valuation.gross_exposure + else + let* () = + if + Scalar.Money.compare valuation.Account.gross_exposure + state.max_gross_exposure + > 0 + then Error "portfolio would exceed maximum gross exposure" + else + let* leveraged_equity = + Scalar.Money.multiply_ratio valuation.equity state.max_leverage + in + if Scalar.Money.compare valuation.gross_exposure leveraged_equity > 0 + then Error "portfolio would exceed maximum leverage" + else Ok () + in + let* () = + List.fold_left + (fun result (position : Account.position_attribution) -> + let* () = result in + let* policy = + match instrument_policy state position.instrument_id with + | Some policy -> Ok policy + | None -> Error "initial position has no instrument risk policy" + in + let* minimum_short = + Scalar.Quantity.negate policy.max_short_position + in + let* () = + if + Scalar.Quantity.compare position.quantity policy.max_long_position + > 0 + then Error "initial position exceeds its maximum long position" + else if Scalar.Quantity.compare position.quantity minimum_short < 0 + then Error "initial position exceeds its maximum short position" + else if + (not policy.shorting_allowed) + && Scalar.Quantity.is_negative position.quantity + then Error "initial position violates its shorting policy" + else Ok () + in + let* notional = Scalar.Money.absolute position.base_market_value in + match policy.max_notional_exposure with + | Some limit when Scalar.Money.compare notional limit > 0 -> + Error + "initial position exceeds the instrument maximum notional \ + exposure" + | _ -> Ok ()) + (Ok ()) valuation.positions + in + let* margin = margin_snapshot state valuation in + let* () = + if Scalar.Money.compare margin.initial_excess Scalar.Money.zero < 0 then + Error "portfolio would violate instrument initial margin requirements" + else Ok () + in + List.fold_left + (fun result (group : group) -> + let* () = result in + let* exposure = + match + List.find_opt + (fun item -> Id.Risk_group.equal item.group_id group.group_id) + margin.group_exposures + with + | Some exposure -> Ok exposure + | None -> Error "initial valuation omitted a configured risk group" + in + let* absolute_net = Scalar.Money.absolute exposure.net_exposure in + let exceeds option observed = + Option.exists + (fun limit -> Scalar.Money.compare observed limit > 0) + option + in + let group_name = Id.Risk_group.to_string group.group_id in + if exceeds group.limits.max_gross_exposure exposure.gross_exposure then + Error + (Printf.sprintf + "initial portfolio exceeds group %s maximum gross exposure" + group_name) + else if exceeds group.limits.max_long_exposure exposure.long_exposure + then + Error + (Printf.sprintf + "initial portfolio exceeds group %s maximum long exposure" + group_name) + else if exceeds group.limits.max_short_exposure exposure.short_exposure + then + Error + (Printf.sprintf + "initial portfolio exceeds group %s maximum short exposure" + group_name) + else if exceeds group.limits.max_absolute_net_exposure absolute_net then + Error + (Printf.sprintf + "initial portfolio exceeds group %s maximum absolute net \ + exposure" + group_name) + else + match group.limits.max_concentration with + | None -> Ok () + | Some _ + when Scalar.Money.compare valuation.equity Scalar.Money.zero <= 0 -> + Error "group concentration requires positive equity" + | Some limit -> + let* threshold = + Scalar.Money.multiply_ratio valuation.equity limit + in + if Scalar.Money.compare exposure.gross_exposure threshold > 0 then + Error + (Printf.sprintf + "initial portfolio exceeds group %s maximum concentration" + group_name) + else Ok ()) + (Ok ()) state.groups let invalid result = Result.map_error (fun message -> Invalid message) result @@ -217,6 +666,181 @@ let check_post_fill state ~before_position ~after_position ~before ~after = check_fill_initial state ~equity:after.equity ~gross_exposure:after.gross_exposure +type projected_value = { + instrument_id : Id.Instrument.t; + quantity : Scalar.Quantity.t; + signed_value : Scalar.Money.t; + absolute_value : Scalar.Money.t; +} + +let sum_values values = + List.fold_left + (fun result value -> + let* gross, long, short, net = result in + let* gross = Scalar.Money.add gross value.absolute_value in + let* net = Scalar.Money.add net value.signed_value in + if Scalar.Money.compare value.signed_value Scalar.Money.zero >= 0 then + let* long = Scalar.Money.add long value.signed_value in + Ok (gross, long, short, net) + else + let* magnitude = Scalar.Money.negate value.signed_value in + let* short = Scalar.Money.add short magnitude in + Ok (gross, long, short, net)) + (Ok + ( Scalar.Money.zero, + Scalar.Money.zero, + Scalar.Money.zero, + Scalar.Money.zero )) + values + +let group_values (group : group) values = + List.filter + (fun value -> + List.exists (Id.Instrument.equal value.instrument_id) group.instrument_ids) + values + |> sum_values + +let check_group_fill_limit state ~equity values = + let rec check = function + | [] -> Ok () + | (group : group) :: rest -> + let* gross, long, short, net = group_values group values |> invalid in + let* absolute_net = Scalar.Money.absolute net |> invalid in + let fail option observed make = + match option with + | Some limit when Scalar.Money.compare observed limit > 0 -> + Error (Limit (make group.group_id limit)) + | _ -> Ok () + in + let* () = + fail group.limits.max_gross_exposure gross (fun id limit -> + Group_maximum_gross (id, limit)) + in + let* () = + fail group.limits.max_long_exposure long (fun id limit -> + Group_maximum_long (id, limit)) + in + let* () = + fail group.limits.max_short_exposure short (fun id limit -> + Group_maximum_short (id, limit)) + in + let* () = + fail group.limits.max_absolute_net_exposure absolute_net + (fun id limit -> Group_maximum_absolute_net (id, limit)) + in + let* () = + match group.limits.max_concentration with + | None -> Ok () + | Some _ when Scalar.Money.compare equity Scalar.Money.zero <= 0 -> + Error (Invalid "group concentration requires positive equity") + | Some limit -> + let* threshold = + Scalar.Money.multiply_ratio equity limit |> invalid + in + if Scalar.Money.compare gross threshold > 0 then + Error + (Limit (Group_maximum_concentration (group.group_id, limit))) + else Ok () + in + check rest + in + check state.groups + +let check_post_fill_for state ~instrument_id ~before_position ~after_position + ~before ~after = + match instrument_policy state instrument_id with + | None -> Error (Invalid "fill has no instrument risk policy") + | Some policy -> + let* before_absolute = + Scalar.Quantity.absolute before_position |> invalid + in + let* after_absolute = + Scalar.Quantity.absolute after_position |> invalid + in + let increasing = + Scalar.Quantity.compare after_absolute before_absolute > 0 + in + let* () = + if not increasing then Ok () + else if + Scalar.Quantity.compare after_position policy.max_long_position > 0 + then + Error + (Limit + (Instrument_maximum_long_position + (instrument_id, policy.max_long_position))) + else + let* minimum_short = + Scalar.Quantity.negate policy.max_short_position |> invalid + in + if Scalar.Quantity.compare after_position minimum_short < 0 then + Error + (Limit + (Instrument_maximum_short_position + (instrument_id, policy.max_short_position))) + else if + (not policy.shorting_allowed) + && Scalar.Quantity.is_negative after_position + then Error (Limit (Instrument_shorting_disabled instrument_id)) + else Ok () + in + let* values = + List.map + (fun (position : Account.position_attribution) -> + let* absolute_value = + Scalar.Money.absolute position.base_market_value |> invalid + in + Ok + { + instrument_id = position.instrument_id; + quantity = position.quantity; + signed_value = position.base_market_value; + absolute_value; + }) + after.Account.positions + |> List.fold_left + (fun result item -> + let* values = result in + let* value = item in + Ok (value :: values)) + (Ok []) + |> Result.map List.rev + in + let* current = + match + List.find_opt + (fun value -> Id.Instrument.equal value.instrument_id instrument_id) + values + with + | Some value -> Ok value + | None -> + Ok + { + instrument_id; + quantity = Scalar.Quantity.zero; + signed_value = Scalar.Money.zero; + absolute_value = Scalar.Money.zero; + } + in + let* () = + match policy.max_notional_exposure with + | Some limit + when increasing + && Scalar.Money.compare current.absolute_value limit > 0 -> + Error (Limit (Instrument_maximum_notional (instrument_id, limit))) + | _ -> Ok () + in + let* gross, _, _, _ = sum_values values |> invalid in + let* () = + if state.versioned then Ok () + else if Scalar.Money.compare gross before.Account.gross_exposure <= 0 + then Ok () + else + check_fill_initial state ~equity:after.Account.equity + ~gross_exposure:gross + in + check_group_fill_limit state ~equity:after.equity values + let check_position state quantity = if Scalar.Quantity.compare quantity state.max_long_position > 0 then Error "position would exceed the maximum long position" @@ -226,6 +850,27 @@ let check_position state quantity = Error "position would exceed the maximum short position" else Ok () +let check_position_for state instrument_id quantity = + match instrument_policy state instrument_id with + | None -> Error "position refers to an unknown instrument risk policy" + | Some policy -> + if Scalar.Quantity.compare quantity policy.max_long_position > 0 then + Error + (if state.versioned then + "position would exceed the instrument maximum long position" + else "position would exceed the maximum long position") + else + let* minimum_short = Scalar.Quantity.negate policy.max_short_position in + if Scalar.Quantity.compare quantity minimum_short < 0 then + Error + (if state.versioned then + "position would exceed the instrument maximum short position" + else "position would exceed the maximum short position") + else if + (not policy.shorting_allowed) && Scalar.Quantity.is_negative quantity + then Error "instrument policy does not allow short positions" + else Ok () + let check_alignment instrument request = if not @@ -334,6 +979,36 @@ let projected_valuation_quantities state ~account ~oms request = (Ok []) |> Result.map List.rev +let fill_projected_quantities state ~account ~oms ~(order : Order.t) + ~filled_quantity = + Id.Instrument.Map.bindings state.instruments + |> List.fold_left + (fun result (instrument_id, _) -> + let* values = result in + let* reservations = reservations_for_instrument ~oms instrument_id in + let* reservations = + if Id.Instrument.equal instrument_id order.request.instrument_id then + match order.request.side with + | Order.Buy -> + let* buys = + Scalar.Quantity.subtract reservations.buys filled_quantity + in + Ok { reservations with buys } + | Order.Sell -> + let* sells = + Scalar.Quantity.subtract reservations.sells filled_quantity + in + Ok { reservations with sells } + else Ok reservations + in + let* positions = + directional_positions ~account instrument_id reservations + in + let* quantity = worst_directional_position positions in + Ok ((instrument_id, quantity) :: values)) + (Ok []) + |> Result.map List.rev + let projected_gross_exposure state ~account ~oms ~marks ~fx_rates request = let* quantities = projected_valuation_quantities state ~account ~oms request @@ -377,13 +1052,318 @@ let projected_gross_exposure state ~account ~oms ~marks ~fx_rates request = in Ok gross_exposure -let check state ~account ~oms ~marks ~fx_rates request = - if Scalar.Quantity.compare request.Order.quantity state.max_order_quantity > 0 - then Error "order exceeds the maximum order quantity" +let projected_values state ~marks ~fx_rates quantities = + let mark_map = + List.fold_left + (fun map (instrument_id, mark) -> + Id.Instrument.Map.add instrument_id mark map) + Id.Instrument.Map.empty marks + in + let module Currency_map = Map.Make (String) in + let fx_map = + List.fold_left + (fun map (currency, rate) -> Currency_map.add currency rate map) + Currency_map.empty fx_rates + in + List.fold_left + (fun result (instrument_id, quantity) -> + let* values = result in + let* instrument = + match instrument state instrument_id with + | Some value -> Ok value + | None -> Error "projected position has no configured instrument" + in + let* mark = + match Id.Instrument.Map.find_opt instrument_id mark_map with + | Some value -> Ok value + | None -> Error "projected position has no current market price" + in + let* rate = + match Currency_map.find_opt instrument.quote_currency fx_map with + | Some value -> Ok value + | None -> Error "projected position has no current FX rate" + in + let* signed_value = Scalar.Money.notional mark quantity in + let* signed_value = Scalar.Money.convert signed_value ~rate in + let* absolute_value = Scalar.Money.absolute signed_value in + Ok ({ instrument_id; quantity; signed_value; absolute_value } :: values)) + (Ok []) quantities + |> Result.map List.rev + +let first_some checks = + let rec loop = function + | [] -> Ok () + | check :: rest -> ( + match check () with Ok () -> loop rest | Error _ as e -> e) + in + loop checks + +let check_projected_values state ~equity values = + let* gross, _, _, _ = sum_values values in + let* () = + if not state.versioned then + check_initial_values state ~equity ~gross_exposure:gross + else if Scalar.Money.compare gross state.max_gross_exposure > 0 then + Error "portfolio would exceed maximum gross exposure" + else + let* leveraged_equity = + Scalar.Money.multiply_ratio equity state.max_leverage + in + if Scalar.Money.compare gross leveraged_equity > 0 then + Error "portfolio would exceed maximum leverage" + else + let* initial_requirement = + List.fold_left + (fun result value -> + let* total = result in + let* policy = + match instrument_policy state value.instrument_id with + | Some policy -> Ok policy + | None -> + Error "projected position has no instrument risk policy" + in + let* requirement = + Scalar.Money.bps_ceil value.absolute_value + ~bps:policy.initial_margin_bps + in + Scalar.Money.add total requirement) + (Ok Scalar.Money.zero) values + in + let* excess = Scalar.Money.subtract equity initial_requirement in + if Scalar.Money.compare excess Scalar.Money.zero < 0 then + Error "portfolio would violate instrument initial margin requirements" + else Ok () + in + let* () = + List.fold_left + (fun result value -> + let* () = result in + let* policy = + match instrument_policy state value.instrument_id with + | Some policy -> Ok policy + | None -> Error "projected position has no instrument risk policy" + in + let* () = check_position_for state value.instrument_id value.quantity in + match policy.max_notional_exposure with + | Some limit when Scalar.Money.compare value.absolute_value limit > 0 -> + Error + "position would exceed the instrument maximum notional exposure" + | _ -> Ok ()) + (Ok ()) values + in + List.fold_left + (fun result (group : group) -> + let* () = result in + let* gross, long, short, net = group_values group values in + let* absolute_net = Scalar.Money.absolute net in + let concentration_exceeded limit = + let* threshold = Scalar.Money.multiply_ratio equity limit in + Ok (Scalar.Money.compare gross threshold > 0) + in + first_some + [ + (fun () -> + match group.limits.max_gross_exposure with + | Some limit when Scalar.Money.compare gross limit > 0 -> + Error + (Printf.sprintf + "position would exceed group %s maximum gross exposure" + (Id.Risk_group.to_string group.group_id)) + | _ -> Ok ()); + (fun () -> + match group.limits.max_long_exposure with + | Some limit when Scalar.Money.compare long limit > 0 -> + Error + (Printf.sprintf + "position would exceed group %s maximum long exposure" + (Id.Risk_group.to_string group.group_id)) + | _ -> Ok ()); + (fun () -> + match group.limits.max_short_exposure with + | Some limit when Scalar.Money.compare short limit > 0 -> + Error + (Printf.sprintf + "position would exceed group %s maximum short exposure" + (Id.Risk_group.to_string group.group_id)) + | _ -> Ok ()); + (fun () -> + match group.limits.max_absolute_net_exposure with + | Some limit when Scalar.Money.compare absolute_net limit > 0 -> + Error + (Printf.sprintf + "position would exceed group %s maximum absolute net \ + exposure" + (Id.Risk_group.to_string group.group_id)) + | _ -> Ok ()); + (fun () -> + match group.limits.max_concentration with + | None -> Ok () + | Some _ when Scalar.Money.compare equity Scalar.Money.zero <= 0 -> + Error "group concentration requires positive equity" + | Some limit -> + let* exceeded = concentration_exceeded limit in + if exceeded then + Error + (Printf.sprintf + "position would exceed group %s maximum concentration" + (Id.Risk_group.to_string group.group_id)) + else Ok ()); + ]) + (Ok ()) state.groups + +let check_reserved_fill state ~account ~oms ~marks ~fx_rates ~(order : Order.t) + ~filled_quantity ~after = + if not state.versioned then Ok () else - match instrument state request.instrument_id with - | None -> Error "order refers to an unknown instrument" - | Some instrument -> + let instrument_id = order.request.instrument_id in + let* quantities = + fill_projected_quantities state ~account ~oms ~order ~filled_quantity + |> invalid + in + let* values = + projected_values state ~marks ~fx_rates quantities |> invalid + in + let* policy = + match instrument_policy state instrument_id with + | Some policy -> Ok policy + | None -> Error (Invalid "fill has no instrument risk policy") + in + let* current = + match + List.find_opt + (fun value -> Id.Instrument.equal value.instrument_id instrument_id) + values + with + | Some value -> Ok value + | None -> Error (Invalid "fill projection omitted its instrument") + in + let* () = + List.fold_left + (fun result value -> + let* () = result in + let* item_policy = + match instrument_policy state value.instrument_id with + | Some policy -> Ok policy + | None -> Error (Invalid "fill projection has no risk policy") + in + if + Scalar.Quantity.compare value.quantity item_policy.max_long_position + > 0 + then + Error + (Limit + (Instrument_maximum_long_position + (value.instrument_id, item_policy.max_long_position))) + else + let* minimum_short = + Scalar.Quantity.negate item_policy.max_short_position |> invalid + in + if Scalar.Quantity.compare value.quantity minimum_short < 0 then + Error + (Limit + (Instrument_maximum_short_position + (value.instrument_id, item_policy.max_short_position))) + else if + (not item_policy.shorting_allowed) + && Scalar.Quantity.is_negative value.quantity + then + Error (Limit (Instrument_shorting_disabled value.instrument_id)) + else + match item_policy.max_notional_exposure with + | Some limit + when Scalar.Money.compare value.absolute_value limit > 0 -> + Error + (Limit + (Instrument_maximum_notional (value.instrument_id, limit))) + | _ -> Ok ()) + (Ok ()) values + in + if Scalar.Quantity.compare current.quantity policy.max_long_position > 0 + then + Error + (Limit + (Instrument_maximum_long_position + (instrument_id, policy.max_long_position))) + else + let* minimum_short = + Scalar.Quantity.negate policy.max_short_position |> invalid + in + if Scalar.Quantity.compare current.quantity minimum_short < 0 then + Error + (Limit + (Instrument_maximum_short_position + (instrument_id, policy.max_short_position))) + else if + (not policy.shorting_allowed) + && Scalar.Quantity.is_negative current.quantity + then Error (Limit (Instrument_shorting_disabled instrument_id)) + else + let* () = + match policy.max_notional_exposure with + | Some limit + when Scalar.Money.compare current.absolute_value limit > 0 -> + Error (Limit (Instrument_maximum_notional (instrument_id, limit))) + | _ -> Ok () + in + let* gross, _, _, _ = sum_values values |> invalid in + let* () = + if Scalar.Money.compare gross state.max_gross_exposure > 0 then + Error (Limit (Maximum_gross_exposure state.max_gross_exposure)) + else + let* leveraged_equity = + Scalar.Money.multiply_ratio after.Account.equity + state.max_leverage + |> invalid + in + if Scalar.Money.compare gross leveraged_equity > 0 then + Error (Limit (Maximum_leverage state.max_leverage)) + else Ok () + in + let* initial_requirement = + List.fold_left + (fun result value -> + let* total = result in + let* item_policy = + match instrument_policy state value.instrument_id with + | Some policy -> Ok policy + | None -> Error (Invalid "fill projection has no risk policy") + in + let* requirement = + Scalar.Money.bps_ceil value.absolute_value + ~bps:item_policy.initial_margin_bps + |> invalid + in + Scalar.Money.add total requirement |> invalid) + (Ok Scalar.Money.zero) values + in + let* excess = + Scalar.Money.subtract after.equity initial_requirement |> invalid + in + if Scalar.Money.compare excess Scalar.Money.zero < 0 then + Error + (Limit + (Instrument_initial_margin + (instrument_id, policy.initial_margin_bps))) + else check_group_fill_limit state ~equity:after.equity values + +let check state ~account ~oms ~marks ~fx_rates (request : Order.request) = + match instrument state request.instrument_id with + | None -> Error "order refers to an unknown instrument" + | Some instrument -> + let* policy = + match instrument_policy state request.instrument_id with + | Some value -> Ok value + | None -> Error "order has no instrument risk policy" + in + if + Scalar.Quantity.compare request.Order.quantity policy.max_order_quantity + > 0 + then + Error + (if state.versioned then + "order exceeds the instrument maximum order quantity" + else "order exceeds the maximum order quantity") + else let* () = check_alignment instrument request in let* () = check_self_cross ~oms request in let* pending, projected = projected_position ~account ~oms request in @@ -392,14 +1372,13 @@ let check state ~account ~oms ~marks ~fx_rates request = if Scalar.Quantity.compare projected_absolute pending_absolute <= 0 then Ok () else - let* () = check_position state projected in + let* () = check_position_for state request.instrument_id projected in let* before = Account.value account ~instruments:(instruments state) ~marks ~fx_rates in - let* projected_gross_exposure = - projected_gross_exposure state ~account ~oms ~marks ~fx_rates - request + let* quantities = + projected_valuation_quantities state ~account ~oms request in - check_initial_values state ~equity:before.equity - ~gross_exposure:projected_gross_exposure + let* values = projected_values state ~marks ~fx_rates quantities in + check_projected_values state ~equity:before.equity values diff --git a/lib/risk.mli b/lib/risk.mli index d457492..710ff91 100644 --- a/lib/risk.mli +++ b/lib/risk.mli @@ -2,12 +2,50 @@ type t +type instrument_policy = private { + instrument_id : Id.Instrument.t; + max_order_quantity : Scalar.Quantity.t; + max_long_position : Scalar.Quantity.t; + max_short_position : Scalar.Quantity.t; + max_notional_exposure : Scalar.Money.t option; + initial_margin_bps : int; + maintenance_margin_bps : int; + shorting_allowed : bool; +} + +type group_kind = Issuer | Sector | Currency | Country | Asset_class | Custom + +type group_limits = private { + max_gross_exposure : Scalar.Money.t option; + max_long_exposure : Scalar.Money.t option; + max_short_exposure : Scalar.Money.t option; + max_absolute_net_exposure : Scalar.Money.t option; + max_concentration : Scalar.Ratio.t option; +} + +type group = private { + group_id : Id.Risk_group.t; + group_kind : group_kind; + instrument_ids : Id.Instrument.t list; + limits : group_limits; +} + +type group_exposure = private { + group_id : Id.Risk_group.t; + gross_exposure : Scalar.Money.t; + net_exposure : Scalar.Money.t; + long_exposure : Scalar.Money.t; + short_exposure : Scalar.Money.t; + concentration : Scalar.Weight.t option; +} + type margin_snapshot = private { initial_requirement : Scalar.Money.t; maintenance_requirement : Scalar.Money.t; initial_excess : Scalar.Money.t; maintenance_excess : Scalar.Money.t; margin_call : bool; + group_exposures : group_exposure list; } type fill_limit = @@ -17,6 +55,16 @@ type fill_limit = | Maximum_gross_exposure of Scalar.Money.t | Maximum_leverage of Scalar.Ratio.t | Initial_margin of int + | Instrument_maximum_long_position of Id.Instrument.t * Scalar.Quantity.t + | Instrument_maximum_short_position of Id.Instrument.t * Scalar.Quantity.t + | Instrument_maximum_notional of Id.Instrument.t * Scalar.Money.t + | Instrument_shorting_disabled of Id.Instrument.t + | Instrument_initial_margin of Id.Instrument.t * int + | Group_maximum_gross of Id.Risk_group.t * Scalar.Money.t + | Group_maximum_long of Id.Risk_group.t * Scalar.Money.t + | Group_maximum_short of Id.Risk_group.t * Scalar.Money.t + | Group_maximum_absolute_net of Id.Risk_group.t * Scalar.Money.t + | Group_maximum_concentration of Id.Risk_group.t * Scalar.Ratio.t type fill_check_error = Limit of fill_limit | Invalid of string @@ -33,9 +81,48 @@ val create : short_borrow_bps:int -> (t, string) result +val create_instrument_policy : + instrument:Instrument.t -> + max_order_quantity:Scalar.Quantity.t -> + max_long_position:Scalar.Quantity.t -> + max_short_position:Scalar.Quantity.t -> + max_notional_exposure:Scalar.Money.t option -> + initial_margin_bps:int -> + maintenance_margin_bps:int -> + shorting_allowed:bool -> + (instrument_policy, string) result + +val create_group_limits : + max_gross_exposure:Scalar.Money.t option -> + max_long_exposure:Scalar.Money.t option -> + max_short_exposure:Scalar.Money.t option -> + max_absolute_net_exposure:Scalar.Money.t option -> + max_concentration:Scalar.Ratio.t option -> + (group_limits, string) result + +val create_group : + group_id:Id.Risk_group.t -> + group_kind:group_kind -> + instrument_ids:Id.Instrument.t list -> + limits:group_limits -> + (group, string) result + +val create_v7 : + base_currency:string -> + instruments:Instrument.t list -> + instrument_policies:instrument_policy list -> + groups:group list -> + max_gross_exposure:Scalar.Money.t -> + max_leverage:Scalar.Ratio.t -> + short_borrow_bps:int -> + (t, string) result + val base_currency : t -> string val instruments : t -> Instrument.t list val instrument : t -> Id.Instrument.t -> Instrument.t option +val instrument_policies : t -> instrument_policy list +val instrument_policy : t -> Id.Instrument.t -> instrument_policy option +val groups : t -> group list val max_order_quantity : t -> Scalar.Quantity.t val max_long_position : t -> Scalar.Quantity.t val max_short_position : t -> Scalar.Quantity.t @@ -44,8 +131,17 @@ val max_leverage : t -> Scalar.Ratio.t val initial_margin_bps : t -> int val maintenance_margin_bps : t -> int val short_borrow_bps : t -> int +val max_order_quantity_for : t -> Id.Instrument.t -> Scalar.Quantity.t option val check_position : t -> Scalar.Quantity.t -> (unit, string) result + +val check_position_for : + t -> Id.Instrument.t -> Scalar.Quantity.t -> (unit, string) result + val margin_snapshot : t -> Account.valuation -> (margin_snapshot, string) result + +val group_exposures : + t -> Account.valuation -> (group_exposure list, string) result + val check_initial : t -> Account.valuation -> (unit, string) result val check_post_fill : @@ -56,6 +152,26 @@ val check_post_fill : after:Account.valuation -> (unit, fill_check_error) result +val check_post_fill_for : + t -> + instrument_id:Id.Instrument.t -> + before_position:Scalar.Quantity.t -> + after_position:Scalar.Quantity.t -> + before:Account.valuation -> + after:Account.valuation -> + (unit, fill_check_error) result + +val check_reserved_fill : + t -> + account:Account.t -> + oms:Oms.t -> + marks:(Id.Instrument.t * Scalar.Price.t) list -> + fx_rates:(string * Scalar.Price.t) list -> + order:Order.t -> + filled_quantity:Scalar.Quantity.t -> + after:Account.valuation -> + (unit, fill_check_error) result + val check : t -> account:Account.t -> diff --git a/lib/scenario.ml b/lib/scenario.ml index 79d58a2..39e39d4 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -323,7 +323,7 @@ let parse_instrument json = let* lot_size = parse_quantity ~name:"lot_size" lot_json in Instrument.create ~id ~symbol ~quote_currency ~tick_size ~lot_size -let parse_risk base_currency instruments json = +let parse_legacy_risk base_currency instruments json = let* fields = object_fields ~name:"risk" ~expected: @@ -365,6 +365,195 @@ let parse_risk base_currency instruments json = ~max_short_position ~max_gross_exposure ~max_leverage ~initial_margin_bps ~maintenance_margin_bps ~short_borrow_bps +let parse_nullable parse ~name = function + | `Null -> Ok None + | json -> parse ~name json |> Result.map Option.some + +let parse_group_kind = function + | "issuer" -> Ok Risk.Issuer + | "sector" -> Ok Risk.Sector + | "currency" -> Ok Risk.Currency + | "country" -> Ok Risk.Country + | "asset_class" -> Ok Risk.Asset_class + | "custom" -> Ok Risk.Custom + | _ -> Error "group_type is unsupported" + +let parse_instrument_policy instrument_map json = + let* fields = + object_fields ~name:"instrument risk policy" + ~expected: + [ + "instrument_id"; + "max_order_quantity"; + "max_long_position"; + "max_short_position"; + "max_notional_exposure"; + "initial_margin_bps"; + "maintenance_margin_bps"; + "shorting_allowed"; + ] + json + in + let* id_json = field fields "instrument_id" in + let* instrument_id = + parse_id Id.Instrument.of_string ~name:"instrument_id" id_json + in + let* instrument = + match Id.Instrument.Map.find_opt instrument_id instrument_map with + | Some instrument -> Ok instrument + | None -> Error "instrument policy refers to an unknown instrument" + in + let* max_order_quantity = + field fields "max_order_quantity" |> fun result -> + Result.bind result (parse_quantity ~name:"max_order_quantity") + in + let* max_long_position = + field fields "max_long_position" |> fun result -> + Result.bind result (parse_quantity ~name:"max_long_position") + in + let* max_short_position = + field fields "max_short_position" |> fun result -> + Result.bind result (parse_quantity ~name:"max_short_position") + in + let* max_notional_exposure = + field fields "max_notional_exposure" |> fun result -> + Result.bind result (parse_money ~name:"max_notional_exposure") + in + let* initial_margin_bps = + field fields "initial_margin_bps" |> fun result -> + Result.bind result (integer ~name:"initial_margin_bps") + in + let* maintenance_margin_bps = + field fields "maintenance_margin_bps" |> fun result -> + Result.bind result (integer ~name:"maintenance_margin_bps") + in + let* shorting_allowed = + match List.assoc "shorting_allowed" fields with + | `Bool value -> Ok value + | _ -> Error "shorting_allowed must be a boolean" + in + Risk.create_instrument_policy ~instrument ~max_order_quantity + ~max_long_position ~max_short_position + ~max_notional_exposure:(Some max_notional_exposure) ~initial_margin_bps + ~maintenance_margin_bps ~shorting_allowed + +let parse_group json = + let* fields = + object_fields ~name:"risk group" + ~expected: + [ + "group_id"; "group_version"; "group_type"; "instrument_ids"; "limits"; + ] + json + in + let* group_id = + field fields "group_id" |> fun result -> + Result.bind result (parse_id Id.Risk_group.of_string ~name:"group_id") + in + let* group_version = + field fields "group_version" |> fun result -> + Result.bind result (string ~name:"group_version") + in + let* () = + if String.equal group_version "1" then Ok () + else Error "group_version must be 1" + in + let* group_kind = + field fields "group_type" |> fun result -> + Result.bind result (string ~name:"group_type") |> fun result -> + Result.bind result parse_group_kind + in + let* instrument_ids_json = + field fields "instrument_ids" |> fun result -> + Result.bind result (list ~name:"instrument_ids") + in + let* instrument_ids = + map_list + (parse_id Id.Instrument.of_string ~name:"instrument_id") + instrument_ids_json + in + let* limits_json = field fields "limits" in + let* limits_fields = + object_fields ~name:"risk group limits" + ~expected: + [ + "max_gross_exposure"; + "max_long_exposure"; + "max_short_exposure"; + "max_absolute_net_exposure"; + "max_concentration"; + ] + limits_json + in + let money_limit name = + field limits_fields name |> fun result -> + Result.bind result (parse_nullable parse_money ~name) + in + let* max_gross_exposure = money_limit "max_gross_exposure" in + let* max_long_exposure = money_limit "max_long_exposure" in + let* max_short_exposure = money_limit "max_short_exposure" in + let* max_absolute_net_exposure = money_limit "max_absolute_net_exposure" in + let* max_concentration = + field limits_fields "max_concentration" |> fun result -> + Result.bind result (parse_nullable parse_ratio ~name:"max_concentration") + in + let* limits = + Risk.create_group_limits ~max_gross_exposure ~max_long_exposure + ~max_short_exposure ~max_absolute_net_exposure ~max_concentration + in + Risk.create_group ~group_id ~group_kind ~instrument_ids ~limits + +let parse_v7_risk base_currency instruments json = + let* fields = + object_fields ~name:"risk" + ~expected: + [ + "max_gross_exposure"; + "max_leverage"; + "short_borrow_bps"; + "instrument_policies"; + "groups"; + ] + json + in + let instrument_map = + List.fold_left + (fun map instrument -> + Id.Instrument.Map.add instrument.Instrument.id instrument map) + Id.Instrument.Map.empty instruments + in + let* policies_json = + field fields "instrument_policies" |> fun result -> + Result.bind result (list ~name:"instrument_policies") + in + let* instrument_policies = + map_list (parse_instrument_policy instrument_map) policies_json + in + let* groups_json = + field fields "groups" |> fun result -> + Result.bind result (list ~name:"groups") + in + let* groups = map_list parse_group groups_json in + let* max_gross_exposure = + field fields "max_gross_exposure" |> fun result -> + Result.bind result (parse_money ~name:"max_gross_exposure") + in + let* max_leverage = + field fields "max_leverage" |> fun result -> + Result.bind result (parse_ratio ~name:"max_leverage") + in + let* short_borrow_bps = + field fields "short_borrow_bps" |> fun result -> + Result.bind result (integer ~name:"short_borrow_bps") + in + Risk.create_v7 ~base_currency ~instruments ~instrument_policies ~groups + ~max_gross_exposure ~max_leverage ~short_borrow_bps + +let parse_risk ~contract_version base_currency instruments json = + if String.equal contract_version "7" then + parse_v7_risk base_currency instruments json + else parse_legacy_risk base_currency instruments json + let parse_execution_values fields = let* participation_json = field fields "participation_bps" in let* participation_bps = @@ -434,7 +623,7 @@ let parse_versioned_execution ~contract_version json = Ok (execution_model, execution) let parse_execution ~contract_version json = - if List.mem contract_version [ "6"; "5" ] then + if List.mem contract_version [ "7"; "6"; "5" ] then parse_versioned_execution ~contract_version json else parse_legacy_execution ~contract_version json @@ -821,7 +1010,7 @@ let construct_header ~root ~contract_path ~contract_version |> at (child root "base_currency") in let* initial_cash, initial_portfolio = - if String.equal contract_version "6" then + if List.mem contract_version [ "7"; "6" ] then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state |> at (child root "initial_portfolio") @@ -867,7 +1056,8 @@ let construct_header ~root ~contract_path ~contract_version ~initial_cash ~instruments ~venue_calendars ~max_internal_events in let* risk = - parse_risk base_currency instruments shape.risk |> at (child root "risk") + parse_risk ~contract_version base_currency instruments shape.risk + |> at (child root "risk") in let* () = match initial_portfolio with diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index b1a5190..150be88 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -68,13 +68,13 @@ 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 String.equal contract_version "6" then "initial_portfolio" + if List.mem contract_version [ "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 [ "6"; "5" ] then + if List.mem contract_version [ "7"; "6"; "5" ] then List.assoc_opt "venue_calendars" fields else None in @@ -105,10 +105,11 @@ let batch json = match preliminary with `String value -> value | _ -> "" in let calendar_fields = - if List.mem contract_version [ "6"; "5" ] then [ "venue_calendars" ] else [] + if List.mem contract_version [ "7"; "6"; "5" ] then [ "venue_calendars" ] + else [] in let initial_field = - if String.equal contract_version "6" then "initial_portfolio" + if List.mem contract_version [ "7"; "6" ] then "initial_portfolio" else "initial_cash" in let* fields = @@ -139,10 +140,11 @@ let batch json = let stream_header ~contract_version json = let root = "$.payload" in let calendar_fields = - if List.mem contract_version [ "6"; "5" ] then [ "venue_calendars" ] else [] + if List.mem contract_version [ "7"; "6"; "5" ] then [ "venue_calendars" ] + else [] in let initial_field = - if String.equal contract_version "6" then "initial_portfolio" + if List.mem contract_version [ "7"; "6" ] then "initial_portfolio" else "initial_cash" in let* fields = diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 9ba25b9..8dc42a1 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -48,7 +48,7 @@ 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 String.equal contract_version "6" then Ok () + if List.mem contract_version [ "7"; "6" ] then Ok () else Account.create ~base_currency ~initial_cash |> Result.map (fun _ -> ()) @@ -66,7 +66,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path:(child root "instruments") "instrument IDs must be unique" else let* () = - if List.mem contract_version [ "6"; "5" ] then + if List.mem contract_version [ "7"; "6"; "5" ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -84,7 +84,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path: (child root - (if String.equal contract_version "6" then + (if List.mem contract_version [ "7"; "6" ] then "initial_portfolio.cash" else "initial_cash")) "initial cash must contain every scenario currency exactly once" @@ -139,7 +139,8 @@ let initial_portfolio ~root ~currencies ~catalog ~instruments ~risk initial = "initial position quantity is not aligned to its \ instrument lot" else - Risk.check_position risk position.quantity + Risk.check_position_for risk position.instrument_id + position.quantity |> at (child path "positions")) (Ok ()) initial.positions in @@ -240,20 +241,26 @@ let validate_portfolio_target ~json_path risk catalog = function then fail ~json_path "target quantity is not aligned to its instrument lot" - else Risk.check_position risk target.quantity |> at json_path) + else + Risk.check_position_for risk target.instrument_id + target.quantity + |> at json_path) (Ok ()) targets | Strategy.Submit_order request -> ( if not (Id.Instrument.Set.mem request.Order.instrument_id catalog) then fail ~json_path "order refers to an unknown instrument" - else if - Scalar.Quantity.compare request.quantity (Risk.max_order_quantity risk) - > 0 - then fail ~json_path "order exceeds the maximum order quantity" else - match Risk.instrument risk request.instrument_id with - | None -> fail ~json_path "order refers to an unknown instrument" - | Some instrument -> ( - if + match + ( Risk.instrument risk request.instrument_id, + Risk.max_order_quantity_for risk request.instrument_id ) + with + | None, _ -> fail ~json_path "order refers to an unknown instrument" + | _, None -> fail ~json_path "order has no instrument risk policy" + | Some instrument, Some order_limit -> ( + if Scalar.Quantity.compare request.quantity order_limit > 0 then + fail ~json_path + "order exceeds the instrument maximum order quantity" + else if not (Scalar.Quantity.is_multiple request.quantity ~lot:instrument.Instrument.lot_size) diff --git a/lib/strategy.ml b/lib/strategy.ml index ba98e14..62681e1 100644 --- a/lib/strategy.ml +++ b/lib/strategy.ml @@ -24,6 +24,7 @@ and portfolio = { cash_weight : Scalar.Weight.t option; cash_balances : Account.cash_attribution list; positions : marked_position list; + group_exposures : Risk.group_exposure list; } type event = @@ -56,7 +57,8 @@ let weight ~equity value = if Scalar.Money.compare equity Scalar.Money.zero <= 0 then Ok None else Scalar.Money.weight_toward_zero value ~equity |> Result.map Option.some -let context ~now ~(valuation : Account.valuation) ~working_orders ~latest_bars = +let context ~now ~(valuation : Account.valuation) ~group_exposures + ~working_orders ~latest_bars = let* cash_weight = weight ~equity:valuation.equity valuation.cash in let* positions = List.fold_left @@ -94,6 +96,7 @@ let context ~now ~(valuation : Account.valuation) ~working_orders ~latest_bars = cash_weight; cash_balances = valuation.cash_balances; positions; + group_exposures; } in Ok { now; portfolio; working_orders; latest_bars } @@ -121,6 +124,8 @@ let working_orders context = context.working_orders let latest_bar context instrument_id = Id.Instrument.Map.find_opt instrument_id context.latest_bars +let group_exposures context = context.portfolio.group_exposures + module type S = sig type state diff --git a/lib/strategy.mli b/lib/strategy.mli index fce158e..8b66edf 100644 --- a/lib/strategy.mli +++ b/lib/strategy.mli @@ -21,6 +21,7 @@ type portfolio = private { cash_weight : Scalar.Weight.t option; cash_balances : Account.cash_attribution list; positions : marked_position list; + group_exposures : Risk.group_exposure list; } type event = @@ -49,6 +50,7 @@ type intent = val context : now:Ptime.t -> valuation:Account.valuation -> + group_exposures:Risk.group_exposure list -> working_orders:Order.t list -> latest_bars:Bar.t list -> (context, string) result @@ -60,6 +62,7 @@ val cash_balances : context -> (string * Scalar.Money.t) list val position : context -> Id.Instrument.t -> Scalar.Quantity.t val working_orders : context -> Order.t list val latest_bar : context -> Id.Instrument.t -> Bar.t option +val group_exposures : context -> Risk.group_exposure list module type S = sig type state diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index 74163de..aa4630a 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -60,19 +60,75 @@ let instrument_to_yojson instrument = ("lot_size", quantity instrument.lot_size); ] -let risk_to_yojson risk = +let group_kind_to_string = function + | Risk.Issuer -> "issuer" + | Risk.Sector -> "sector" + | Risk.Currency -> "currency" + | Risk.Country -> "country" + | Risk.Asset_class -> "asset_class" + | Risk.Custom -> "custom" + +let nullable render = Option.fold ~none:`Null ~some:render + +let instrument_policy_to_yojson (policy : Risk.instrument_policy) = + `Assoc + [ + ("instrument_id", instrument_id policy.instrument_id); + ("max_order_quantity", quantity policy.max_order_quantity); + ("max_long_position", quantity policy.max_long_position); + ("max_short_position", quantity policy.max_short_position); + ("max_notional_exposure", nullable money policy.max_notional_exposure); + ("initial_margin_bps", `Int policy.initial_margin_bps); + ("maintenance_margin_bps", `Int policy.maintenance_margin_bps); + ("shorting_allowed", `Bool policy.shorting_allowed); + ] + +let group_to_yojson (group : Risk.group) = + let limits = group.limits in `Assoc [ - ("max_order_quantity", quantity (Risk.max_order_quantity risk)); - ("max_long_position", quantity (Risk.max_long_position risk)); - ("max_short_position", quantity (Risk.max_short_position risk)); - ("max_gross_exposure", money (Risk.max_gross_exposure risk)); - ("max_leverage", ratio (Risk.max_leverage risk)); - ("initial_margin_bps", `Int (Risk.initial_margin_bps risk)); - ("maintenance_margin_bps", `Int (Risk.maintenance_margin_bps risk)); - ("short_borrow_bps", `Int (Risk.short_borrow_bps risk)); + ("group_id", string (Id.Risk_group.to_string group.group_id)); + ("group_version", string "1"); + ("group_type", string (group_kind_to_string group.group_kind)); + ("instrument_ids", `List (List.map instrument_id group.instrument_ids)); + ( "limits", + `Assoc + [ + ("max_gross_exposure", nullable money limits.max_gross_exposure); + ("max_long_exposure", nullable money limits.max_long_exposure); + ("max_short_exposure", nullable money limits.max_short_exposure); + ( "max_absolute_net_exposure", + nullable money limits.max_absolute_net_exposure ); + ("max_concentration", nullable ratio limits.max_concentration); + ] ); ] +let risk_to_yojson ~protocol_version risk = + if String.equal protocol_version version then + `Assoc + [ + ("max_gross_exposure", money (Risk.max_gross_exposure risk)); + ("max_leverage", ratio (Risk.max_leverage risk)); + ("short_borrow_bps", `Int (Risk.short_borrow_bps risk)); + ( "instrument_policies", + `List + (List.map instrument_policy_to_yojson + (Risk.instrument_policies risk)) ); + ("groups", `List (List.map group_to_yojson (Risk.groups risk))); + ] + else + `Assoc + [ + ("max_order_quantity", quantity (Risk.max_order_quantity risk)); + ("max_long_position", quantity (Risk.max_long_position risk)); + ("max_short_position", quantity (Risk.max_short_position risk)); + ("max_gross_exposure", money (Risk.max_gross_exposure risk)); + ("max_leverage", ratio (Risk.max_leverage risk)); + ("initial_margin_bps", `Int (Risk.initial_margin_bps risk)); + ("maintenance_margin_bps", `Int (Risk.maintenance_margin_bps risk)); + ("short_borrow_bps", `Int (Risk.short_borrow_bps risk)); + ] + let execution_to_yojson ~protocol_version model execution = if String.equal protocol_version version then `Assoc @@ -99,7 +155,11 @@ let execution_to_yojson ~protocol_version model execution = let protocol_version initialization = if String.equal initialization.scenario_contract_version Contract.version then version - else Contract.previous_strategy_protocol_version + else if + String.equal initialization.scenario_contract_version + Contract.previous_version + then Contract.previous_strategy_protocol_version + else "3" let initialize_message ~sequence:message_sequence initialization = let protocol_version = protocol_version initialization in @@ -124,7 +184,7 @@ let initialize_message ~sequence:message_sequence initialization = ("base_currency", string initialization.base_currency); ("initial_cash", `List (List.map cash_balance_to_yojson initial_cash)); ("instruments", `List (List.map instrument_to_yojson instruments)); - ("risk", risk_to_yojson initialization.risk); + ("risk", risk_to_yojson ~protocol_version initialization.risk); ( "execution", execution_to_yojson ~protocol_version initialization.execution_model initialization.execution ); @@ -167,7 +227,19 @@ let marked_position_to_yojson (position : Strategy.marked_position) = ("weight", Option.fold ~none:`Null ~some:weight position.weight); ] -let context_to_yojson context = +let group_exposure_to_yojson (exposure : Risk.group_exposure) = + `Assoc + [ + ("group_id", string (Id.Risk_group.to_string exposure.group_id)); + ("gross_exposure", money exposure.gross_exposure); + ("net_exposure", money exposure.net_exposure); + ("long_exposure", money exposure.long_exposure); + ("short_exposure", money exposure.short_exposure); + ( "concentration", + Option.fold ~none:`Null ~some:weight exposure.concentration ); + ] + +let context_to_yojson ~protocol_version context = let portfolio = Strategy.portfolio context in let cash_balances = List.sort @@ -193,26 +265,36 @@ let context_to_yojson context = positions |> List.filter_map Fun.id in + let portfolio_fields = + [ + ("base_currency", string portfolio.base_currency); + ("cash", money portfolio.cash); + ("net_market_value", money portfolio.net_market_value); + ("long_market_value", money portfolio.long_market_value); + ("short_market_value", money portfolio.short_market_value); + ("gross_exposure", money portfolio.gross_exposure); + ("equity", money portfolio.equity); + ("weights_available", `Bool (Option.is_some portfolio.cash_weight)); + ("cash_weight", Option.fold ~none:`Null ~some:weight portfolio.cash_weight); + ( "cash_balances", + `List (List.map cash_attribution_to_yojson cash_balances) ); + ("positions", `List (List.map marked_position_to_yojson positions)); + ] + in + let portfolio_fields = + if String.equal protocol_version version then + portfolio_fields + @ [ + ( "group_exposures", + `List (List.map group_exposure_to_yojson portfolio.group_exposures) + ); + ] + else portfolio_fields + in `Assoc [ ("now", timestamp (Strategy.now context)); - ( "portfolio", - `Assoc - [ - ("base_currency", string portfolio.base_currency); - ("cash", money portfolio.cash); - ("net_market_value", money portfolio.net_market_value); - ("long_market_value", money portfolio.long_market_value); - ("short_market_value", money portfolio.short_market_value); - ("gross_exposure", money portfolio.gross_exposure); - ("equity", money portfolio.equity); - ("weights_available", `Bool (Option.is_some portfolio.cash_weight)); - ( "cash_weight", - Option.fold ~none:`Null ~some:weight portfolio.cash_weight ); - ( "cash_balances", - `List (List.map cash_attribution_to_yojson cash_balances) ); - ("positions", `List (List.map marked_position_to_yojson positions)); - ] ); + ("portfolio", `Assoc portfolio_fields); ("working_orders", `List (List.map Codec.order_to_yojson working_orders)); ("latest_bars", `List (List.map Codec.bar_to_yojson latest_bars)); ] @@ -243,7 +325,8 @@ let event_message ?(protocol_version = version) ~sequence:message_sequence message ~protocol_version ~sequence:message_sequence ~message_type:"event" (`Assoc [ - ("context", context_to_yojson context); ("event", event_to_yojson event); + ("context", context_to_yojson ~protocol_version context); + ("event", event_to_yojson event); ]) let shutdown_message_for ~protocol_version ~sequence:message_sequence = diff --git a/mkdocs.yml b/mkdocs.yml index 1e70145..50caa34 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,14 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v6: contracts/v6/README.md + - Current v7: contracts/v7/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 v4: contracts/strategy/v4/README.md + - Current v5: contracts/strategy/v5/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-documentation.py b/scripts/check-documentation.py index 00be5ee..7745b05 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/v6/README.md", + "contracts/v7/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v4/README.md", + "contracts/strategy/v5/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 af53d34..7a1aab7 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/v6/scenario.schema.json", - "share/trading_engine/contracts/v6/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v7/scenario.schema.json", + "share/trading_engine/contracts/v7/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/v6/fixtures/demo.scenario.json", + "contracts/v7/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/v6/fixtures/demo.scenario.json", - "contracts/strategy/v4/message.schema.json", + "contracts/v7/fixtures/demo.scenario.json", + "contracts/strategy/v5/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v6/scenario.schema.json", + "contracts/v7/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index 58e5748..bb85f0c 100644 --- a/test/cli.t +++ b/test/cli.t @@ -2,22 +2,22 @@ 1.0.0 $ ../bin/main.exe --capabilities - {"engine_version":"1.0.0","scenario_contract_versions":["6","5","4","3"],"journal_contract_versions":["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":["1"],"scenario_contract_versions":["6","5","4","3"],"required_fields":["version","participation_bps","fixed_fee","fee_bps"],"supported_order_types":["market","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":["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":["7","6","5","4","3"],"journal_contract_versions":["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":["1"],"scenario_contract_versions":["7","6","5","4","3"],"required_fields":["version","participation_bps","fixed_fee","fee_bps"],"supported_order_types":["market","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":["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/v6/fixtures/demo.scenario.json - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=c98534261412acebf4b49d87619dc7c951538581c2a7dd05f315eb64d761cec2 + $ ../bin/main.exe --validate-only --input ../contracts/v7/fixtures/demo.scenario.json + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=d1991fa67140bff80fcbeb9b04b211d8c9cf4f41d4fba39dcec66d5ef3e5fab9 - $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v6/fixtures/demo.scenario.jsonl - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=b0859d248708402801a1155a68026ca8b799b19a425223fa3d614b7fa6224253 + $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v7/fixtures/demo.scenario.jsonl + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=61486a162021bad1302fbce67fde0926821f3d521dd7376164a288d4b3f03e76 - $ ../bin/main.exe --input-format jsonl --input ../contracts/v6/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts + $ ../bin/main.exe --input-format jsonl --input ../contracts/v7/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts run=demo audits=22 orders=3 active=0 filled=2 rejected=0 cash=9846.65392 equity=10111.65392 gross=265 realized=18.965682 unrealized=7.688238 fees=3.16608 journal=streamed.journal.jsonl $ wc -l < streamed.journal.jsonl 22 - $ head -n 5 ../contracts/v6/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl + $ head -n 5 ../contracts/v7/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl $ ../bin/main.exe --validate-only --input-format jsonl --input truncated.scenario.jsonl trading-engine: scenario_end must terminate the scenario stream [123] @@ -32,32 +32,32 @@ 1 scenario_stream.invalid validation 6 6 None - $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v6/fixtures/demo.scenario.json > invalid-tick.json + $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v7/fixtures/demo.scenario.json > invalid-tick.json $ ../bin/main.exe --validate-only --input invalid-tick.json trading-engine: market prices and volumes must align with instrument increments [123] - $ ../bin/main.exe --input ../contracts/v6/fixtures/demo.scenario.json + $ ../bin/main.exe --input ../contracts/v7/fixtures/demo.scenario.json trading-engine: --journal is required unless --validate-only is set [123] - $ ../bin/main.exe --validate-only --input ../contracts/v6/fixtures/demo.scenario.json --journal validation.journal.jsonl + $ ../bin/main.exe --validate-only --input ../contracts/v7/fixtures/demo.scenario.json --journal validation.journal.jsonl trading-engine: --journal cannot be used with --validate-only [123] $ test ! -e validation.journal.jsonl - $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v6/fixtures/demo.scenario.json + $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v7/fixtures/demo.scenario.json trading-engine: --durable-artifacts cannot be used with --validate-only [123] - $ ../bin/main.exe --input ../contracts/v6/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 + $ ../bin/main.exe --input ../contracts/v7/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 trading-engine: --strategy-arg, --strategy-timeout, and --strategy-transcript require --strategy-executable [123] $ test ! -e ignored.journal.jsonl $ mkdir external - $ ../bin/main.exe --input ../contracts/strategy/v4/fixtures/external.scenario.json --journal external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external/run.strategy.jsonl --strategy-timeout 5 --durable-artifacts + $ ../bin/main.exe --input ../contracts/strategy/v5/fixtures/external.scenario.json --journal external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external/run.strategy.jsonl --strategy-timeout 5 --durable-artifacts run=external-demo audits=12 orders=1 active=0 filled=1 rejected=0 cash=9793.544 equity=10007.544 gross=214 realized=0 unrealized=7.544 fees=0.456 journal=external/run.journal.jsonl @@ -65,10 +65,10 @@ $ python3 -c 'from pathlib import Path; print(len(Path("external/run.journal.jsonl").read_text().splitlines()), len(Path("external/run.strategy.jsonl").read_text().splitlines()))' 12 14 - $ diff -u ../contracts/strategy/v4/fixtures/external.strategy.jsonl external/run.strategy.jsonl + $ diff -u ../contracts/strategy/v5/fixtures/external.strategy.jsonl external/run.strategy.jsonl $ mkdir callback-ordering - $ ../bin/main.exe --input ../contracts/strategy/v4/fixtures/external.scenario.json --journal callback-ordering/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg cancel-next --strategy-transcript callback-ordering/run.strategy.jsonl --strategy-timeout 5 + $ ../bin/main.exe --input ../contracts/strategy/v5/fixtures/external.scenario.json --journal callback-ordering/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg cancel-next --strategy-transcript callback-ordering/run.strategy.jsonl --strategy-timeout 5 run=external-demo audits=12 orders=2 active=0 filled=1 rejected=0 cash=9896.647 equity=10003.647 gross=107 realized=0 unrealized=3.647 fees=0.353 journal=callback-ordering/run.journal.jsonl @@ -79,7 +79,7 @@ 107 107 $ mkdir failed-external - $ ../bin/main.exe --input ../contracts/strategy/v4/fixtures/external.scenario.json --journal failed-external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg stall --strategy-transcript failed-external/run.strategy.jsonl --strategy-timeout 0.01 + $ ../bin/main.exe --input ../contracts/strategy/v5/fixtures/external.scenario.json --journal failed-external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg stall --strategy-transcript failed-external/run.strategy.jsonl --strategy-timeout 0.01 trading-engine: strategy initialization: external strategy timed out [123] $ test ! -e failed-external/run.journal.jsonl @@ -92,7 +92,7 @@ > expected="$2" > directory="fault-$mode" > mkdir "$directory" - > output=$(../bin/main.exe --input ../contracts/strategy/v4/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 5 2>&1) + > output=$(../bin/main.exe --input ../contracts/strategy/v5/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 5 2>&1) > status=$? > test "$status" -eq 123 || return 1 > case "$output" in *"$expected"*) ;; *) return 1 ;; esac @@ -169,7 +169,7 @@ > directory="process-tree-$mode" > mkdir "$directory" > pid_path="$directory/grandchild.pid" - > output=$(../bin/main.exe --input ../contracts/strategy/v4/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-arg "$pid_path" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 0.2 2>&1) + > output=$(../bin/main.exe --input ../contracts/strategy/v5/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-arg "$pid_path" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 0.2 2>&1) > status=$? > test "$status" -eq 123 || return 1 > case "$output" in *"$expected"*) ;; *) return 1 ;; esac @@ -198,7 +198,7 @@ grandchild-malformed: process tree reaped $ mkdir external-stream - $ ../bin/main.exe --input-format jsonl --input ../contracts/strategy/v4/fixtures/external.scenario.jsonl --journal external-stream/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-stream/run.strategy.jsonl --strategy-timeout 5 + $ ../bin/main.exe --input-format jsonl --input ../contracts/strategy/v5/fixtures/external.scenario.jsonl --journal external-stream/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-stream/run.strategy.jsonl --strategy-timeout 5 run=external-demo audits=12 orders=1 active=0 filled=1 rejected=0 cash=9793.544 equity=10007.544 gross=214 realized=0 unrealized=7.544 fees=0.456 journal=external-stream/run.journal.jsonl diff --git a/test/dune b/test/dune index f322ca3..cb9e70e 100644 --- a/test/dune +++ b/test/dune @@ -13,17 +13,20 @@ test_contract_conformance test_boundary_failures test_venue_calendar + test_risk_groups test_scenario test_engine) (deps - ../contracts/v6/fixtures/demo.journal.jsonl + ../contracts/v7/fixtures/demo.journal.jsonl + ../contracts/v7/fixtures/demo.scenario.json + ../contracts/v7/fixtures/demo.scenario.jsonl + ../contracts/v7/fixtures/fill-clipped.journal.jsonl + ../contracts/v7/fixtures/fill-clipped.scenario.json + ../contracts/v7/journal.schema.json + ../contracts/v7/scenario-stream.schema.json + ../contracts/v7/scenario.schema.json ../contracts/v6/fixtures/demo.scenario.json ../contracts/v6/fixtures/demo.scenario.jsonl - ../contracts/v6/fixtures/fill-clipped.journal.jsonl - ../contracts/v6/fixtures/fill-clipped.scenario.json - ../contracts/v6/journal.schema.json - ../contracts/v6/scenario-stream.schema.json - ../contracts/v6/scenario.schema.json ../contracts/v5/fixtures/demo.scenario.json ../contracts/v5/fixtures/demo.scenario.jsonl ../contracts/v4/fixtures/demo.scenario.json @@ -31,6 +34,7 @@ ../contracts/v3/fixtures/demo.scenario.json ../contracts/v3/fixtures/demo.scenario.jsonl ../contracts/conformance/cases.json + ../contracts/strategy/v5/fixtures/external.strategy.jsonl ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries @@ -61,53 +65,53 @@ (deps ../bin/main.exe fake_strategy.py - ../contracts/strategy/v4/fixtures/external.scenario.json - ../contracts/strategy/v4/fixtures/external.scenario.jsonl - ../contracts/strategy/v4/fixtures/external.strategy.jsonl - ../contracts/v6/fixtures/demo.scenario.json - ../contracts/v6/fixtures/demo.scenario.jsonl)) + ../contracts/strategy/v5/fixtures/external.scenario.json + ../contracts/strategy/v5/fixtures/external.scenario.jsonl + ../contracts/strategy/v5/fixtures/external.strategy.jsonl + ../contracts/v7/fixtures/demo.scenario.json + ../contracts/v7/fixtures/demo.scenario.jsonl)) (rule (alias runtest) (deps validate_schemas.py - ../contracts/v6/fixtures/demo.journal.jsonl - ../contracts/v6/fixtures/demo.scenario.json - ../contracts/v6/fixtures/demo.scenario.jsonl - ../contracts/v6/journal.schema.json - ../contracts/v6/scenario-stream.schema.json - ../contracts/v6/scenario.schema.json) + ../contracts/v7/fixtures/demo.journal.jsonl + ../contracts/v7/fixtures/demo.scenario.json + ../contracts/v7/fixtures/demo.scenario.jsonl + ../contracts/v7/journal.schema.json + ../contracts/v7/scenario-stream.schema.json + ../contracts/v7/scenario.schema.json) (action (run python3 %{dep:validate_schemas.py} - %{dep:../contracts/v6/scenario.schema.json} - %{dep:../contracts/v6/scenario-stream.schema.json} - %{dep:../contracts/v6/journal.schema.json} - %{dep:../contracts/v6/fixtures/demo.scenario.json} - %{dep:../contracts/v6/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v6/fixtures/demo.journal.jsonl}))) + %{dep:../contracts/v7/scenario.schema.json} + %{dep:../contracts/v7/scenario-stream.schema.json} + %{dep:../contracts/v7/journal.schema.json} + %{dep:../contracts/v7/fixtures/demo.scenario.json} + %{dep:../contracts/v7/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v7/fixtures/demo.journal.jsonl}))) (rule (alias runtest) (deps validate_schemas.py - ../contracts/v6/fixtures/fill-clipped.journal.jsonl - ../contracts/v6/fixtures/fill-clipped.scenario.json - ../contracts/v6/fixtures/demo.scenario.jsonl - ../contracts/v6/journal.schema.json - ../contracts/v6/scenario-stream.schema.json - ../contracts/v6/scenario.schema.json) + ../contracts/v7/fixtures/fill-clipped.journal.jsonl + ../contracts/v7/fixtures/fill-clipped.scenario.json + ../contracts/v7/fixtures/demo.scenario.jsonl + ../contracts/v7/journal.schema.json + ../contracts/v7/scenario-stream.schema.json + ../contracts/v7/scenario.schema.json) (action (run python3 %{dep:validate_schemas.py} - %{dep:../contracts/v6/scenario.schema.json} - %{dep:../contracts/v6/scenario-stream.schema.json} - %{dep:../contracts/v6/journal.schema.json} - %{dep:../contracts/v6/fixtures/fill-clipped.scenario.json} - %{dep:../contracts/v6/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v6/fixtures/fill-clipped.journal.jsonl}))) + %{dep:../contracts/v7/scenario.schema.json} + %{dep:../contracts/v7/scenario-stream.schema.json} + %{dep:../contracts/v7/journal.schema.json} + %{dep:../contracts/v7/fixtures/fill-clipped.scenario.json} + %{dep:../contracts/v7/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v7/fixtures/fill-clipped.journal.jsonl}))) (rule (alias runtest) @@ -134,22 +138,22 @@ (alias runtest) (deps validate_strategy_schema.py - ../contracts/v6/scenario.schema.json - ../contracts/v6/journal.schema.json + ../contracts/v7/scenario.schema.json + ../contracts/v7/journal.schema.json ../contracts/diagnostic/v1/diagnostic.schema.json - ../contracts/strategy/v4/message.schema.json - ../contracts/strategy/v4/transcript.schema.json - ../contracts/strategy/v4/fixtures/external.strategy.jsonl) + ../contracts/strategy/v5/message.schema.json + ../contracts/strategy/v5/transcript.schema.json + ../contracts/strategy/v5/fixtures/external.strategy.jsonl) (action (run python3 %{dep:validate_strategy_schema.py} - %{dep:../contracts/v6/scenario.schema.json} - %{dep:../contracts/v6/journal.schema.json} + %{dep:../contracts/v7/scenario.schema.json} + %{dep:../contracts/v7/journal.schema.json} %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} - %{dep:../contracts/strategy/v4/message.schema.json} - %{dep:../contracts/strategy/v4/transcript.schema.json} - %{dep:../contracts/strategy/v4/fixtures/external.strategy.jsonl}))) + %{dep:../contracts/strategy/v5/message.schema.json} + %{dep:../contracts/strategy/v5/transcript.schema.json} + %{dep:../contracts/strategy/v5/fixtures/external.strategy.jsonl}))) (rule (alias runtest) @@ -197,6 +201,6 @@ (deps test_benchmark_replay.py ../bench/benchmark_replay.py - ../contracts/v6/fixtures/demo.scenario.json) + ../contracts/v7/fixtures/demo.scenario.json) (action (run python3 %{dep:test_benchmark_replay.py}))) diff --git a/test/fake_strategy.py b/test/fake_strategy.py index 0302937..4aa20cc 100755 --- a/test/fake_strategy.py +++ b/test/fake_strategy.py @@ -121,7 +121,7 @@ def response(request: dict[str, object]) -> dict[str, object]: response_type = "error" payload = {"message": "unsupported request"} return { - "strategy_protocol_version": "4", + "strategy_protocol_version": request["strategy_protocol_version"], "strategy_sequence": sequence, "message_type": response_type, "payload": payload, diff --git a/test/test_contract_conformance.ml b/test/test_contract_conformance.ml index 65e75dd..bd9b7f0 100644 --- a/test/test_contract_conformance.ml +++ b/test/test_contract_conformance.ml @@ -182,7 +182,13 @@ let runtime_result case = let expected_sequence = string_field "expected_sequence" case |> Int64.of_string in - T.Strategy_protocol.response_of_yojson ~expected_sequence response + let protocol_version = + match optional_field "protocol_version" case with + | Some (`String value) -> value + | _ -> T.Contract.strategy_protocol_version + in + T.Strategy_protocol.response_of_yojson ~protocol_version + ~expected_sequence response |> Result.map (fun _ -> ()) | kind -> Alcotest.failf "unsupported differential runtime kind %s" kind diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 00643f5..723dcf5 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -117,7 +117,8 @@ let capabilities_describe_execution_contracts () = "configuration versions" [ "1" ] (strings "configuration_versions"); Alcotest.(check (list string)) - "scenario contracts" [ "6"; "5"; "4"; "3" ] + "scenario contracts" + [ "7"; "6"; "5"; "4"; "3" ] (strings "scenario_contract_versions"); Alcotest.(check (list string)) "required fields" diff --git a/test/test_engine.ml b/test/test_engine.ml index 38d6a8c..4dd258d 100644 --- a/test/test_engine.ml +++ b/test/test_engine.ml @@ -12,5 +12,6 @@ let () = ("contract-conformance", Test_contract_conformance.tests); ("boundary-failures", Test_boundary_failures.tests); ("venue-calendar", Test_venue_calendar.tests); + ("risk-groups", Test_risk_groups.tests); ("scenario", Test_scenario.tests); ] diff --git a/test/test_risk_groups.ml b/test/test_risk_groups.ml new file mode 100644 index 0000000..7829941 --- /dev/null +++ b/test/test_risk_groups.ml @@ -0,0 +1,194 @@ +open Test_support +module T = Trading_engine + +let policy instrument ?(max_order = "100") ?(max_long = "100") + ?(max_short = "100") ?(max_notional = "100000") ?(initial_margin_bps = 5000) + ?(maintenance_margin_bps = 2500) ?(shorting_allowed = true) () = + T.Risk.create_instrument_policy ~instrument + ~max_order_quantity:(quantity max_order) + ~max_long_position:(quantity max_long) + ~max_short_position:(quantity max_short) + ~max_notional_exposure:(Some (money max_notional)) + ~initial_margin_bps ~maintenance_margin_bps ~shorting_allowed + |> ok + +let limits ?gross ?long ?short ?absolute_net ?concentration () = + T.Risk.create_group_limits ~max_gross_exposure:(Option.map money gross) + ~max_long_exposure:(Option.map money long) + ~max_short_exposure:(Option.map money short) + ~max_absolute_net_exposure:(Option.map money absolute_net) + ~max_concentration: + (Option.map + (fun value -> T.Scalar.Ratio.of_decimal_string value |> ok) + concentration) + |> ok + +let group id instruments limits = + T.Risk.create_group + ~group_id:(T.Id.Risk_group.of_string_exn id) + ~group_kind:T.Risk.Issuer + ~instrument_ids:(List.map (fun item -> item.T.Instrument.id) instruments) + ~limits + |> ok + +let setup ?(groups = []) ?(shorting_allowed = true) () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let second = instrument ~id:"second" ~symbol:"SECOND" () in + let policies = + [ + policy first ~shorting_allowed (); policy second ~shorting_allowed:true (); + ] + in + let risk = + T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + ~instrument_policies:policies ~groups ~max_gross_exposure:(money "100000") + ~max_leverage:(T.Scalar.Ratio.of_decimal_string "10" |> ok) + ~short_borrow_bps:0 + |> ok + in + (first, second, risk) + +let exact_coverage_and_short_policy () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let second = instrument ~id:"second" ~symbol:"SECOND" () in + let result = + T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + ~instrument_policies:[ policy first () ] + ~groups:[] ~max_gross_exposure:(money "100000") + ~max_leverage:(T.Scalar.Ratio.of_decimal_string "10" |> ok) + ~short_borrow_bps:0 + in + Alcotest.(check string) + "policy required for every instrument" + "risk must define exactly one policy for every instrument" (error result); + let first, _, risk = setup ~shorting_allowed:false () in + let account = test_account () in + let request = + request ~instrument:first.id ~side:T.Order.Sell ~quantity_value:"1" () + in + let result = + T.Risk.check risk ~account ~oms:T.Oms.empty + ~marks:[ (first.id, price "100"); (instrument_id "second", price "100") ] + ~fx_rates:[ ("USD", price "1") ] + request + in + Alcotest.(check string) + "short prohibition is explicit" + "instrument policy does not allow short positions" (error result) + +let overlapping_groups_are_deterministic () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let second = instrument ~id:"second" ~symbol:"SECOND" () in + let constrained = limits ~gross:"900" () in + let groups = + [ + group "z-group" [ first; second ] constrained; + group "a-group" [ first; second ] constrained; + ] + in + let _, _, risk = setup ~groups () in + let account = test_account () in + let first_request = + request ~instrument:first.id ~quantity_value:"5" + ~kind:(T.Order.Limit (price "100")) + () + in + let oms, _ = oms_with_order first_request in + let second_request = request ~instrument:second.id ~quantity_value:"5" () in + let result = + T.Risk.check risk ~account ~oms + ~marks:[ (first.id, price "100"); (second.id, price "100") ] + ~fx_rates:[ ("USD", price "1") ] + second_request + in + Alcotest.(check string) + "lexically first limiting group" + "position would exceed group a-group maximum gross exposure" (error result) + +let fill_reserves_remainder_and_reports_group () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let second = instrument ~id:"second" ~symbol:"SECOND" () in + let groups = [ group "issuer" [ first; second ] (limits ~gross:"700" ()) ] in + let _, _, risk = setup ~groups () in + let account = test_account () in + let oms, order = + request ~instrument:first.id ~quantity_value:"10" () |> oms_with_order + in + let candidate = fill ~quantity_value:"5" order in + let account = T.Account.apply_fill account candidate |> ok in + let valuation = + account_value ~instruments:[ first; second ] account + ~marks:[ (first.id, price "100"); (second.id, price "100") ] + in + (match + T.Risk.check_reserved_fill risk ~account ~oms + ~marks:[ (first.id, price "100"); (second.id, price "100") ] + ~fx_rates:[ ("USD", price "1") ] + ~order ~filled_quantity:(quantity "5") ~after:valuation + with + | Error (T.Risk.Limit (T.Risk.Group_maximum_gross (id, limit))) -> + Alcotest.(check string) + "limiting group" "issuer" + (T.Id.Risk_group.to_string id); + Alcotest.check money_testable "group threshold" (money "700") limit + | _ -> Alcotest.fail "expected group gross fill limit"); + let snapshot = T.Risk.group_exposures risk valuation |> ok |> List.hd in + Alcotest.check money_testable "actual group gross" (money "500") + snapshot.gross_exposure + +let initialized_positions_use_instrument_margin_and_groups () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let second = instrument ~id:"second" ~symbol:"SECOND" () in + let group_limit = limits ~gross:"150" () in + let risk = + T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + ~instrument_policies: + [ + policy first ~initial_margin_bps:10_000 ~maintenance_margin_bps:5000 + (); + policy second ~initial_margin_bps:2500 ~maintenance_margin_bps:1000 (); + ] + ~groups:[ group "initial-group" [ first; second ] group_limit ] + ~max_gross_exposure:(money "100000") + ~max_leverage:(T.Scalar.Ratio.of_decimal_string "10" |> ok) + ~short_borrow_bps:0 + |> ok + in + let account = test_account () in + let first_order = + request ~instrument:first.id ~quantity_value:"1" () + |> accepted_order ~id:"first-order" + in + let second_order = + request ~instrument:second.id ~quantity_value:"1" () + |> accepted_order ~id:"second-order" + in + let account = + T.Account.apply_fill account (fill ~id:"first-fill" first_order) |> ok + in + let account = + T.Account.apply_fill account (fill ~id:"second-fill" second_order) |> ok + in + let valuation = + account_value ~instruments:[ first; second ] account + ~marks:[ (first.id, price "100"); (second.id, price "100") ] + in + let margin = T.Risk.margin_snapshot risk valuation |> ok in + Alcotest.check money_testable "per-instrument initial margin" (money "125") + margin.initial_requirement; + Alcotest.(check string) + "initialized group exposure enforced" + "initial portfolio exceeds group initial-group maximum gross exposure" + (T.Risk.check_initial risk valuation |> error) + +let tests = + [ + Alcotest.test_case "exact policy coverage and short prohibition" `Quick + exact_coverage_and_short_policy; + Alcotest.test_case "overlapping groups use deterministic IDs" `Quick + overlapping_groups_are_deterministic; + Alcotest.test_case "fill reserves remainder and reports group" `Quick + fill_reserves_remainder_and_reports_group; + Alcotest.test_case "initialized positions use exact risk policies" `Quick + initialized_positions_use_instrument_margin_and_groups; + ] diff --git a/test/test_scenario.ml b/test/test_scenario.ml index 10c88fb..aa77861 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/v6/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v7/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/v6/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v7/fixtures/demo.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -125,9 +125,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v6/scenario.schema.json"; - check_schema "../contracts/v6/scenario-stream.schema.json"; - check_schema "../contracts/v6/journal.schema.json" + check_schema "../contracts/v7/scenario.schema.json"; + check_schema "../contracts/v7/scenario-stream.schema.json"; + check_schema "../contracts/v7/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -189,7 +189,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 6, 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 7, 6, 5, 4, \ + 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -871,7 +872,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v6/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v7/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -899,7 +900,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v6/fixtures/fill-clipped.scenario.json" In_channel.input_all + "../contracts/v7/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in let result = @@ -912,7 +913,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v6/fixtures/fill-clipped.journal.jsonl" In_channel.input_all + "../contracts/v7/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index 990e14f..b206385 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -27,7 +27,7 @@ let initialize_message_is_complete () = T.Strategy_protocol.initialize_message ~sequence:1L (initialization ()) in Alcotest.(check string) - "protocol version" "4" + "protocol version" "5" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -85,8 +85,8 @@ let event_message_contains_complete_context () = account_value account ~marks:[ (instrument_id "test-equity", price "105") ] in let context = - T.Strategy.context ~now:slice.received_at ~valuation ~working_orders:[] - ~latest_bars:slice.bars + T.Strategy.context ~now:slice.received_at ~valuation ~group_exposures:[] + ~working_orders:[] ~latest_bars:slice.bars |> ok in let message = @@ -137,8 +137,8 @@ let nonpositive_equity_omits_weights () = account_value account ~marks:[ (instrument_id "test-equity", price "105") ] in let context = - T.Strategy.context ~now:slice.received_at ~valuation ~working_orders:[] - ~latest_bars:slice.bars + T.Strategy.context ~now:slice.received_at ~valuation ~group_exposures:[] + ~working_orders:[] ~latest_bars:slice.bars |> ok in let message = @@ -165,7 +165,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "4"); + ("strategy_protocol_version", `String "5"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); @@ -223,8 +223,8 @@ let responses_are_strict_and_typed () = let duplicate = `Assoc [ - ("strategy_protocol_version", `String "4"); - ("strategy_protocol_version", `String "4"); + ("strategy_protocol_version", `String "5"); + ("strategy_protocol_version", `String "5"); ("strategy_sequence", `String "3"); ("message_type", `String "stopped"); ("payload", `Assoc []); @@ -251,7 +251,7 @@ let responses_are_strict_and_typed () = let unknown_field = `Assoc [ - ("strategy_protocol_version", `String "4"); + ("strategy_protocol_version", `String "5"); ("strategy_sequence", `String "3"); ("message_type", `String "stopped"); ("payload", `Assoc []); From 65cf039c2a648be5af176a03224dcc70d5b2bd50 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 12:14:00 -0400 Subject: [PATCH 41/57] refactor: consolidate risk policy enforcement --- lib/risk.ml | 260 ++++---------------- test/test_risk_groups.ml | 504 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 544 insertions(+), 220 deletions(-) diff --git a/lib/risk.ml b/lib/risk.ml index 225fbd0..00dca5c 100644 --- a/lib/risk.ml +++ b/lib/risk.ml @@ -153,9 +153,7 @@ let create_group_limits ~max_gross_exposure ~max_long_exposure then Error "group money limits must be positive" else if Option.exists - (fun value -> - Scalar.Ratio.compare value Scalar.Ratio.one > 0 - || Scalar.Ratio.to_micros value <= 0L) + (fun value -> Scalar.Ratio.compare value Scalar.Ratio.one > 0) max_concentration then Error "group concentration must be greater than zero and at most one" else if @@ -607,9 +605,6 @@ let check_initial state valuation = else match group.limits.max_concentration with | None -> Ok () - | Some _ - when Scalar.Money.compare valuation.equity Scalar.Money.zero <= 0 -> - Error "group concentration requires positive equity" | Some limit -> let* threshold = Scalar.Money.multiply_ratio valuation.equity limit @@ -731,8 +726,6 @@ let check_group_fill_limit state ~equity values = let* () = match group.limits.max_concentration with | None -> Ok () - | Some _ when Scalar.Money.compare equity Scalar.Money.zero <= 0 -> - Error (Invalid "group concentration requires positive equity") | Some limit -> let* threshold = Scalar.Money.multiply_ratio equity limit |> invalid @@ -746,100 +739,10 @@ let check_group_fill_limit state ~equity values = in check state.groups -let check_post_fill_for state ~instrument_id ~before_position ~after_position +let check_post_fill_for state ~instrument_id:_ ~before_position ~after_position ~before ~after = - match instrument_policy state instrument_id with - | None -> Error (Invalid "fill has no instrument risk policy") - | Some policy -> - let* before_absolute = - Scalar.Quantity.absolute before_position |> invalid - in - let* after_absolute = - Scalar.Quantity.absolute after_position |> invalid - in - let increasing = - Scalar.Quantity.compare after_absolute before_absolute > 0 - in - let* () = - if not increasing then Ok () - else if - Scalar.Quantity.compare after_position policy.max_long_position > 0 - then - Error - (Limit - (Instrument_maximum_long_position - (instrument_id, policy.max_long_position))) - else - let* minimum_short = - Scalar.Quantity.negate policy.max_short_position |> invalid - in - if Scalar.Quantity.compare after_position minimum_short < 0 then - Error - (Limit - (Instrument_maximum_short_position - (instrument_id, policy.max_short_position))) - else if - (not policy.shorting_allowed) - && Scalar.Quantity.is_negative after_position - then Error (Limit (Instrument_shorting_disabled instrument_id)) - else Ok () - in - let* values = - List.map - (fun (position : Account.position_attribution) -> - let* absolute_value = - Scalar.Money.absolute position.base_market_value |> invalid - in - Ok - { - instrument_id = position.instrument_id; - quantity = position.quantity; - signed_value = position.base_market_value; - absolute_value; - }) - after.Account.positions - |> List.fold_left - (fun result item -> - let* values = result in - let* value = item in - Ok (value :: values)) - (Ok []) - |> Result.map List.rev - in - let* current = - match - List.find_opt - (fun value -> Id.Instrument.equal value.instrument_id instrument_id) - values - with - | Some value -> Ok value - | None -> - Ok - { - instrument_id; - quantity = Scalar.Quantity.zero; - signed_value = Scalar.Money.zero; - absolute_value = Scalar.Money.zero; - } - in - let* () = - match policy.max_notional_exposure with - | Some limit - when increasing - && Scalar.Money.compare current.absolute_value limit > 0 -> - Error (Limit (Instrument_maximum_notional (instrument_id, limit))) - | _ -> Ok () - in - let* gross, _, _, _ = sum_values values |> invalid in - let* () = - if state.versioned then Ok () - else if Scalar.Money.compare gross before.Account.gross_exposure <= 0 - then Ok () - else - check_fill_initial state ~equity:after.Account.equity - ~gross_exposure:gross - in - check_group_fill_limit state ~equity:after.equity values + if state.versioned then Ok () + else check_post_fill state ~before_position ~after_position ~before ~after let check_position state quantity = if Scalar.Quantity.compare quantity state.max_long_position > 0 then @@ -1009,49 +912,6 @@ let fill_projected_quantities state ~account ~oms ~(order : Order.t) (Ok []) |> Result.map List.rev -let projected_gross_exposure state ~account ~oms ~marks ~fx_rates request = - let* quantities = - projected_valuation_quantities state ~account ~oms request - in - let mark_map = - List.fold_left - (fun map (instrument_id, mark) -> - Id.Instrument.Map.add instrument_id mark map) - Id.Instrument.Map.empty marks - in - let module Currency_map = Map.Make (String) in - let fx_map = - List.fold_left - (fun map (currency, rate) -> Currency_map.add currency rate map) - Currency_map.empty fx_rates - in - let* gross_exposure = - List.fold_left - (fun result (instrument_id, quantity) -> - let* gross = result in - let* instrument = - match instrument state instrument_id with - | Some value -> Ok value - | None -> Error "projected position has no configured instrument" - in - let* mark = - match Id.Instrument.Map.find_opt instrument_id mark_map with - | Some value -> Ok value - | None -> Error "projected position has no current market price" - in - let* rate = - match Currency_map.find_opt instrument.quote_currency fx_map with - | Some value -> Ok value - | None -> Error "projected position has no current FX rate" - in - let* value = Scalar.Money.notional mark quantity in - let* value = Scalar.Money.absolute value in - let* value = Scalar.Money.convert value ~rate in - Scalar.Money.add gross value) - (Ok Scalar.Money.zero) quantities - in - Ok gross_exposure - let projected_values state ~marks ~fx_rates quantities = let mark_map = List.fold_left @@ -1198,8 +1058,6 @@ let check_projected_values state ~equity values = (fun () -> match group.limits.max_concentration with | None -> Ok () - | Some _ when Scalar.Money.compare equity Scalar.Money.zero <= 0 -> - Error "group concentration requires positive equity" | Some limit -> let* exceeded = concentration_exceeded limit in if exceeded then @@ -1228,15 +1086,6 @@ let check_reserved_fill state ~account ~oms ~marks ~fx_rates ~(order : Order.t) | Some policy -> Ok policy | None -> Error (Invalid "fill has no instrument risk policy") in - let* current = - match - List.find_opt - (fun value -> Id.Instrument.equal value.instrument_id instrument_id) - values - with - | Some value -> Ok value - | None -> Error (Invalid "fill projection omitted its instrument") - in let* () = List.fold_left (fun result value -> @@ -1278,73 +1127,44 @@ let check_reserved_fill state ~account ~oms ~marks ~fx_rates ~(order : Order.t) | _ -> Ok ()) (Ok ()) values in - if Scalar.Quantity.compare current.quantity policy.max_long_position > 0 - then - Error - (Limit - (Instrument_maximum_long_position - (instrument_id, policy.max_long_position))) - else - let* minimum_short = - Scalar.Quantity.negate policy.max_short_position |> invalid - in - if Scalar.Quantity.compare current.quantity minimum_short < 0 then - Error - (Limit - (Instrument_maximum_short_position - (instrument_id, policy.max_short_position))) - else if - (not policy.shorting_allowed) - && Scalar.Quantity.is_negative current.quantity - then Error (Limit (Instrument_shorting_disabled instrument_id)) + let* gross, _, _, _ = sum_values values |> invalid in + let* () = + if Scalar.Money.compare gross state.max_gross_exposure > 0 then + Error (Limit (Maximum_gross_exposure state.max_gross_exposure)) else - let* () = - match policy.max_notional_exposure with - | Some limit - when Scalar.Money.compare current.absolute_value limit > 0 -> - Error (Limit (Instrument_maximum_notional (instrument_id, limit))) - | _ -> Ok () - in - let* gross, _, _, _ = sum_values values |> invalid in - let* () = - if Scalar.Money.compare gross state.max_gross_exposure > 0 then - Error (Limit (Maximum_gross_exposure state.max_gross_exposure)) - else - let* leveraged_equity = - Scalar.Money.multiply_ratio after.Account.equity - state.max_leverage - |> invalid - in - if Scalar.Money.compare gross leveraged_equity > 0 then - Error (Limit (Maximum_leverage state.max_leverage)) - else Ok () - in - let* initial_requirement = - List.fold_left - (fun result value -> - let* total = result in - let* item_policy = - match instrument_policy state value.instrument_id with - | Some policy -> Ok policy - | None -> Error (Invalid "fill projection has no risk policy") - in - let* requirement = - Scalar.Money.bps_ceil value.absolute_value - ~bps:item_policy.initial_margin_bps - |> invalid - in - Scalar.Money.add total requirement |> invalid) - (Ok Scalar.Money.zero) values - in - let* excess = - Scalar.Money.subtract after.equity initial_requirement |> invalid + let* leveraged_equity = + Scalar.Money.multiply_ratio after.Account.equity state.max_leverage + |> invalid in - if Scalar.Money.compare excess Scalar.Money.zero < 0 then - Error - (Limit - (Instrument_initial_margin - (instrument_id, policy.initial_margin_bps))) - else check_group_fill_limit state ~equity:after.equity values + if Scalar.Money.compare gross leveraged_equity > 0 then + Error (Limit (Maximum_leverage state.max_leverage)) + else Ok () + in + let* initial_requirement = + List.fold_left + (fun result value -> + let* total = result in + let* item_policy = + match instrument_policy state value.instrument_id with + | Some policy -> Ok policy + | None -> Error (Invalid "fill projection has no risk policy") + in + let* requirement = + Scalar.Money.bps_ceil value.absolute_value + ~bps:item_policy.initial_margin_bps + |> invalid + in + Scalar.Money.add total requirement |> invalid) + (Ok Scalar.Money.zero) values + in + let* excess = + Scalar.Money.subtract after.equity initial_requirement |> invalid + in + if Scalar.Money.compare excess Scalar.Money.zero < 0 then + Error + (Limit + (Instrument_initial_margin (instrument_id, policy.initial_margin_bps))) + else check_group_fill_limit state ~equity:after.equity values let check state ~account ~oms ~marks ~fx_rates (request : Order.request) = match instrument state request.instrument_id with diff --git a/test/test_risk_groups.ml b/test/test_risk_groups.ml index 7829941..1269510 100644 --- a/test/test_risk_groups.ml +++ b/test/test_risk_groups.ml @@ -181,6 +181,492 @@ let initialized_positions_use_instrument_margin_and_groups () = "initial portfolio exceeds group initial-group maximum gross exposure" (T.Risk.check_initial risk valuation |> error) +let reserved_result ?(side = T.Order.Buy) ?(initial_cash = "10000") + ?(max_long = "100") ?(max_short = "100") ?(max_notional = "100000") + ?(shorting_allowed = true) ?(initial_margin_bps = 5000) + ?(max_gross = "100000") ?(max_leverage = "10") ?group_limits + ?(include_mark = true) ?(include_fx = true) () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let second = instrument ~id:"second" ~symbol:"SECOND" () in + let groups = + Option.to_list + (Option.map + (fun value -> group "group-a" [ first; second ] value) + group_limits) + in + let risk = + T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + ~instrument_policies: + [ + policy first ~max_long ~max_short ~max_notional ~shorting_allowed + ~initial_margin_bps + ~maintenance_margin_bps:(min initial_margin_bps 2500) + (); + policy second (); + ] + ~groups ~max_gross_exposure:(money max_gross) + ~max_leverage:(T.Scalar.Ratio.of_decimal_string max_leverage |> ok) + ~short_borrow_bps:0 + |> ok + in + let account = test_account ~initial_cash:[ ("USD", money initial_cash) ] () in + let oms, order = + request ~instrument:first.id ~side ~quantity_value:"10" () |> oms_with_order + in + let candidate = fill ~quantity_value:"1" order in + let account = T.Account.apply_fill account candidate |> ok in + let valuation = + account_value ~instruments:[ first; second ] account + ~marks:[ (first.id, price "100"); (second.id, price "100") ] + in + T.Risk.check_reserved_fill risk ~account ~oms + ~marks: + (if include_mark then + [ (first.id, price "100"); (second.id, price "100") ] + else []) + ~fx_rates:(if include_fx then [ ("USD", price "1") ] else []) + ~order ~filled_quantity:(quantity "1") ~after:valuation + +let clipping_taxonomy_is_exact () = + let is_expected expected = function + | Error (T.Risk.Limit actual) when expected actual -> () + | _ -> Alcotest.fail "unexpected reserved fill result" + in + reserved_result ~max_long:"7" () + |> is_expected (function + | T.Risk.Instrument_maximum_long_position _ -> true + | _ -> false); + reserved_result ~side:T.Order.Sell ~max_short:"7" () + |> is_expected (function + | T.Risk.Instrument_maximum_short_position _ -> true + | _ -> false); + reserved_result ~max_notional:"700" () + |> is_expected (function + | T.Risk.Instrument_maximum_notional _ -> true + | _ -> false); + reserved_result ~side:T.Order.Sell ~shorting_allowed:false () + |> is_expected (function + | T.Risk.Instrument_shorting_disabled _ -> true + | _ -> false); + reserved_result ~max_gross:"700" () + |> is_expected (function + | T.Risk.Maximum_gross_exposure _ -> true + | _ -> false); + reserved_result ~max_leverage:"0.05" () + |> is_expected (function T.Risk.Maximum_leverage _ -> true | _ -> false); + reserved_result ~initial_cash:"500" ~initial_margin_bps:10_000 () + |> is_expected (function + | T.Risk.Instrument_initial_margin _ -> true + | _ -> false); + reserved_result ~group_limits:(limits ~long:"700" ()) () + |> is_expected (function T.Risk.Group_maximum_long _ -> true | _ -> false); + reserved_result ~side:T.Order.Sell ~group_limits:(limits ~short:"700" ()) () + |> is_expected (function T.Risk.Group_maximum_short _ -> true | _ -> false); + reserved_result ~group_limits:(limits ~absolute_net:"700" ()) () + |> is_expected (function + | T.Risk.Group_maximum_absolute_net _ -> true + | _ -> false); + reserved_result ~group_limits:(limits ~concentration:"0.05" ()) () + |> is_expected (function + | T.Risk.Group_maximum_concentration _ -> true + | _ -> false) + +let constructors_reject_ambiguous_policies () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let bad_policy ?(max_order = "10") ?(max_long = "10") ?(max_short = "10") + ?(notional = Some (money "100")) ?(initial = 5000) ?(maintenance = 2500) + () = + T.Risk.create_instrument_policy ~instrument:first + ~max_order_quantity:(quantity max_order) + ~max_long_position:(quantity max_long) + ~max_short_position:(quantity max_short) ~max_notional_exposure:notional + ~initial_margin_bps:initial ~maintenance_margin_bps:maintenance + ~shorting_allowed:true + in + List.iter + (fun result -> + Alcotest.(check bool) "invalid policy" true (Result.is_error result)) + [ + bad_policy ~max_order:"0" (); + bad_policy ~max_long:"0" (); + bad_policy ~max_short:"0" (); + bad_policy ~notional:(Some (money "0")) (); + bad_policy ~initial:0 (); + bad_policy ~initial:10_001 (); + bad_policy ~maintenance:0 (); + bad_policy ~maintenance:10_001 (); + bad_policy ~initial:1000 ~maintenance:2000 (); + ]; + List.iter + (fun result -> + Alcotest.(check bool) "invalid group limits" true (Result.is_error result)) + [ + T.Risk.create_group_limits ~max_gross_exposure:None + ~max_long_exposure:None ~max_short_exposure:None + ~max_absolute_net_exposure:None ~max_concentration:None; + T.Risk.create_group_limits + ~max_gross_exposure:(Some (money "0")) + ~max_long_exposure:None ~max_short_exposure:None + ~max_absolute_net_exposure:None ~max_concentration:None; + T.Risk.create_group_limits ~max_gross_exposure:None + ~max_long_exposure:None ~max_short_exposure:None + ~max_absolute_net_exposure:None + ~max_concentration:(Some (T.Scalar.Ratio.of_decimal_string "2" |> ok)); + ]; + let valid_limits = limits ~gross:"100" () in + Alcotest.(check bool) + "empty group rejected" true + (Result.is_error + (T.Risk.create_group + ~group_id:(T.Id.Risk_group.of_string_exn "group") + ~group_kind:T.Risk.Custom ~instrument_ids:[] ~limits:valid_limits)); + Alcotest.(check bool) + "duplicate membership rejected" true + (Result.is_error + (T.Risk.create_group + ~group_id:(T.Id.Risk_group.of_string_exn "group") + ~group_kind:T.Risk.Custom ~instrument_ids:[ first.id; first.id ] + ~limits:valid_limits)) + +let create_v7_rejects_inconsistent_configuration () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let second = instrument ~id:"second" ~symbol:"SECOND" () in + let first_policy = policy first () in + let second_policy = policy second () in + let unknown = instrument ~id:"unknown" ~symbol:"UNKNOWN" () in + let unknown_policy = policy unknown () in + let valid_group = group "group" [ first ] (limits ~gross:"100" ()) in + let unknown_group = + group "unknown-group" [ unknown ] (limits ~gross:"100" ()) + in + let create ?(base_currency = "USD") ?(instruments = [ first; second ]) + ?(policies = [ first_policy; second_policy ]) ?(groups = [ valid_group ]) + ?(max_gross = "100000") ?(short_borrow_bps = 0) () = + T.Risk.create_v7 ~base_currency ~instruments ~instrument_policies:policies + ~groups ~max_gross_exposure:(money max_gross) + ~max_leverage:(T.Scalar.Ratio.of_decimal_string "10" |> ok) + ~short_borrow_bps + in + List.iter + (fun result -> + Alcotest.(check bool) + "invalid v7 configuration" true (Result.is_error result)) + [ + create ~base_currency:"" (); + create ~instruments:[] ~policies:[] ~groups:[] (); + create ~max_gross:"0" (); + create ~short_borrow_bps:(-1) (); + create ~short_borrow_bps:10_001 (); + create ~instruments:[ first; first ] (); + create ~policies:[ first_policy; unknown_policy ] (); + create ~policies:[ first_policy; first_policy ] (); + create ~policies:[ first_policy ] (); + create ~groups:[ valid_group; valid_group ] (); + create ~groups:[ unknown_group ] (); + ] + +let admission_result ?(side = T.Order.Buy) ?(initial_cash = "10000") + ?(max_order = "100") ?(max_long = "100") ?(max_short = "100") + ?(max_notional = "100000") ?(shorting_allowed = true) + ?(initial_margin_bps = 5000) ?(max_gross = "100000") ?(max_leverage = "10") + ?group_limits () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let second = instrument ~id:"second" ~symbol:"SECOND" () in + let groups = + Option.to_list + (Option.map + (fun value -> group "group-a" [ first; second ] value) + group_limits) + in + let risk = + T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + ~instrument_policies: + [ + policy first ~max_order ~max_long ~max_short ~max_notional + ~shorting_allowed ~initial_margin_bps + ~maintenance_margin_bps:(min initial_margin_bps 2500) + (); + policy second (); + ] + ~groups ~max_gross_exposure:(money max_gross) + ~max_leverage:(T.Scalar.Ratio.of_decimal_string max_leverage |> ok) + ~short_borrow_bps:0 + |> ok + in + T.Risk.check risk + ~account:(test_account ~initial_cash:[ ("USD", money initial_cash) ] ()) + ~oms:T.Oms.empty + ~marks:[ (first.id, price "100"); (second.id, price "100") ] + ~fx_rates:[ ("USD", price "1") ] + (request ~instrument:first.id ~side ~quantity_value:"10" ()) + +let admission_enforces_every_v7_limit () = + let check_error expected result = + Alcotest.(check string) "admission error" expected (error result) + in + admission_result ~max_order:"7" () + |> check_error "order exceeds the instrument maximum order quantity"; + admission_result ~max_long:"7" () + |> check_error "position would exceed the instrument maximum long position"; + admission_result ~side:T.Order.Sell ~max_short:"7" () + |> check_error "position would exceed the instrument maximum short position"; + admission_result ~side:T.Order.Sell ~shorting_allowed:false () + |> check_error "instrument policy does not allow short positions"; + admission_result ~max_notional:"700" () + |> check_error + "position would exceed the instrument maximum notional exposure"; + admission_result ~max_gross:"700" () + |> check_error "portfolio would exceed maximum gross exposure"; + admission_result ~max_leverage:"0.05" () + |> check_error "portfolio would exceed maximum leverage"; + admission_result ~initial_cash:"500" ~initial_margin_bps:10_000 () + |> check_error + "portfolio would violate instrument initial margin requirements"; + admission_result ~group_limits:(limits ~long:"700" ()) () + |> check_error "position would exceed group group-a maximum long exposure"; + admission_result ~side:T.Order.Sell ~group_limits:(limits ~short:"700" ()) () + |> check_error "position would exceed group group-a maximum short exposure"; + admission_result ~group_limits:(limits ~absolute_net:"700" ()) () + |> check_error + "position would exceed group group-a maximum absolute net exposure"; + admission_result ~group_limits:(limits ~concentration:"0.05" ()) () + |> check_error "position would exceed group group-a maximum concentration" + +let group_exposures_include_short_and_zero_equity () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let second = instrument ~id:"second" ~symbol:"SECOND" () in + let risk = + T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + ~instrument_policies:[ policy first (); policy second () ] + ~groups:[ group "group" [ first ] (limits ~gross:"100000" ()) ] + ~max_gross_exposure:(money "100000") + ~max_leverage:(T.Scalar.Ratio.of_decimal_string "10" |> ok) + ~short_borrow_bps:0 + |> ok + in + let order = + request ~instrument:first.id ~side:T.Order.Sell ~quantity_value:"1" () + |> accepted_order + in + let account = + test_account ~initial_cash:[ ("USD", money "0") ] () |> fun account -> + T.Account.apply_fill account (fill order) |> ok + in + let valuation = + account_value ~instruments:[ first; second ] account + ~marks:[ (first.id, price "100"); (second.id, price "100") ] + in + let exposure = T.Risk.group_exposures risk valuation |> ok |> List.hd in + Alcotest.check money_testable "short exposure" (money "100") + exposure.short_exposure; + Alcotest.(check bool) + "zero-equity concentration omitted" true + (Option.is_none exposure.concentration) + +let initial_result ?(side = T.Order.Buy) ?(initial_cash = "10000") + ?(max_long = "100") ?(max_short = "100") ?(max_notional = "100000") + ?(shorting_allowed = true) ?(initial_margin_bps = 5000) + ?(max_gross = "100000") ?(max_leverage = "10") ?group_limits () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let second = instrument ~id:"second" ~symbol:"SECOND" () in + let groups = + Option.to_list + (Option.map + (fun value -> group "group-a" [ first; second ] value) + group_limits) + in + let risk = + T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + ~instrument_policies: + [ + policy first ~max_long ~max_short ~max_notional ~shorting_allowed + ~initial_margin_bps + ~maintenance_margin_bps:(min initial_margin_bps 2500) + (); + policy second (); + ] + ~groups ~max_gross_exposure:(money max_gross) + ~max_leverage:(T.Scalar.Ratio.of_decimal_string max_leverage |> ok) + ~short_borrow_bps:0 + |> ok + in + let order = + request ~instrument:first.id ~side ~quantity_value:"10" () |> accepted_order + in + let account = + test_account ~initial_cash:[ ("USD", money initial_cash) ] () + |> fun account -> + T.Account.apply_fill account (fill ~quantity_value:"10" order) |> ok + in + let valuation = + account_value ~instruments:[ first; second ] account + ~marks:[ (first.id, price "100"); (second.id, price "100") ] + in + T.Risk.check_initial risk valuation + +let initial_portfolio_enforces_every_v7_limit () = + let check_error expected result = + Alcotest.(check string) "initial portfolio error" expected (error result) + in + initial_result ~max_gross:"700" () + |> check_error "portfolio would exceed maximum gross exposure"; + initial_result ~max_leverage:"0.05" () + |> check_error "portfolio would exceed maximum leverage"; + initial_result ~max_long:"7" () + |> check_error "initial position exceeds its maximum long position"; + initial_result ~side:T.Order.Sell ~max_short:"7" () + |> check_error "initial position exceeds its maximum short position"; + initial_result ~side:T.Order.Sell ~shorting_allowed:false () + |> check_error "initial position violates its shorting policy"; + initial_result ~max_notional:"700" () + |> check_error + "initial position exceeds the instrument maximum notional exposure"; + initial_result ~initial_cash:"500" ~initial_margin_bps:10_000 () + |> check_error + "portfolio would violate instrument initial margin requirements"; + initial_result ~group_limits:(limits ~long:"700" ()) () + |> check_error "initial portfolio exceeds group group-a maximum long exposure"; + initial_result ~side:T.Order.Sell ~group_limits:(limits ~short:"700" ()) () + |> check_error + "initial portfolio exceeds group group-a maximum short exposure"; + initial_result ~group_limits:(limits ~absolute_net:"700" ()) () + |> check_error + "initial portfolio exceeds group group-a maximum absolute net exposure"; + initial_result ~group_limits:(limits ~concentration:"0.05" ()) () + |> check_error "initial portfolio exceeds group group-a maximum concentration" + +let legacy_and_policy_boundaries_are_rejected () = + let first = instrument ~id:"first" ~symbol:"FIRST" () in + let large_lot = instrument ~id:"large" ~symbol:"LARGE" ~lot_size:"2" () in + let ratio = T.Scalar.Ratio.of_decimal_string "10" |> ok in + let create ?(base_currency = "USD") ?(instruments = [ first ]) + ?(max_order = "10") ?(max_long = "10") ?(max_short = "10") + ?(max_gross = "1000") ?(initial = 5000) ?(maintenance = 2500) + ?(borrow = 0) () = + T.Risk.create ~base_currency ~instruments + ~max_order_quantity:(quantity max_order) + ~max_long_position:(quantity max_long) + ~max_short_position:(quantity max_short) + ~max_gross_exposure:(money max_gross) ~max_leverage:ratio + ~initial_margin_bps:initial ~maintenance_margin_bps:maintenance + ~short_borrow_bps:borrow + in + List.iter + (fun result -> + Alcotest.(check bool) "invalid legacy risk" true (Result.is_error result)) + [ + create ~base_currency:"" (); + create ~max_order:"0" (); + create ~max_long:"0" (); + create ~max_short:"0" (); + create ~max_gross:"0" (); + create ~initial:0 (); + create ~maintenance:0 (); + create ~initial:10_001 (); + create ~maintenance:10_001 (); + create ~initial:1000 ~maintenance:2000 (); + create ~borrow:(-1) (); + create ~borrow:10_001 (); + create ~instruments:[] (); + create ~instruments:[ large_lot ] ~max_order:"1" (); + create ~instruments:[ large_lot ] ~max_long:"1" (); + create ~instruments:[ large_lot ] ~max_short:"1" (); + create ~instruments:[ first; first ] (); + ]; + let invalid_policy value selector = + T.Risk.create_instrument_policy ~instrument:large_lot + ~max_order_quantity:(quantity (selector "order" value)) + ~max_long_position:(quantity (selector "long" value)) + ~max_short_position:(quantity (selector "short" value)) + ~max_notional_exposure:None ~initial_margin_bps:5000 + ~maintenance_margin_bps:2500 ~shorting_allowed:true + in + let selected field value target = + if String.equal field target then value else "10" + in + List.iter + (fun result -> + Alcotest.(check bool) "policy below lot" true (Result.is_error result)) + [ + invalid_policy "1" (fun field value -> selected field value "order"); + invalid_policy "1" (fun field value -> selected field value "long"); + invalid_policy "1" (fun field value -> selected field value "short"); + ] + +let public_checks_cover_success_and_diagnostics () = + let legacy = risk ~max_order:"7" ~max_long:"7" ~max_short:"7" () in + Alcotest.(check bool) + "position accepted" true + (Result.is_ok (T.Risk.check_position legacy (quantity "7"))); + Alcotest.(check string) + "long rejected" "position would exceed the maximum long position" + (T.Risk.check_position legacy (quantity "8") |> error); + Alcotest.(check string) + "short rejected" "position would exceed the maximum short position" + (T.Risk.check_position legacy (quantity "-8") |> error); + Alcotest.(check string) + "unknown policy" "position refers to an unknown instrument risk policy" + (T.Risk.check_position_for legacy (instrument_id "unknown") (quantity "1") + |> error); + let unknown_request = request ~instrument:(instrument_id "unknown") () in + Alcotest.(check string) + "unknown instrument" "order refers to an unknown instrument" + (T.Risk.check legacy ~account:(test_account ()) ~oms:T.Oms.empty ~marks:[] + ~fx_rates:[] unknown_request + |> error); + Alcotest.(check string) + "legacy order limit" "order exceeds the maximum order quantity" + (risk_check legacy ~account:(test_account ()) ~oms:T.Oms.empty + (request ~quantity_value:"8" ()) + |> error); + (match reserved_result ~include_mark:false () with + | Error (T.Risk.Invalid message) -> + Alcotest.(check string) + "missing mark" "projected position has no current market price" message + | _ -> Alcotest.fail "expected missing mark diagnostic"); + (match reserved_result ~include_fx:false () with + | Error (T.Risk.Invalid message) -> + Alcotest.(check string) + "missing FX" "projected position has no current FX rate" message + | _ -> Alcotest.fail "expected missing FX diagnostic"); + Alcotest.(check bool) + "unconstrained group fill accepted" true + (Result.is_ok (reserved_result ~group_limits:(limits ~gross:"10000" ()) ())); + Alcotest.(check bool) + "concentration-compliant fill accepted" true + (Result.is_ok + (reserved_result ~group_limits:(limits ~concentration:"1" ()) ())); + Alcotest.(check bool) + "unconstrained group admission accepted" true + (Result.is_ok + (admission_result ~group_limits:(limits ~gross:"10000" ()) ())) + +let legacy_post_fill_covers_gross_and_reduction () = + let first = instrument () in + let before_account = test_account () in + let before = + account_value before_account ~marks:[ (first.id, price "100") ] + in + let order = + request ~instrument:first.id ~quantity_value:"10" () |> accepted_order + in + let after_account = + T.Account.apply_fill before_account (fill ~quantity_value:"10" order) |> ok + in + let after = account_value after_account ~marks:[ (first.id, price "100") ] in + let limited = risk ~max_gross:"700" () in + (match + T.Risk.check_post_fill limited ~before_position:(quantity "0") + ~after_position:(quantity "10") ~before ~after + with + | Error (T.Risk.Limit (T.Risk.Maximum_gross_exposure _)) -> () + | _ -> Alcotest.fail "expected legacy gross fill limit"); + Alcotest.(check bool) + "gross-reducing fill accepted" true + (Result.is_ok + (T.Risk.check_post_fill limited ~before_position:(quantity "10") + ~after_position:(quantity "9") ~before:after ~after:before)) + let tests = [ Alcotest.test_case "exact policy coverage and short prohibition" `Quick @@ -191,4 +677,22 @@ let tests = fill_reserves_remainder_and_reports_group; Alcotest.test_case "initialized positions use exact risk policies" `Quick initialized_positions_use_instrument_margin_and_groups; + Alcotest.test_case "clipping taxonomy is exact" `Quick + clipping_taxonomy_is_exact; + Alcotest.test_case "constructors reject ambiguous policies" `Quick + constructors_reject_ambiguous_policies; + Alcotest.test_case "v7 rejects inconsistent configuration" `Quick + create_v7_rejects_inconsistent_configuration; + Alcotest.test_case "admission enforces every v7 limit" `Quick + admission_enforces_every_v7_limit; + Alcotest.test_case "group exposures include short and zero equity" `Quick + group_exposures_include_short_and_zero_equity; + Alcotest.test_case "initial portfolio enforces every v7 limit" `Quick + initial_portfolio_enforces_every_v7_limit; + Alcotest.test_case "legacy and policy boundaries are rejected" `Quick + legacy_and_policy_boundaries_are_rejected; + Alcotest.test_case "public checks cover success and diagnostics" `Quick + public_checks_cover_success_and_diagnostics; + Alcotest.test_case "legacy post-fill covers gross and reduction" `Quick + legacy_post_fill_covers_gross_and_reduction; ] From 8e257dacdebbccd9b86d7a29f710488f2d08ac5a Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 12:55:26 -0400 Subject: [PATCH 42/57] feat: add order lifetimes and stop orders --- CHANGELOG.md | 5 + README.md | 22 +- bench/benchmark_replay.py | 26 +- bench/latency_strategy.py | 2 +- contracts/conformance/cases.json | 121 +++- contracts/conformance/manifest.json | 53 ++ contracts/strategy/v6/README.md | 54 ++ contracts/strategy/v6/dune | 15 + .../v6/fixtures/external.scenario.json | 204 ++++++ .../v6/fixtures/external.scenario.jsonl | 4 + .../v6/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v6/message.schema.json | 298 ++++++++ contracts/strategy/v6/transcript.schema.json | 82 +++ contracts/v8/README.md | 48 ++ contracts/v8/dune | 16 + contracts/v8/fixtures/demo.journal.jsonl | 22 + contracts/v8/fixtures/demo.scenario.json | 302 ++++++++ contracts/v8/fixtures/demo.scenario.jsonl | 6 + .../v8/fixtures/fill-clipped.journal.jsonl | 11 + .../v8/fixtures/fill-clipped.scenario.json | 177 +++++ contracts/v8/journal.schema.json | 238 +++++++ contracts/v8/scenario-stream.schema.json | 76 ++ contracts/v8/scenario.schema.json | 442 ++++++++++++ docs/api-reference.md | 2 +- docs/persistra.md | 2 +- docs/scenario.md | 10 +- lib/audit.ml | 10 + lib/audit.mli | 5 + lib/codec.ml | 78 +- lib/codec.mli | 1 + lib/contract.ml | 18 +- lib/engine.ml | 157 ++++- lib/engine.mli | 9 + lib/execution.ml | 174 +++-- lib/execution.mli | 8 +- lib/execution_model.ml | 4 +- lib/external_replay.ml | 13 +- lib/oms.ml | 12 + lib/oms.mli | 8 + lib/order.ml | 128 +++- lib/order.mli | 49 +- lib/replay.ml | 11 +- lib/risk.ml | 10 +- lib/scenario.ml | 109 ++- lib/scenario.mli | 6 +- lib/scenario_shape.ml | 14 +- lib/scenario_validation.ml | 20 +- lib/strategy_protocol.ml | 84 ++- lib/strategy_protocol.mli | 1 + mkdocs.yml | 4 +- scripts/check-deterministic-journals | 8 + scripts/check-documentation.py | 4 +- scripts/release_artifacts.py | 12 +- test/cli.t | 38 +- test/dune | 85 ++- test/fake_strategy.py | 5 + test/test_boundary_failures.ml | 1 + test/test_checkpoint4.ml | 3 +- test/test_diagnostic.ml | 5 +- test/test_engine.ml | 1 + test/test_execution.ml | 9 +- test/test_order_lifetimes.ml | 665 ++++++++++++++++++ test/test_scenario.ml | 20 +- test/test_strategy_protocol.ml | 52 +- test/test_support.ml | 17 + 65 files changed, 3880 insertions(+), 230 deletions(-) create mode 100644 contracts/strategy/v6/README.md create mode 100644 contracts/strategy/v6/dune create mode 100644 contracts/strategy/v6/fixtures/external.scenario.json create mode 100644 contracts/strategy/v6/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v6/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v6/message.schema.json create mode 100644 contracts/strategy/v6/transcript.schema.json create mode 100644 contracts/v8/README.md create mode 100644 contracts/v8/dune create mode 100644 contracts/v8/fixtures/demo.journal.jsonl create mode 100644 contracts/v8/fixtures/demo.scenario.json create mode 100644 contracts/v8/fixtures/demo.scenario.jsonl create mode 100644 contracts/v8/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v8/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v8/journal.schema.json create mode 100644 contracts/v8/scenario-stream.schema.json create mode 100644 contracts/v8/scenario.schema.json create mode 100644 test/test_order_lifetimes.ml diff --git a/CHANGELOG.md b/CHANGELOG.md index a21bbe8..1ddbc62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Add explicit GTC, IOC, FOK, DAY, and GTD order lifetimes plus completed-bar stop and stop-limit + activation in scenario contract v8 and external strategy protocol v6. + +## Unreleased + - Add contract v7 exact per-instrument risk policies, versioned overlapping exposure groups, reservation-aware admission and fill clipping, group diagnostics, and strategy protocol v5. diff --git a/README.md b/README.md index 52f98d9..ba7ae3f 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v7/fixtures/demo.scenario.json \ + --input contracts/v8/fixtures/demo.scenario.json \ --validate-only ``` @@ -94,7 +94,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v7/fixtures/demo.scenario.json \ + --input contracts/v8/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -102,7 +102,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v7/fixtures/demo.scenario.jsonl \ + --input contracts/v8/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -111,7 +111,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v5/fixtures/external.scenario.json \ + --input contracts/strategy/v6/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -218,19 +218,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 v7 and conformance fixtures](contracts/v7/README.md) +- [Current contract v8 and conformance fixtures](contracts/v8/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v7/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v7/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v7/journal.schema.json) -- [External strategy protocol v5](contracts/strategy/v5/README.md) +- [Scenario JSON Schema](contracts/v8/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v8/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v8/journal.schema.json) +- [External strategy protocol v6](contracts/strategy/v6/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/v5/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v5/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v6/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v6/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/bench/benchmark_replay.py b/bench/benchmark_replay.py index ab1389c..58b62b7 100644 --- a/bench/benchmark_replay.py +++ b/bench/benchmark_replay.py @@ -23,7 +23,7 @@ ROOT = Path(__file__).resolve().parents[1] DEFAULT_EXECUTABLE = ROOT / "_build/default/bin/main.exe" DEFAULT_BASELINE = ROOT / "bench/baselines/linux-x86_64.json" -FIXTURE = ROOT / "contracts/v6/fixtures/demo.scenario.json" +FIXTURE = ROOT / "contracts/v8/fixtures/demo.scenario.json" STRATEGY = ROOT / "bench/latency_strategy.py" SUMMARY_PATTERN = re.compile( r"\baudits=(?P[0-9]+).*\bactive=(?P[0-9]+)" @@ -149,7 +149,12 @@ def build_scenario(case: BenchmarkCase) -> dict[str, object]: "side": "buy", "quantity": "1", "order_kind": "limit", + "trigger_price": None, "limit_price": "1", + "time_in_force": "gtc", + "venue_id": None, + "calendar_id": None, + "expires_at": None, } for _ in range(case.active_order_count) ], @@ -194,14 +199,23 @@ def build_scenario(case: BenchmarkCase) -> dict[str, object]: } ], "risk": { - "max_order_quantity": "1000000", - "max_long_position": "1000000", - "max_short_position": "1000000", "max_gross_exposure": "1000000000", "max_leverage": "1000000", - "initial_margin_bps": 1, - "maintenance_margin_bps": 1, "short_borrow_bps": 0, + "instrument_policies": [ + { + "instrument_id": instrument["instrument_id"], + "max_order_quantity": "1000000", + "max_long_position": "1000000", + "max_short_position": "1000000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 1, + "maintenance_margin_bps": 1, + "shorting_allowed": True, + } + for instrument in instruments + ], + "groups": [], }, "execution": { "model": "completed_bar_v1", diff --git a/bench/latency_strategy.py b/bench/latency_strategy.py index ffdf458..20396e2 100644 --- a/bench/latency_strategy.py +++ b/bench/latency_strategy.py @@ -36,7 +36,7 @@ print( json.dumps( { - "strategy_protocol_version": "4", + "strategy_protocol_version": request["strategy_protocol_version"], "strategy_sequence": request["strategy_sequence"], "message_type": response_type, "payload": payload, diff --git a/contracts/conformance/cases.json b/contracts/conformance/cases.json index 8d28ebd..a9820a2 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -252,9 +252,9 @@ }, { "name": "strategy-ready-valid", - "artifact": "strategy-message-v5", + "artifact": "strategy-message-v6", "kind": "strategy_response", - "source": "strategy/v5/fixtures/external.strategy.jsonl", + "source": "strategy/v6/fixtures/external.strategy.jsonl", "record": 2, "extract": [ "message" @@ -267,9 +267,9 @@ }, { "name": "strategy-intents-valid", - "artifact": "strategy-message-v5", + "artifact": "strategy-message-v6", "kind": "strategy_response", - "source": "strategy/v5/fixtures/external.strategy.jsonl", + "source": "strategy/v6/fixtures/external.strategy.jsonl", "record": 4, "extract": [ "message" @@ -282,9 +282,9 @@ }, { "name": "strategy-stopped-valid", - "artifact": "strategy-message-v5", + "artifact": "strategy-message-v6", "kind": "strategy_response", - "source": "strategy/v5/fixtures/external.strategy.jsonl", + "source": "strategy/v6/fixtures/external.strategy.jsonl", "record": 14, "extract": [ "message" @@ -297,9 +297,9 @@ }, { "name": "strategy-error-valid", - "artifact": "strategy-message-v5", + "artifact": "strategy-message-v6", "kind": "strategy_response", - "source": "strategy/v5/fixtures/external.strategy.jsonl", + "source": "strategy/v6/fixtures/external.strategy.jsonl", "record": 14, "extract": [ "message" @@ -329,9 +329,9 @@ }, { "name": "strategy-missing-version", - "artifact": "strategy-message-v5", + "artifact": "strategy-message-v6", "kind": "strategy_response", - "source": "strategy/v5/fixtures/external.strategy.jsonl", + "source": "strategy/v6/fixtures/external.strategy.jsonl", "record": 2, "extract": [ "message" @@ -351,9 +351,9 @@ }, { "name": "strategy-unknown-field", - "artifact": "strategy-message-v5", + "artifact": "strategy-message-v6", "kind": "strategy_response", - "source": "strategy/v5/fixtures/external.strategy.jsonl", + "source": "strategy/v6/fixtures/external.strategy.jsonl", "record": 2, "extract": [ "message" @@ -374,9 +374,9 @@ }, { "name": "strategy-wrong-sequence", - "artifact": "strategy-message-v5", + "artifact": "strategy-message-v6", "kind": "strategy_response", - "source": "strategy/v5/fixtures/external.strategy.jsonl", + "source": "strategy/v6/fixtures/external.strategy.jsonl", "record": 2, "extract": [ "message" @@ -387,6 +387,69 @@ "runtime_expectation": "reject", "rule": "semantic" }, + { + "name": "strategy-ready-valid-v5", + "artifact": "strategy-message-v5", + "kind": "strategy_response", + "source": "strategy/v5/fixtures/external.strategy.jsonl", + "record": 2, + "extract": ["message"], + "expected_sequence": "1", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural", + "protocol_version": "5" + }, + { + "name": "strategy-intents-valid-v5", + "artifact": "strategy-message-v5", + "kind": "strategy_response", + "source": "strategy/v5/fixtures/external.strategy.jsonl", + "record": 4, + "extract": ["message"], + "expected_sequence": "2", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural", + "protocol_version": "5" + }, + { + "name": "strategy-stopped-valid-v5", + "artifact": "strategy-message-v5", + "kind": "strategy_response", + "source": "strategy/v5/fixtures/external.strategy.jsonl", + "record": 14, + "extract": ["message"], + "expected_sequence": "7", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural", + "protocol_version": "5" + }, + { + "name": "strategy-error-valid-v5", + "artifact": "strategy-message-v5", + "kind": "strategy_response", + "source": "strategy/v5/fixtures/external.strategy.jsonl", + "record": 14, + "extract": ["message"], + "expected_sequence": "7", + "mutations": [ + { "op": "replace", "path": ["message_type"], "value": "error" }, + { + "op": "replace", + "path": ["payload"], + "value": { "message": "intentional conformance error" } + } + ], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural", + "protocol_version": "5" + }, { "name": "strategy-ready-valid-v4", "artifact": "strategy-message-v4", @@ -704,6 +767,36 @@ "mutations": [], "schema_expectation": "accept", "source": "strategy/v5/fixtures/external.strategy.jsonl" + }, + { + "name": "strategy-v6-rejected-response-branch", + "artifact": "strategy-transcript-v6", + "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", + "source": "strategy/v6/fixtures/external.strategy.jsonl" } ] } diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index 57b1e0a..0d126e8 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -471,6 +471,59 @@ "format": "jsonl" } ] + }, + { + "name": "scenario-v8", + "schema": "v8/scenario.schema.json", + "version_field": "contract_version", + "version": "8", + "sources": [ + { "path": "v8/fixtures/demo.scenario.json", "format": "json" }, + { "path": "v8/fixtures/fill-clipped.scenario.json", "format": "json" }, + { "path": "strategy/v6/fixtures/external.scenario.json", "format": "json" } + ] + }, + { + "name": "scenario-stream-v8", + "schema": "v8/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "8", + "sources": [ + { "path": "v8/fixtures/demo.scenario.jsonl", "format": "jsonl" }, + { "path": "strategy/v6/fixtures/external.scenario.jsonl", "format": "jsonl" } + ] + }, + { + "name": "journal-v8", + "schema": "v8/journal.schema.json", + "version_field": "contract_version", + "version": "8", + "sources": [ + { "path": "v8/fixtures/demo.journal.jsonl", "format": "jsonl" }, + { "path": "v8/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" } + ] + }, + { + "name": "strategy-message-v6", + "schema": "strategy/v6/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "6", + "sources": [ + { + "path": "strategy/v6/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": ["message"] + } + ] + }, + { + "name": "strategy-transcript-v6", + "schema": "strategy/v6/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "6", + "sources": [ + { "path": "strategy/v6/fixtures/external.strategy.jsonl", "format": "jsonl" } + ] } ] } diff --git a/contracts/strategy/v6/README.md b/contracts/strategy/v6/README.md new file mode 100644 index 0000000..c0cd6b1 --- /dev/null +++ b/contracts/strategy/v6/README.md @@ -0,0 +1,54 @@ +# External strategy protocol v6 + +Version 6 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`. +Protocols v4 and v3 remain available for legacy scenario contracts and retain their frozen shapes. + +Every message repeats `strategy_protocol_version: "6"` 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 v8 and includes the exact `initial_portfolio` +snapshot alongside the legacy cash projection. It also carries the complete versioned venue +calendars and nested execution configuration, so a strategy can construct DAY orders and reject +incompatible 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, fills, order updates, and rejected intents. Response +intents use the scenario v8 intent shapes. + +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/v6/dune b/contracts/strategy/v6/dune new file mode 100644 index 0000000..626dd76 --- /dev/null +++ b/contracts/strategy/v6/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v6/message.schema.json) + (transcript.schema.json as contracts/strategy/v6/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v6/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v6/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v6/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v6/fixtures/external.scenario.json b/contracts/strategy/v6/fixtures/external.scenario.json new file mode 100644 index 0000000..1cd0108 --- /dev/null +++ b/contracts/strategy/v6/fixtures/external.scenario.json @@ -0,0 +1,204 @@ +{ + "contract_version": "8", + "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": "1", + "participation_bps": 5000, + "fixed_fee": "0.25", + "fee_bps": 10 + } + }, + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/strategy/v6/fixtures/external.scenario.jsonl b/contracts/strategy/v6/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..6c1f91c --- /dev/null +++ b/contracts/strategy/v6/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"8","payload":{"metadata":{"producer":"strategy-protocol-fixture"},"run_id":"external-demo","base_currency":"USD","initial_portfolio":{"cash":[{"amount":"10000","currency":"USD"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"1","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"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":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000},"record_type":"scenario_header","scenario_sequence":"1"} +{"contract_version":"8","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"}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"8","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"}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"8","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v6/fixtures/external.strategy.jsonl b/contracts/strategy/v6/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..6bb8ac8 --- /dev/null +++ b/contracts/strategy/v6/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"6","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"8","scenario_sha256":"9399ec91936b6beff0701c5b730188f238921203553f6f4c1d9d92dc77110afe","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"}]}]}],"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":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"metadata":{"producer":"strategy-protocol-fixture"}}}} +{"strategy_protocol_version":"6","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"6","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":[]}}}}} +{"strategy_protocol_version":"6","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","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":"6","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":"6","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"6","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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"}}}}} +{"strategy_protocol_version":"6","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"6","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":"6","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"6","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":[]}}}}} +{"strategy_protocol_version":"6","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"6","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"6","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v6/message.schema.json b/contracts/strategy/v6/message.schema.json new file mode 100644 index 0000000..143e284 --- /dev/null +++ b/contracts/strategy/v6/message.schema.json @@ -0,0 +1,298 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v6/message.schema.json", + "title": "Trading Engine external strategy protocol v6 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": "6" }, + "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", "metadata"], + "properties": { + "engine_version": { "type": "string", "minLength": 1 }, + "scenario_contract_version": { "const": "8" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/venueCalendar" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/execution" }, + "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/v8/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/v8/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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/v8/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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/v8/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/v8/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/signedDecimal" } + ] + }, + "markedPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], + "properties": { + "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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/v8/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/v8/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/v8/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/v8/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/v6/transcript.schema.json b/contracts/strategy/v6/transcript.schema.json new file mode 100644 index 0000000..3ec1993 --- /dev/null +++ b/contracts/strategy/v6/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/v6/transcript.schema.json", + "title": "Trading Engine external strategy protocol v6 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": "6" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v6/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/v8/README.md b/contracts/v8/README.md new file mode 100644 index 0000000..b459d5a --- /dev/null +++ b/contracts/v8/README.md @@ -0,0 +1,48 @@ +# Trading Engine contract v8 + +This directory is the authoritative v8 process and file contract shared by Trading Engine and its +clients. Versions 7, 6, 5, 4, and 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 8 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 v8 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 v8 scenario, stream record, and journal record carries `"contract_version": "8"`. + +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 v8 `execution` object retains the versioned configuration introduced by v5. +`completed_bar_v1` configuration version `"1"` requires participation basis points, fixed fee, +and fee basis points. Runtime capabilities describe its required fields, supported market and limit +orders (including stop and stop-limit), completed-OHLCV data requirement, and numeric limits. diff --git a/contracts/v8/dune b/contracts/v8/dune new file mode 100644 index 0000000..2f462da --- /dev/null +++ b/contracts/v8/dune @@ -0,0 +1,16 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v8/journal.schema.json) + (scenario-stream.schema.json as contracts/v8/scenario-stream.schema.json) + (scenario.schema.json as contracts/v8/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v8/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v8/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl as contracts/v8/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v8/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v8/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v8/fixtures/demo.journal.jsonl b/contracts/v8/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..77fb100 --- /dev/null +++ b/contracts/v8/fixtures/demo.journal.jsonl @@ -0,0 +1,22 @@ +{"contract_version":"8","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":"85f7c99e0666159579c79256b3d0dc5f9c328e1275b79465fe1d4c93883e68f1","execution_model":"completed_bar_v1"}} +{"contract_version":"8","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} +{"contract_version":"8","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} +{"contract_version":"8","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":[]}} +{"contract_version":"8","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} +{"contract_version":"8","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":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} +{"contract_version":"8","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004","demo-event-000000000005"],"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-000000000007","updated_event_id":"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}} +{"contract_version":"8","engine_sequence":"8","event_id":"demo-event-000000000008","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","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"14","equity":"10104","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"}],"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"}],"margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052","maintenance_excess":"10078","margin_call":false},"group_exposures":[]}} +{"contract_version":"8","engine_sequence":"9","event_id":"demo-event-000000000009","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} +{"contract_version":"8","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} +{"contract_version":"8","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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-000000000007","updated_event_id":"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":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"immediate_or_cancel"}} +{"contract_version":"8","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000005","demo-event-000000000009"],"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.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-000000000012","updated_event_id":"demo-event-000000000012","created_sequence":"12","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":"8","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000009"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5","unrealized_pnl":"40.132","equity":"10130.132","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618"}],"margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.632","maintenance_excess":"9942.882","margin_call":false},"group_exposures":[]}} +{"contract_version":"8","engine_sequence":"14","event_id":"demo-event-000000000014","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":[]}} +{"contract_version":"8","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.715","price":"107","notional":"290.505","fee":"0.540505","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} +{"contract_version":"8","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000014"],"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":"8","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":["demo-event-000000000014","demo-event-000000000016"],"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-000000000017","updated_event_id":"demo-event-000000000017","created_sequence":"17","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":"8","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000014"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.086495","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5","unrealized_pnl":"20.161495","equity":"10110.161495","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.086495","fx_rate":"1","base_value":"9090.086495"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505"}],"margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.123995","maintenance_excess":"9855.142745","margin_call":false},"group_exposures":[]}} +{"contract_version":"8","engine_sequence":"19","event_id":"demo-event-000000000019","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":[]}} +{"contract_version":"8","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000017","demo-event-000000000019"],"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":"105","notional":"757.575","fee":"1.007575","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} +{"contract_version":"8","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false},"group_exposures":[]}} +{"contract_version":"8","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"85f7c99e0666159579c79256b3d0dc5f9c328e1275b79465fe1d4c93883e68f1","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v8/fixtures/demo.scenario.json b/contracts/v8/fixtures/demo.scenario.json new file mode 100644 index 0000000..ba9ab60 --- /dev/null +++ b/contracts/v8/fixtures/demo.scenario.json @@ -0,0 +1,302 @@ +{ + "contract_version": "8", + "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_v1", + "configuration": { + "version": "1", + "participation_bps": 5000, + "fixed_fee": "0.25", + "fee_bps": 10 + } + }, + "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": [] + }, + { + "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": "12" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [] + }, + { + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v8/fixtures/demo.scenario.jsonl b/contracts/v8/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..3b4f365 --- /dev/null +++ b/contracts/v8/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"8","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_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000}} +{"contract_version":"8","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"}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"8","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"8","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"}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"8","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"}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"8","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v8/fixtures/fill-clipped.journal.jsonl b/contracts/v8/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..26542b7 --- /dev/null +++ b/contracts/v8/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,11 @@ +{"contract_version":"8","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":"1935c1744a959181894f6610c539e6d3d27ebce56d5737c8286f3e1b4417cb21","execution_model":"completed_bar_v1"}} +{"contract_version":"8","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} +{"contract_version":"8","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"8","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":[]}} +{"contract_version":"8","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":"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-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","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":"8","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":"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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"8","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","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":[]}} +{"contract_version":"8","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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":"8","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","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":"immediate_or_cancel"}} +{"contract_version":"8","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000007"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21: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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"8","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000010"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"1935c1744a959181894f6610c539e6d3d27ebce56d5737c8286f3e1b4417cb21","execution_model":"completed_bar_v1","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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v8/fixtures/fill-clipped.scenario.json b/contracts/v8/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..8f66605 --- /dev/null +++ b/contracts/v8/fixtures/fill-clipped.scenario.json @@ -0,0 +1,177 @@ +{ + "contract_version": "8", + "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": "1", + "participation_bps": 10000, + "fixed_fee": "10", + "fee_bps": 0 + } + }, + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v8/journal.schema.json b/contracts/v8/journal.schema.json new file mode 100644 index 0000000..d7e3107 --- /dev/null +++ b/contracts/v8/journal.schema.json @@ -0,0 +1,238 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v8/journal.schema.json", + "title": "Trading Engine v6 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": "8" }, + "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", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] + }, + "payload": { "type": "object" } + }, + "allOf": [ + { "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } + }, + "initialState": { + "type": "object", "additionalProperties": false, + "required": ["portfolio", "valuation"], + "properties": { + "portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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" } } } + ] + }, + "marketSlice": { + "type": "object", "additionalProperties": false, + "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], + "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" } } + } + }, + "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"] }, "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"] } } + }, + "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" } } + }, + "orderAdjusted": { + "type": "object", "additionalProperties": false, "required": ["order", "action_id"], + "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } + }, + "fill": { + "type": "object", "additionalProperties": false, + "required": ["fill_id", "order_id", "instrument_id", "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", "slice_sequence"], + "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/unsignedDecimal" }, "executed_at": { "$ref": "#/$defs/timestamp" }, "slice_sequence": { "$ref": "#/$defs/sequence" } } + }, + "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" } } + }, + "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": "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_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" } } + }, + "cashAttribution": { + "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], + "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } + }, + "positionAttribution": { + "type": "object", "additionalProperties": false, + "required": ["instrument_id", "quote_currency", "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"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, "quote_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "base_execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_total_fees": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "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", "net_market_value", "long_market_value", "short_market_value", "gross_exposure", "cost_basis", "realized_pnl", "unrealized_pnl", "equity", "dividend_pnl", "execution_fees", "borrow_fees", "total_fees", "cash_balances", "positions", "margin", "group_exposures"], + "properties": { + "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "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" } } + } + }, + "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": { "const": "completed_bar_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/v8/scenario-stream.schema.json b/contracts/v8/scenario-stream.schema.json new file mode 100644 index 0000000..7798820 --- /dev/null +++ b/contracts/v8/scenario-stream.schema.json @@ -0,0 +1,76 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v8/scenario-stream.schema.json", + "title": "Trading Engine v6 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": "8" }, + "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": "8" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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": "8" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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", "max_internal_events"], + "properties": { + "metadata": { "type": "object" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/execution" }, + "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/v8/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v8/scenario.schema.json b/contracts/v8/scenario.schema.json new file mode 100644 index 0000000..de87de4 --- /dev/null +++ b/contracts/v8/scenario.schema.json @@ -0,0 +1,442 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json", + "title": "Trading Engine v6 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", "max_internal_events", "schedule", "slices"], + "properties": { + "contract_version": { "const": "8" }, + "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" }, + "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": { + "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": { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + "completedBarV1Configuration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fixed_fee", "fee_bps"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, + "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "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"], + "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" } } + } + }, + "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" } + } + } + ] + } + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index e546ded..519bc97 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/v7/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v8/README.md) remain authoritative for process boundaries. diff --git a/docs/persistra.md b/docs/persistra.md index dab8b11..abcabaa 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -58,7 +58,7 @@ journal output for v3 inputs. The engine parser is authoritative for ordering, c causality, tick, lot, risk, and accounting invariants that JSON Schema cannot express. External strategies use the separate -[strategy protocol v5](../contracts/strategy/v5/README.md). Persistra's host turns protocol +[strategy protocol v6](../contracts/strategy/v6/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, diff --git a/docs/scenario.md b/docs/scenario.md index 41d70ad..8c88717 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 v6 demo](../contracts/v7/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v7/scenario.schema.json) provides structural validation. +Use [the v8 demo](../contracts/v8/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v8/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/v7/scenario-stream.schema.json) validates each line, -and [the v6 stream fixture](../contracts/v7/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v8/scenario-stream.schema.json) validates each line, +and [the v8 stream fixture](../contracts/v8/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. @@ -214,7 +214,7 @@ than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v7/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v8/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 0615c7f..5c02e64 100644 --- a/lib/audit.ml +++ b/lib/audit.ml @@ -2,6 +2,10 @@ type cancellation_reason = | Strategy_requested | Target_replaced | Market_ioc + | Immediate_or_cancel + | Fill_or_kill + | Day_expired + | Gtd_expired | Margin_call type target_basis = Weights | Quantities @@ -33,6 +37,7 @@ type event = } | Order_accepted of Order.t | Order_rejected of Order.t + | Order_triggered of Order.t | Order_cancelled of { order : Order.t; reason : cancellation_reason } | Split_applied of { action : Corporate_action.t; @@ -113,6 +118,10 @@ let cancellation_reason_to_string = function | Strategy_requested -> "strategy_requested" | Target_replaced -> "target_replaced" | Market_ioc -> "market_ioc" + | Immediate_or_cancel -> "immediate_or_cancel" + | Fill_or_kill -> "fill_or_kill" + | Day_expired -> "day_expired" + | Gtd_expired -> "gtd_expired" | Margin_call -> "margin_call" let target_basis_to_string = function @@ -126,6 +135,7 @@ let event_name = function | Target_portfolio_requested _ -> "target_portfolio_requested" | Order_accepted _ -> "order_accepted" | Order_rejected _ -> "order_rejected" + | Order_triggered _ -> "order_triggered" | Order_cancelled _ -> "order_cancelled" | Split_applied _ -> "split_applied" | Cash_dividend_applied _ -> "cash_dividend_applied" diff --git a/lib/audit.mli b/lib/audit.mli index e48b7fe..23e594a 100644 --- a/lib/audit.mli +++ b/lib/audit.mli @@ -4,6 +4,10 @@ type cancellation_reason = | Strategy_requested | Target_replaced | Market_ioc + | Immediate_or_cancel + | Fill_or_kill + | Day_expired + | Gtd_expired | Margin_call type target_basis = Weights | Quantities @@ -35,6 +39,7 @@ type event = } | Order_accepted of Order.t | Order_rejected of Order.t + | Order_triggered of Order.t | Order_cancelled of { order : Order.t; reason : cancellation_reason } | Split_applied of { action : Corporate_action.t; diff --git a/lib/codec.ml b/lib/codec.ml index 3332d9d..1bbe9a7 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -239,6 +239,8 @@ let request_fields request = match request.Order.kind with | Order.Market -> ("market", `Null) | Order.Limit value -> ("limit", price value) + | Order.Stop value -> ("stop", price value) + | Order.Stop_limit { limit_price; _ } -> ("stop_limit", price limit_price) in [ ("instrument_id", instrument_id request.instrument_id); @@ -249,6 +251,41 @@ let request_fields request = ("origin", string (Order.origin_to_string request.origin)); ] +let request_fields_v8 request = + let kind, trigger_price, limit_price = + match request.Order.kind with + | Order.Market -> ("market", `Null, `Null) + | Order.Limit value -> ("limit", `Null, price value) + | Order.Stop value -> ("stop", price value, `Null) + | Order.Stop_limit { trigger_price; limit_price } -> + ("stop_limit", price trigger_price, price limit_price) + in + let tif, venue_id, calendar_id, expires_at = + match request.time_in_force with + | Order.Gtc -> ("gtc", `Null, `Null, `Null) + | Order.Ioc -> ("ioc", `Null, `Null, `Null) + | Order.Fok -> ("fok", `Null, `Null, `Null) + | Order.Day { venue_id; calendar_id } -> + ( "day", + string (Id.Venue.to_string venue_id), + string (Id.Venue_calendar.to_string calendar_id), + `Null ) + | Order.Gtd value -> ("gtd", `Null, `Null, timestamp value) + in + [ + ("instrument_id", instrument_id request.instrument_id); + ("side", string (Order.side_to_string request.side)); + ("quantity", quantity request.quantity); + ("order_kind", string kind); + ("trigger_price", trigger_price); + ("limit_price", limit_price); + ("time_in_force", string tif); + ("venue_id", venue_id); + ("calendar_id", calendar_id); + ("expires_at", expires_at); + ("origin", string (Order.origin_to_string request.origin)); + ] + let order_to_yojson order = let rejection_reason = match order.Order.status with @@ -270,6 +307,39 @@ let order_to_yojson order = ("rejection_reason", rejection_reason); ]) +let order_to_yojson_v8 order = + let rejection_reason = + match order.Order.status with + | Order.Rejected reason -> string reason + | _ -> `Null + in + let triggered_at, triggered_slice_sequence = + match order.trigger_state with + | Some (Order.Triggered { triggered_at; triggered_slice_sequence }) -> + (timestamp triggered_at, int64 triggered_slice_sequence) + | Some Order.Dormant | None -> (`Null, `Null) + in + `Assoc + ((("order_id", order_id order.id) :: request_fields_v8 order.request) + @ [ + ("created_event_id", string (Id.Event.to_string order.created_event_id)); + ("updated_event_id", string (Id.Event.to_string order.updated_event_id)); + ("created_sequence", int64 order.created_sequence); + ("created_at", timestamp order.created_at); + ( "eligible_after_slice_sequence", + int64 order.eligible_after_slice_sequence ); + ("triggered_at", triggered_at); + ("triggered_slice_sequence", triggered_slice_sequence); + ("filled_quantity", quantity order.filled_quantity); + ("filled_notional", money order.filled_notional); + ("status", string (Order.status_to_string order.status)); + ("rejection_reason", rejection_reason); + ]) + +let versioned_order_to_yojson ~contract_version order = + if String.equal contract_version "8" then order_to_yojson_v8 order + else order_to_yojson order + let fill_to_yojson fill = `Assoc [ @@ -466,11 +536,13 @@ let payload_to_yojson ~contract_version = function ("targets", `List (List.map requested_target_to_yojson targets)); ] | Audit.Order_accepted order | Audit.Order_rejected order -> - order_to_yojson order + versioned_order_to_yojson ~contract_version order + | Audit.Order_triggered order -> + versioned_order_to_yojson ~contract_version order | Audit.Order_cancelled { order; reason } -> `Assoc [ - ("order", order_to_yojson order); + ("order", versioned_order_to_yojson ~contract_version order); ("reason", string (Audit.cancellation_reason_to_string reason)); ] | Audit.Split_applied { action; previous_quantity; adjusted_quantity } -> @@ -490,7 +562,7 @@ let payload_to_yojson ~contract_version = function | Audit.Order_adjusted { order; action_id } -> `Assoc [ - ("order", order_to_yojson order); + ("order", versioned_order_to_yojson ~contract_version order); ("action_id", string (Id.Corporate_action.to_string action_id)); ] | Audit.Fill_applied fill -> fill_to_yojson fill diff --git a/lib/codec.mli b/lib/codec.mli index 8c36bc7..81690d0 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -5,6 +5,7 @@ val ptime_of_string : string -> (Ptime.t, string) result val bar_to_yojson : Bar.t -> Yojson.Safe.t val market_slice_to_yojson : 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 val initial_portfolio_to_yojson : Initial_portfolio.t -> Yojson.Safe.t val audit_to_yojson : Audit.t -> Yojson.Safe.t diff --git a/lib/contract.ml b/lib/contract.ml index c9da0e3..b62a8e5 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,13 +1,13 @@ -let version = "7" -let previous_version = "6" +let version = "8" +let previous_version = "7" let legacy_journal_version = "3" let supported_versions = - [ version; previous_version; "5"; "4"; legacy_journal_version ] + [ version; previous_version; "6"; "5"; "4"; legacy_journal_version ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "5" -let previous_strategy_protocol_version = "4" +let strategy_protocol_version = "6" +let previous_strategy_protocol_version = "5" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -23,8 +23,12 @@ let capabilities_to_yojson () = ("execution_model_contracts", Execution_model.capabilities_to_yojson ()); ( "strategy_protocol_versions", strings - [ strategy_protocol_version; previous_strategy_protocol_version; "3" ] - ); + [ + strategy_protocol_version; + previous_strategy_protocol_version; + "4"; + "3"; + ] ); ("resource_limits", Resource_limits.to_yojson ()); ] diff --git a/lib/engine.ml b/lib/engine.ml index fd929bf..d81adf6 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -1,13 +1,14 @@ type config = { contract_version : string; risk : Risk.t; + venue_calendars : Venue_calendar.t list; execution_model : Execution_model.t; execution : Execution.t; max_internal_events : int; } -let config ~contract_version ~risk ~execution_model ~execution - ~max_internal_events = +let make_config ~venue_calendars ~contract_version ~risk ~execution_model + ~execution ~max_internal_events = if not (Contract.is_supported contract_version) then Error "engine contract version is unsupported" else if max_internal_events <= 0 then @@ -21,11 +22,22 @@ let config ~contract_version ~risk ~execution_model ~execution { contract_version; risk; + venue_calendars; execution_model; execution; max_internal_events; } +let config ~contract_version ~risk ~execution_model ~execution + ~max_internal_events = + make_config ~venue_calendars:[] ~contract_version ~risk ~execution_model + ~execution ~max_internal_events + +let config_v8 ~contract_version ~risk ~venue_calendars ~execution_model + ~execution ~max_internal_events = + make_config ~venue_calendars ~contract_version ~risk ~execution_model + ~execution ~max_internal_events + let valid_sha256 value = String.length value = 64 && String.for_all @@ -335,6 +347,30 @@ module Interactive = struct ~engine_sequence:order_sequence in match + let* () = + match request.Order.time_in_force with + | Order.Day { venue_id; calendar_id } -> ( + match + List.find_opt + (fun calendar -> + Id.Venue_calendar.equal calendar.Venue_calendar.id + calendar_id) + reduction.state.config.venue_calendars + with + | None -> Error "DAY order refers to an unknown calendar" + | Some calendar -> + if not (Id.Venue.equal calendar.venue_id venue_id) then + Error "DAY order venue differs from its calendar" + else if + not + (Id.Instrument.Set.mem request.instrument_id + calendar.instrument_ids) + then + Error + "DAY order calendar does not cover its instrument" + else Ok ()) + | Order.Gtc | Order.Ioc | Order.Fok | Order.Gtd _ -> Ok () + in let marks = Id.Instrument.Map.bindings reduction.state.latest_marks in @@ -427,6 +463,33 @@ module Interactive = struct in cancel reduction order_ids + let trigger_order reduction order_id ~triggered_at ~triggered_slice_sequence = + let* sequence = next_sequence reduction.state.engine_sequence in + let updated_event_id = + Audit.event_id ~run_id:reduction.state.run_id ~engine_sequence:sequence + in + let* oms, order = + Oms.trigger reduction.state.oms order_id ~updated_event_id ~triggered_at + ~triggered_slice_sequence + in + let reduction = + { reduction with state = { reduction.state with oms } } + |> fun reduction -> + with_causes reduction + (order.Order.created_event_id :: reduction.causation_ids) + in + let* reduction, emitted_id = + emit_with_id reduction (Audit.Order_triggered order) + in + if not (Id.Event.equal emitted_id updated_event_id) then + Error "order trigger event ID prediction diverged" + else + let* pending = + notification reduction ~causation_ids:[ emitted_id ] + (Strategy.Order_updated order) + in + Ok (enqueue reduction [ pending ]) + let configured_instruments state = Risk.instruments state.config.risk |> List.sort (fun left right -> @@ -514,12 +577,23 @@ module Interactive = struct else match order.request.kind with | Order.Market -> Ok () - | Order.Limit price -> + | Order.Limit price | Order.Stop price -> if Scalar.Price.is_multiple price ~tick:instrument.tick_size then Ok () else Error - "split-adjusted limit price is not aligned to the \ + "split-adjusted order price is not aligned to the \ + instrument tick" + | Order.Stop_limit { trigger_price; limit_price } -> + if + Scalar.Price.is_multiple trigger_price + ~tick:instrument.tick_size + && Scalar.Price.is_multiple limit_price + ~tick:instrument.tick_size + then Ok () + else + Error + "split-adjusted order price is not aligned to the \ instrument tick") (Ok ()) adjusted in @@ -1133,6 +1207,15 @@ module Interactive = struct let* permitted_quantity, fee, limit = permitted_fill reduction.state market_slice order proposed instrument in + let permitted_quantity = + if + Order.is_fok order + && Scalar.Quantity.compare permitted_quantity + (Order.remaining_quantity order) + < 0 + then Scalar.Quantity.zero + else permitted_quantity + in let* reduction = match limit with | None -> Ok reduction @@ -1168,19 +1251,28 @@ module Interactive = struct apply_fill reduction market_slice proposed permitted_quantity fee |> Result.map (fun reduction -> (reduction, permitted_quantity)) - let cancel_market_remainders reduction order_ids = + let cancel_immediate_remainders reduction order_ids = let causation_ids = reduction.causation_ids in let rec cancel reduction = function | [] -> Ok (with_causes reduction causation_ids) | order_id :: remaining -> ( match Oms.find reduction.state.oms order_id with - | None -> Error "market IOC order disappeared during matching" + | None -> Error "immediate order disappeared during matching" | Some order -> ( + let reason = + if + (not + (String.equal reduction.state.config.contract_version "8")) + && Order.is_market order + then Audit.Market_ioc + else if Order.is_fok order then Audit.Fill_or_kill + else Audit.Immediate_or_cancel + in let result = if Order.is_active order then cancel_order (with_causes reduction causation_ids) - ~reason:Audit.Market_ioc order_id + ~reason order_id else Ok reduction in match result with @@ -1189,6 +1281,45 @@ module Interactive = struct in cancel reduction order_ids + let cancel_expired_gtd reduction (market_slice : Market_slice.t) = + Oms.active_orders reduction.state.oms + |> List.filter_map (fun order -> + match order.Order.request.time_in_force with + | Order.Gtd expires_at + when Ptime.compare expires_at market_slice.end_at <= 0 -> + Some order.id + | Order.Gtc | Order.Ioc | Order.Fok | Order.Day _ | Order.Gtd _ -> None) + |> cancel_orders reduction ~reason:Audit.Gtd_expired + + let day_session_closed state (market_slice : Market_slice.t) order = + match order.Order.request.time_in_force with + | Order.Day { calendar_id; _ } -> ( + match + List.find_opt + (fun calendar -> + Id.Venue_calendar.equal calendar.Venue_calendar.id calendar_id) + state.config.venue_calendars + with + | None -> false + | Some calendar -> + List.exists + (fun (session : Venue_calendar.session) -> + match List.rev session.phases with + | [] -> false + | phase :: _ -> + Ptime.compare phase.closes_at order.Order.created_at > 0 + && Ptime.compare phase.closes_at market_slice.end_at <= 0) + calendar.sessions) + | Order.Gtc | Order.Ioc | Order.Fok | Order.Gtd _ -> false + + let cancel_expired_day reduction market_slice = + Oms.active_orders reduction.state.oms + |> List.filter_map (fun order -> + if day_session_closed reduction.state market_slice order then + Some order.Order.id + else None) + |> cancel_orders reduction ~reason:Audit.Day_expired + let audit_valuation state = let* account = value state in let* margin = Risk.margin_snapshot state.config.risk account in @@ -1447,6 +1578,7 @@ module Interactive = struct module Actions_phase = struct let run market_slice reduction = + let* reduction = cancel_expired_gtd reduction market_slice in apply_corporate_actions reduction market_slice.Market_slice.corporate_actions end @@ -1498,6 +1630,14 @@ module Interactive = struct let run market_slice cursor reduction = match Execution.next cursor ~oms:reduction.state.oms with | Error _ as error -> error + | Ok + (Execution.Triggered + (order_id, triggered_at, triggered_slice_sequence, cursor)) -> + let* reduction = + trigger_order reduction order_id ~triggered_at + ~triggered_slice_sequence + in + Ok (Continue (reduction, cursor)) | Ok (Execution.Proposed (proposed, advance)) -> let* reduction, applied_quantity = apply_proposed_fill reduction market_slice proposed @@ -1512,8 +1652,9 @@ module Interactive = struct in let reduction = with_causes reduction [ slice_event_id ] in let* reduction = - cancel_market_remainders reduction market_ioc_orders + cancel_immediate_remainders reduction market_ioc_orders in + let* reduction = cancel_expired_day reduction market_slice in let* pending = notification reduction ~causation_ids:[ slice_event_id ] (Strategy.Market_slice_closed market_slice) diff --git a/lib/engine.mli b/lib/engine.mli index 72b3835..fc1d6f8 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -10,6 +10,15 @@ val config : max_internal_events:int -> (config, string) result +val config_v8 : + contract_version:string -> + risk:Risk.t -> + venue_calendars:Venue_calendar.t list -> + execution_model:Execution_model.t -> + execution:Execution.t -> + 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 6cf73c6..23a8cfa 100644 --- a/lib/execution.ml +++ b/lib/execution.ml @@ -10,6 +10,7 @@ type proposed_fill = { type match_result = { fills : proposed_fill list; + triggers : (Id.Order.t * Ptime.t * int64) list; market_ioc_orders : Id.Order.t list; } @@ -19,6 +20,7 @@ type cursor = Cursor of (Oms.t -> (step, string) result) and step = | Finished of Id.Order.t list + | Triggered of Id.Order.t * Ptime.t * int64 * cursor | Proposed of proposed_fill * (Scalar.Quantity.t -> (cursor, string) result) let cursor next = Cursor (fun oms -> next ~oms) @@ -37,9 +39,11 @@ let fixed_fee state = state.fixed_fee let fee_bps state = state.fee_bps let execution_price order market_slice bar = - match order.Order.request.kind with - | Order.Market -> Some (bar.Bar.open_price, market_slice.Market_slice.start_at) - | Order.Limit limit -> ( + match Order.effective_kind order with + | None -> None + | Some Order.Market -> + Some (bar.Bar.open_price, market_slice.Market_slice.start_at) + | Some (Order.Limit limit) -> ( match order.request.side with | Order.Buy -> if Scalar.Price.compare bar.open_price limit <= 0 then @@ -53,6 +57,25 @@ let execution_price order market_slice bar = else if Scalar.Price.compare bar.high_price limit >= 0 then Some (limit, market_slice.end_at) else None) + | Some (Order.Stop _ | Order.Stop_limit _) -> None + +let stop_trigger order market_slice bar = + match (order.Order.request.kind, order.request.side) with + | Order.Stop trigger_price, Order.Buy + | Order.Stop_limit { trigger_price; _ }, Order.Buy -> + if Scalar.Price.compare bar.Bar.open_price trigger_price >= 0 then + Some market_slice.Market_slice.start_at + else if Scalar.Price.compare bar.high_price trigger_price >= 0 then + Some market_slice.end_at + else None + | Order.Stop trigger_price, Order.Sell + | Order.Stop_limit { trigger_price; _ }, Order.Sell -> + if Scalar.Price.compare bar.Bar.open_price trigger_price <= 0 then + Some market_slice.Market_slice.start_at + else if Scalar.Price.compare bar.low_price trigger_price <= 0 then + Some market_slice.end_at + else None + | (Order.Market | Order.Limit _), _ -> None let available_quantity capacity remaining = match capacity with @@ -138,12 +161,21 @@ let start_slice state ~instruments ~oms (market_slice : Market_slice.t) = Int64.compare order.Order.eligible_after_slice_sequence market_slice.slice_sequence < 0 - && Ptime.compare order.created_at market_slice.start_at <= 0) + && Ptime.compare order.created_at market_slice.start_at <= 0 + && + match order.trigger_state with + | Some (Order.Triggered { triggered_slice_sequence; _ }) -> + Int64.compare triggered_slice_sequence market_slice.slice_sequence + < 0 + | Some Order.Dormant | None -> true) |> List.sort compare_execution_order in let market_ioc_orders = List.filter_map - (fun order -> if Order.is_market order then Some order.Order.id else None) + (fun order -> + if Order.is_ioc order && not (Order.is_dormant_stop order) then + Some order.Order.id + else None) eligible in let eligible_order_ids = List.map (fun order -> order.Order.id) eligible in @@ -169,50 +201,70 @@ let start_slice state ~instruments ~oms (market_slice : Market_slice.t) = | None, _, _ | _, None, _ | _, _, None -> Error "eligible order has no configured bar in the market slice" | Some bar, Some capacity, Some instrument -> ( - match execution_price order market_slice bar with - | None -> - let (Cursor next) = make_cursor capacities remaining in - next current_oms - | Some (price, executed_at) -> - let quantity = - available_quantity capacity (Order.remaining_quantity order) - in - if Scalar.Quantity.is_zero quantity then + if Order.is_dormant_stop order then + match stop_trigger order market_slice bar with + | None -> let (Cursor next) = make_cursor capacities remaining in next current_oms - else - let* notional = Scalar.Money.notional price quantity in - let* fee = - Scalar.Money.fee ~fixed:state.fixed_fee ~bps:state.fee_bps - ~notional - in - let proposed = - { order_id = order.id; quantity; price; fee; 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 - in - Ok (make_cursor capacities remaining) + | Some triggered_at -> + Ok + (Triggered + ( order.id, + triggered_at, + market_slice.slice_sequence, + make_cursor capacities remaining )) + else + match execution_price order market_slice bar with + | None -> + let (Cursor next) = make_cursor capacities remaining in + next current_oms + | Some (price, executed_at) -> + let quantity = + available_quantity capacity (Order.remaining_quantity order) in - Ok (Proposed (proposed, continue))) + if + Scalar.Quantity.is_zero quantity + || Order.is_fok order + && Scalar.Quantity.compare quantity + (Order.remaining_quantity order) + < 0 + then + let (Cursor next) = make_cursor capacities remaining in + next current_oms + else + let* notional = Scalar.Money.notional price quantity in + let* fee = + Scalar.Money.fee ~fixed:state.fixed_fee ~bps:state.fee_bps + ~notional + in + let proposed = + { order_id = order.id; quantity; price; fee; 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 + in + Ok (make_cursor capacities remaining) + in + Ok (Proposed (proposed, continue))) in Ok (make_cursor capacities eligible_order_ids) @@ -230,6 +282,8 @@ let fold_slice state ~instruments ~oms market_slice ~init ~apply = match next cursor ~oms with | Error _ as error -> error | Ok (Finished market_ioc_orders) -> Ok (accumulator, market_ioc_orders) + | Ok (Triggered _) -> + Error "fold_slice cannot persist a triggered conditional order" | Ok (Proposed (proposed, continue)) -> let* accumulator, applied_quantity = apply accumulator proposed in let* cursor = continue applied_quantity in @@ -238,8 +292,26 @@ let fold_slice state ~instruments ~oms market_slice ~init ~apply = fold init cursor let match_slice state ~instruments ~oms market_slice = - let apply fills proposed = Ok (proposed :: fills, proposed.quantity) in - match fold_slice state ~instruments ~oms market_slice ~init:[] ~apply with - | Error _ as error -> error - | Ok (fills, market_ioc_orders) -> - Ok { fills = List.rev fills; market_ioc_orders } + let ( let* ) result function_ = + match result with Ok value -> function_ value | Error _ as error -> error + in + let* cursor = start_slice state ~instruments ~oms market_slice in + let rec collect fills triggers cursor = + match next cursor ~oms with + | Error _ as error -> error + | Ok (Finished market_ioc_orders) -> + Ok + { + fills = List.rev fills; + triggers = List.rev triggers; + market_ioc_orders; + } + | Ok (Triggered (order_id, triggered_at, slice_sequence, cursor)) -> + collect fills + ((order_id, triggered_at, slice_sequence) :: triggers) + cursor + | Ok (Proposed (proposed, continue)) -> + let* cursor = continue proposed.quantity in + collect (proposed :: fills) triggers cursor + in + collect [] [] cursor diff --git a/lib/execution.mli b/lib/execution.mli index f44b169..88bcce4 100644 --- a/lib/execution.mli +++ b/lib/execution.mli @@ -12,6 +12,7 @@ type proposed_fill = private { type match_result = private { fills : proposed_fill list; + triggers : (Id.Order.t * Ptime.t * int64) list; market_ioc_orders : Id.Order.t list; } @@ -19,6 +20,7 @@ type cursor type step = | Finished of Id.Order.t list + | Triggered of Id.Order.t * Ptime.t * int64 * cursor | Proposed of proposed_fill * (Scalar.Quantity.t -> (cursor, string) result) val cursor : (oms:Oms.t -> (step, string) result) -> cursor @@ -62,7 +64,9 @@ val fold_slice : ('a * Id.Order.t list, string) result (** Fold executable orders in liquidation-first, then sell-before-buy/FIFO order. The callback returns the quantity it actually applied; only that - quantity consumes the shared per-instrument slice capacity. *) + quantity consumes the shared per-instrument slice capacity. Returns an error + when a dormant stop triggers because this compatibility helper has no + callback through which to persist trigger state. *) val match_slice : t -> @@ -70,3 +74,5 @@ val match_slice : oms:Oms.t -> Market_slice.t -> (match_result, string) result +(** Pure deterministic matching. Conditional activations are returned in + [triggers] and cannot fill until a later slice. *) diff --git a/lib/execution_model.ml b/lib/execution_model.ml index cc1a628..866ecfc 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -33,9 +33,9 @@ let supported = List.map name builtins let completed_bar_v1_contract = { version = "1"; - scenario_contract_versions = [ "7"; "6"; "5"; "4"; "3" ]; + scenario_contract_versions = [ "8"; "7"; "6"; "5"; "4"; "3" ]; required_fields = [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ]; - supported_order_types = [ "market"; "limit" ]; + supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; data_requirements = [ "completed_ohlcv_bars" ]; limits = `Assoc diff --git a/lib/external_replay.ml b/lib/external_replay.ml index 130c2ba..559262d 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -65,6 +65,7 @@ let initialization_of_scenario ~scenario_sha256 (scenario : Scenario.t) = initial_cash = scenario.initial_cash; initial_portfolio = scenario.initial_portfolio; instruments = scenario.instruments; + venue_calendars = scenario.venue_calendars; risk = scenario.risk; execution_model = scenario.execution_model; execution = scenario.execution; @@ -82,17 +83,18 @@ let initialization_of_header ~scenario_sha256 (header : Scenario.stream_header) initial_cash = header.initial_cash; initial_portfolio = header.initial_portfolio; instruments = header.instruments; + venue_calendars = header.venue_calendars; risk = header.risk; execution_model = header.execution_model; execution = header.execution; } let create_runner ~contract_version ~run_id ~scenario_sha256 ~risk - ~execution_model ~execution ~max_internal_events ~initial_cash - ~initial_portfolio = + ~venue_calendars ~execution_model ~execution ~max_internal_events + ~initial_cash ~initial_portfolio = let* config = - Engine.config ~contract_version ~risk ~execution_model ~execution - ~max_internal_events + Engine.config_v8 ~contract_version ~risk ~venue_calendars ~execution_model + ~execution ~max_internal_events |> reducer_result in match initial_portfolio with @@ -177,6 +179,7 @@ let run ?(durability = Artifact_writer.Buffered) ~env ~scenario_sha256 let* initial = create_runner ~contract_version:scenario.contract_version ~run_id:scenario.run_id ~scenario_sha256 ~risk:scenario.risk + ~venue_calendars:scenario.venue_calendars ~execution_model:scenario.execution_model ~execution:scenario.execution ~max_internal_events:scenario.max_internal_events ~initial_cash:scenario.initial_cash @@ -232,6 +235,7 @@ let validate_stream_pass ~scenario_sha256 channel = let* runner = create_runner ~contract_version:header.contract_version ~run_id:header.Scenario.run_id ~scenario_sha256 ~risk:header.risk + ~venue_calendars:header.venue_calendars ~execution_model:header.execution_model ~execution:header.execution ~max_internal_events:header.max_internal_events ~initial_cash:header.initial_cash @@ -261,6 +265,7 @@ let replay_stream_pass ~scenario_sha256 ~journal ~session channel = let* runner = create_runner ~contract_version:header.contract_version ~run_id:header.Scenario.run_id ~scenario_sha256 ~risk:header.risk + ~venue_calendars:header.venue_calendars ~execution_model:header.execution_model ~execution:header.execution ~max_internal_events:header.max_internal_events ~initial_cash:header.initial_cash diff --git a/lib/oms.ml b/lib/oms.ml index 7a6f495..6b4c8ab 100644 --- a/lib/oms.ml +++ b/lib/oms.ml @@ -58,6 +58,18 @@ let cancel state order_id = | Error _ as error -> error | Ok cancelled -> Ok (insert state cancelled, cancelled)) +let trigger state order_id ~updated_event_id ~triggered_at + ~triggered_slice_sequence = + match find state order_id with + | None -> Error "cannot trigger an unknown order" + | Some order -> ( + match + Order.trigger order ~updated_event_id ~triggered_at + ~triggered_slice_sequence + with + | Error _ as error -> error + | Ok triggered -> Ok (insert state triggered, triggered)) + let adjust_for_split state ~instrument_id ~updated_event_ids ~numerator ~denominator = let active = active_for_instrument state instrument_id in diff --git a/lib/oms.mli b/lib/oms.mli index 07f4295..a52c464 100644 --- a/lib/oms.mli +++ b/lib/oms.mli @@ -32,6 +32,14 @@ val reject : val cancel : t -> Id.Order.t -> (t * Order.t, string) result +val trigger : + t -> + Id.Order.t -> + updated_event_id:Id.Event.t -> + triggered_at:Ptime.t -> + triggered_slice_sequence:int64 -> + (t * Order.t, string) result + val adjust_for_split : t -> instrument_id:Id.Instrument.t -> diff --git a/lib/order.ml b/lib/order.ml index 3b54230..0475ff3 100644 --- a/lib/order.ml +++ b/lib/order.ml @@ -1,5 +1,21 @@ type side = Buy | Sell -type kind = Market | Limit of Scalar.Price.t + +type kind = + | Market + | Limit of Scalar.Price.t + | Stop of Scalar.Price.t + | Stop_limit of { + trigger_price : Scalar.Price.t; + limit_price : Scalar.Price.t; + } + +type time_in_force = + | Gtc + | Ioc + | Fok + | Day of { venue_id : Id.Venue.t; calendar_id : Id.Venue_calendar.t } + | Gtd of Ptime.t + type origin = Direct | Target_rebalance | Margin_liquidation type request = { @@ -7,9 +23,14 @@ type request = { side : side; quantity : Scalar.Quantity.t; kind : kind; + time_in_force : time_in_force; origin : origin; } +type trigger_state = + | Dormant + | Triggered of { triggered_at : Ptime.t; triggered_slice_sequence : int64 } + type status = | Working | Partially_filled @@ -30,13 +51,33 @@ type t = { eligible_after_slice_sequence : int64; filled_quantity : Scalar.Quantity.t; filled_notional : Scalar.Money.t; + trigger_state : trigger_state option; status : status; } -let request ~instrument_id ~side ~quantity ~kind ~origin = +let compatibility_time_in_force = function Market -> Ioc | _ -> Gtc + +let valid_stop_limit side trigger_price limit_price = + match side with + | Buy -> Scalar.Price.compare limit_price trigger_price >= 0 + | Sell -> Scalar.Price.compare limit_price trigger_price <= 0 + +let request_v8 ~instrument_id ~side ~quantity ~kind ~time_in_force ~origin = if not (Scalar.Quantity.is_positive quantity) then Error "order quantity must be positive" - else Ok { instrument_id; side; quantity; kind; origin } + else + match kind with + | Stop_limit { trigger_price; limit_price } + when not (valid_stop_limit side trigger_price limit_price) -> + Error + "buy stop-limit prices require limit >= trigger and sell stop-limit \ + prices require limit <= trigger" + | _ -> Ok { instrument_id; side; quantity; kind; time_in_force; origin } + +let request ~instrument_id ~side ~quantity ~kind ~origin = + request_v8 ~instrument_id ~side ~quantity ~kind + ~time_in_force:(compatibility_time_in_force kind) + ~origin let make ~id ~created_event_id ~sequence ~created_at ~eligible_after_slice_sequence ~request ~status = @@ -44,6 +85,11 @@ let make ~id ~created_event_id ~sequence ~created_at Error "order sequence must be nonnegative" else if Int64.compare eligible_after_slice_sequence 0L < 0 then Error "order eligibility sequence must be nonnegative" + else if + match request.time_in_force with + | Gtd expires_at -> Ptime.compare expires_at created_at <= 0 + | Gtc | Ioc | Fok | Day _ -> false + then Error "GTD expiry must follow order creation" else Ok { @@ -56,6 +102,10 @@ let make ~id ~created_event_id ~sequence ~created_at eligible_after_slice_sequence; filled_quantity = Scalar.Quantity.zero; filled_notional = Scalar.Money.zero; + trigger_state = + (match request.kind with + | Stop _ | Stop_limit _ -> Some Dormant + | Market | Limit _ -> None); status; } @@ -88,7 +138,47 @@ let is_active order = let is_terminal order = not (is_active order) let is_market order = - match order.request.kind with Market -> true | Limit _ -> false + match order.request.kind with + | Market -> true + | Limit _ | Stop _ | Stop_limit _ -> false + +let is_ioc order = + match order.request.time_in_force with Ioc | Fok -> true | _ -> false + +let is_fok order = + match order.request.time_in_force with Fok -> true | _ -> false + +let is_dormant_stop order = + match order.trigger_state with + | Some Dormant -> true + | Some (Triggered _) | None -> false + +let trigger order ~updated_event_id ~triggered_at ~triggered_slice_sequence = + if not (is_active order) then Error "cannot trigger a terminal order" + else + match order.trigger_state with + | None -> Error "cannot trigger an unconditional order" + | Some (Triggered _) -> Error "cannot trigger an already-triggered order" + | Some Dormant -> + if Int64.compare triggered_slice_sequence 0L < 0 then + Error "trigger slice sequence must be nonnegative" + else + Ok + { + order with + updated_event_id; + trigger_state = + Some (Triggered { triggered_at; triggered_slice_sequence }); + } + +let effective_kind order = + match (order.request.kind, order.trigger_state) with + | ((Market | Limit _) as kind), _ -> Some kind + | Stop _, Some Dormant | Stop_limit _, Some Dormant -> None + | Stop _, Some (Triggered _) -> Some Market + | Stop_limit { limit_price; _ }, Some (Triggered _) -> + Some (Limit limit_price) + | (Stop _ | Stop_limit _), None -> None let apply_fill order ~quantity ~notional = if not (is_active order) then Error "cannot fill a terminal order" @@ -131,6 +221,20 @@ let adjust_for_split order ~updated_event_id ~numerator ~denominator = Scalar.Price.scale_ratio_exact price ~numerator:denominator ~denominator:numerator |> Result.map (fun price -> Limit price) + | Stop trigger_price -> + Scalar.Price.scale_ratio_exact trigger_price ~numerator:denominator + ~denominator:numerator + |> Result.map (fun price -> Stop price) + | Stop_limit { trigger_price; limit_price } -> + let* trigger_price = + Scalar.Price.scale_ratio_exact trigger_price ~numerator:denominator + ~denominator:numerator + in + let* limit_price = + Scalar.Price.scale_ratio_exact limit_price ~numerator:denominator + ~denominator:numerator + in + Ok (Stop_limit { trigger_price; limit_price }) in if not (Scalar.Quantity.is_positive quantity) then Error "split-adjusted order quantity must be positive" @@ -148,6 +252,22 @@ let side_to_string = function Buy -> "buy" | Sell -> "sell" let kind_to_string = function | Market -> "market" | Limit price -> "limit@" ^ Scalar.Price.to_decimal_string price + | Stop price -> "stop@" ^ Scalar.Price.to_decimal_string price + | Stop_limit { trigger_price; limit_price } -> + "stop_limit@" + ^ Scalar.Price.to_decimal_string trigger_price + ^ "/" + ^ Scalar.Price.to_decimal_string limit_price + +let time_in_force_to_string = function + | Gtc -> "gtc" + | Ioc -> "ioc" + | Fok -> "fok" + | Day { venue_id; calendar_id } -> + Printf.sprintf "day@%s/%s" + (Id.Venue.to_string venue_id) + (Id.Venue_calendar.to_string calendar_id) + | Gtd expires_at -> "gtd@" ^ Ptime.to_rfc3339 expires_at let origin_to_string = function | Direct -> "direct" diff --git a/lib/order.mli b/lib/order.mli index dffa9d4..b10c9e6 100644 --- a/lib/order.mli +++ b/lib/order.mli @@ -1,7 +1,23 @@ (** Immutable orders and their legal state transitions. *) type side = Buy | Sell -type kind = Market | Limit of Scalar.Price.t + +type kind = + | Market + | Limit of Scalar.Price.t + | Stop of Scalar.Price.t + | Stop_limit of { + trigger_price : Scalar.Price.t; + limit_price : Scalar.Price.t; + } + +type time_in_force = + | Gtc + | Ioc + | Fok + | Day of { venue_id : Id.Venue.t; calendar_id : Id.Venue_calendar.t } + | Gtd of Ptime.t + type origin = Direct | Target_rebalance | Margin_liquidation type request = private { @@ -9,9 +25,14 @@ type request = private { side : side; quantity : Scalar.Quantity.t; kind : kind; + time_in_force : time_in_force; origin : origin; } +type trigger_state = + | Dormant + | Triggered of { triggered_at : Ptime.t; triggered_slice_sequence : int64 } + type status = | Working | Partially_filled @@ -29,9 +50,14 @@ type t = private { eligible_after_slice_sequence : int64; filled_quantity : Scalar.Quantity.t; filled_notional : Scalar.Money.t; + trigger_state : trigger_state option; status : status; } +val compatibility_time_in_force : kind -> time_in_force +(** Preserve the pre-v8 mapping: market orders are IOC and all other kinds are + GTC. *) + val request : instrument_id:Id.Instrument.t -> side:side -> @@ -40,6 +66,15 @@ val request : origin:origin -> (request, string) result +val request_v8 : + instrument_id:Id.Instrument.t -> + side:side -> + quantity:Scalar.Quantity.t -> + kind:kind -> + time_in_force:time_in_force -> + origin:origin -> + (request, string) result + val accept : id:Id.Order.t -> created_event_id:Id.Event.t -> @@ -63,6 +98,17 @@ val remaining_quantity : t -> Scalar.Quantity.t val is_active : t -> bool val is_terminal : t -> bool val is_market : t -> bool +val is_ioc : t -> bool +val is_fok : t -> bool +val is_dormant_stop : t -> bool +val effective_kind : t -> kind option + +val trigger : + t -> + updated_event_id:Id.Event.t -> + triggered_at:Ptime.t -> + triggered_slice_sequence:int64 -> + (t, string) result val apply_fill : t -> @@ -81,6 +127,7 @@ val adjust_for_split : val side_to_string : side -> string val kind_to_string : kind -> string +val time_in_force_to_string : time_in_force -> string val origin_to_string : origin -> string val status_to_string : status -> string val pp : Format.formatter -> t -> unit diff --git a/lib/replay.ml b/lib/replay.ml index d914d89..6288f3a 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -74,9 +74,9 @@ let run ~scenario_sha256 ?journal_path ?(durability = Artifact_writer.Buffered) Scripted_strategy.create scenario.Scenario.schedule |> reducer_result in let* config = - Engine.config ~contract_version:scenario.contract_version - ~risk:scenario.risk ~execution_model:scenario.execution_model - ~execution:scenario.execution + Engine.config_v8 ~contract_version:scenario.contract_version + ~risk:scenario.risk ~venue_calendars:scenario.venue_calendars + ~execution_model:scenario.execution_model ~execution:scenario.execution ~max_internal_events:scenario.max_internal_events |> reducer_result in @@ -153,8 +153,9 @@ let run_stream_pass ~scenario_sha256 ~journal channel = | Error _ as error -> error | Ok strategy_state -> ( match - Engine.config ~contract_version:header.contract_version - ~risk:header.Scenario.risk ~execution_model:header.execution_model + Engine.config_v8 ~contract_version:header.contract_version + ~risk:header.Scenario.risk ~venue_calendars:header.venue_calendars + ~execution_model:header.execution_model ~execution:header.execution ~max_internal_events:header.max_internal_events |> reducer_result diff --git a/lib/risk.ml b/lib/risk.ml index 00dca5c..ae000f8 100644 --- a/lib/risk.ml +++ b/lib/risk.ml @@ -783,9 +783,15 @@ let check_alignment instrument request = else match request.kind with | Order.Market -> Ok () - | Order.Limit price -> + | Order.Limit price | Order.Stop price -> if Scalar.Price.is_multiple price ~tick:instrument.tick_size then Ok () - else Error "limit price is not aligned to the instrument tick size" + else Error "order price is not aligned to the instrument tick size" + | Order.Stop_limit { trigger_price; limit_price } -> + if + Scalar.Price.is_multiple trigger_price ~tick:instrument.tick_size + && Scalar.Price.is_multiple limit_price ~tick:instrument.tick_size + then Ok () + else Error "order price is not aligned to the instrument tick size" type reservations = { buys : Scalar.Quantity.t; sells : Scalar.Quantity.t } diff --git a/lib/scenario.ml b/lib/scenario.ml index 39e39d4..f58799c 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -550,7 +550,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 String.equal contract_version "7" then + if List.mem contract_version [ "8"; "7" ] then parse_v7_risk base_currency instruments json else parse_legacy_risk base_currency instruments json @@ -623,7 +623,7 @@ let parse_versioned_execution ~contract_version json = Ok (execution_model, execution) let parse_execution ~contract_version json = - if List.mem contract_version [ "7"; "6"; "5" ] then + if List.mem contract_version [ "8"; "7"; "6"; "5" ] then parse_versioned_execution ~contract_version json else parse_legacy_execution ~contract_version json @@ -671,18 +671,34 @@ let parse_portfolio_intent ~name ~parse_target make json = let* targets = map_list parse_target targets_json in Ok (make targets) -let parse_submit_intent json = +let parse_submit_intent ~contract_version json = + let versioned = String.equal contract_version "8" in let* fields = object_fields ~name:"submit_order intent" ~expected: - [ - "type"; - "instrument_id"; - "side"; - "quantity"; - "order_kind"; - "limit_price"; - ] + (if versioned then + [ + "type"; + "instrument_id"; + "side"; + "quantity"; + "order_kind"; + "trigger_price"; + "limit_price"; + "time_in_force"; + "venue_id"; + "calendar_id"; + "expires_at"; + ] + else + [ + "type"; + "instrument_id"; + "side"; + "quantity"; + "order_kind"; + "limit_price"; + ]) json in let* instrument_json = field fields "instrument_id" in @@ -696,17 +712,53 @@ let parse_submit_intent json = let* kind_json = field fields "order_kind" in let* kind_name = string ~name:"order_kind" kind_json in let* limit_json = field fields "limit_price" in + let* trigger_json = + if versioned then field fields "trigger_price" else Ok `Null + in let* kind = - match (kind_name, limit_json) with - | "market", `Null -> Ok Order.Market - | "limit", value -> + match (kind_name, trigger_json, limit_json) with + | "market", `Null, `Null -> Ok Order.Market + | "limit", `Null, value -> let* limit = parse_price ~name:"limit_price" value in Ok (Order.Limit limit) - | "market", _ -> Error "market order limit_price must be null" + | "stop", trigger, `Null when versioned -> + let* trigger = parse_price ~name:"trigger_price" trigger in + Ok (Order.Stop trigger) + | "stop_limit", trigger, limit when versioned -> + let* trigger_price = parse_price ~name:"trigger_price" trigger in + let* limit_price = parse_price ~name:"limit_price" limit in + Ok (Order.Stop_limit { trigger_price; limit_price }) + | "market", _, _ -> + Error "market order trigger_price and limit_price must be null" | _ -> Error "invalid order_kind" in + let* time_in_force = + if not versioned then Ok (Order.compatibility_time_in_force kind) + else + let* tif_json = field fields "time_in_force" in + let* tif = string ~name:"time_in_force" tif_json in + let* venue_json = field fields "venue_id" in + let* calendar_json = field fields "calendar_id" in + let* expires_json = field fields "expires_at" in + match (tif, venue_json, calendar_json, expires_json) with + | "gtc", `Null, `Null, `Null -> Ok Order.Gtc + | "ioc", `Null, `Null, `Null -> Ok Order.Ioc + | "fok", `Null, `Null, `Null -> Ok Order.Fok + | "day", venue, calendar, `Null -> + let* venue_id = parse_id Id.Venue.of_string ~name:"venue_id" venue in + let* calendar_id = + parse_id Id.Venue_calendar.of_string ~name:"calendar_id" calendar + in + Ok (Order.Day { venue_id; calendar_id }) + | "gtd", `Null, `Null, expires -> + let* value = string ~name:"expires_at" expires in + let* expires_at = Codec.ptime_of_string value in + Ok (Order.Gtd expires_at) + | _ -> Error "time_in_force companion fields are inconsistent" + in let* request = - Order.request ~instrument_id ~side ~quantity ~kind ~origin:Order.Direct + Order.request_v8 ~instrument_id ~side ~quantity ~kind ~time_in_force + ~origin:Order.Direct in Ok (Strategy.Submit_order request) @@ -731,7 +783,7 @@ let parse_metric_intent json = let* value = string ~name:"metric value" value_json in Ok (Strategy.Emit_metric { name; value }) -let parse_intent json = +let parse_intent ~contract_version json = match json with | `Assoc fields -> ( match List.assoc_opt "type" fields with @@ -745,20 +797,21 @@ let parse_intent json = ~parse_target:parse_quantity_target (fun targets -> Strategy.Target_quantities targets) json - | Some (`String "submit_order") -> parse_submit_intent json + | Some (`String "submit_order") -> + parse_submit_intent ~contract_version json | Some (`String "cancel_order") -> parse_cancel_intent json | Some (`String "emit_metric") -> parse_metric_intent json | Some _ -> Error "unsupported intent type" | None -> Error "intent is missing type") | _ -> Error "intent must be a JSON object" -let intent_of_yojson json = - parse_intent json +let intent_of_yojson ?(contract_version = Contract.previous_version) json = + parse_intent ~contract_version json |> Result.map_error (fun message -> Diagnostic.make ~code:Diagnostic.Scenario_invalid ~phase:Diagnostic.Validation ~json_path:"$" message) -let parse_schedule_item json = +let parse_schedule_item ~contract_version json = let* fields = object_fields ~name:"schedule item" ~expected:[ "after_slice_sequence"; "intents" ] @@ -773,7 +826,7 @@ let parse_schedule_item json = (Printf.sprintf "intent count is %d; limit is %d" (List.length intents_json) Resource_limits.intents_per_batch) else - let* intents = map_list parse_intent intents_json in + let* intents = map_list (parse_intent ~contract_version) intents_json in Ok (sequence, intents) let parse_volume = function @@ -1010,7 +1063,7 @@ let construct_header ~root ~contract_path ~contract_version |> at (child root "base_currency") in let* initial_cash, initial_portfolio = - if List.mem contract_version [ "7"; "6" ] then + if List.mem contract_version [ "8"; "7"; "6" ] then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state |> at (child root "initial_portfolio") @@ -1100,7 +1153,11 @@ let construct_batch (shape : Scenario_shape.batch) = let* schedule_json = list ~name:"schedule" shape.schedule |> at "$.schedule" in - let* schedule = map_list_at "$.schedule" parse_schedule_item schedule_json in + let* schedule = + map_list_at "$.schedule" + (parse_schedule_item ~contract_version) + schedule_json + in let* slices_json = list ~name:"slices" shape.slices |> at "$.slices" in let* slices = map_list_at "$.slices" parse_slice slices_json in let* () = @@ -1205,7 +1262,9 @@ let stream_item_of_yojson header ~previous json = |> Result.map_error (diagnostic code) in let* intents = - map_list_at "$.payload.intents" parse_intent intents_json + map_list_at "$.payload.intents" + (parse_intent ~contract_version:header.contract_version) + intents_json |> Result.map_error (diagnostic code) in let previous_slice, previous_intents, prior_action_ids = diff --git a/lib/scenario.mli b/lib/scenario.mli index 5b6953c..8a49b17 100644 --- a/lib/scenario.mli +++ b/lib/scenario.mli @@ -41,7 +41,11 @@ type stream_item = private { val of_yojson : Yojson.Safe.t -> (t, Diagnostic.t) result val of_string : string -> (t, Diagnostic.t) result val read_file : string -> (t, Diagnostic.t) result -val intent_of_yojson : Yojson.Safe.t -> (Strategy.intent, Diagnostic.t) result + +val intent_of_yojson : + ?contract_version:string -> + Yojson.Safe.t -> + (Strategy.intent, Diagnostic.t) result val stream_header_of_yojson : contract_version:string -> diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index 150be88..0d2c348 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -68,13 +68,13 @@ 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 [ "7"; "6" ] then "initial_portfolio" + if List.mem contract_version [ "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 [ "7"; "6"; "5" ] then + if List.mem contract_version [ "8"; "7"; "6"; "5" ] then List.assoc_opt "venue_calendars" fields else None in @@ -105,11 +105,12 @@ let batch json = match preliminary with `String value -> value | _ -> "" in let calendar_fields = - if List.mem contract_version [ "7"; "6"; "5" ] then [ "venue_calendars" ] + if List.mem contract_version [ "8"; "7"; "6"; "5" ] then + [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "7"; "6" ] then "initial_portfolio" + if List.mem contract_version [ "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in let* fields = @@ -140,11 +141,12 @@ let batch json = let stream_header ~contract_version json = let root = "$.payload" in let calendar_fields = - if List.mem contract_version [ "7"; "6"; "5" ] then [ "venue_calendars" ] + if List.mem contract_version [ "8"; "7"; "6"; "5" ] then + [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "7"; "6" ] then "initial_portfolio" + if List.mem contract_version [ "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in let* fields = diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 8dc42a1..840b3fd 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -48,7 +48,7 @@ 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 [ "7"; "6" ] then Ok () + if List.mem contract_version [ "8"; "7"; "6" ] then Ok () else Account.create ~base_currency ~initial_cash |> Result.map (fun _ -> ()) @@ -66,7 +66,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path:(child root "instruments") "instrument IDs must be unique" else let* () = - if List.mem contract_version [ "7"; "6"; "5" ] then + if List.mem contract_version [ "8"; "7"; "6"; "5" ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -84,7 +84,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path: (child root - (if List.mem contract_version [ "7"; "6" ] then + (if List.mem contract_version [ "8"; "7"; "6" ] then "initial_portfolio.cash" else "initial_cash")) "initial cash must contain every scenario currency exactly once" @@ -270,12 +270,22 @@ let validate_portfolio_target ~json_path risk catalog = function else match request.kind with | Order.Market -> Ok () - | Order.Limit price -> + | Order.Limit price | Order.Stop price -> if Scalar.Price.is_multiple price ~tick:instrument.tick_size then Ok () else fail ~json_path - "limit price is not aligned to the instrument tick size")) + "order price is not aligned to the instrument tick size" + | Order.Stop_limit { trigger_price; limit_price } -> + if + Scalar.Price.is_multiple trigger_price + ~tick:instrument.tick_size + && Scalar.Price.is_multiple limit_price + ~tick:instrument.tick_size + then Ok () + else + fail ~json_path + "order price is not aligned to the instrument tick size")) | Strategy.Cancel_order _ | Strategy.Emit_metric _ -> Ok () let validate_slices_at ~paths ~base_currency ~currencies ~instruments slices = diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index aa4630a..9351ac6 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -10,6 +10,7 @@ type initialization = { initial_cash : (string * Scalar.Money.t) list; initial_portfolio : Initial_portfolio.t option; instruments : Instrument.t list; + venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; @@ -60,6 +61,35 @@ let instrument_to_yojson instrument = ("lot_size", quantity instrument.lot_size); ] +let phase_to_yojson (phase : Venue_calendar.phase) = + `Assoc + [ + ("phase", string (Venue_calendar.phase_kind_to_string phase.kind)); + ("opens_at", timestamp phase.opens_at); + ("closes_at", timestamp phase.closes_at); + ] + +let session_to_yojson (session : Venue_calendar.session) = + `Assoc + [ + ("session_date", string session.session_date); + ("policy", string (Venue_calendar.session_kind_to_string session.kind)); + ("phases", `List (List.map phase_to_yojson session.phases)); + ] + +let venue_calendar_to_yojson (calendar : Venue_calendar.t) = + `Assoc + [ + ("calendar_id", string (Id.Venue_calendar.to_string calendar.id)); + ("calendar_version", string calendar.version); + ("venue_id", string (Id.Venue.to_string calendar.venue_id)); + ( "instrument_ids", + `List + (calendar.instrument_ids |> Id.Instrument.Set.elements + |> List.map instrument_id) ); + ("sessions", `List (List.map session_to_yojson calendar.sessions)); + ] + let group_kind_to_string = function | Risk.Issuer -> "issuer" | Risk.Sector -> "sector" @@ -69,6 +99,7 @@ let group_kind_to_string = function | Risk.Custom -> "custom" let nullable render = Option.fold ~none:`Null ~some:render +let modern_protocol protocol_version = List.mem protocol_version [ "6"; "5" ] let instrument_policy_to_yojson (policy : Risk.instrument_policy) = `Assoc @@ -104,7 +135,7 @@ let group_to_yojson (group : Risk.group) = ] let risk_to_yojson ~protocol_version risk = - if String.equal protocol_version version then + if modern_protocol protocol_version then `Assoc [ ("max_gross_exposure", money (Risk.max_gross_exposure risk)); @@ -130,7 +161,7 @@ let risk_to_yojson ~protocol_version risk = ] let execution_to_yojson ~protocol_version model execution = - if String.equal protocol_version version then + if modern_protocol protocol_version then `Assoc [ ("model", string (Execution_model.name model)); @@ -174,6 +205,12 @@ let initialize_message ~sequence:message_sequence initialization = (fun (left, _) (right, _) -> String.compare left right) initialization.initial_cash in + let venue_calendars = + List.sort + (fun left right -> + Id.Venue_calendar.compare left.Venue_calendar.id right.Venue_calendar.id) + initialization.venue_calendars + in let fields = [ ("engine_version", string Contract.engine_version); @@ -193,6 +230,21 @@ let initialize_message ~sequence:message_sequence initialization = in let fields = if String.equal protocol_version version then + let initial_portfolio = + Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson + initialization.initial_portfolio + in + List.concat + [ + List.take 6 fields; + [ + ("initial_portfolio", initial_portfolio); + ( "venue_calendars", + `List (List.map venue_calendar_to_yojson venue_calendars) ); + ]; + List.drop 6 fields; + ] + else if modern_protocol protocol_version then let initial_portfolio = Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson initialization.initial_portfolio @@ -282,7 +334,7 @@ let context_to_yojson ~protocol_version context = ] in let portfolio_fields = - if String.equal protocol_version version then + if modern_protocol protocol_version then portfolio_fields @ [ ( "group_exposures", @@ -295,11 +347,17 @@ let context_to_yojson ~protocol_version context = [ ("now", timestamp (Strategy.now context)); ("portfolio", `Assoc portfolio_fields); - ("working_orders", `List (List.map Codec.order_to_yojson working_orders)); + ( "working_orders", + `List + (List.map + (if String.equal protocol_version version then + Codec.order_to_yojson_v8 + else Codec.order_to_yojson) + working_orders) ); ("latest_bars", `List (List.map Codec.bar_to_yojson latest_bars)); ] -let event_to_yojson = function +let event_to_yojson ~protocol_version = function | Strategy.Market_slice_closed market_slice -> `Assoc [ @@ -315,7 +373,10 @@ let event_to_yojson = function `Assoc [ ("type", string "order_updated"); - ("order", Codec.order_to_yojson order); + ( "order", + if String.equal protocol_version version then + Codec.order_to_yojson_v8 order + else Codec.order_to_yojson order ); ] | Strategy.Intent_rejected reason -> `Assoc [ ("type", string "intent_rejected"); ("reason", string reason) ] @@ -326,7 +387,7 @@ let event_message ?(protocol_version = version) ~sequence:message_sequence (`Assoc [ ("context", context_to_yojson ~protocol_version context); - ("event", event_to_yojson event); + ("event", event_to_yojson ~protocol_version event); ]) let shutdown_message_for ~protocol_version ~sequence:message_sequence = @@ -384,7 +445,7 @@ let parse_ready_payload json = let* version = optional_string ~name:"strategy_version" version_json in Ok (Ready { name; version }) -let parse_intents_payload json = +let parse_intents_payload ~protocol_version json = let* fields = object_fields ~name:"strategy intents payload" ~expected:[ "intents" ] json in @@ -399,7 +460,10 @@ let parse_intents_payload json = (fun result value -> let* intents = result in let* intent = - Scenario.intent_of_yojson value + Scenario.intent_of_yojson + ~contract_version: + (if String.equal protocol_version version then "8" else "7") + value |> Result.map_error Diagnostic.to_human in Ok (intent :: intents)) @@ -447,7 +511,7 @@ let response_of_yojson_result ~protocol_version ~expected_sequence json = let* payload = field fields "payload" in match message_type with | "ready" -> parse_ready_payload payload - | "intents" -> parse_intents_payload payload + | "intents" -> parse_intents_payload ~protocol_version payload | "stopped" -> parse_stopped_payload payload | "error" -> parse_error_payload payload diff --git a/lib/strategy_protocol.mli b/lib/strategy_protocol.mli index 936911f..170e614 100644 --- a/lib/strategy_protocol.mli +++ b/lib/strategy_protocol.mli @@ -12,6 +12,7 @@ type initialization = { initial_cash : (string * Scalar.Money.t) list; initial_portfolio : Initial_portfolio.t option; instruments : Instrument.t list; + venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; diff --git a/mkdocs.yml b/mkdocs.yml index 50caa34..fe99493 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,14 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v7: contracts/v7/README.md + - Current v8: contracts/v8/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 v5: contracts/strategy/v5/README.md + - Current v6: contracts/strategy/v6/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 3d15677..ca96b46 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -50,3 +50,11 @@ compare_journal \ v5-fill-clipped \ contracts/v5/fixtures/fill-clipped.scenario.json \ contracts/v5/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v8-demo \ + contracts/v8/fixtures/demo.scenario.json \ + contracts/v8/fixtures/demo.journal.jsonl +compare_journal \ + v8-fill-clipped \ + contracts/v8/fixtures/fill-clipped.scenario.json \ + contracts/v8/fixtures/fill-clipped.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index 7745b05..e3ed051 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/v7/README.md", + "contracts/v8/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v5/README.md", + "contracts/strategy/v6/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 7a1aab7..39a0eda 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/v7/scenario.schema.json", - "share/trading_engine/contracts/v7/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v8/scenario.schema.json", + "share/trading_engine/contracts/v8/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/v7/fixtures/demo.scenario.json", + "contracts/v8/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/v7/fixtures/demo.scenario.json", - "contracts/strategy/v5/message.schema.json", + "contracts/v8/fixtures/demo.scenario.json", + "contracts/strategy/v6/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v7/scenario.schema.json", + "contracts/v8/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index bb85f0c..9f571c2 100644 --- a/test/cli.t +++ b/test/cli.t @@ -2,22 +2,22 @@ 1.0.0 $ ../bin/main.exe --capabilities - {"engine_version":"1.0.0","scenario_contract_versions":["7","6","5","4","3"],"journal_contract_versions":["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":["1"],"scenario_contract_versions":["7","6","5","4","3"],"required_fields":["version","participation_bps","fixed_fee","fee_bps"],"supported_order_types":["market","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":["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":["8","7","6","5","4","3"],"journal_contract_versions":["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":["1"],"scenario_contract_versions":["8","7","6","5","4","3"],"required_fields":["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":["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/v7/fixtures/demo.scenario.json - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=d1991fa67140bff80fcbeb9b04b211d8c9cf4f41d4fba39dcec66d5ef3e5fab9 + $ ../bin/main.exe --validate-only --input ../contracts/v8/fixtures/demo.scenario.json + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=85f7c99e0666159579c79256b3d0dc5f9c328e1275b79465fe1d4c93883e68f1 - $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v7/fixtures/demo.scenario.jsonl - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=61486a162021bad1302fbce67fde0926821f3d521dd7376164a288d4b3f03e76 + $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=786f38d8bd10faac03b6b15c7aa8ae0a867eedc609ca6eaa75cfd93ae3ffdcae - $ ../bin/main.exe --input-format jsonl --input ../contracts/v7/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts + $ ../bin/main.exe --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts run=demo audits=22 orders=3 active=0 filled=2 rejected=0 cash=9846.65392 equity=10111.65392 gross=265 realized=18.965682 unrealized=7.688238 fees=3.16608 journal=streamed.journal.jsonl $ wc -l < streamed.journal.jsonl 22 - $ head -n 5 ../contracts/v7/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl + $ head -n 5 ../contracts/v8/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl $ ../bin/main.exe --validate-only --input-format jsonl --input truncated.scenario.jsonl trading-engine: scenario_end must terminate the scenario stream [123] @@ -32,32 +32,32 @@ 1 scenario_stream.invalid validation 6 6 None - $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v7/fixtures/demo.scenario.json > invalid-tick.json + $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v8/fixtures/demo.scenario.json > invalid-tick.json $ ../bin/main.exe --validate-only --input invalid-tick.json trading-engine: market prices and volumes must align with instrument increments [123] - $ ../bin/main.exe --input ../contracts/v7/fixtures/demo.scenario.json + $ ../bin/main.exe --input ../contracts/v8/fixtures/demo.scenario.json trading-engine: --journal is required unless --validate-only is set [123] - $ ../bin/main.exe --validate-only --input ../contracts/v7/fixtures/demo.scenario.json --journal validation.journal.jsonl + $ ../bin/main.exe --validate-only --input ../contracts/v8/fixtures/demo.scenario.json --journal validation.journal.jsonl trading-engine: --journal cannot be used with --validate-only [123] $ test ! -e validation.journal.jsonl - $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v7/fixtures/demo.scenario.json + $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v8/fixtures/demo.scenario.json trading-engine: --durable-artifacts cannot be used with --validate-only [123] - $ ../bin/main.exe --input ../contracts/v7/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 + $ ../bin/main.exe --input ../contracts/v8/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 trading-engine: --strategy-arg, --strategy-timeout, and --strategy-transcript require --strategy-executable [123] $ test ! -e ignored.journal.jsonl $ mkdir external - $ ../bin/main.exe --input ../contracts/strategy/v5/fixtures/external.scenario.json --journal external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external/run.strategy.jsonl --strategy-timeout 5 --durable-artifacts + $ ../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external/run.strategy.jsonl --strategy-timeout 5 --durable-artifacts run=external-demo audits=12 orders=1 active=0 filled=1 rejected=0 cash=9793.544 equity=10007.544 gross=214 realized=0 unrealized=7.544 fees=0.456 journal=external/run.journal.jsonl @@ -65,10 +65,10 @@ $ python3 -c 'from pathlib import Path; print(len(Path("external/run.journal.jsonl").read_text().splitlines()), len(Path("external/run.strategy.jsonl").read_text().splitlines()))' 12 14 - $ diff -u ../contracts/strategy/v5/fixtures/external.strategy.jsonl external/run.strategy.jsonl + $ diff -u ../contracts/strategy/v6/fixtures/external.strategy.jsonl external/run.strategy.jsonl $ mkdir callback-ordering - $ ../bin/main.exe --input ../contracts/strategy/v5/fixtures/external.scenario.json --journal callback-ordering/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg cancel-next --strategy-transcript callback-ordering/run.strategy.jsonl --strategy-timeout 5 + $ ../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal callback-ordering/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg cancel-next --strategy-transcript callback-ordering/run.strategy.jsonl --strategy-timeout 5 run=external-demo audits=12 orders=2 active=0 filled=1 rejected=0 cash=9896.647 equity=10003.647 gross=107 realized=0 unrealized=3.647 fees=0.353 journal=callback-ordering/run.journal.jsonl @@ -79,7 +79,7 @@ 107 107 $ mkdir failed-external - $ ../bin/main.exe --input ../contracts/strategy/v5/fixtures/external.scenario.json --journal failed-external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg stall --strategy-transcript failed-external/run.strategy.jsonl --strategy-timeout 0.01 + $ ../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal failed-external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg stall --strategy-transcript failed-external/run.strategy.jsonl --strategy-timeout 0.01 trading-engine: strategy initialization: external strategy timed out [123] $ test ! -e failed-external/run.journal.jsonl @@ -92,7 +92,7 @@ > expected="$2" > directory="fault-$mode" > mkdir "$directory" - > output=$(../bin/main.exe --input ../contracts/strategy/v5/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 5 2>&1) + > output=$(../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 5 2>&1) > status=$? > test "$status" -eq 123 || return 1 > case "$output" in *"$expected"*) ;; *) return 1 ;; esac @@ -169,7 +169,7 @@ > directory="process-tree-$mode" > mkdir "$directory" > pid_path="$directory/grandchild.pid" - > output=$(../bin/main.exe --input ../contracts/strategy/v5/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-arg "$pid_path" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 0.2 2>&1) + > output=$(../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-arg "$pid_path" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 0.2 2>&1) > status=$? > test "$status" -eq 123 || return 1 > case "$output" in *"$expected"*) ;; *) return 1 ;; esac @@ -198,7 +198,7 @@ grandchild-malformed: process tree reaped $ mkdir external-stream - $ ../bin/main.exe --input-format jsonl --input ../contracts/strategy/v5/fixtures/external.scenario.jsonl --journal external-stream/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-stream/run.strategy.jsonl --strategy-timeout 5 + $ ../bin/main.exe --input-format jsonl --input ../contracts/strategy/v6/fixtures/external.scenario.jsonl --journal external-stream/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-stream/run.strategy.jsonl --strategy-timeout 5 run=external-demo audits=12 orders=1 active=0 filled=1 rejected=0 cash=9793.544 equity=10007.544 gross=214 realized=0 unrealized=7.544 fees=0.456 journal=external-stream/run.journal.jsonl diff --git a/test/dune b/test/dune index cb9e70e..41366b2 100644 --- a/test/dune +++ b/test/dune @@ -6,6 +6,7 @@ test_domain test_accounting test_execution + test_order_lifetimes test_reducer test_reducer_properties test_checkpoint4 @@ -25,6 +26,14 @@ ../contracts/v7/journal.schema.json ../contracts/v7/scenario-stream.schema.json ../contracts/v7/scenario.schema.json + ../contracts/v8/fixtures/demo.journal.jsonl + ../contracts/v8/fixtures/demo.scenario.json + ../contracts/v8/fixtures/demo.scenario.jsonl + ../contracts/v8/fixtures/fill-clipped.journal.jsonl + ../contracts/v8/fixtures/fill-clipped.scenario.json + ../contracts/v8/journal.schema.json + ../contracts/v8/scenario-stream.schema.json + ../contracts/v8/scenario.schema.json ../contracts/v6/fixtures/demo.scenario.json ../contracts/v6/fixtures/demo.scenario.jsonl ../contracts/v5/fixtures/demo.scenario.json @@ -35,6 +44,7 @@ ../contracts/v3/fixtures/demo.scenario.jsonl ../contracts/conformance/cases.json ../contracts/strategy/v5/fixtures/external.strategy.jsonl + ../contracts/strategy/v6/fixtures/external.strategy.jsonl ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries @@ -65,11 +75,53 @@ (deps ../bin/main.exe fake_strategy.py - ../contracts/strategy/v5/fixtures/external.scenario.json - ../contracts/strategy/v5/fixtures/external.scenario.jsonl - ../contracts/strategy/v5/fixtures/external.strategy.jsonl - ../contracts/v7/fixtures/demo.scenario.json - ../contracts/v7/fixtures/demo.scenario.jsonl)) + ../contracts/strategy/v6/fixtures/external.scenario.json + ../contracts/strategy/v6/fixtures/external.scenario.jsonl + ../contracts/strategy/v6/fixtures/external.strategy.jsonl + ../contracts/v8/fixtures/demo.scenario.json + ../contracts/v8/fixtures/demo.scenario.jsonl)) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v8/fixtures/demo.journal.jsonl + ../contracts/v8/fixtures/demo.scenario.json + ../contracts/v8/fixtures/demo.scenario.jsonl + ../contracts/v8/journal.schema.json + ../contracts/v8/scenario-stream.schema.json + ../contracts/v8/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v8/scenario.schema.json} + %{dep:../contracts/v8/scenario-stream.schema.json} + %{dep:../contracts/v8/journal.schema.json} + %{dep:../contracts/v8/fixtures/demo.scenario.json} + %{dep:../contracts/v8/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v8/fixtures/demo.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v8/fixtures/fill-clipped.journal.jsonl + ../contracts/v8/fixtures/fill-clipped.scenario.json + ../contracts/v8/fixtures/demo.scenario.jsonl + ../contracts/v8/journal.schema.json + ../contracts/v8/scenario-stream.schema.json + ../contracts/v8/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v8/scenario.schema.json} + %{dep:../contracts/v8/scenario-stream.schema.json} + %{dep:../contracts/v8/journal.schema.json} + %{dep:../contracts/v8/fixtures/fill-clipped.scenario.json} + %{dep:../contracts/v8/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v8/fixtures/fill-clipped.journal.jsonl}))) (rule (alias runtest) @@ -134,6 +186,27 @@ %{dep:../contracts/v3/fixtures/demo.scenario.jsonl} %{dep:../contracts/v3/fixtures/demo.journal.jsonl}))) +(rule + (alias runtest) + (deps + validate_strategy_schema.py + ../contracts/v8/scenario.schema.json + ../contracts/v8/journal.schema.json + ../contracts/diagnostic/v1/diagnostic.schema.json + ../contracts/strategy/v6/message.schema.json + ../contracts/strategy/v6/transcript.schema.json + ../contracts/strategy/v6/fixtures/external.strategy.jsonl) + (action + (run + python3 + %{dep:validate_strategy_schema.py} + %{dep:../contracts/v8/scenario.schema.json} + %{dep:../contracts/v8/journal.schema.json} + %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} + %{dep:../contracts/strategy/v6/message.schema.json} + %{dep:../contracts/strategy/v6/transcript.schema.json} + %{dep:../contracts/strategy/v6/fixtures/external.strategy.jsonl}))) + (rule (alias runtest) (deps @@ -201,6 +274,6 @@ (deps test_benchmark_replay.py ../bench/benchmark_replay.py - ../contracts/v7/fixtures/demo.scenario.json) + ../contracts/v8/fixtures/demo.scenario.json) (action (run python3 %{dep:test_benchmark_replay.py}))) diff --git a/test/fake_strategy.py b/test/fake_strategy.py index 4aa20cc..260725b 100755 --- a/test/fake_strategy.py +++ b/test/fake_strategy.py @@ -75,7 +75,12 @@ def response(request: dict[str, object]) -> dict[str, object]: "side": "buy", "quantity": "1", "order_kind": "market", + "trigger_price": None, "limit_price": None, + "time_in_force": "ioc", + "venue_id": None, + "calendar_id": None, + "expires_at": None, } payload = {"intents": [order, order]} else: diff --git a/test/test_boundary_failures.ml b/test/test_boundary_failures.ml index b8ce96a..dc9a4af 100644 --- a/test/test_boundary_failures.ml +++ b/test/test_boundary_failures.ml @@ -326,6 +326,7 @@ let initialization () = initial_cash = [ ("USD", money "10000") ]; initial_portfolio = None; instruments = [ instrument ]; + venue_calendars = []; risk = risk ~instruments:[ instrument ] (); execution_model = T.Execution_model.find "completed_bar_v1" |> ok; execution = execution (); diff --git a/test/test_checkpoint4.ml b/test/test_checkpoint4.ml index 36c9900..a350f59 100644 --- a/test/test_checkpoint4.ml +++ b/test/test_checkpoint4.ml @@ -159,7 +159,8 @@ let split_adjusts_working_order () = (match order.request.kind with | T.Order.Limit limit -> Alcotest.check price_testable "limit price halves" (price "25") limit - | T.Order.Market -> Alcotest.fail "expected adjusted limit order"); + | T.Order.Market | T.Order.Stop _ | T.Order.Stop_limit _ -> + Alcotest.fail "expected adjusted limit order"); Alcotest.(check (list string)) "causal adjustment events" [ "market_slice_received"; "split_applied"; "order_adjusted"; "valuation" ] diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 723dcf5..8eaa77c 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -118,14 +118,15 @@ let capabilities_describe_execution_contracts () = (strings "configuration_versions"); Alcotest.(check (list string)) "scenario contracts" - [ "7"; "6"; "5"; "4"; "3" ] + [ "8"; "7"; "6"; "5"; "4"; "3" ] (strings "scenario_contract_versions"); Alcotest.(check (list string)) "required fields" [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ] (strings "required_fields"); Alcotest.(check (list string)) - "order types" [ "market"; "limit" ] + "order types" + [ "market"; "limit"; "stop"; "stop_limit" ] (strings "supported_order_types"); Alcotest.(check (list string)) "market data" [ "completed_ohlcv_bars" ] diff --git a/test/test_engine.ml b/test/test_engine.ml index 4dd258d..be83d00 100644 --- a/test/test_engine.ml +++ b/test/test_engine.ml @@ -5,6 +5,7 @@ let () = ("domain", Test_domain.tests); ("accounting", Test_accounting.tests); ("execution", Test_execution.tests); + ("order-lifetimes", Test_order_lifetimes.tests); ("reducer", Test_reducer.tests); ("reducer-properties", Test_reducer_properties.tests); ("checkpoint4", Test_checkpoint4.tests); diff --git a/test/test_execution.ml b/test/test_execution.ml index 9881a24..b8a7503 100644 --- a/test/test_execution.ml +++ b/test/test_execution.ml @@ -348,7 +348,8 @@ let cursor_reads_current_oms_and_preserves_capacity () = Alcotest.check order_id_testable "first proposal" first.id proposed.order_id; advance proposed.quantity |> ok - | T.Execution.Finished _ -> Alcotest.fail "expected first proposal" + | T.Execution.Finished _ | T.Execution.Triggered _ -> + Alcotest.fail "expected first proposal" in let oms, _ = T.Oms.cancel oms second.id |> ok in let cursor = @@ -359,11 +360,13 @@ let cursor_reads_current_oms_and_preserves_capacity () = Alcotest.check quantity_testable "unused capacity reaches third order" (quantity "1") proposed.quantity; advance proposed.quantity |> ok - | T.Execution.Finished _ -> Alcotest.fail "expected third-order proposal" + | T.Execution.Finished _ | T.Execution.Triggered _ -> + Alcotest.fail "expected third-order proposal" in match T.Execution.next cursor ~oms |> ok with | T.Execution.Finished _ -> () - | T.Execution.Proposed _ -> Alcotest.fail "expected completed cursor" + | T.Execution.Proposed _ | T.Execution.Triggered _ -> + Alcotest.fail "expected completed cursor" let fills_respect_lot_size () = let configured = instrument ~lot_size:"10" () in diff --git a/test/test_order_lifetimes.ml b/test/test_order_lifetimes.ml new file mode 100644 index 0000000..2ba0f4c --- /dev/null +++ b/test/test_order_lifetimes.ml @@ -0,0 +1,665 @@ +open Test_support +module T = Trading_engine +module Runner = T.Engine.Make (T.Scripted_strategy) + +let event_names events = + List.map (fun event -> T.Audit.event_name event.T.Audit.event) events + +let runner ?(initial_cash = "10000") ?(risk = risk ()) ?(venue_calendars = []) + schedule = + let strategy_state = T.Scripted_strategy.create schedule |> ok in + Runner.create ~run_id:(run_id "lifetime-run") ~scenario_sha256 + ~config:(engine_config_v8 ~risk ~venue_calendars ()) + ~initial_cash:[ ("USD", money initial_cash) ] + ~strategy_state + |> ok + +let calendar ?(venue = "XNAS") ?(covered_instrument = "test-equity") () = + let phase = + T.Venue_calendar.create_phase ~kind:T.Venue_calendar.Regular + ~opens_at:(timestamp "2026-01-03T14:30:00Z") + ~closes_at:(timestamp "2026-01-03T21:00:00Z") + |> ok + in + let session = + T.Venue_calendar.create_session ~session_date:"2026-01-03" + ~kind:T.Venue_calendar.Regular_session ~phases:[ phase ] + |> ok + in + T.Venue_calendar.create + ~id:(T.Id.Venue_calendar.of_string_exn "xnas-test") + ~version:"1" + ~venue_id:(T.Id.Venue.of_string_exn venue) + ~instrument_ids:[ instrument_id covered_instrument ] + ~sessions:[ session ] + |> ok + +let compatibility_mapping () = + let market = request () in + let limit = request ~kind:(T.Order.Limit (price "100")) () in + Alcotest.(check string) + "legacy market is IOC" "ioc" + (T.Order.time_in_force_to_string market.time_in_force); + Alcotest.(check string) + "legacy limit is GTC" "gtc" + (T.Order.time_in_force_to_string limit.time_in_force) + +let validates_stop_limit_and_gtd () = + Alcotest.(check bool) + "invalid buy stop-limit" true + (Result.is_error + (T.Order.request_v8 + ~instrument_id:(instrument_id "test-equity") + ~side:T.Order.Buy ~quantity:(quantity "1") + ~kind: + (T.Order.Stop_limit + { trigger_price = price "100"; limit_price = price "99" }) + ~time_in_force:T.Order.Gtc ~origin:T.Order.Direct)); + let request = + request_v8 + ~time_in_force:(T.Order.Gtd (timestamp "2026-01-02T20:00:00Z")) + () + in + Alcotest.(check bool) + "expiry follows creation" true + (Result.is_error + (T.Order.accept ~id:(order_id "expired") + ~created_event_id:(event_id "expired-event") ~accepted_sequence:1L + ~created_at:(timestamp "2026-01-02T21:00:00Z") + ~eligible_after_slice_sequence:1L request)) + +let v8_intent_requires_explicit_companions () = + let intent = + `Assoc + [ + ("type", `String "submit_order"); + ("instrument_id", `String "test-equity"); + ("side", `String "buy"); + ("quantity", `String "1"); + ("order_kind", `String "stop"); + ("trigger_price", `String "110"); + ("limit_price", `Null); + ("time_in_force", `String "gtd"); + ("venue_id", `Null); + ("calendar_id", `Null); + ("expires_at", `String "2026-01-03T20:00:00Z"); + ] + in + Alcotest.(check bool) + "explicit stop/GTD parses" true + (Result.is_ok (T.Scenario.intent_of_yojson ~contract_version:"8" intent)); + let missing_trigger = + match intent with + | `Assoc fields -> `Assoc (List.remove_assoc "trigger_price" fields) + | _ -> assert false + in + Alcotest.(check bool) + "missing companion is rejected" true + (Result.is_error + (T.Scenario.intent_of_yojson ~contract_version:"8" missing_trigger)); + let submit kind trigger limit tif venue calendar expires = + `Assoc + [ + ("type", `String "submit_order"); + ("instrument_id", `String "test-equity"); + ("side", `String "buy"); + ("quantity", `String "1"); + ("order_kind", `String kind); + ("trigger_price", trigger); + ("limit_price", limit); + ("time_in_force", `String tif); + ("venue_id", venue); + ("calendar_id", calendar); + ("expires_at", expires); + ] + in + let valid = + [ + submit "market" `Null `Null "ioc" `Null `Null `Null; + submit "limit" `Null (`String "100") "fok" `Null `Null `Null; + submit "stop_limit" (`String "100") (`String "101") "day" (`String "XNAS") + (`String "xnas-test") `Null; + submit "limit" `Null (`String "100") "gtc" `Null `Null `Null; + ] + in + List.iter + (fun json -> + Alcotest.(check bool) + "v8 order variant parses" true + (Result.is_ok (T.Scenario.intent_of_yojson ~contract_version:"8" json))) + valid; + Alcotest.(check bool) + "inconsistent TIF companions rejected" true + (Result.is_error + (T.Scenario.intent_of_yojson ~contract_version:"8" + (submit "market" `Null `Null "gtc" (`String "XNAS") `Null `Null))) + +let order_validation_and_serialization_branches () = + Alcotest.(check bool) + "nonpositive quantity rejected" true + (Result.is_error + (T.Order.request_v8 + ~instrument_id:(instrument_id "test-equity") + ~side:T.Order.Buy ~quantity:T.Scalar.Quantity.zero + ~kind:T.Order.Market ~time_in_force:T.Order.Gtc ~origin:T.Order.Direct)); + Alcotest.(check bool) + "invalid sell stop-limit rejected" true + (Result.is_error + (T.Order.request_v8 + ~instrument_id:(instrument_id "test-equity") + ~side:T.Order.Sell ~quantity:(quantity "1") + ~kind: + (T.Order.Stop_limit + { trigger_price = price "100"; limit_price = price "101" }) + ~time_in_force:T.Order.Gtc ~origin:T.Order.Direct)); + let ordinary = request_v8 () in + Alcotest.(check bool) + "negative accepted sequence rejected" true + (Result.is_error + (T.Order.accept + ~id:(order_id "negative-sequence") + ~created_event_id:(event_id "negative-sequence-event") + ~accepted_sequence:(-1L) + ~created_at:(timestamp "2026-01-02T21:00:00Z") + ~eligible_after_slice_sequence:0L ordinary)); + Alcotest.(check bool) + "negative eligibility rejected" true + (Result.is_error + (T.Order.accept + ~id:(order_id "negative-eligibility") + ~created_event_id:(event_id "negative-eligibility-event") + ~accepted_sequence:1L + ~created_at:(timestamp "2026-01-02T21:00:00Z") + ~eligible_after_slice_sequence:(-1L) ordinary)); + Alcotest.(check bool) + "empty rejection reason rejected" true + (Result.is_error + (T.Order.reject + ~id:(order_id "empty-rejection") + ~created_event_id:(event_id "empty-rejection-event") + ~rejected_sequence:1L + ~created_at:(timestamp "2026-01-02T21:00:00Z") + ~eligible_after_slice_sequence:0L ordinary ~reason:"")); + let cases = + [ + (T.Order.Market, T.Order.Ioc); + (T.Order.Limit (price "100"), T.Order.Fok); + (T.Order.Stop (price "110"), T.Order.Gtc); + ( T.Order.Stop_limit + { trigger_price = price "110"; limit_price = price "111" }, + T.Order.Day + { + venue_id = T.Id.Venue.of_string_exn "XNAS"; + calendar_id = T.Id.Venue_calendar.of_string_exn "xnas-test"; + } ); + ( T.Order.Limit (price "100"), + T.Order.Gtd (timestamp "2026-01-03T20:00:00Z") ); + ] + in + List.iteri + (fun index (kind, time_in_force) -> + let request = request_v8 ~kind ~time_in_force () in + ignore (T.Order.kind_to_string kind); + ignore (T.Order.time_in_force_to_string time_in_force); + let order = + accepted_order ~id:(Printf.sprintf "serialized-%d" index) request + in + ignore (T.Order.is_market order); + ignore (T.Order.effective_kind order); + match T.Codec.order_to_yojson_v8 order with + | `Assoc fields -> + Alcotest.(check bool) + "TIF serialized" true + (List.mem_assoc "time_in_force" fields) + | _ -> Alcotest.fail "serialized order must be an object") + cases; + let unconditional = accepted_order (request_v8 ()) in + Alcotest.(check bool) + "unconditional order cannot trigger" true + (Result.is_error + (T.Order.trigger unconditional + ~updated_event_id:(event_id "invalid-trigger") + ~triggered_at:(timestamp "2026-01-03T20:00:00Z") + ~triggered_slice_sequence:2L)); + let conditional = + accepted_order (request_v8 ~kind:(T.Order.Stop (price "110")) ()) + in + Alcotest.(check bool) + "negative trigger sequence rejected" true + (Result.is_error + (T.Order.trigger conditional + ~updated_event_id:(event_id "invalid-sequence") + ~triggered_at:(timestamp "2026-01-03T20:00:00Z") + ~triggered_slice_sequence:(-1L))); + let triggered = + T.Order.trigger conditional ~updated_event_id:(event_id "valid-trigger") + ~triggered_at:(timestamp "2026-01-03T20:00:00Z") + ~triggered_slice_sequence:2L + |> ok + in + ignore (T.Codec.order_to_yojson_v8 triggered); + Alcotest.(check bool) + "duplicate trigger rejected" true + (Result.is_error + (T.Order.trigger triggered + ~updated_event_id:(event_id "duplicate-trigger") + ~triggered_at:(timestamp "2026-01-03T20:00:00Z") + ~triggered_slice_sequence:3L)); + let cancelled = T.Order.cancel conditional |> ok in + Alcotest.(check bool) + "cancelled order is terminal" true + (T.Order.is_terminal cancelled); + Alcotest.(check bool) + "terminal trigger rejected" true + (Result.is_error + (T.Order.trigger cancelled + ~updated_event_id:(event_id "terminal-trigger") + ~triggered_at:(timestamp "2026-01-03T20:00:00Z") + ~triggered_slice_sequence:2L)) + +let trigger_then_execute_on_following_slice () = + let request = + request_v8 ~kind:(T.Order.Stop (price "110")) ~time_in_force:T.Order.Gtc () + in + let oms, order = oms_with_order request in + let trigger_slice = + market_slice + ~bars:[ bar ~open_price:"100" ~high_price:"115" ~low_price:"95" 2L ] + 2L + in + let pure_match = + T.Execution.match_slice (execution ()) + ~instruments:[ instrument () ] + ~oms trigger_slice + |> ok + in + Alcotest.(check int) + "pure match reports one trigger" 1 + (List.length pure_match.triggers); + Alcotest.(check int) + "trigger slice has no fill" 0 + (List.length pure_match.fills); + let cursor = + T.Execution.start_slice (execution ()) + ~instruments:[ instrument () ] + ~oms trigger_slice + |> ok + in + let triggered_at, triggered_sequence = + match T.Execution.next cursor ~oms |> ok with + | T.Execution.Triggered (id, at, sequence, _) -> + Alcotest.check order_id_testable "triggered order" order.id id; + (at, sequence) + | T.Execution.Finished _ | T.Execution.Proposed _ -> + Alcotest.fail "expected a trigger" + in + Alcotest.(check string) + "intrabar trigger is timestamped at bar end" "2026-01-03T21:00:00.000000Z" + (T.Codec.ptime_to_string triggered_at); + Alcotest.(check int64) "trigger slice persisted" 2L triggered_sequence; + let oms, _ = + T.Oms.trigger oms order.id ~updated_event_id:(event_id "trigger-event") + ~triggered_at ~triggered_slice_sequence:triggered_sequence + |> ok + in + let next = + T.Execution.match_slice (execution ()) + ~instruments:[ instrument () ] + ~oms + (market_slice ~bars:[ bar ~open_price:"112" 3L ] 3L) + |> ok + in + match next.fills with + | [ fill ] -> + Alcotest.check price_testable "stop becomes next-slice market order" + (price "112") fill.price + | _ -> Alcotest.fail "expected one next-slice fill" + +let stop_limit_uses_limit_after_trigger () = + let request = + request_v8 + ~kind: + (T.Order.Stop_limit + { trigger_price = price "110"; limit_price = price "111" }) + () + in + let oms, order = oms_with_order request in + let oms, _ = + T.Oms.trigger oms order.id ~updated_event_id:(event_id "trigger-event") + ~triggered_at:(timestamp "2026-01-03T21:00:00Z") + ~triggered_slice_sequence:2L + |> ok + in + let missed = + T.Execution.match_slice (execution ()) + ~instruments:[ instrument () ] + ~oms + (market_slice + ~bars: + [ + bar ~open_price:"115" ~high_price:"118" ~low_price:"112" + ~close_price:"115" 3L; + ] + 3L) + |> ok + in + Alcotest.(check int) + "activated limit can remain working" 0 (List.length missed.fills); + let touched = + T.Execution.match_slice (execution ()) + ~instruments:[ instrument () ] + ~oms + (market_slice + ~bars: + [ + bar ~open_price:"115" ~high_price:"118" ~low_price:"110" + ~close_price:"115" 4L; + ] + 4L) + |> ok + in + Alcotest.check price_testable "activated limit fills at its limit" + (price "111") (List.hd touched.fills).price + +let sell_stop_gap_and_partial_fill () = + let request = + request_v8 ~side:T.Order.Sell ~quantity_value:"10" + ~kind:(T.Order.Stop (price "90")) + () + in + let oms, order = oms_with_order request in + let gap_slice = + market_slice + ~bars: + [ + bar ~open_price:"85" ~high_price:"90" ~low_price:"80" + ~close_price:"85" 2L; + ] + 2L + in + let matched = + T.Execution.match_slice (execution ()) + ~instruments:[ instrument () ] + ~oms gap_slice + |> ok + in + let triggered_at = + match matched.triggers with + | [ (id, triggered_at, 2L) ] -> + Alcotest.check order_id_testable "sell stop ID" order.id id; + triggered_at + | _ -> Alcotest.fail "expected one sell-stop trigger" + in + Alcotest.(check string) + "gap trigger uses bar start" "2026-01-03T14:30:00.000000Z" + (T.Codec.ptime_to_string triggered_at); + let oms, _ = + T.Oms.trigger oms order.id ~updated_event_id:(event_id "sell-trigger") + ~triggered_at ~triggered_slice_sequence:2L + |> ok + in + let partial = + T.Execution.match_slice (execution ()) + ~instruments:[ instrument () ] + ~oms + (market_slice ~bars:[ bar ~volume:(Some "4") 3L ] 3L) + |> ok + in + match partial.fills with + | [ fill ] -> + Alcotest.check quantity_testable "activated stop can partially fill" + (quantity "4") fill.quantity + | _ -> Alcotest.fail "expected one partial sell-stop fill" + +let fok_is_all_or_cancel () = + let request = request_v8 ~quantity_value:"10" ~time_in_force:T.Order.Fok () in + let oms, order = oms_with_order request in + let matched = + T.Execution.match_slice (execution ()) + ~instruments:[ instrument () ] + ~oms + (market_slice ~bars:[ bar ~volume:(Some "5") 2L ] 2L) + |> ok + in + Alcotest.(check int) "no partial FOK fill" 0 (List.length matched.fills); + Alcotest.check order_id_testable "FOK is cancelled" order.id + (List.hd matched.market_ioc_orders) + +let split_adjusts_stop_prices () = + let request = + request_v8 ~quantity_value:"10" + ~kind: + (T.Order.Stop_limit + { trigger_price = price "110"; limit_price = price "112" }) + () + in + let order = accepted_order request in + let adjusted = + T.Order.adjust_for_split order ~updated_event_id:(event_id "split-event") + ~numerator:2L ~denominator:1L + |> ok + in + Alcotest.check quantity_testable "quantity doubles" (quantity "20") + adjusted.request.quantity; + match adjusted.request.kind with + | T.Order.Stop_limit { trigger_price; limit_price } -> + Alcotest.check price_testable "trigger halves" (price "55") trigger_price; + Alcotest.check price_testable "limit halves" (price "56") limit_price + | T.Order.Market | T.Order.Limit _ | T.Order.Stop _ -> + Alcotest.fail "expected adjusted stop-limit" + +let engine_audits_trigger_and_defers_fill () = + let request = + request_v8 ~kind:(T.Order.Stop (price "110")) ~time_in_force:T.Order.Gtc () + in + let state = runner [ (1L, [ T.Strategy.Submit_order request ]) ] in + let state, _ = Runner.process_slice state (market_slice 1L) |> ok in + let state, triggered = + Runner.process_slice state + (market_slice + ~bars: + [ + bar ~open_price:"100" ~high_price:"115" ~low_price:"95" + ~close_price:"105" 2L; + ] + 2L) + |> ok + in + Alcotest.(check bool) + "trigger is audited" true + (List.mem "order_triggered" (event_names triggered)); + Alcotest.(check bool) + "trigger slice does not fill" false + (List.mem "fill_applied" (event_names triggered)); + let _, filled = + Runner.process_slice state + (market_slice ~bars:[ bar ~open_price:"112" 3L ] 3L) + |> ok + in + Alcotest.(check bool) + "following slice fills" true + (List.mem "fill_applied" (event_names filled)) + +let gtd_and_day_expire_deterministically () = + let gtd = + request_v8 + ~kind:(T.Order.Limit (price "90")) + ~time_in_force:(T.Order.Gtd (timestamp "2026-01-03T20:00:00Z")) + () + in + let state = runner [ (1L, [ T.Strategy.Submit_order gtd ]) ] in + let state, _ = Runner.process_slice state (market_slice 1L) |> ok in + let _, expired = Runner.process_slice state (market_slice 2L) |> ok in + let reason = + match + List.find_map + (fun audit -> + match audit.T.Audit.event with + | T.Audit.Order_cancelled { reason; _ } -> Some reason + | _ -> None) + expired + with + | Some reason -> reason + | None -> + Alcotest.failf "missing GTD cancellation in [%s]" + (String.concat ", " (event_names expired)) + in + Alcotest.(check string) + "GTD reason" "gtd_expired" + (T.Audit.cancellation_reason_to_string reason); + let calendar = calendar () in + let day = + request_v8 + ~kind:(T.Order.Stop (price "101")) + ~time_in_force: + (T.Order.Day + { + venue_id = T.Id.Venue.of_string_exn "XNAS"; + calendar_id = T.Id.Venue_calendar.of_string_exn "xnas-test"; + }) + () + in + let state = + runner ~venue_calendars:[ calendar ] + [ (1L, [ T.Strategy.Submit_order day ]) ] + in + let state, _ = Runner.process_slice state (market_slice 1L) |> ok in + let _, expired = + Runner.process_slice state + (market_slice + ~bars: + [ + bar ~open_price:"100" ~high_price:"105" ~low_price:"95" + ~close_price:"100" 2L; + ] + 2L) + |> ok + in + let reason = + List.find_map + (fun audit -> + match audit.T.Audit.event with + | T.Audit.Order_cancelled { reason; _ } -> Some reason + | _ -> None) + expired + |> Option.get + in + Alcotest.(check string) + "DAY reason" "day_expired" + (T.Audit.cancellation_reason_to_string reason); + Alcotest.(check bool) + "DAY stop triggers at session boundary" true + (List.mem "order_triggered" (event_names expired)); + Alcotest.(check bool) + "DAY stop cannot fill after its session" false + (List.mem "fill_applied" (event_names expired)) + +let fok_rejects_risk_clipped_fill () = + let order = request_v8 ~time_in_force:T.Order.Fok () in + let state = + runner ~initial_cash:"550" + ~risk:(risk ~max_leverage:"1" ()) + [ (1L, [ T.Strategy.Submit_order order ]) ] + in + let state, _ = + Runner.process_slice state + (market_slice ~bars:[ bar ~close_price:"50" ~low_price:"50" 1L ] 1L) + |> ok + in + let state, events = + Runner.process_slice state + (market_slice ~bars:[ bar ~open_price:"100" ~close_price:"100" 2L ] 2L) + |> ok + in + Alcotest.check quantity_testable "FOK applies no position" + T.Scalar.Quantity.zero + (T.Account.position_quantity (Runner.account state) + (instrument_id "test-equity")); + Alcotest.(check bool) + "risk clipping is audited" true + (List.mem "fill_clipped" (event_names events)); + let reason = + List.find_map + (fun audit -> + match audit.T.Audit.event with + | T.Audit.Order_cancelled { reason; _ } -> Some reason + | _ -> None) + events + |> Option.get + in + Alcotest.(check string) + "FOK cancellation reason" "fill_or_kill" + (T.Audit.cancellation_reason_to_string reason) + +let day_identity_is_validated () = + let day venue = + request_v8 + ~kind:(T.Order.Limit (price "90")) + ~time_in_force: + (T.Order.Day + { + venue_id = T.Id.Venue.of_string_exn venue; + calendar_id = T.Id.Venue_calendar.of_string_exn "xnas-test"; + }) + () + in + let rejection state = + let _, events = Runner.process_slice state (market_slice 1L) |> ok in + List.find_map + (fun audit -> + match audit.T.Audit.event with + | T.Audit.Order_rejected { status = T.Order.Rejected reason; _ } -> + Some reason + | _ -> None) + events + |> Option.get + in + let unknown = runner [ (1L, [ T.Strategy.Submit_order (day "XNAS") ]) ] in + Alcotest.(check string) + "unknown calendar" "DAY order refers to an unknown calendar" + (rejection unknown); + let wrong_venue = + runner + ~venue_calendars:[ calendar () ] + [ (1L, [ T.Strategy.Submit_order (day "XNYS") ]) ] + in + Alcotest.(check string) + "venue mismatch" "DAY order venue differs from its calendar" + (rejection wrong_venue); + let uncovered = + runner + ~venue_calendars:[ calendar ~covered_instrument:"other-equity" () ] + [ (1L, [ T.Strategy.Submit_order (day "XNAS") ]) ] + in + Alcotest.(check string) + "instrument coverage" "DAY order calendar does not cover its instrument" + (rejection uncovered) + +let tests = + [ + Alcotest.test_case "legacy compatibility mapping" `Quick + compatibility_mapping; + Alcotest.test_case "stop-limit and GTD validation" `Quick + validates_stop_limit_and_gtd; + Alcotest.test_case "v8 intent companions" `Quick + v8_intent_requires_explicit_companions; + Alcotest.test_case "order validation and serialization" `Quick + order_validation_and_serialization_branches; + Alcotest.test_case "stop triggers before later execution" `Quick + trigger_then_execute_on_following_slice; + Alcotest.test_case "stop-limit activation" `Quick + stop_limit_uses_limit_after_trigger; + Alcotest.test_case "sell stop gap and partial fill" `Quick + sell_stop_gap_and_partial_fill; + Alcotest.test_case "FOK all-or-cancel" `Quick fok_is_all_or_cancel; + Alcotest.test_case "split adjusts stop prices" `Quick + split_adjusts_stop_prices; + Alcotest.test_case "engine audits stop trigger" `Quick + engine_audits_trigger_and_defers_fill; + Alcotest.test_case "GTD and DAY expiration" `Quick + gtd_and_day_expire_deterministically; + Alcotest.test_case "FOK rejects risk clipping" `Quick + fok_rejects_risk_clipped_fill; + Alcotest.test_case "DAY identity validation" `Quick + day_identity_is_validated; + ] diff --git a/test/test_scenario.ml b/test/test_scenario.ml index aa77861..f7bb4f0 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/v7/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v8/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/v7/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v8/fixtures/demo.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -125,9 +125,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v7/scenario.schema.json"; - check_schema "../contracts/v7/scenario-stream.schema.json"; - check_schema "../contracts/v7/journal.schema.json" + check_schema "../contracts/v8/scenario.schema.json"; + check_schema "../contracts/v8/scenario-stream.schema.json"; + check_schema "../contracts/v8/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -189,8 +189,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 7, 6, 5, 4, \ - 3)" + "unsupported scenario contract_version \"2\" (expected one of 8, 7, 6, 5, \ + 4, 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -872,7 +872,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v7/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v8/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -900,7 +900,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v7/fixtures/fill-clipped.scenario.json" In_channel.input_all + "../contracts/v8/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in let result = @@ -913,7 +913,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v7/fixtures/fill-clipped.journal.jsonl" In_channel.input_all + "../contracts/v8/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index b206385..54dcc28 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -13,6 +13,7 @@ let initialization () = initial_cash = [ ("USD", money "10000") ]; initial_portfolio = None; instruments = [ instrument ]; + venue_calendars = []; risk = risk ~instruments:[ instrument ] (); execution_model = T.Execution_model.find "completed_bar_v1" |> ok; execution = execution (); @@ -27,7 +28,7 @@ let initialize_message_is_complete () = T.Strategy_protocol.initialize_message ~sequence:1L (initialization ()) in Alcotest.(check string) - "protocol version" "5" + "protocol version" "6" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -48,6 +49,45 @@ let initialize_message_is_complete () = | `List values -> List.length values | _ -> Alcotest.fail "expected instruments") +let initialize_message_includes_calendars () = + let phase = + T.Venue_calendar.create_phase ~kind:T.Venue_calendar.Regular + ~opens_at:(timestamp "2026-01-02T14:30:00Z") + ~closes_at:(timestamp "2026-01-02T21:00:00Z") + |> ok + in + let session = + T.Venue_calendar.create_session ~session_date:"2026-01-02" + ~kind:T.Venue_calendar.Regular_session ~phases:[ phase ] + |> ok + in + let holiday = + T.Venue_calendar.create_session ~session_date:"2026-01-03" + ~kind:T.Venue_calendar.Holiday ~phases:[] + |> ok + in + let calendar = + T.Venue_calendar.create + ~id:(T.Id.Venue_calendar.of_string_exn "xnas-test") + ~version:"1" + ~venue_id:(T.Id.Venue.of_string_exn "XNAS") + ~instrument_ids:[ instrument_id "test-equity" ] + ~sessions:[ session; holiday ] + |> ok + in + let message = + T.Strategy_protocol.initialize_message ~sequence:1L + { (initialization ()) with venue_calendars = [ calendar ] } + in + match field "payload" message |> field "venue_calendars" with + | `List [ calendar ] -> + Alcotest.(check string) + "calendar identity" "xnas-test" + (match field "calendar_id" calendar with + | `String value -> value + | _ -> Alcotest.fail "expected calendar ID") + | _ -> Alcotest.fail "expected one serialized venue calendar" + let legacy_initialize_message_remains_frozen () = let initialization = { @@ -165,7 +205,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "5"); + ("strategy_protocol_version", `String "6"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); @@ -223,8 +263,8 @@ let responses_are_strict_and_typed () = let duplicate = `Assoc [ - ("strategy_protocol_version", `String "5"); - ("strategy_protocol_version", `String "5"); + ("strategy_protocol_version", `String "6"); + ("strategy_protocol_version", `String "6"); ("strategy_sequence", `String "3"); ("message_type", `String "stopped"); ("payload", `Assoc []); @@ -251,7 +291,7 @@ let responses_are_strict_and_typed () = let unknown_field = `Assoc [ - ("strategy_protocol_version", `String "5"); + ("strategy_protocol_version", `String "6"); ("strategy_sequence", `String "3"); ("message_type", `String "stopped"); ("payload", `Assoc []); @@ -435,6 +475,8 @@ let tests = [ Alcotest.test_case "initialize message is complete" `Quick initialize_message_is_complete; + Alcotest.test_case "initialize message includes calendars" `Quick + initialize_message_includes_calendars; Alcotest.test_case "legacy initialize message remains frozen" `Quick legacy_initialize_message_remains_frozen; Alcotest.test_case "event context is complete" `Quick diff --git a/test/test_support.ml b/test/test_support.ml index 2000368..3a25e54 100644 --- a/test/test_support.ml +++ b/test/test_support.ml @@ -93,6 +93,13 @@ let request ?(instrument = instrument_id "test-equity") ?(side = T.Order.Buy) ~quantity:(quantity quantity_value) ~kind ~origin |> ok +let request_v8 ?(instrument = instrument_id "test-equity") ?(side = T.Order.Buy) + ?(quantity_value = "10") ?(kind = T.Order.Market) + ?(time_in_force = T.Order.Gtc) ?(origin = T.Order.Direct) () = + T.Order.request_v8 ~instrument_id:instrument ~side + ~quantity:(quantity quantity_value) ~kind ~time_in_force ~origin + |> ok + let accepted_order ?(id = "order-1") ?(accepted_sequence = 1L) ?(created_at = timestamp "2026-01-02T21:00:02Z") ?(eligible_after_slice_sequence = 1L) request = @@ -149,6 +156,16 @@ let engine_config ?(contract_version = T.Contract.version) ?(risk = risk ()) ~max_internal_events |> ok +let engine_config_v8 ?(risk = risk ()) ?(venue_calendars = []) ?execution_model + ?(execution = execution ()) ?(max_internal_events = 1000) () = + let execution_model = + Option.value execution_model + ~default:(T.Execution_model.find "completed_bar_v1" |> ok) + in + T.Engine.config_v8 ~contract_version:T.Contract.version ~risk ~venue_calendars + ~execution_model ~execution ~max_internal_events + |> ok + let risk_check risk ~account ~oms request = T.Risk.check risk ~account ~oms ~marks:[ (instrument_id "test-equity", price "100") ] From ba01608624f810db7db95ff5c8bfdbb802b3dc49 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 13:26:26 -0400 Subject: [PATCH 43/57] feat: add instrument-aware fee schedules --- CHANGELOG.md | 10 +- README.md | 30 +- bench/benchmark_replay.py | 2 +- contracts/conformance/cases.json | 184 ++++++- contracts/conformance/manifest.json | 123 ++++- contracts/strategy/v7/README.md | 55 ++ contracts/strategy/v7/dune | 15 + .../v7/fixtures/external.scenario.json | 215 ++++++++ .../v7/fixtures/external.scenario.jsonl | 4 + .../v7/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v7/message.schema.json | 299 +++++++++++ contracts/strategy/v7/transcript.schema.json | 83 +++ contracts/v9/README.md | 50 ++ contracts/v9/dune | 16 + contracts/v9/fixtures/demo.journal.jsonl | 22 + contracts/v9/fixtures/demo.scenario.json | 314 ++++++++++++ contracts/v9/fixtures/demo.scenario.jsonl | 6 + .../v9/fixtures/fill-clipped.journal.jsonl | 11 + .../v9/fixtures/fill-clipped.scenario.json | 187 +++++++ contracts/v9/journal.schema.json | 248 +++++++++ contracts/v9/scenario-stream.schema.json | 77 +++ contracts/v9/scenario.schema.json | 471 ++++++++++++++++++ docs/api-reference.md | 2 +- docs/continuous-integration.md | 4 +- docs/execution-model.md | 40 +- docs/persistra.md | 10 +- docs/scenario.md | 28 +- lib/account.ml | 130 ++++- lib/account.mli | 21 + lib/codec.ml | 80 ++- lib/codec.mli | 1 + lib/contract.ml | 11 +- lib/engine.ml | 62 ++- lib/execution.ml | 101 +++- lib/execution.mli | 17 + lib/execution_model.ml | 35 +- lib/execution_model.mli | 3 + lib/fee_schedule.ml | 277 ++++++++++ lib/fee_schedule.mli | 70 +++ lib/fill.ml | 66 ++- lib/fill.mli | 15 + lib/scenario.ml | 145 +++++- lib/scenario_shape.ml | 12 +- lib/scenario_validation.ml | 17 +- lib/strategy_protocol.ml | 77 ++- mkdocs.yml | 4 +- scripts/check-deterministic-journals | 8 + scripts/check-documentation.py | 4 +- scripts/release_artifacts.py | 12 +- test/cli.t | 2 +- test/dune | 75 ++- test/test_diagnostic.ml | 6 +- test/test_engine.ml | 1 + test/test_fee_schedules.ml | 135 +++++ test/test_scenario.ml | 26 +- test/test_strategy_protocol.ml | 27 +- test/test_support.ml | 2 +- 57 files changed, 3749 insertions(+), 213 deletions(-) create mode 100644 contracts/strategy/v7/README.md create mode 100644 contracts/strategy/v7/dune create mode 100644 contracts/strategy/v7/fixtures/external.scenario.json create mode 100644 contracts/strategy/v7/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v7/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v7/message.schema.json create mode 100644 contracts/strategy/v7/transcript.schema.json create mode 100644 contracts/v9/README.md create mode 100644 contracts/v9/dune create mode 100644 contracts/v9/fixtures/demo.journal.jsonl create mode 100644 contracts/v9/fixtures/demo.scenario.json create mode 100644 contracts/v9/fixtures/demo.scenario.jsonl create mode 100644 contracts/v9/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v9/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v9/journal.schema.json create mode 100644 contracts/v9/scenario-stream.schema.json create mode 100644 contracts/v9/scenario.schema.json create mode 100644 lib/fee_schedule.ml create mode 100644 lib/fee_schedule.mli create mode 100644 test/test_fee_schedules.ml diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ddbc62..38c8936 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,17 @@ ## Unreleased +- Added instrument-aware, composable fee schedules with named fixed, notional, and per-unit + components; explicit rounding; maker/taker applicability; per-fill minimums and caps; rebates; + and deterministic multi-currency conversion. +- Added signed fee-component attribution to fills, positions, valuations, journals, and external + strategy events in scenario/journal contract v9 and strategy protocol v7. +- Preserved completed-bar configuration v1, scenario/journal v8, and strategy protocol v6 as + compatibility contracts. + - Add explicit GTC, IOC, FOK, DAY, and GTD order lifetimes plus completed-bar stop and stop-limit activation in scenario contract v8 and external strategy protocol v6. -## Unreleased - - Add contract v7 exact per-instrument risk policies, versioned overlapping exposure groups, reservation-aware admission and fill clipping, group diagnostics, and strategy protocol v5. diff --git a/README.md b/README.md index ba7ae3f..5db1024 100644 --- a/README.md +++ b/README.md @@ -47,16 +47,18 @@ scenario slices and scheduled or external intents - Shared per-instrument volume participation, partial fills, and GTC limits - One-slice IOC market orders - Risk-aware fractional-lot clipping with structured `fill_clipped` reasons and thresholds -- Fixed and notional fees with explicit rounding +- Instrument-aware fixed, notional, and per-unit fee schedules with explicit rounding, + maker/taker applicability, minimums, caps, rebates, and deterministic FX conversion - Explicit multi-currency cash ledgers and complete per-slice FX marks in a base currency - Explicit signed initial portfolios with cost basis, P&L and fee history, marks, and FX state - Split and cash-dividend processing before matching, including target and order adjustment - Short borrow accrual, maintenance-margin calls, and deterministic liquidation orders - Signed average-cost accounting, realized and unrealized P&L, and equity reconciliation -- Per-currency cash and per-instrument quantity, mark, value, basis, P&L, and fee attribution +- Per-currency cash and per-instrument quantity, mark, value, basis, P&L, aggregate fee, and named + 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; v7 currently exposes `completed_bar_v1` + capability descriptors; v9 currently exposes `completed_bar_v1` configuration v2 - 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 @@ -86,7 +88,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v8/fixtures/demo.scenario.json \ + --input contracts/v9/fixtures/demo.scenario.json \ --validate-only ``` @@ -94,7 +96,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v8/fixtures/demo.scenario.json \ + --input contracts/v9/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -102,7 +104,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v8/fixtures/demo.scenario.jsonl \ + --input contracts/v9/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -111,7 +113,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v6/fixtures/external.scenario.json \ + --input contracts/strategy/v7/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -218,19 +220,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 v8 and conformance fixtures](contracts/v8/README.md) +- [Current contract v9 and conformance fixtures](contracts/v9/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v8/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v8/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v8/journal.schema.json) -- [External strategy protocol v6](contracts/strategy/v6/README.md) +- [Scenario JSON Schema](contracts/v9/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v9/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v9/journal.schema.json) +- [External strategy protocol v7](contracts/strategy/v7/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/v6/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v6/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v7/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v7/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/bench/benchmark_replay.py b/bench/benchmark_replay.py index 58b62b7..e3a7cb8 100644 --- a/bench/benchmark_replay.py +++ b/bench/benchmark_replay.py @@ -23,7 +23,7 @@ ROOT = Path(__file__).resolve().parents[1] DEFAULT_EXECUTABLE = ROOT / "_build/default/bin/main.exe" DEFAULT_BASELINE = ROOT / "bench/baselines/linux-x86_64.json" -FIXTURE = ROOT / "contracts/v8/fixtures/demo.scenario.json" +FIXTURE = ROOT / "contracts/v9/fixtures/demo.scenario.json" STRATEGY = ROOT / "bench/latency_strategy.py" SUMMARY_PATTERN = re.compile( r"\baudits=(?P[0-9]+).*\bactive=(?P[0-9]+)" diff --git a/contracts/conformance/cases.json b/contracts/conformance/cases.json index a9820a2..424e173 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -263,7 +263,8 @@ "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", - "rule": "structural" + "rule": "structural", + "protocol_version": "6" }, { "name": "strategy-intents-valid", @@ -278,7 +279,8 @@ "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", - "rule": "structural" + "rule": "structural", + "protocol_version": "6" }, { "name": "strategy-stopped-valid", @@ -293,7 +295,8 @@ "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", - "rule": "structural" + "rule": "structural", + "protocol_version": "6" }, { "name": "strategy-error-valid", @@ -325,7 +328,8 @@ ], "schema_expectation": "accept", "runtime_expectation": "accept", - "rule": "structural" + "rule": "structural", + "protocol_version": "6" }, { "name": "strategy-missing-version", @@ -347,7 +351,8 @@ ], "schema_expectation": "reject", "runtime_expectation": "reject", - "rule": "structural" + "rule": "structural", + "protocol_version": "6" }, { "name": "strategy-unknown-field", @@ -370,7 +375,8 @@ ], "schema_expectation": "reject", "runtime_expectation": "reject", - "rule": "structural" + "rule": "structural", + "protocol_version": "6" }, { "name": "strategy-wrong-sequence", @@ -385,7 +391,8 @@ "mutations": [], "schema_expectation": "accept", "runtime_expectation": "reject", - "rule": "semantic" + "rule": "semantic", + "protocol_version": "6" }, { "name": "strategy-ready-valid-v5", @@ -393,7 +400,9 @@ "kind": "strategy_response", "source": "strategy/v5/fixtures/external.strategy.jsonl", "record": 2, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "1", "mutations": [], "schema_expectation": "accept", @@ -407,7 +416,9 @@ "kind": "strategy_response", "source": "strategy/v5/fixtures/external.strategy.jsonl", "record": 4, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "2", "mutations": [], "schema_expectation": "accept", @@ -421,7 +432,9 @@ "kind": "strategy_response", "source": "strategy/v5/fixtures/external.strategy.jsonl", "record": 14, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "7", "mutations": [], "schema_expectation": "accept", @@ -435,14 +448,26 @@ "kind": "strategy_response", "source": "strategy/v5/fixtures/external.strategy.jsonl", "record": 14, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "7", "mutations": [ - { "op": "replace", "path": ["message_type"], "value": "error" }, { "op": "replace", - "path": ["payload"], - "value": { "message": "intentional conformance error" } + "path": [ + "message_type" + ], + "value": "error" + }, + { + "op": "replace", + "path": [ + "payload" + ], + "value": { + "message": "intentional conformance error" + } } ], "schema_expectation": "accept", @@ -593,6 +618,107 @@ "runtime_expectation": "reject", "rule": "semantic", "protocol_version": "4" + }, + { + "name": "scenario-v9-valid", + "artifact": "scenario-v9", + "kind": "scenario", + "source": "v9/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-stream-v9-valid", + "artifact": "scenario-stream-v9", + "kind": "scenario_stream", + "source": "v9/fixtures/demo.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-ready-valid-v7", + "artifact": "strategy-message-v7", + "kind": "strategy_response", + "source": "strategy/v7/fixtures/external.strategy.jsonl", + "record": 2, + "extract": [ + "message" + ], + "expected_sequence": "1", + "protocol_version": "7", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-intents-valid-v7", + "artifact": "strategy-message-v7", + "kind": "strategy_response", + "source": "strategy/v7/fixtures/external.strategy.jsonl", + "record": 4, + "extract": [ + "message" + ], + "expected_sequence": "2", + "protocol_version": "7", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-stopped-valid-v7", + "artifact": "strategy-message-v7", + "kind": "strategy_response", + "source": "strategy/v7/fixtures/external.strategy.jsonl", + "record": 14, + "extract": [ + "message" + ], + "expected_sequence": "7", + "protocol_version": "7", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-error-valid-v7", + "artifact": "strategy-message-v7", + "kind": "strategy_response", + "source": "strategy/v7/fixtures/external.strategy.jsonl", + "record": 14, + "extract": [ + "message" + ], + "expected_sequence": "7", + "protocol_version": "7", + "mutations": [ + { + "op": "replace", + "path": [ + "message_type" + ], + "value": "error" + }, + { + "op": "replace", + "path": [ + "payload" + ], + "value": { + "message": "fixture failure" + } + } + ], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" } ], "schema_only_cases": [ @@ -797,6 +923,36 @@ "mutations": [], "schema_expectation": "accept", "source": "strategy/v6/fixtures/external.strategy.jsonl" + }, + { + "name": "strategy-v7-rejected-response-branch", + "artifact": "strategy-transcript-v7", + "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", + "source": "strategy/v7/fixtures/external.strategy.jsonl" } ] } diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index 0d126e8..df297da 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -478,9 +478,18 @@ "version_field": "contract_version", "version": "8", "sources": [ - { "path": "v8/fixtures/demo.scenario.json", "format": "json" }, - { "path": "v8/fixtures/fill-clipped.scenario.json", "format": "json" }, - { "path": "strategy/v6/fixtures/external.scenario.json", "format": "json" } + { + "path": "v8/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v8/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "strategy/v6/fixtures/external.scenario.json", + "format": "json" + } ] }, { @@ -489,8 +498,14 @@ "version_field": "contract_version", "version": "8", "sources": [ - { "path": "v8/fixtures/demo.scenario.jsonl", "format": "jsonl" }, - { "path": "strategy/v6/fixtures/external.scenario.jsonl", "format": "jsonl" } + { + "path": "v8/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v6/fixtures/external.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -499,8 +514,14 @@ "version_field": "contract_version", "version": "8", "sources": [ - { "path": "v8/fixtures/demo.journal.jsonl", "format": "jsonl" }, - { "path": "v8/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" } + { + "path": "v8/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v8/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -512,7 +533,9 @@ { "path": "strategy/v6/fixtures/external.strategy.jsonl", "format": "jsonl", - "extract": ["message"] + "extract": [ + "message" + ] } ] }, @@ -522,7 +545,89 @@ "version_field": "strategy_protocol_version", "version": "6", "sources": [ - { "path": "strategy/v6/fixtures/external.strategy.jsonl", "format": "jsonl" } + { + "path": "strategy/v6/fixtures/external.strategy.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "scenario-v9", + "schema": "v9/scenario.schema.json", + "version_field": "contract_version", + "version": "9", + "sources": [ + { + "path": "v9/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v9/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "strategy/v7/fixtures/external.scenario.json", + "format": "json" + } + ] + }, + { + "name": "scenario-stream-v9", + "schema": "v9/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "9", + "sources": [ + { + "path": "v9/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v7/fixtures/external.scenario.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "journal-v9", + "schema": "v9/journal.schema.json", + "version_field": "contract_version", + "version": "9", + "sources": [ + { + "path": "v9/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v9/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "strategy-message-v7", + "schema": "strategy/v7/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "7", + "sources": [ + { + "path": "strategy/v7/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } + ] + }, + { + "name": "strategy-transcript-v7", + "schema": "strategy/v7/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "7", + "sources": [ + { + "path": "strategy/v7/fixtures/external.strategy.jsonl", + "format": "jsonl" + } ] } ] diff --git a/contracts/strategy/v7/README.md b/contracts/strategy/v7/README.md new file mode 100644 index 0000000..8f89470 --- /dev/null +++ b/contracts/strategy/v7/README.md @@ -0,0 +1,55 @@ +# External strategy protocol v7 + +Version 7 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 v6 remains available for scenario contract v8; earlier versions retain their frozen +shapes. + +Every message repeats `strategy_protocol_version: "7"` 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 v9 and includes the exact `initial_portfolio` +snapshot alongside the legacy cash projection. It also carries the complete versioned venue +calendars and nested execution configuration, so a strategy can construct DAY orders and reject +incompatible 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, fills, order updates, and rejected intents. Response +intents use the scenario v9 intent shapes. + +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/v7/dune b/contracts/strategy/v7/dune new file mode 100644 index 0000000..fdc117e --- /dev/null +++ b/contracts/strategy/v7/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v7/message.schema.json) + (transcript.schema.json as contracts/strategy/v7/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v7/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v7/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v7/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v7/fixtures/external.scenario.json b/contracts/strategy/v7/fixtures/external.scenario.json new file mode 100644 index 0000000..75f84da --- /dev/null +++ b/contracts/strategy/v7/fixtures/external.scenario.json @@ -0,0 +1,215 @@ +{ + "contract_version": "9", + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/strategy/v7/fixtures/external.scenario.jsonl b/contracts/strategy/v7/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..34227d4 --- /dev/null +++ b/contracts/strategy/v7/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"9","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}} +{"contract_version":"9","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"}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"9","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"}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"9","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v7/fixtures/external.strategy.jsonl b/contracts/strategy/v7/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..85cdd80 --- /dev/null +++ b/contracts/strategy/v7/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"7","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"9","scenario_sha256":"46f4461cb699182509ef3f7a637cf5a8b33781252da7967a50e7af32399f402e","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"}]}]}],"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":"7","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"7","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":[]}}}}} +{"strategy_protocol_version":"7","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","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":"7","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":"7","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"7","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":"7","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"7","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":"7","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"7","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":[]}}}}} +{"strategy_protocol_version":"7","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"7","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"7","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v7/message.schema.json b/contracts/strategy/v7/message.schema.json new file mode 100644 index 0000000..7939e75 --- /dev/null +++ b/contracts/strategy/v7/message.schema.json @@ -0,0 +1,299 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v7/message.schema.json", + "title": "Trading Engine external strategy protocol v7 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": "7" }, + "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", "metadata"], + "properties": { + "engine_version": { "type": "string", "minLength": 1 }, + "scenario_contract_version": { "const": "9" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/venueCalendar" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/execution" }, + "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/v9/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/v9/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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/v9/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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/v9/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/v9/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/signedDecimal" } + ] + }, + "markedPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], + "properties": { + "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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/v9/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/v9/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/v9/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/v9/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/v7/transcript.schema.json b/contracts/strategy/v7/transcript.schema.json new file mode 100644 index 0000000..03891d3 --- /dev/null +++ b/contracts/strategy/v7/transcript.schema.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v7/transcript.schema.json", + "title": "Trading Engine external strategy protocol v7 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": "7" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v7/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/v9/README.md b/contracts/v9/README.md new file mode 100644 index 0000000..f9ad441 --- /dev/null +++ b/contracts/v9/README.md @@ -0,0 +1,50 @@ +# Trading Engine contract v9 + +This directory is the authoritative v9 process and file contract shared by Trading Engine and its +clients. Versions 8, 7, 6, 5, 4, and 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 v9 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 v9 scenario, stream record, and journal record carries `"contract_version": "9"`. + +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 v9 `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. diff --git a/contracts/v9/dune b/contracts/v9/dune new file mode 100644 index 0000000..4396587 --- /dev/null +++ b/contracts/v9/dune @@ -0,0 +1,16 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v9/journal.schema.json) + (scenario-stream.schema.json as contracts/v9/scenario-stream.schema.json) + (scenario.schema.json as contracts/v9/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v9/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v9/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl as contracts/v9/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v9/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v9/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v9/fixtures/demo.journal.jsonl b/contracts/v9/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..71efc14 --- /dev/null +++ b/contracts/v9/fixtures/demo.journal.jsonl @@ -0,0 +1,22 @@ +{"contract_version":"9","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":"8a65aeaaa3c548704b926ecc99c0795bc51fc8279677b5c37848e8919e0fea1d","execution_model":"completed_bar_v1"}} +{"contract_version":"9","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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} +{"contract_version":"9","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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} +{"contract_version":"9","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":[]}} +{"contract_version":"9","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} +{"contract_version":"9","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":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} +{"contract_version":"9","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004","demo-event-000000000005"],"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-000000000007","updated_event_id":"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}} +{"contract_version":"9","engine_sequence":"8","event_id":"demo-event-000000000008","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","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"14","equity":"10104","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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052","maintenance_excess":"10078","margin_call":false},"group_exposures":[]}} +{"contract_version":"9","engine_sequence":"9","event_id":"demo-event-000000000009","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} +{"contract_version":"9","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","price":"103","notional":"618","fee":"0.868","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.618","quote_amount":"0.618"}]}} +{"contract_version":"9","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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-000000000007","updated_event_id":"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":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"9","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000005","demo-event-000000000009"],"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.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-000000000012","updated_event_id":"demo-event-000000000012","created_sequence":"12","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":"9","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000009"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5","unrealized_pnl":"40.132","equity":"10130.132","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618","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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}]}],"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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.632","maintenance_excess":"9942.882","margin_call":false},"group_exposures":[]}} +{"contract_version":"9","engine_sequence":"14","event_id":"demo-event-000000000014","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":[]}} +{"contract_version":"9","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.715","price":"107","notional":"290.505","fee":"0.540505","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.290505","quote_amount":"0.290505"}]}} +{"contract_version":"9","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000014"],"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":"9","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":["demo-event-000000000014","demo-event-000000000016"],"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-000000000017","updated_event_id":"demo-event-000000000017","created_sequence":"17","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":"9","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000014"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.086495","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5","unrealized_pnl":"20.161495","equity":"10110.161495","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.086495","fx_rate":"1","base_value":"9090.086495"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505","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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}]}],"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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.123995","maintenance_excess":"9855.142745","margin_call":false},"group_exposures":[]}} +{"contract_version":"9","engine_sequence":"19","event_id":"demo-event-000000000019","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":[]}} +{"contract_version":"9","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000017","demo-event-000000000019"],"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":"105","notional":"757.575","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.757575","quote_amount":"0.757575"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_amount":"-0.007575"}]}} +{"contract_version":"9","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.661495","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.973257","unrealized_pnl":"7.688238","equity":"10111.661495","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.661495","fx_rate":"1","base_value":"9846.661495"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}]}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.161495","maintenance_excess":"10045.411495","margin_call":false},"group_exposures":[]}} +{"contract_version":"9","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"8a65aeaaa3c548704b926ecc99c0795bc51fc8279677b5c37848e8919e0fea1d","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.661495","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.973257","unrealized_pnl":"7.688238","equity":"10111.661495","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.661495","fx_rate":"1","base_value":"9846.661495"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}]}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.161495","maintenance_excess":"10045.411495","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v9/fixtures/demo.scenario.json b/contracts/v9/fixtures/demo.scenario.json new file mode 100644 index 0000000..f4e41af --- /dev/null +++ b/contracts/v9/fixtures/demo.scenario.json @@ -0,0 +1,314 @@ +{ + "contract_version": "9", + "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_v1", + "configuration": { + "version": "2", + "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" } + ] + } + ] + } + }, + "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": [] + }, + { + "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": "12" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [] + }, + { + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v9/fixtures/demo.scenario.jsonl b/contracts/v9/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..d32bd25 --- /dev/null +++ b/contracts/v9/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"9","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_v1","configuration":{"version":"2","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"}]}]}},"max_internal_events":1000}} +{"contract_version":"9","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"}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"9","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"9","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"}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"9","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"}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"9","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v9/fixtures/fill-clipped.journal.jsonl b/contracts/v9/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..272c5b9 --- /dev/null +++ b/contracts/v9/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,11 @@ +{"contract_version":"9","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":"d2b5effb91be9d9725df41aa6a908b761931c4631aff5cbd9fad7d3f14a3c3cb","execution_model":"completed_bar_v1"}} +{"contract_version":"9","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"}],"positions":[],"execution_fee_components":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} +{"contract_version":"9","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"}],"positions":[],"execution_fee_components":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"9","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":[]}} +{"contract_version":"9","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":"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-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","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":"9","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":"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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"9","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","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":[]}} +{"contract_version":"9","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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":"9","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","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":"9","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000007"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21: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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"9","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000010"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"d2b5effb91be9d9725df41aa6a908b761931c4631aff5cbd9fad7d3f14a3c3cb","execution_model":"completed_bar_v1","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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v9/fixtures/fill-clipped.scenario.json b/contracts/v9/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..d14bf55 --- /dev/null +++ b/contracts/v9/fixtures/fill-clipped.scenario.json @@ -0,0 +1,187 @@ +{ + "contract_version": "9", + "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": [] + }, + { + "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": [] + } + ] +} diff --git a/contracts/v9/journal.schema.json b/contracts/v9/journal.schema.json new file mode 100644 index 0000000..1e6baa6 --- /dev/null +++ b/contracts/v9/journal.schema.json @@ -0,0 +1,248 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v9/journal.schema.json", + "title": "Trading Engine v6 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": "9" }, + "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", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] + }, + "payload": { "type": "object" } + }, + "allOf": [ + { "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } + }, + "initialState": { + "type": "object", "additionalProperties": false, + "required": ["portfolio", "valuation"], + "properties": { + "portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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" } } } + ] + }, + "marketSlice": { + "type": "object", "additionalProperties": false, + "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], + "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" } } + } + }, + "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"] }, "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"] } } + }, + "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" } } + }, + "orderAdjusted": { + "type": "object", "additionalProperties": false, "required": ["order", "action_id"], + "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } + }, + "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" } } + }, + "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": "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_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" } } + }, + "cashAttribution": { + "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], + "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } + }, + "positionAttribution": { + "type": "object", "additionalProperties": false, + "required": ["instrument_id", "quote_currency", "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" }, "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/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "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", "net_market_value", "long_market_value", "short_market_value", "gross_exposure", "cost_basis", "realized_pnl", "unrealized_pnl", "equity", "dividend_pnl", "execution_fees", "borrow_fees", "total_fees", "cash_balances", "positions", "margin", "group_exposures", "execution_fee_components"], + "properties": { + "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "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" } } + } + }, + "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": { "const": "completed_bar_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/v9/scenario-stream.schema.json b/contracts/v9/scenario-stream.schema.json new file mode 100644 index 0000000..a18a7ea --- /dev/null +++ b/contracts/v9/scenario-stream.schema.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v9/scenario-stream.schema.json", + "title": "Trading Engine v6 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": "9" }, + "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": "9" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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": "9" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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", "max_internal_events"], + "properties": { + "metadata": { "type": "object" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/execution" }, + "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/v9/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/intent" } } + } + } + } +} + diff --git a/contracts/v9/scenario.schema.json b/contracts/v9/scenario.schema.json new file mode 100644 index 0000000..e90b4d8 --- /dev/null +++ b/contracts/v9/scenario.schema.json @@ -0,0 +1,471 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json", + "title": "Trading Engine v6 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", "max_internal_events", "schedule", "slices"], + "properties": { + "contract_version": { "const": "9" }, + "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" }, + "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": { + "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": { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + "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" } } + } + }, + "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"], + "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" } } + } + }, + "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" } + } + } + ] + } + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index 519bc97..4e07468 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/v8/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v9/README.md) remain authoritative for process boundaries. diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 5153d11..460101d 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -21,8 +21,8 @@ Every runtime cell replays the frozen v3 demo, v5 demo, and v5 risk-limited fill canonical fixtures. Standard output and standard error are captured separately because human diagnostics may contain platform-specific paths or process details and are not part of the journal contract. -The full test suite additionally validates and replays the current v7 batch, stream, journal, and -strategy-v4 fixtures, including the initial portfolio and its reconciled first valuation. +The full test suite additionally validates and replays the current v9 batch, stream, journal, and +strategy-v7 fixtures, including fee-component attribution and the reconciled first valuation. Coverage runs once in the exact locked Ubuntu environment. The required Persistra job also runs once against its full pinned commit; it is not repeated across dependency or operating-system diff --git a/docs/execution-model.md b/docs/execution-model.md index c7a2de8..8754523 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,11 +1,11 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v7 advertises and accepts `completed_bar_v1`; embedders can inject another module through +Contract v9 advertises and accepts `completed_bar_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 v7 envelope separates selection from +Each compiled model owns a strict configuration contract. The v9 envelope separates selection from model-specific parameters: ```json @@ -13,10 +13,21 @@ model-specific parameters: "execution": { "model": "completed_bar_v1", "configuration": { - "version": "1", + "version": "2", "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 + "fee_schedules": [ + { + "schedule_id": "acme-fees-v1", + "instrument_id": "acme", + "settlement_currency": "USD", + "minimum": "0.3", + "maximum": "5", + "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" } + ] + } + ] } } } @@ -24,7 +35,7 @@ model-specific parameters: The model and configuration version are validated before replay. Unknown models, unsupported model/version pairs, missing fields, and fields from another model are rejected. Contracts v3 and -v4 retain their frozen flat execution object; v5 retains its frozen configured envelope. +v4 retain their frozen flat execution object; v8 and earlier configured envelopes remain frozen. `--capabilities` preserves the `execution_models` name list and publishes one deterministic descriptor per model under `execution_model_contracts`: supported scenario and configuration @@ -138,11 +149,16 @@ micro-unit. ## Risk-limited fills and fees -Each proposed fill pays: +Contract v9 selects exactly one fee schedule per instrument. A schedule composes named `fixed`, +`notional_bps`, and `per_unit` components. Each component declares its currency, `up`, `down`, or +`nearest` rounding, and `any`, `maker`, or `taker` applicability. A limit filled at its intrabar +touch is maker liquidity; market orders and limits marketable at the open are takers. -```text -fixed_fee + ceil(fill_notional × fee_bps / 10,000) -``` +Component amounts are calculated in their declared currencies. Slice FX rates convert quote +notional into the component currency and each result back into the fill's quote currency. The +schedule then applies its optional minimum and maximum in the settlement currency. Any difference +is retained as a named `minimum_adjustment` or `maximum_adjustment`, so the component list always +sums exactly to the signed aggregate fill fee. Negative components are rebates. Liquidation proposals are processed first, followed by sells and then buys within each origin class. For each proposal, the engine searches for the largest lot-aligned quantity whose signed @@ -160,7 +176,9 @@ This bounded-fill policy preserves split-adjusted GTC limit orders: an oversized fill over multiple slices. Market orders remain IOC, so they fill at most one bounded quantity and cancel any remainder after their eligible slice. -Each partial fill pays its own fixed fee, so fragmentation affects total cost. +The complete schedule, including minimum and maximum, is evaluated independently for every partial +fill, so fragmentation can change total cost. Contract v8 configuration v1 retains its frozen +`fixed_fee + ceil(notional × fee_bps / 10,000)` rule. ## Exact values diff --git a/docs/persistra.md b/docs/persistra.md index abcabaa..54d41d5 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -53,12 +53,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 v7 while retaining v6, v5, v4, and exact v3 -journal output for v3 inputs. The engine parser is authoritative for ordering, catalog coverage, +structural checks. The engine advertises current contract v9 while retaining v8 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 v6](../contracts/strategy/v6/README.md). Persistra's host turns protocol +[strategy protocol v7](../contracts/strategy/v7/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, @@ -78,14 +78,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 v7 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v9 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 v4; a host must complete its exact initialization, event, + external boundary is strategy v7; 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 8c88717..eef507f 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 v8 demo](../contracts/v8/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v8/scenario.schema.json) provides structural validation. +Use [the v9 demo](../contracts/v9/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v9/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/v8/scenario-stream.schema.json) validates each line, -and [the v8 stream fixture](../contracts/v8/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v9/scenario-stream.schema.json) validates each line, +and [the v9 stream fixture](../contracts/v9/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; v7 is `"7"` | +| `contract_version` | Required string identifying this file contract; v9 is `"9"` | | `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 | @@ -110,17 +110,23 @@ they may overlap and can constrain gross, long, short, absolute net, and gross-t concentration exposure. Admission and fill clipping include working-order reservations. Every applicable group is enforced, with group identity providing deterministic tie ordering. -Contract v7 execution contains a stable `model` and a model-owned `configuration`. For -`completed_bar_v1`, configuration version `"1"` contains: +Contract v9 execution contains a stable `model` and a model-owned `configuration`. For +`completed_bar_v1`, configuration version `"2"` contains: - `version`, the strict model-configuration contract version - `participation_bps`, from 0 through 10,000 -- `fixed_fee`, a nonnegative money string -- `fee_bps`, from 0 through 10,000 +- `fee_schedules`, exactly one schedule per instrument. Each schedule has a stable ID, instrument, + settlement currency, nullable minimum and maximum, and one or more named components. + +Each component declares `currency`, `kind` (`fixed`, `notional_bps`, or `per_unit`), a signed +`value`, `rounding` (`up`, `down`, or `nearest`), and `applies_to` (`any`, `maker`, or `taker`). +Signed values permit rebates. Minimums and maximums are nonnegative and apply per fill after the +component values are converted into the settlement currency. The engine advertises each model's scenario and configuration versions, required fields, supported order types, data requirements, and limits through `--capabilities.execution_model_contracts`. The -v3 and v4 scenario contracts preserve their flat execution object unchanged; v5 remains frozen. +v8 and earlier contracts retain completed-bar configuration version `"1"`; v3 and v4 preserve +their flat execution object unchanged. ## Schedule and intents @@ -214,7 +220,7 @@ than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v8/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v9/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/account.ml b/lib/account.ml index d63f652..141fa2e 100644 --- a/lib/account.ml +++ b/lib/account.ml @@ -7,6 +7,25 @@ type position = { dividend_pnl : Scalar.Money.t; execution_fees : Scalar.Money.t; borrow_fees : Scalar.Money.t; + execution_fee_components : execution_fee_component list; +} + +and execution_fee_component = { + name : string; + kind : string; + currency : string; + amount : Scalar.Money.t; + quote_amount : Scalar.Money.t; +} + +type execution_fee_component_attribution = { + name : string; + kind : string; + currency : string; + amount : Scalar.Money.t; + quote_currency : string; + quote_amount : Scalar.Money.t; + base_amount : Scalar.Money.t; } type cash_attribution = { @@ -38,6 +57,7 @@ type position_attribution = { base_borrow_fees : Scalar.Money.t; total_fees : Scalar.Money.t; base_total_fees : Scalar.Money.t; + execution_fee_components : execution_fee_component_attribution list; } type t = { @@ -64,6 +84,7 @@ type valuation = { total_fees : Scalar.Money.t; cash_balances : cash_attribution list; positions : position_attribution list; + execution_fee_components : execution_fee_component_attribution list; } let ( let* ) result function_ = @@ -77,6 +98,7 @@ let empty_position = dividend_pnl = Scalar.Money.zero; execution_fees = Scalar.Money.zero; borrow_fees = Scalar.Money.zero; + execution_fee_components = []; } let valid_currency value = @@ -133,6 +155,7 @@ let of_initial_portfolio (initial : Initial_portfolio.t) = dividend_pnl = value.dividend_pnl; execution_fees = value.execution_fees; borrow_fees = value.borrow_fees; + execution_fee_components = []; } positions) Id.Instrument.Map.empty initial.positions @@ -172,6 +195,7 @@ let update_position (positions : position Id.Instrument.Map.t) instrument_id && Scalar.Money.equal value.dividend_pnl Scalar.Money.zero && Scalar.Money.equal value.execution_fees Scalar.Money.zero && Scalar.Money.equal value.borrow_fees Scalar.Money.zero + && value.execution_fee_components = [] then Id.Instrument.Map.remove instrument_id positions else Id.Instrument.Map.add instrument_id value positions @@ -182,9 +206,46 @@ let adjust_cash (state : t) currency delta = let* amount = Scalar.Money.add current delta in Ok { state with cash = Currency_map.add currency amount state.cash } -let add_execution_fee (position : position) fee = +let add_fee_component (components : execution_fee_component list) + (component : Fee_schedule.calculated_component) = + let rec add prefix = function + | [] -> + Ok + (List.rev_append prefix + [ + { + name = component.name; + kind = component.kind; + currency = component.currency; + amount = component.amount; + quote_amount = component.quote_amount; + }; + ]) + | (current : execution_fee_component) :: remaining + when String.equal current.name component.name + && String.equal current.kind component.kind + && String.equal current.currency component.currency -> + let* amount = Scalar.Money.add current.amount component.amount in + let* quote_amount = + Scalar.Money.add current.quote_amount component.quote_amount + in + Ok + (List.rev_append prefix + ({ current with amount; quote_amount } :: remaining)) + | current :: remaining -> add (current :: prefix) remaining + in + add [] components + +let add_execution_fee (position : position) fee fee_components = let* execution_fees = Scalar.Money.add position.execution_fees fee in - Ok { position with execution_fees } + let* execution_fee_components = + List.fold_left + (fun result component -> + let* components = result in + add_fee_component components component) + (Ok position.execution_fee_components) fee_components + in + Ok { position with execution_fees; execution_fee_components } let ensure_no_cross current delta = let* projected = Scalar.Quantity.add current delta in @@ -202,7 +263,9 @@ let apply_open_long (state : t) fill (current : position) projected = let* state = adjust_cash state fill.quote_currency cash_delta in let* cost_basis = Scalar.Money.add current.cost_basis acquisition_cost in let* updated = - add_execution_fee { current with quantity = projected; cost_basis } fill.fee + add_execution_fee + { current with quantity = projected; cost_basis } + fill.fee fill.fee_components in Ok { @@ -216,7 +279,9 @@ let apply_open_short (state : t) fill (current : position) projected = let* basis_delta = Scalar.Money.negate net_proceeds in let* cost_basis = Scalar.Money.add current.cost_basis basis_delta in let* updated = - add_execution_fee { current with quantity = projected; cost_basis } fill.fee + add_execution_fee + { current with quantity = projected; cost_basis } + fill.fee fill.fee_components in Ok { @@ -239,7 +304,7 @@ let apply_close_long (state : t) fill (current : position) projected = let* updated = add_execution_fee { current with quantity = projected; cost_basis; realized_pnl } - fill.fee + fill.fee fill.fee_components in Ok { @@ -267,7 +332,7 @@ let apply_close_short (state : t) fill (current : position) projected = let* updated = add_execution_fee { current with quantity = projected; cost_basis; realized_pnl } - fill.fee + fill.fee fill.fee_components in Ok { @@ -405,6 +470,28 @@ let value (state : t) ~instruments ~marks ~fx_rates = let* base_execution_fees = convert current.execution_fees in let* base_borrow_fees = convert current.borrow_fees in let* base_total_fees = convert total_fees in + let* execution_fee_components = + List.fold_left + (fun result (component : execution_fee_component) -> + let* values = result in + let* component_fx = fx component.currency in + let* base_amount = + Scalar.Money.convert component.amount ~rate:component_fx + in + Ok + ({ + name = component.name; + kind = component.kind; + currency = component.currency; + amount = component.amount; + quote_currency = instrument.quote_currency; + quote_amount = component.quote_amount; + base_amount; + } + :: values)) + (Ok []) current.execution_fee_components + |> Result.map List.rev + in Ok { instrument_id; @@ -428,6 +515,7 @@ let value (state : t) ~instruments ~marks ~fx_rates = base_borrow_fees; total_fees; base_total_fees; + execution_fee_components; } in let unknown_mark = @@ -564,6 +652,35 @@ let value (state : t) ~instruments ~marks ~fx_rates = in let* gross_exposure = add long_market_value short_market_value in let* equity = add cash net_market_value in + let add_attribution (components : execution_fee_component_attribution list) + (component : execution_fee_component_attribution) = + let rec add_component prefix = function + | [] -> Ok (List.rev_append prefix [ component ]) + | (current : execution_fee_component_attribution) :: remaining + when String.equal current.name component.name + && String.equal current.kind component.kind + && String.equal current.currency component.currency + && String.equal current.quote_currency component.quote_currency -> + let* amount = add current.amount component.amount in + let* quote_amount = add current.quote_amount component.quote_amount in + let* base_amount = add current.base_amount component.base_amount in + Ok + (List.rev_append prefix + ({ current with amount; quote_amount; base_amount } :: remaining)) + | current :: remaining -> add_component (current :: prefix) remaining + in + add_component [] components + in + let* execution_fee_components = + List.fold_left + (fun result (position : position_attribution) -> + List.fold_left + (fun result component -> + let* components = result in + add_attribution components component) + result position.execution_fee_components) + (Ok []) positions + in Ok { base_currency = state.base_currency; @@ -582,6 +699,7 @@ let value (state : t) ~instruments ~marks ~fx_rates = total_fees; cash_balances; positions; + execution_fee_components; } let pp_valuation formatter valuation = diff --git a/lib/account.mli b/lib/account.mli index d581d4a..ccf7985 100644 --- a/lib/account.mli +++ b/lib/account.mli @@ -1,6 +1,24 @@ (** Exact multi-currency cash, signed-position, cost-basis, and P&L accounting. *) +type execution_fee_component = private { + name : string; + kind : string; + currency : string; + amount : Scalar.Money.t; + quote_amount : Scalar.Money.t; +} + +type execution_fee_component_attribution = private { + name : string; + kind : string; + currency : string; + amount : Scalar.Money.t; + quote_currency : string; + quote_amount : Scalar.Money.t; + base_amount : Scalar.Money.t; +} + type position = private { quantity : Scalar.Quantity.t; cost_basis : Scalar.Money.t; @@ -8,6 +26,7 @@ type position = private { dividend_pnl : Scalar.Money.t; execution_fees : Scalar.Money.t; borrow_fees : Scalar.Money.t; + execution_fee_components : execution_fee_component list; } type cash_attribution = private { @@ -39,6 +58,7 @@ type position_attribution = private { base_borrow_fees : Scalar.Money.t; total_fees : Scalar.Money.t; base_total_fees : Scalar.Money.t; + execution_fee_components : execution_fee_component_attribution list; } type t @@ -60,6 +80,7 @@ type valuation = private { total_fees : Scalar.Money.t; cash_balances : cash_attribution list; positions : position_attribution list; + execution_fee_components : execution_fee_component_attribution list; } val create : diff --git a/lib/codec.ml b/lib/codec.ml index 1bbe9a7..d258318 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -337,7 +337,7 @@ let order_to_yojson_v8 order = ]) let versioned_order_to_yojson ~contract_version order = - if String.equal contract_version "8" then order_to_yojson_v8 order + if List.mem contract_version [ "9"; "8" ] then order_to_yojson_v8 order else order_to_yojson order let fill_to_yojson fill = @@ -356,6 +356,29 @@ let fill_to_yojson fill = ("slice_sequence", int64 fill.slice_sequence); ] +let calculated_fee_component_to_yojson component = + `Assoc + [ + ("name", string component.Fee_schedule.name); + ("kind", string component.kind); + ("currency", string component.currency); + ("amount", money component.amount); + ("quote_amount", money component.quote_amount); + ] + +let fill_to_yojson_v9 fill = + match fill_to_yojson fill with + | `Assoc fields -> + `Assoc + (fields + @ [ + ( "fee_components", + `List + (List.map calculated_fee_component_to_yojson + fill.Fill.fee_components) ); + ]) + | _ -> assert false + let initial_position_to_yojson (position : Initial_portfolio.position) = `Assoc [ @@ -422,6 +445,31 @@ let position_attribution_to_yojson position = ("base_total_fees", money position.base_total_fees); ] +let execution_fee_component_attribution_to_yojson component = + `Assoc + [ + ("name", string component.Account.name); + ("kind", string component.kind); + ("currency", string component.currency); + ("amount", money component.amount); + ("quote_currency", string component.quote_currency); + ("quote_amount", money component.quote_amount); + ("base_amount", money component.base_amount); + ] + +let position_attribution_to_yojson_v9 position = + match position_attribution_to_yojson position with + | `Assoc fields -> + `Assoc + (fields + @ [ + ( "execution_fee_components", + `List + (List.map execution_fee_component_attribution_to_yojson + position.Account.execution_fee_components) ); + ]) + | _ -> assert false + let cash_attribution_to_yojson cash = `Assoc [ @@ -431,7 +479,7 @@ let cash_attribution_to_yojson cash = ("base_value", money cash.base_value); ] -let account_valuation_to_yojson valuation = +let account_valuation_to_yojson ?(contract_version = "8") valuation = `Assoc [ ("base_currency", string valuation.Account.base_currency); @@ -451,8 +499,24 @@ let account_valuation_to_yojson valuation = ( "cash_balances", `List (List.map cash_attribution_to_yojson valuation.cash_balances) ); ( "positions", - `List (List.map position_attribution_to_yojson valuation.positions) ); + `List + (List.map + (if String.equal contract_version "9" then + position_attribution_to_yojson_v9 + else position_attribution_to_yojson) + valuation.positions) ); ] + |> function + | `Assoc fields when String.equal contract_version "9" -> + `Assoc + (fields + @ [ + ( "execution_fee_components", + `List + (List.map execution_fee_component_attribution_to_yojson + valuation.Account.execution_fee_components) ); + ]) + | json -> json let margin_to_yojson margin = `Assoc @@ -477,11 +541,13 @@ let group_exposure_to_yojson (exposure : Risk.group_exposure) = ] let valuation_to_yojson ~contract_version valuation = - match account_valuation_to_yojson valuation.Audit.account with + match + account_valuation_to_yojson ~contract_version valuation.Audit.account + with | `Assoc fields -> let fields = fields @ [ ("margin", margin_to_yojson valuation.margin) ] in let fields = - if String.equal contract_version Contract.version then + if List.mem contract_version [ "9"; "8" ] then fields @ [ ( "group_exposures", @@ -565,7 +631,9 @@ 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.Fill_applied fill -> fill_to_yojson fill + | Audit.Fill_applied fill -> + if String.equal contract_version "9" then fill_to_yojson_v9 fill + else fill_to_yojson fill | Audit.Margin_limited { order_id = id; diff --git a/lib/codec.mli b/lib/codec.mli index 81690d0..8083351 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 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 +val fill_to_yojson_v9 : Fill.t -> Yojson.Safe.t val initial_portfolio_to_yojson : Initial_portfolio.t -> Yojson.Safe.t val audit_to_yojson : Audit.t -> Yojson.Safe.t val audit_to_string : Audit.t -> string diff --git a/lib/contract.ml b/lib/contract.ml index b62a8e5..b015114 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,13 +1,13 @@ -let version = "8" -let previous_version = "7" +let version = "9" +let previous_version = "8" let legacy_journal_version = "3" let supported_versions = - [ version; previous_version; "6"; "5"; "4"; legacy_journal_version ] + [ version; previous_version; "7"; "6"; "5"; "4"; legacy_journal_version ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "6" -let previous_strategy_protocol_version = "5" +let strategy_protocol_version = "7" +let previous_strategy_protocol_version = "6" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -26,6 +26,7 @@ let capabilities_to_yojson () = [ strategy_protocol_version; previous_strategy_protocol_version; + "5"; "4"; "3"; ] ); diff --git a/lib/engine.ml b/lib/engine.ml index d81adf6..2c7a9f2 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -985,8 +985,12 @@ module Interactive = struct in if List.length ids <> List.length actual || actual <> expected then Error "market slice must contain each configured instrument exactly once" - else if actual_currencies <> expected_currencies then - Error "market slice must contain each configured currency FX rate" + else if + not + (List.for_all + (fun currency -> List.mem currency actual_currencies) + expected_currencies) + then Error "market slice must contain each configured currency FX rate" else if not (Option.exists @@ -1012,12 +1016,23 @@ module Interactive = struct Error "market slice receipt time must not move backward" | _ -> Ok ())) - let fill_fee execution price quantity = + let fill_fee execution instrument market_slice liquidity price quantity = let* notional = Scalar.Money.notional price quantity in - Scalar.Money.fee - ~fixed:(Execution.fixed_fee execution) - ~bps:(Execution.fee_bps execution) - ~notional + Execution.calculate_fee execution ~instrument ~notional ~quantity ~liquidity + ~fx_rates: + (List.map + (fun mark -> (mark.Market_slice.currency, mark.rate)) + market_slice.Market_slice.fx_rates) + + let create_execution_fill execution ~id ~order_id ~instrument_id + ~quote_currency ~side ~quantity ~price ~fee ~fee_components ~executed_at + ~slice_sequence = + if Execution.fee_schedules execution = [] then + Fill.create ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity + ~price ~fee ~executed_at ~slice_sequence + else + Fill.create_v9 ~id ~order_id ~instrument_id ~quote_currency ~side + ~quantity ~price ~fee ~fee_components ~executed_at ~slice_sequence let slice_open_marks market_slice = List.map @@ -1036,14 +1051,15 @@ module Interactive = struct in let candidate quantity = let prepared = - let* fee = - fill_fee state.config.execution proposed.Execution.price quantity + let* fee_components, fee = + fill_fee state.config.execution instrument market_slice + proposed.Execution.liquidity proposed.price quantity in let* fill = - Fill.create ~id:(fill_id state) ~order_id:order.Order.id - ~instrument_id:instrument.id + create_execution_fill state.config.execution ~id:(fill_id state) + ~order_id:order.Order.id ~instrument_id:instrument.id ~quote_currency:instrument.quote_currency ~side:order.request.side - ~quantity ~price:proposed.price ~fee + ~quantity ~price:proposed.price ~fee ~fee_components ~executed_at:proposed.executed_at ~slice_sequence:market_slice.Market_slice.slice_sequence in @@ -1053,11 +1069,11 @@ module Interactive = struct Account.value account ~instruments ~marks ~fx_rates:state.latest_fx_rates in - Ok (fee, account, after_position, after) + Ok (fee_components, fee, account, after_position, after) in match prepared with | Error message -> Error (`Invalid message) - | Ok (fee, account, after_position, after) -> ( + | Ok (fee_components, fee, account, after_position, after) -> ( let checked = let* () = Risk.check_post_fill_for state.config.risk @@ -1069,7 +1085,7 @@ module Interactive = struct ~filled_quantity:quantity ~after in match checked with - | Ok () -> Ok fee + | Ok () -> Ok (fee_components, fee) | Error (Risk.Limit limit) -> Error (`Limit limit) | Error (Risk.Invalid message) -> Error (`Invalid message)) in @@ -1117,14 +1133,14 @@ module Interactive = struct | Ok _ -> Error "fill clipping search produced a nonmaximal quantity" in if Scalar.Quantity.is_zero quantity then - Ok (quantity, Scalar.Money.zero, limit) + Ok (quantity, [], Scalar.Money.zero, limit) else match candidate quantity with - | Ok fee -> Ok (quantity, fee, limit) + | Ok (fee_components, fee) -> Ok (quantity, fee_components, fee, limit) | Error (`Invalid message) -> Error message | Error (`Limit _) -> Error "permitted fill violates its limiting policy" - let apply_fill reduction market_slice proposed quantity fee = + let apply_fill reduction market_slice proposed quantity fee_components fee = match Oms.find reduction.state.oms proposed.Execution.order_id with | None -> Error "execution proposal refers to an unknown order" | Some order -> ( @@ -1140,11 +1156,12 @@ module Interactive = struct | Ok state -> ( let reduction = { reduction with state } in match - Fill.create ~id ~order_id:order.id + create_execution_fill reduction.state.config.execution ~id + ~order_id:order.id ~instrument_id:order.request.instrument_id ~quote_currency:instrument.Instrument.quote_currency ~side:order.request.side ~quantity ~price:proposed.price - ~fee ~executed_at:proposed.executed_at + ~fee ~fee_components ~executed_at:proposed.executed_at ~slice_sequence:market_slice.Market_slice.slice_sequence with | Error _ as error -> error @@ -1204,7 +1221,7 @@ module Interactive = struct | Some value -> Ok value | None -> Error "execution order refers to an unknown instrument" in - let* permitted_quantity, fee, limit = + let* permitted_quantity, fee_components, fee, limit = permitted_fill reduction.state market_slice order proposed instrument in let permitted_quantity = @@ -1248,7 +1265,8 @@ module Interactive = struct if Scalar.Quantity.is_zero permitted_quantity then Ok (reduction, permitted_quantity) else - apply_fill reduction market_slice proposed permitted_quantity fee + apply_fill reduction market_slice proposed permitted_quantity + fee_components fee |> Result.map (fun reduction -> (reduction, permitted_quantity)) let cancel_immediate_remainders reduction order_ids = diff --git a/lib/execution.ml b/lib/execution.ml index 23a8cfa..4b9f0d2 100644 --- a/lib/execution.ml +++ b/lib/execution.ml @@ -1,10 +1,16 @@ -type t = { participation_bps : int; fixed_fee : Scalar.Money.t; fee_bps : int } +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 proposed_fill = { order_id : Id.Order.t; quantity : Scalar.Quantity.t; price : Scalar.Price.t; fee : Scalar.Money.t; + fee_components : Fee_schedule.calculated_component list; + liquidity : Fee_schedule.liquidity; executed_at : Ptime.t; } @@ -32,30 +38,86 @@ let create ~participation_bps ~fixed_fee ~fee_bps = Error "fixed fee must be nonnegative" else if fee_bps < 0 || fee_bps > 10_000 then Error "fee basis points must be between 0 and 10000" - else Ok { participation_bps; fixed_fee; fee_bps } + else + Ok { participation_bps; fee_configuration = Legacy { fixed_fee; fee_bps } } + +let create_v2 ~participation_bps ~fee_schedules = + if participation_bps < 0 || participation_bps > 10_000 then + Error "participation basis points must be between 0 and 10000" + else + let add result schedule = + let ( let* ) result function_ = + match result with + | Ok value -> function_ value + | Error _ as error -> error + in + let* schedules = result in + let instrument_id = Fee_schedule.instrument_id schedule in + if Id.Instrument.Map.mem instrument_id schedules then + Error "fee schedules must have unique instrument IDs" + else Ok (Id.Instrument.Map.add instrument_id schedule schedules) + in + Result.map + (fun schedules -> + { participation_bps; fee_configuration = Schedules schedules }) + (List.fold_left add (Ok Id.Instrument.Map.empty) fee_schedules) let participation_bps state = state.participation_bps -let fixed_fee state = state.fixed_fee -let fee_bps state = state.fee_bps + +let fixed_fee state = + match state.fee_configuration with + | Legacy { fixed_fee; _ } -> fixed_fee + | Schedules _ -> Scalar.Money.zero + +let fee_bps state = + match state.fee_configuration with + | Legacy { fee_bps; _ } -> fee_bps + | Schedules _ -> 0 + +let fee_schedules state = + match state.fee_configuration with + | Legacy _ -> [] + | Schedules schedules -> Id.Instrument.Map.bindings schedules |> List.map snd + +let calculate_fee state ~instrument ~notional ~quantity ~liquidity ~fx_rates = + match state.fee_configuration with + | Legacy { fixed_fee; fee_bps } -> + let ( let* ) result function_ = + match result with + | Ok value -> function_ value + | Error _ as error -> error + in + let* fee = Scalar.Money.fee ~fixed:fixed_fee ~bps:fee_bps ~notional in + Ok ([], fee) + | Schedules schedules -> ( + match Id.Instrument.Map.find_opt instrument.Instrument.id schedules with + | None -> Error "execution instrument has no configured fee schedule" + | Some schedule -> + Fee_schedule.calculate schedule + ~quote_currency:instrument.quote_currency ~notional ~quantity + ~liquidity ~fx_rates) let execution_price order market_slice bar = match Order.effective_kind order with | None -> None | Some Order.Market -> - Some (bar.Bar.open_price, market_slice.Market_slice.start_at) + Some + ( bar.Bar.open_price, + market_slice.Market_slice.start_at, + Fee_schedule.Taker ) | Some (Order.Limit limit) -> ( match order.request.side with | Order.Buy -> if Scalar.Price.compare bar.open_price limit <= 0 then - Some (bar.open_price, market_slice.start_at) + 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) + Some (limit, 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) + 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) + Some (limit, market_slice.end_at, Fee_schedule.Maker) else None) | Some (Order.Stop _ | Order.Stop_limit _) -> None @@ -218,7 +280,7 @@ let start_slice state ~instruments ~oms (market_slice : Market_slice.t) = | None -> let (Cursor next) = make_cursor capacities remaining in next current_oms - | Some (price, executed_at) -> + | Some (price, executed_at, liquidity) -> let quantity = available_quantity capacity (Order.remaining_quantity order) in @@ -233,12 +295,23 @@ let start_slice state ~instruments ~oms (market_slice : Market_slice.t) = next current_oms else let* notional = Scalar.Money.notional price quantity in - let* fee = - Scalar.Money.fee ~fixed:state.fixed_fee ~bps:state.fee_bps - ~notional + 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 let proposed = - { order_id = order.id; quantity; price; fee; executed_at } + { + order_id = order.id; + quantity; + price; + fee; + fee_components; + liquidity; + executed_at; + } in let continue applied_quantity = if diff --git a/lib/execution.mli b/lib/execution.mli index 88bcce4..e2d9f0e 100644 --- a/lib/execution.mli +++ b/lib/execution.mli @@ -7,6 +7,8 @@ type proposed_fill = private { quantity : Scalar.Quantity.t; price : Scalar.Price.t; fee : Scalar.Money.t; + fee_components : Fee_schedule.calculated_component list; + liquidity : Fee_schedule.liquidity; executed_at : Ptime.t; } @@ -32,9 +34,24 @@ val create : fee_bps:int -> (t, string) result +val create_v2 : + participation_bps:int -> + fee_schedules:Fee_schedule.t list -> + (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 calculate_fee : + t -> + instrument:Instrument.t -> + notional:Scalar.Money.t -> + quantity:Scalar.Quantity.t -> + liquidity:Fee_schedule.liquidity -> + fx_rates:(string * Scalar.Price.t) list -> + (Fee_schedule.calculated_component list * Scalar.Money.t, string) result val start_slice : t -> diff --git a/lib/execution_model.ml b/lib/execution_model.ml index 866ecfc..87d8643 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -13,8 +13,10 @@ type t = (module S) type configuration_contract = { version : string; + previous_versions : string list; scenario_contract_versions : string list; required_fields : string list; + legacy_required_fields : string list; supported_order_types : string list; data_requirements : string list; limits : Yojson.Safe.t; @@ -32,9 +34,12 @@ let supported = List.map name builtins let completed_bar_v1_contract = { - version = "1"; - scenario_contract_versions = [ "8"; "7"; "6"; "5"; "4"; "3" ]; - required_fields = [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ]; + version = "2"; + previous_versions = [ "1" ]; + scenario_contract_versions = [ "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; + required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; + legacy_required_fields = + [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ]; supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; data_requirements = [ "completed_ohlcv_bars" ]; limits = @@ -57,7 +62,20 @@ let configuration_contract model = unsupported) let supports_configuration model version = - String.equal (configuration_contract model).version version + let contract = configuration_contract model in + String.equal contract.version version + || List.mem version contract.previous_versions + +let required_fields model version = + let contract = configuration_contract model in + if String.equal version contract.version then Ok contract.required_fields + else if List.mem version contract.previous_versions then + Ok contract.legacy_required_fields + else + Error + (Printf.sprintf + "unsupported execution configuration version %S for model %S" version + (name model)) let supports_contract model version = List.mem version (configuration_contract model).scenario_contract_versions @@ -72,10 +90,17 @@ let capabilities_to_yojson () = `Assoc [ ("name", `String (name model)); - ("configuration_versions", strings [ contract.version ]); + ( "configuration_versions", + strings (contract.version :: contract.previous_versions) ); ( "scenario_contract_versions", strings contract.scenario_contract_versions ); ("required_fields", strings contract.required_fields); + ( "configuration_required_fields", + `Assoc + [ + (contract.version, strings contract.required_fields); + ("1", strings contract.legacy_required_fields); + ] ); ("supported_order_types", strings contract.supported_order_types); ("data_requirements", strings contract.data_requirements); ("limits", contract.limits); diff --git a/lib/execution_model.mli b/lib/execution_model.mli index e346b9f..25968aa 100644 --- a/lib/execution_model.mli +++ b/lib/execution_model.mli @@ -21,8 +21,10 @@ type t type configuration_contract = private { version : string; + previous_versions : string list; scenario_contract_versions : string list; required_fields : string list; + legacy_required_fields : string list; supported_order_types : string list; data_requirements : string list; limits : Yojson.Safe.t; @@ -34,6 +36,7 @@ val find : string -> (t, string) result val supported : string list val configuration_contract : t -> configuration_contract val supports_configuration : t -> string -> bool +val required_fields : t -> string -> (string list, string) result val supports_contract : t -> string -> bool val capabilities_to_yojson : unit -> Yojson.Safe.t diff --git a/lib/fee_schedule.ml b/lib/fee_schedule.ml new file mode 100644 index 0000000..3840253 --- /dev/null +++ b/lib/fee_schedule.ml @@ -0,0 +1,277 @@ +type rounding = Up | Down | Nearest +type liquidity = Maker | Taker +type applicability = Any | Maker_only | Taker_only + +type basis = + | Fixed of Scalar.Money.t + | Notional_bps of int + | Per_unit of Scalar.Money.t + +type component = { + name : string; + currency : string; + basis : basis; + rounding : rounding; + applicability : applicability; +} + +type t = { + schedule_id : string; + instrument_id : Id.Instrument.t; + settlement_currency : string; + minimum : Scalar.Money.t option; + maximum : Scalar.Money.t option; + components : component list; +} + +type calculated_component = { + name : string; + kind : string; + currency : string; + amount : Scalar.Money.t; + quote_amount : Scalar.Money.t; +} + +let ( let* ) result function_ = + match result with Ok value -> function_ value | Error _ as error -> error + +let valid_token value = + String.length value > 0 + && String.for_all + (fun character -> + let code = Char.code character in + code >= 0x21 && code <> 0x7f) + value + +let create_component ~name ~currency ~basis ~rounding ~applicability = + if not (valid_token name) then + Error "fee component name must not be empty or contain whitespace" + else if not (valid_token currency) then + Error "fee component currency must not be empty or contain whitespace" + else + match basis with + | Notional_bps bps when bps < -10_000 || bps > 10_000 -> + Error "fee component basis points must be between -10000 and 10000" + | _ -> Ok { name; currency; basis; rounding; applicability } + +let create ~schedule_id ~instrument_id ~settlement_currency ~minimum ~maximum + ~(components : component list) = + let nonnegative = function + | None -> true + | Some value -> Scalar.Money.compare value Scalar.Money.zero >= 0 + in + let names = + List.map (fun (component : component) -> component.name) components + in + if not (valid_token schedule_id) then + Error "fee schedule ID must not be empty or contain whitespace" + else if not (valid_token settlement_currency) then + Error "fee settlement currency must not be empty or contain whitespace" + else if components = [] then Error "fee schedule must contain a component" + else if List.length names <> List.length (List.sort_uniq String.compare names) + then Error "fee component names must be unique within a schedule" + else if not (nonnegative minimum) then Error "fee minimum must be nonnegative" + else if not (nonnegative maximum) then Error "fee maximum must be nonnegative" + else + match (minimum, maximum) with + | Some lower, Some upper when Scalar.Money.compare lower upper > 0 -> + Error "fee minimum must not exceed fee maximum" + | _ -> + Ok + { + schedule_id; + instrument_id; + settlement_currency; + minimum; + maximum; + components; + } + +let schedule_id value = value.schedule_id +let instrument_id value = value.instrument_id +let settlement_currency value = value.settlement_currency +let minimum value = value.minimum +let maximum value = value.maximum +let components value = value.components +let component_name (value : component) = value.name +let component_currency (value : component) = value.currency +let component_basis (value : component) = value.basis +let component_rounding (value : component) = value.rounding +let component_applicability (value : component) = value.applicability + +let rounding_to_string = function + | Up -> "up" + | Down -> "down" + | Nearest -> "nearest" + +let rounding_of_string = function + | "up" -> Ok Up + | "down" -> Ok Down + | "nearest" -> Ok Nearest + | value -> Error (Printf.sprintf "unsupported fee rounding %S" value) + +let applicability_to_string = function + | Any -> "any" + | Maker_only -> "maker" + | Taker_only -> "taker" + +let applicability_of_string = function + | "any" -> Ok Any + | "maker" -> Ok Maker_only + | "taker" -> Ok Taker_only + | value -> Error (Printf.sprintf "unsupported fee applicability %S" value) + +let basis_kind = function + | Fixed _ -> "fixed" + | Notional_bps _ -> "notional_bps" + | Per_unit _ -> "per_unit" + +let divide ~rounding numerator denominator = + if Z.equal denominator Z.zero then + Error "fee conversion rate must be positive" + else + let sign = Z.sign numerator in + let absolute = Z.abs numerator in + let quotient, remainder = Z.ediv_rem absolute denominator in + let rounded = + match rounding with + | Down -> quotient + | Up -> if Z.equal remainder Z.zero then quotient else Z.succ quotient + | Nearest -> + if Z.compare (Z.mul remainder (Z.of_int 2)) denominator >= 0 then + Z.succ quotient + else quotient + in + let signed = if sign < 0 then Z.neg rounded else rounded in + if Z.fits_int64 signed then Ok (Scalar.Money.of_micros (Z.to_int64 signed)) + else Error "fee calculation overflow" + +let rate currency fx_rates = + match List.assoc_opt currency fx_rates with + | Some value -> Ok value + | None -> Error ("missing fee FX rate for currency " ^ currency) + +let convert ~rounding ~fx_rates ~source_currency ~target_currency amount = + if String.equal source_currency target_currency then Ok amount + else + let* source_rate = rate source_currency fx_rates in + let* target_rate = rate target_currency fx_rates in + divide ~rounding + Z.( + mul + (of_int64 (Scalar.Money.to_micros amount)) + (of_int64 (Scalar.Price.to_micros source_rate))) + (Z.of_int64 (Scalar.Price.to_micros target_rate)) + +let applies component liquidity = + match (component.applicability, liquidity) with + | Any, _ | Maker_only, Maker | Taker_only, Taker -> true + | Maker_only, Taker | Taker_only, Maker -> false + +let raw_amount component ~notional ~quantity ~quote_currency ~fx_rates = + match component.basis with + | Fixed value -> Ok value + | Notional_bps bps -> + let* native_notional = + convert ~rounding:component.rounding ~fx_rates + ~source_currency:quote_currency ~target_currency:component.currency + notional + in + divide ~rounding:component.rounding + Z.(mul (of_int64 (Scalar.Money.to_micros native_notional)) (of_int bps)) + (Z.of_int 10_000) + | Per_unit value -> + divide ~rounding:component.rounding + Z.( + mul + (of_int64 (Scalar.Money.to_micros value)) + (of_int64 (Scalar.Quantity.to_micros quantity))) + (Z.of_int64 Scalar.Quantity.scale) + +let calculate schedule ~quote_currency ~notional ~quantity ~liquidity ~fx_rates + = + let calculate_component component = + let* amount = + raw_amount component ~notional ~quantity ~quote_currency ~fx_rates + in + let* quote_amount = + convert ~rounding:component.rounding ~fx_rates + ~source_currency:component.currency ~target_currency:quote_currency + amount + in + Ok + { + name = component.name; + kind = basis_kind component.basis; + currency = component.currency; + amount; + quote_amount; + } + in + let rec collect result = function + | [] -> Ok (List.rev result) + | component :: remaining when not (applies component liquidity) -> + collect result remaining + | component :: remaining -> + let* calculated = calculate_component component in + collect (calculated :: result) remaining + in + let* calculated = collect [] schedule.components in + let* settlement_total = + List.fold_left + (fun result component -> + let* total = result in + let* amount = + convert ~rounding:Nearest ~fx_rates + ~source_currency:component.currency + ~target_currency:schedule.settlement_currency component.amount + in + Scalar.Money.add total amount) + (Ok Scalar.Money.zero) calculated + in + let bounded = + let after_minimum = + match schedule.minimum with + | Some minimum when Scalar.Money.compare settlement_total minimum < 0 -> + minimum + | _ -> settlement_total + in + match schedule.maximum with + | Some maximum when Scalar.Money.compare after_minimum maximum > 0 -> + maximum + | _ -> after_minimum + in + let* adjustment = Scalar.Money.subtract bounded settlement_total in + let* calculated = + if Scalar.Money.equal adjustment Scalar.Money.zero then Ok calculated + else + let* quote_amount = + convert ~rounding:Nearest ~fx_rates + ~source_currency:schedule.settlement_currency + ~target_currency:quote_currency adjustment + in + let name, kind = + if Scalar.Money.compare adjustment Scalar.Money.zero > 0 then + ("minimum_adjustment", "minimum_adjustment") + else ("maximum_adjustment", "maximum_adjustment") + in + Ok + (calculated + @ [ + { + name; + kind; + currency = schedule.settlement_currency; + amount = adjustment; + quote_amount; + }; + ]) + in + let* total = + List.fold_left + (fun result component -> + let* total = result in + Scalar.Money.add total component.quote_amount) + (Ok Scalar.Money.zero) calculated + in + Ok (calculated, total) diff --git a/lib/fee_schedule.mli b/lib/fee_schedule.mli new file mode 100644 index 0000000..636f832 --- /dev/null +++ b/lib/fee_schedule.mli @@ -0,0 +1,70 @@ +(** Deterministic, composable execution-fee schedules. *) + +type rounding = + | Up + | Down + | Nearest + (** Rounding is sign-symmetric: [Up] rounds away from zero, [Down] rounds + toward zero, and [Nearest] rounds half away from zero. *) + +type liquidity = Maker | Taker +type applicability = Any | Maker_only | Taker_only + +type basis = + | Fixed of Scalar.Money.t + | Notional_bps of int + | Per_unit of Scalar.Money.t + +type component +type t + +type calculated_component = private { + name : string; + kind : string; + currency : string; + amount : Scalar.Money.t; + quote_amount : Scalar.Money.t; +} + +val create_component : + name:string -> + currency:string -> + basis:basis -> + rounding:rounding -> + applicability:applicability -> + (component, string) result + +val create : + schedule_id:string -> + instrument_id:Id.Instrument.t -> + settlement_currency:string -> + minimum:Scalar.Money.t option -> + maximum:Scalar.Money.t option -> + components:component list -> + (t, string) result + +val schedule_id : t -> string +val instrument_id : t -> Id.Instrument.t +val settlement_currency : t -> string +val minimum : t -> Scalar.Money.t option +val maximum : t -> Scalar.Money.t option +val components : t -> component list +val component_name : component -> string +val component_currency : component -> string +val component_basis : component -> basis +val component_rounding : component -> rounding +val component_applicability : component -> applicability +val rounding_to_string : rounding -> string +val rounding_of_string : string -> (rounding, string) result +val applicability_to_string : applicability -> string +val applicability_of_string : string -> (applicability, string) result +val basis_kind : basis -> string + +val calculate : + t -> + quote_currency:string -> + notional:Scalar.Money.t -> + quantity:Scalar.Quantity.t -> + liquidity:liquidity -> + fx_rates:(string * Scalar.Price.t) list -> + (calculated_component list * Scalar.Money.t, string) result diff --git a/lib/fill.ml b/lib/fill.ml index b190905..2af8476 100644 --- a/lib/fill.ml +++ b/lib/fill.ml @@ -8,12 +8,13 @@ type t = { price : Scalar.Price.t; notional : Scalar.Money.t; fee : Scalar.Money.t; + fee_components : Fee_schedule.calculated_component list; executed_at : Ptime.t; slice_sequence : int64; } -let create ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity ~price - ~fee ~executed_at ~slice_sequence = +let create_internal ~allow_rebate ~fee_components ~id ~order_id ~instrument_id + ~quote_currency ~side ~quantity ~price ~fee ~executed_at ~slice_sequence = if not (Scalar.Quantity.is_positive quantity) then Error "fill quantity must be positive" else if String.length quote_currency = 0 then @@ -26,8 +27,8 @@ let create ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity ~price code >= 0x21 && code <> 0x7f) quote_currency) then Error "fill quote currency must not contain whitespace" - else if Scalar.Money.compare fee Scalar.Money.zero < 0 then - Error "fill fee must be nonnegative" + else if (not allow_rebate) && Scalar.Money.compare fee Scalar.Money.zero < 0 + then Error "fill fee must be nonnegative" else if Int64.compare slice_sequence 0L <= 0 then Error "fill slice sequence must be positive" else @@ -36,20 +37,48 @@ let create ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity ~price | Ok notional when Scalar.Money.equal notional Scalar.Money.zero -> Error "fill notional must be at least one money micro-unit" | Ok notional -> - Ok - { - id; - order_id; - instrument_id; - quote_currency; - side; - quantity; - price; - notional; - fee; - executed_at; - slice_sequence; - } + let component_total = + List.fold_left + (fun result component -> + Result.bind result (fun total -> + Scalar.Money.add total component.Fee_schedule.quote_amount)) + (Ok Scalar.Money.zero) fee_components + in + let component_total_valid = + match component_total with + | Ok total -> Scalar.Money.equal total fee + | Error _ -> false + in + if fee_components <> [] && not component_total_valid then + Error "fill fee components must sum to the fill fee" + else + Ok + { + id; + order_id; + instrument_id; + quote_currency; + side; + quantity; + price; + notional; + fee; + fee_components; + executed_at; + slice_sequence; + } + +let create ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity ~price + ~fee ~executed_at ~slice_sequence = + create_internal ~allow_rebate:false ~fee_components:[] ~id ~order_id + ~instrument_id ~quote_currency ~side ~quantity ~price ~fee ~executed_at + ~slice_sequence + +let create_v9 ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity + ~price ~fee ~fee_components ~executed_at ~slice_sequence = + create_internal ~allow_rebate:true ~fee_components ~id ~order_id + ~instrument_id ~quote_currency ~side ~quantity ~price ~fee ~executed_at + ~slice_sequence let equal left right = Id.Fill.equal left.id right.id @@ -61,6 +90,7 @@ let equal left right = && Scalar.Price.equal left.price right.price && Scalar.Money.equal left.notional right.notional && Scalar.Money.equal left.fee right.fee + && left.fee_components = right.fee_components && Ptime.equal left.executed_at right.executed_at && Int64.equal left.slice_sequence right.slice_sequence diff --git a/lib/fill.mli b/lib/fill.mli index fabf5c6..e9d6d9b 100644 --- a/lib/fill.mli +++ b/lib/fill.mli @@ -10,6 +10,7 @@ type t = private { price : Scalar.Price.t; notional : Scalar.Money.t; fee : Scalar.Money.t; + fee_components : Fee_schedule.calculated_component list; executed_at : Ptime.t; slice_sequence : int64; } @@ -27,5 +28,19 @@ val create : slice_sequence:int64 -> (t, string) result +val create_v9 : + id:Id.Fill.t -> + order_id:Id.Order.t -> + instrument_id:Id.Instrument.t -> + quote_currency:string -> + side:Order.side -> + quantity:Scalar.Quantity.t -> + price:Scalar.Price.t -> + fee:Scalar.Money.t -> + fee_components:Fee_schedule.calculated_component list -> + executed_at:Ptime.t -> + slice_sequence:int64 -> + (t, string) result + val equal : t -> t -> bool val pp : Format.formatter -> t -> unit diff --git a/lib/scenario.ml b/lib/scenario.ml index f58799c..919b6d4 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -550,7 +550,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 [ "8"; "7" ] then + if List.mem contract_version [ "9"; "8"; "7" ] then parse_v7_risk base_currency instruments json else parse_legacy_risk base_currency instruments json @@ -565,6 +565,118 @@ let parse_execution_values fields = let* fee_bps = integer ~name:"fee_bps" fee_json in Execution.create ~participation_bps ~fixed_fee ~fee_bps +let parse_fee_component json = + let* fields = + object_fields ~name:"fee component" + ~expected: + [ "name"; "currency"; "kind"; "value"; "rounding"; "applies_to" ] + json + in + let* name_json = field fields "name" in + let* name = string ~name:"fee component name" name_json in + let* currency_json = field fields "currency" in + let* currency = string ~name:"fee component currency" currency_json in + let* kind_json = field fields "kind" in + let* kind = string ~name:"fee component kind" kind_json in + let* value = field fields "value" in + let* basis = + match kind with + | "fixed" -> + Result.map + (fun value -> Fee_schedule.Fixed value) + (parse_money ~name:"fixed fee value" value) + | "notional_bps" -> + Result.map + (fun value -> Fee_schedule.Notional_bps value) + (integer ~name:"notional fee basis points" value) + | "per_unit" -> + Result.map + (fun value -> Fee_schedule.Per_unit value) + (parse_money ~name:"per-unit fee value" value) + | value -> Error (Printf.sprintf "unsupported fee component kind %S" value) + in + let* rounding_json = field fields "rounding" in + let* rounding_name = string ~name:"fee rounding" rounding_json in + let* rounding = Fee_schedule.rounding_of_string rounding_name in + let* applicability_json = field fields "applies_to" in + let* applicability_name = + string ~name:"fee applicability" applicability_json + in + let* applicability = + Fee_schedule.applicability_of_string applicability_name + in + Fee_schedule.create_component ~name ~currency ~basis ~rounding ~applicability + +let parse_optional_money ~name = function + | `Null -> Ok None + | json -> Result.map Option.some (parse_money ~name json) + +let parse_fee_schedule instrument_ids json = + let* fields = + object_fields ~name:"fee schedule" + ~expected: + [ + "schedule_id"; + "instrument_id"; + "settlement_currency"; + "minimum"; + "maximum"; + "components"; + ] + json + in + let* schedule_id_json = field fields "schedule_id" in + let* schedule_id = string ~name:"fee schedule ID" schedule_id_json in + let* instrument_id_json = field fields "instrument_id" in + let* instrument_id = + parse_id Id.Instrument.of_string ~name:"fee schedule instrument_id" + instrument_id_json + in + let* () = + if Id.Instrument.Set.mem instrument_id instrument_ids then Ok () + else Error "fee schedule refers to an unknown instrument" + in + let* settlement_currency_json = field fields "settlement_currency" in + let* settlement_currency = + string ~name:"fee settlement currency" settlement_currency_json + in + let* minimum_json = field fields "minimum" in + let* minimum = parse_optional_money ~name:"fee minimum" minimum_json in + let* maximum_json = field fields "maximum" in + let* maximum = parse_optional_money ~name:"fee maximum" maximum_json in + let* components_value = field fields "components" in + let* components_json = list ~name:"fee components" components_value in + let* components = map_list parse_fee_component components_json in + Fee_schedule.create ~schedule_id ~instrument_id ~settlement_currency ~minimum + ~maximum ~components + +let parse_execution_v2 instruments fields = + let* participation_json = field fields "participation_bps" in + let* participation_bps = + integer ~name:"participation_bps" participation_json + in + let instrument_ids = + List.fold_left + (fun ids instrument -> Id.Instrument.Set.add instrument.Instrument.id ids) + Id.Instrument.Set.empty instruments + in + let* schedules_value = field fields "fee_schedules" in + let* schedules_json = list ~name:"fee_schedules" schedules_value in + let* schedules = + map_list (parse_fee_schedule instrument_ids) schedules_json + in + let scheduled = + List.map Fee_schedule.instrument_id schedules + |> List.sort_uniq Id.Instrument.compare + in + let expected = Id.Instrument.Set.elements instrument_ids in + let* () = + if scheduled = expected then Ok () + else + Error "fee schedules must cover every configured instrument exactly once" + in + Execution.create_v2 ~participation_bps ~fee_schedules:schedules + let parse_legacy_execution ~contract_version json = let* fields = object_fields ~name:"execution" @@ -586,7 +698,7 @@ let parse_legacy_execution ~contract_version json = let* execution = parse_execution_values fields in Ok (execution_model, execution) -let parse_versioned_execution ~contract_version json = +let parse_versioned_execution ~contract_version ~instruments json = let* fields = object_fields ~name:"execution" ~expected:[ "model"; "configuration" ] json in @@ -603,28 +715,35 @@ let parse_versioned_execution ~contract_version json = contract_version) in let* configuration_json = field fields "configuration" in - let expected = - (Execution_model.configuration_contract execution_model).required_fields + let* loose_fields = + match configuration_json with + | `Assoc fields -> Ok fields + | _ -> Error (model_name ^ " execution configuration must be a JSON object") in + let* version_json = field loose_fields "version" in + let* version = string ~name:"execution configuration version" version_json in + let* expected = Execution_model.required_fields execution_model version in let* configuration = object_fields ~name:(model_name ^ " execution configuration") ~expected configuration_json in - let* version_json = field configuration "version" in - let* version = string ~name:"execution configuration version" version_json in if not (Execution_model.supports_configuration execution_model version) then Error (Printf.sprintf "unsupported execution configuration version %S for model %S" version model_name) else - let* execution = parse_execution_values configuration in + let* execution = + 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 json = - if List.mem contract_version [ "8"; "7"; "6"; "5" ] then - parse_versioned_execution ~contract_version json +let parse_execution ~contract_version ~instruments json = + if List.mem contract_version [ "9"; "8"; "7"; "6"; "5" ] then + parse_versioned_execution ~contract_version ~instruments json else parse_legacy_execution ~contract_version json let parse_side json = @@ -672,7 +791,7 @@ let parse_portfolio_intent ~name ~parse_target make json = Ok (make targets) let parse_submit_intent ~contract_version json = - let versioned = String.equal contract_version "8" in + let versioned = List.mem contract_version [ "9"; "8" ] in let* fields = object_fields ~name:"submit_order intent" ~expected: @@ -1063,7 +1182,7 @@ let construct_header ~root ~contract_path ~contract_version |> at (child root "base_currency") in let* initial_cash, initial_portfolio = - if List.mem contract_version [ "8"; "7"; "6" ] then + if List.mem contract_version [ "9"; "8"; "7"; "6" ] then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state |> at (child root "initial_portfolio") @@ -1120,7 +1239,7 @@ let construct_header ~root ~contract_path ~contract_version ~instruments ~risk portfolio in let* execution_model, execution = - parse_execution ~contract_version shape.execution + parse_execution ~contract_version ~instruments shape.execution |> at (child root "execution") in let header : stream_header = diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index 0d2c348..3076a4a 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -68,13 +68,13 @@ 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 [ "8"; "7"; "6" ] then "initial_portfolio" + if List.mem contract_version [ "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 [ "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "9"; "8"; "7"; "6"; "5" ] then List.assoc_opt "venue_calendars" fields else None in @@ -105,12 +105,12 @@ let batch json = match preliminary with `String value -> value | _ -> "" in let calendar_fields = - if List.mem contract_version [ "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "8"; "7"; "6" ] then "initial_portfolio" + if List.mem contract_version [ "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in let* fields = @@ -141,12 +141,12 @@ let batch json = let stream_header ~contract_version json = let root = "$.payload" in let calendar_fields = - if List.mem contract_version [ "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "8"; "7"; "6" ] then "initial_portfolio" + if List.mem contract_version [ "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in let* fields = diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 840b3fd..8dea9b9 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -48,7 +48,7 @@ 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 [ "8"; "7"; "6" ] then Ok () + if List.mem contract_version [ "9"; "8"; "7"; "6" ] then Ok () else Account.create ~base_currency ~initial_cash |> Result.map (fun _ -> ()) @@ -66,7 +66,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path:(child root "instruments") "instrument IDs must be unique" else let* () = - if List.mem contract_version [ "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "9"; "8"; "7"; "6"; "5" ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -84,7 +84,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path: (child root - (if List.mem contract_version [ "8"; "7"; "6" ] then + (if List.mem contract_version [ "9"; "8"; "7"; "6" ] then "initial_portfolio.cash" else "initial_cash")) "initial cash must contain every scenario currency exactly once" @@ -107,9 +107,14 @@ let initial_portfolio ~root ~currencies ~catalog ~instruments ~risk initial = if List.sort String.compare cash_currencies <> expected_currencies then fail ~json_path:(child path "cash") "initial cash must contain every scenario currency exactly once" - else if List.sort String.compare fx_currencies <> expected_currencies then + else if + not + (List.for_all + (fun currency -> List.mem currency fx_currencies) + expected_currencies) + then fail ~json_path:(child path "fx_rates") - "initial FX rates must contain every scenario currency exactly once" + "initial FX rates must contain every scenario currency" else let instrument_map = List.fold_left @@ -358,7 +363,7 @@ let validate_slices_at ~paths ~base_currency ~currencies ~instruments slices = if not (Id.Instrument.Set.equal catalog ids) then fail ~json_path:(child root "bars") "each market slice must contain every configured instrument" - else if not (String_set.equal expected_currencies fx_currencies) then + else if not (String_set.subset expected_currencies fx_currencies) then fail ~json_path:(child root "fx_rates") "each market slice must contain every scenario currency FX rate" else if diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index 9351ac6..c8f135b 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -99,7 +99,9 @@ let group_kind_to_string = function | Risk.Custom -> "custom" let nullable render = Option.fold ~none:`Null ~some:render -let modern_protocol protocol_version = List.mem protocol_version [ "6"; "5" ] + +let modern_protocol protocol_version = + List.mem protocol_version [ "7"; "6"; "5" ] let instrument_policy_to_yojson (policy : Risk.instrument_policy) = `Assoc @@ -161,7 +163,62 @@ let risk_to_yojson ~protocol_version risk = ] let execution_to_yojson ~protocol_version model execution = - if modern_protocol protocol_version then + let fee_component_to_yojson component = + let value = + match Fee_schedule.component_basis component with + | Fee_schedule.Fixed value | Fee_schedule.Per_unit value -> money value + | Fee_schedule.Notional_bps value -> `Int value + in + `Assoc + [ + ("name", string (Fee_schedule.component_name component)); + ("currency", string (Fee_schedule.component_currency component)); + ( "kind", + string + (Fee_schedule.basis_kind (Fee_schedule.component_basis component)) + ); + ("value", value); + ( "rounding", + string + (Fee_schedule.rounding_to_string + (Fee_schedule.component_rounding component)) ); + ( "applies_to", + string + (Fee_schedule.applicability_to_string + (Fee_schedule.component_applicability component)) ); + ] + in + let fee_schedule_to_yojson schedule = + `Assoc + [ + ("schedule_id", string (Fee_schedule.schedule_id schedule)); + ("instrument_id", instrument_id (Fee_schedule.instrument_id schedule)); + ( "settlement_currency", + string (Fee_schedule.settlement_currency schedule) ); + ("minimum", nullable money (Fee_schedule.minimum schedule)); + ("maximum", nullable money (Fee_schedule.maximum schedule)); + ( "components", + `List + (List.map fee_component_to_yojson + (Fee_schedule.components schedule)) ); + ] + in + if String.equal protocol_version "7" then + `Assoc + [ + ("model", string (Execution_model.name model)); + ( "configuration", + `Assoc + [ + ("version", string "2"); + ("participation_bps", `Int (Execution.participation_bps execution)); + ( "fee_schedules", + `List + (List.map fee_schedule_to_yojson + (Execution.fee_schedules execution)) ); + ] ); + ] + else if modern_protocol protocol_version then `Assoc [ ("model", string (Execution_model.name model)); @@ -229,7 +286,7 @@ let initialize_message ~sequence:message_sequence initialization = ] in let fields = - if String.equal protocol_version version then + if List.mem protocol_version [ "7"; "6" ] then let initial_portfolio = Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson initialization.initial_portfolio @@ -350,7 +407,7 @@ let context_to_yojson ~protocol_version context = ( "working_orders", `List (List.map - (if String.equal protocol_version version then + (if List.mem protocol_version [ "7"; "6" ] then Codec.order_to_yojson_v8 else Codec.order_to_yojson) working_orders) ); @@ -367,14 +424,18 @@ let event_to_yojson ~protocol_version = function | Strategy.Fill_received fill -> `Assoc [ - ("type", string "fill_received"); ("fill", Codec.fill_to_yojson fill); + ("type", string "fill_received"); + ( "fill", + if String.equal protocol_version "7" then + Codec.fill_to_yojson_v9 fill + else Codec.fill_to_yojson fill ); ] | Strategy.Order_updated order -> `Assoc [ ("type", string "order_updated"); ( "order", - if String.equal protocol_version version then + if List.mem protocol_version [ "7"; "6" ] then Codec.order_to_yojson_v8 order else Codec.order_to_yojson order ); ] @@ -462,7 +523,9 @@ let parse_intents_payload ~protocol_version json = let* intent = Scenario.intent_of_yojson ~contract_version: - (if String.equal protocol_version version then "8" else "7") + (if String.equal protocol_version "7" then "9" + else if String.equal protocol_version "6" then "8" + else "7") value |> Result.map_error Diagnostic.to_human in diff --git a/mkdocs.yml b/mkdocs.yml index fe99493..f746a31 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,14 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v8: contracts/v8/README.md + - Current v9: contracts/v9/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 v6: contracts/strategy/v6/README.md + - Current v7: contracts/strategy/v7/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 ca96b46..77e367d 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -58,3 +58,11 @@ compare_journal \ v8-fill-clipped \ contracts/v8/fixtures/fill-clipped.scenario.json \ contracts/v8/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v9-demo \ + contracts/v9/fixtures/demo.scenario.json \ + contracts/v9/fixtures/demo.journal.jsonl +compare_journal \ + v9-fill-clipped \ + contracts/v9/fixtures/fill-clipped.scenario.json \ + contracts/v9/fixtures/fill-clipped.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index e3ed051..6ea8f8f 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/v8/README.md", + "contracts/v9/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v6/README.md", + "contracts/strategy/v7/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 39a0eda..5226355 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/v8/scenario.schema.json", - "share/trading_engine/contracts/v8/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v9/scenario.schema.json", + "share/trading_engine/contracts/v9/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/v8/fixtures/demo.scenario.json", + "contracts/v9/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/v8/fixtures/demo.scenario.json", - "contracts/strategy/v6/message.schema.json", + "contracts/v9/fixtures/demo.scenario.json", + "contracts/strategy/v7/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v8/scenario.schema.json", + "contracts/v9/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index 9f571c2..d195f29 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":["8","7","6","5","4","3"],"journal_contract_versions":["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":["1"],"scenario_contract_versions":["8","7","6","5","4","3"],"required_fields":["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":["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":["9","8","7","6","5","4","3"],"journal_contract_versions":["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":["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":["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 41366b2..170c607 100644 --- a/test/dune +++ b/test/dune @@ -7,6 +7,7 @@ test_accounting test_execution test_order_lifetimes + test_fee_schedules test_reducer test_reducer_properties test_checkpoint4 @@ -34,6 +35,14 @@ ../contracts/v8/journal.schema.json ../contracts/v8/scenario-stream.schema.json ../contracts/v8/scenario.schema.json + ../contracts/v9/fixtures/demo.journal.jsonl + ../contracts/v9/fixtures/demo.scenario.json + ../contracts/v9/fixtures/demo.scenario.jsonl + ../contracts/v9/fixtures/fill-clipped.journal.jsonl + ../contracts/v9/fixtures/fill-clipped.scenario.json + ../contracts/v9/journal.schema.json + ../contracts/v9/scenario-stream.schema.json + ../contracts/v9/scenario.schema.json ../contracts/v6/fixtures/demo.scenario.json ../contracts/v6/fixtures/demo.scenario.jsonl ../contracts/v5/fixtures/demo.scenario.json @@ -45,6 +54,7 @@ ../contracts/conformance/cases.json ../contracts/strategy/v5/fixtures/external.strategy.jsonl ../contracts/strategy/v6/fixtures/external.strategy.jsonl + ../contracts/strategy/v7/fixtures/external.strategy.jsonl ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries @@ -63,6 +73,48 @@ (modules fuzz_protocol) (libraries trading_engine yojson unix)) +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v9/fixtures/demo.journal.jsonl + ../contracts/v9/fixtures/demo.scenario.json + ../contracts/v9/fixtures/demo.scenario.jsonl + ../contracts/v9/journal.schema.json + ../contracts/v9/scenario-stream.schema.json + ../contracts/v9/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v9/scenario.schema.json} + %{dep:../contracts/v9/scenario-stream.schema.json} + %{dep:../contracts/v9/journal.schema.json} + %{dep:../contracts/v9/fixtures/demo.scenario.json} + %{dep:../contracts/v9/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v9/fixtures/demo.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v9/fixtures/fill-clipped.journal.jsonl + ../contracts/v9/fixtures/fill-clipped.scenario.json + ../contracts/v9/fixtures/demo.scenario.jsonl + ../contracts/v9/journal.schema.json + ../contracts/v9/scenario-stream.schema.json + ../contracts/v9/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v9/scenario.schema.json} + %{dep:../contracts/v9/scenario-stream.schema.json} + %{dep:../contracts/v9/journal.schema.json} + %{dep:../contracts/v9/fixtures/fill-clipped.scenario.json} + %{dep:../contracts/v9/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v9/fixtures/fill-clipped.journal.jsonl}))) + (rule (alias runtest) (deps @@ -81,6 +133,27 @@ ../contracts/v8/fixtures/demo.scenario.json ../contracts/v8/fixtures/demo.scenario.jsonl)) +(rule + (alias runtest) + (deps + validate_strategy_schema.py + ../contracts/v9/scenario.schema.json + ../contracts/v9/journal.schema.json + ../contracts/diagnostic/v1/diagnostic.schema.json + ../contracts/strategy/v7/message.schema.json + ../contracts/strategy/v7/transcript.schema.json + ../contracts/strategy/v7/fixtures/external.strategy.jsonl) + (action + (run + python3 + %{dep:validate_strategy_schema.py} + %{dep:../contracts/v9/scenario.schema.json} + %{dep:../contracts/v9/journal.schema.json} + %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} + %{dep:../contracts/strategy/v7/message.schema.json} + %{dep:../contracts/strategy/v7/transcript.schema.json} + %{dep:../contracts/strategy/v7/fixtures/external.strategy.jsonl}))) + (rule (alias runtest) (deps @@ -274,6 +347,6 @@ (deps test_benchmark_replay.py ../bench/benchmark_replay.py - ../contracts/v8/fixtures/demo.scenario.json) + ../contracts/v9/fixtures/demo.scenario.json) (action (run python3 %{dep:test_benchmark_replay.py}))) diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 8eaa77c..da1f2dc 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -114,15 +114,15 @@ let capabilities_describe_execution_contracts () = | _ -> Alcotest.fail (name ^ " must be an array") in Alcotest.(check (list string)) - "configuration versions" [ "1" ] + "configuration versions" [ "2"; "1" ] (strings "configuration_versions"); Alcotest.(check (list string)) "scenario contracts" - [ "8"; "7"; "6"; "5"; "4"; "3" ] + [ "9"; "8"; "7"; "6"; "5"; "4"; "3" ] (strings "scenario_contract_versions"); Alcotest.(check (list string)) "required fields" - [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ] + [ "version"; "participation_bps"; "fee_schedules" ] (strings "required_fields"); Alcotest.(check (list string)) "order types" diff --git a/test/test_engine.ml b/test/test_engine.ml index be83d00..3de01c2 100644 --- a/test/test_engine.ml +++ b/test/test_engine.ml @@ -6,6 +6,7 @@ let () = ("accounting", Test_accounting.tests); ("execution", Test_execution.tests); ("order-lifetimes", Test_order_lifetimes.tests); + ("fee-schedules", Test_fee_schedules.tests); ("reducer", Test_reducer.tests); ("reducer-properties", Test_reducer_properties.tests); ("checkpoint4", Test_checkpoint4.tests); diff --git a/test/test_fee_schedules.ml b/test/test_fee_schedules.ml new file mode 100644 index 0000000..b94e6cb --- /dev/null +++ b/test/test_fee_schedules.ml @@ -0,0 +1,135 @@ +open Test_support +module T = Trading_engine + +let component ?(currency = "USD") ?(rounding = T.Fee_schedule.Up) + ?(applies_to = T.Fee_schedule.Any) name basis = + T.Fee_schedule.create_component ~name ~currency ~basis ~rounding + ~applicability:applies_to + |> ok + +let schedule ?(minimum = None) ?(maximum = None) components = + T.Fee_schedule.create ~schedule_id:"test-fees-v1" + ~instrument_id:(instrument_id "test-equity") + ~settlement_currency:"USD" ~minimum ~maximum ~components + |> ok + +let calculate schedule ~liquidity ~notional_value ~quantity_value = + T.Fee_schedule.calculate schedule ~quote_currency:"USD" + ~notional:(money notional_value) ~quantity:(quantity quantity_value) + ~liquidity + ~fx_rates:[ ("USD", price "1"); ("EUR", price "1.2") ] + |> ok + +let components_minimums_caps_and_fx () = + let fees = + schedule + ~minimum:(Some (money "0.5")) + ~maximum:(Some (money "2")) + [ + component "broker" (T.Fee_schedule.Fixed (money "0.1")); + component ~currency:"EUR" "exchange" (T.Fee_schedule.Notional_bps 10); + component ~rounding:T.Fee_schedule.Nearest + ~applies_to:T.Fee_schedule.Maker_only "maker_rebate" + (T.Fee_schedule.Notional_bps (-5)); + component "regulatory" (T.Fee_schedule.Per_unit (money "0.01")); + ] + in + let taker_components, taker = + calculate fees ~liquidity:T.Fee_schedule.Taker ~notional_value:"100" + ~quantity_value:"10" + in + Alcotest.check money_testable "minimum applied after FX" (money "0.5") taker; + Alcotest.(check int) + "three charges and minimum adjustment" 4 + (List.length taker_components); + let maker_components, maker = + calculate fees ~liquidity:T.Fee_schedule.Maker ~notional_value:"100" + ~quantity_value:"10" + in + Alcotest.check money_testable "rebate still observes minimum" (money "0.5") + maker; + Alcotest.(check bool) + "maker attribution includes rebate" true + (List.exists + (fun component -> + String.equal component.T.Fee_schedule.name "maker_rebate" + && T.Scalar.Money.compare component.quote_amount T.Scalar.Money.zero + < 0) + maker_components); + let capped = + schedule + ~maximum:(Some (money "2")) + [ component "broker" (T.Fee_schedule.Fixed (money "3")) ] + in + let capped_components, capped_total = + calculate capped ~liquidity:T.Fee_schedule.Taker ~notional_value:"100" + ~quantity_value:"1" + in + Alcotest.check money_testable "cap" (money "2") capped_total; + Alcotest.(check bool) + "cap is attributed" true + (List.exists + (fun component -> + String.equal component.T.Fee_schedule.kind "maximum_adjustment") + capped_components) + +let fragmented_fills_pay_per_fill_minimum () = + let fees = + schedule + ~minimum:(Some (money "0.5")) + [ component "exchange" (T.Fee_schedule.Notional_bps 1) ] + in + let _, whole = + calculate fees ~liquidity:T.Fee_schedule.Taker ~notional_value:"100" + ~quantity_value:"10" + in + let _, fragment = + calculate fees ~liquidity:T.Fee_schedule.Taker ~notional_value:"50" + ~quantity_value:"5" + in + let fragmented = T.Scalar.Money.add fragment fragment |> ok in + Alcotest.check money_testable "whole minimum" (money "0.5") whole; + Alcotest.check money_testable "two fill minimums" (money "1") fragmented + +let rebate_settles_and_is_attributed () = + let fees = + schedule + [ + component ~rounding:T.Fee_schedule.Nearest "maker_rebate" + (T.Fee_schedule.Notional_bps (-10)); + ] + in + let fee_components, fee = + calculate fees ~liquidity:T.Fee_schedule.Maker ~notional_value:"100" + ~quantity_value:"1" + in + Alcotest.check money_testable "negative rebate" (money "-0.1") fee; + let request = request ~quantity_value:"1" () in + let fill = + T.Fill.create_v9 ~id:(fill_id "rebate-fill") + ~order_id:(order_id "rebate-order") ~instrument_id:request.instrument_id + ~quote_currency:"USD" ~side:T.Order.Buy ~quantity:(quantity "1") + ~price:(price "100") ~fee ~fee_components + ~executed_at:(timestamp "2026-01-03T14:30:00Z") + ~slice_sequence:1L + |> ok + in + let account = T.Account.apply_fill (test_account ()) fill |> ok in + Alcotest.check money_testable "rebate increases cash" (money "9900.1") + (T.Account.cash account "USD" |> Option.get); + let position = T.Account.position account request.instrument_id in + Alcotest.check money_testable "signed execution fee" (money "-0.1") + position.execution_fees; + Alcotest.(check int) + "position component attribution" 1 + (List.length position.execution_fee_components) + +let tests = + [ + Alcotest.test_case "components, FX, minimums, and caps" `Quick + components_minimums_caps_and_fx; + Alcotest.test_case "fragmented minimums" `Quick + fragmented_fills_pay_per_fill_minimum; + Alcotest.test_case "rebate accounting attribution" `Quick + rebate_settles_and_is_attributed; + ] diff --git a/test/test_scenario.ml b/test/test_scenario.ml index f7bb4f0..0b56434 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/v8/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v9/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/v8/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v9/fixtures/demo.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -125,9 +125,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v8/scenario.schema.json"; - check_schema "../contracts/v8/scenario-stream.schema.json"; - check_schema "../contracts/v8/journal.schema.json" + check_schema "../contracts/v9/scenario.schema.json"; + check_schema "../contracts/v9/scenario-stream.schema.json"; + check_schema "../contracts/v9/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -189,8 +189,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 8, 7, 6, 5, \ - 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 9, 8, 7, 6, \ + 5, 4, 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -751,11 +751,11 @@ let execution_model_is_required_and_supported () = "configuration version required" true (Result.is_error (T.Scenario.of_yojson missing_version)); let unsupported_version = - change_configuration (change_field "version" (`String "2")) + change_configuration (change_field "version" (`String "99")) in Alcotest.(check string) "unsupported model/version diagnosed" - "unsupported execution configuration version \"2\" for model \ + "unsupported execution configuration version \"99\" for model \ \"completed_bar_v1\"" (T.Scenario.of_yojson unsupported_version |> diagnostic_message); let extra_configuration = @@ -872,7 +872,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v8/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v9/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -900,7 +900,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v8/fixtures/fill-clipped.scenario.json" In_channel.input_all + "../contracts/v9/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in let result = @@ -913,7 +913,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v8/fixtures/fill-clipped.journal.jsonl" In_channel.input_all + "../contracts/v9/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual @@ -1013,7 +1013,7 @@ let streamed_replay_matches_batch_semantics () = Alcotest.(check int64) "two schedule batches" 2L result.schedule_count; Alcotest.(check int) "one instrument" 1 result.instrument_count; Alcotest.(check int64) "twenty-two audits" 22L result.audit_count; - Alcotest.check money_testable "same equity" (money "10111.65392") + Alcotest.check money_testable "same equity" (money "10111.661495") 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 54dcc28..a6eeea2 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -3,6 +3,18 @@ module T = Trading_engine let initialization () = let instrument = instrument () in + let component = + T.Fee_schedule.create_component ~name:"broker" ~currency:"USD" + ~basis:(T.Fee_schedule.Fixed (money "0.25")) + ~rounding:T.Fee_schedule.Up ~applicability:T.Fee_schedule.Any + |> ok + in + let fee_schedule = + T.Fee_schedule.create ~schedule_id:"test-fees-v1" + ~instrument_id:instrument.id ~settlement_currency:"USD" ~minimum:None + ~maximum:None ~components:[ component ] + |> ok + in T.Strategy_protocol. { scenario_contract_version = T.Contract.version; @@ -16,7 +28,10 @@ let initialization () = venue_calendars = []; risk = risk ~instruments:[ instrument ] (); execution_model = T.Execution_model.find "completed_bar_v1" |> ok; - execution = execution (); + execution = + T.Execution.create_v2 ~participation_bps:10_000 + ~fee_schedules:[ fee_schedule ] + |> ok; } let field name = function @@ -28,7 +43,7 @@ let initialize_message_is_complete () = T.Strategy_protocol.initialize_message ~sequence:1L (initialization ()) in Alcotest.(check string) - "protocol version" "6" + "protocol version" "7" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -205,7 +220,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "6"); + ("strategy_protocol_version", `String "7"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); @@ -263,8 +278,8 @@ let responses_are_strict_and_typed () = let duplicate = `Assoc [ - ("strategy_protocol_version", `String "6"); - ("strategy_protocol_version", `String "6"); + ("strategy_protocol_version", `String "7"); + ("strategy_protocol_version", `String "7"); ("strategy_sequence", `String "3"); ("message_type", `String "stopped"); ("payload", `Assoc []); @@ -291,7 +306,7 @@ let responses_are_strict_and_typed () = let unknown_field = `Assoc [ - ("strategy_protocol_version", `String "6"); + ("strategy_protocol_version", `String "7"); ("strategy_sequence", `String "3"); ("message_type", `String "stopped"); ("payload", `Assoc []); diff --git a/test/test_support.ml b/test/test_support.ml index 3a25e54..93402b4 100644 --- a/test/test_support.ml +++ b/test/test_support.ml @@ -162,7 +162,7 @@ let engine_config_v8 ?(risk = risk ()) ?(venue_calendars = []) ?execution_model Option.value execution_model ~default:(T.Execution_model.find "completed_bar_v1" |> ok) in - T.Engine.config_v8 ~contract_version:T.Contract.version ~risk ~venue_calendars + T.Engine.config_v8 ~contract_version:"8" ~risk ~venue_calendars ~execution_model ~execution ~max_internal_events |> ok From 064d0023f356f1efe7a83f6bd9e764e90072222b Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 14:10:54 -0400 Subject: [PATCH 44/57] feat: add borrow availability and financing accrual --- CHANGELOG.md | 6 + README.md | 34 +- bench/benchmark_replay.py | 21 +- contracts/conformance/cases.json | 131 + contracts/conformance/manifest.json | 79 + contracts/strategy/v8/README.md | 56 + contracts/strategy/v8/dune | 15 + .../v8/fixtures/external.scenario.json | 271 ++ .../v8/fixtures/external.scenario.jsonl | 4 + .../v8/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v8/message.schema.json | 299 +++ contracts/strategy/v8/transcript.schema.json | 82 + contracts/v10/README.md | 62 + contracts/v10/dune | 18 + contracts/v10/fixtures/demo.journal.jsonl | 26 + contracts/v10/fixtures/demo.scenario.json | 411 +++ contracts/v10/fixtures/demo.scenario.jsonl | 6 + .../v10/fixtures/fill-clipped.journal.jsonl | 13 + .../v10/fixtures/fill-clipped.scenario.json | 236 ++ contracts/v10/journal.schema.json | 2193 +++++++++++++++++ contracts/v10/scenario-stream.schema.json | 77 + contracts/v10/scenario.schema.json | 510 ++++ docs/api-reference.md | 2 +- docs/continuous-integration.md | 4 +- docs/execution-model.md | 29 +- docs/persistra.md | 13 +- docs/scenario.md | 40 +- lib/account.ml | 43 +- lib/account.mli | 6 + lib/audit.ml | 32 + lib/audit.mli | 28 + lib/codec.ml | 151 +- lib/codec.mli | 1 + lib/contract.ml | 11 +- lib/engine.ml | 388 ++- lib/engine.mli | 10 + lib/execution.ml | 6 +- lib/execution_model.ml | 2 +- lib/external_replay.ml | 16 +- lib/financing.ml | 164 ++ lib/financing.mli | 73 + lib/market_slice.ml | 53 +- lib/market_slice.mli | 15 + lib/order.ml | 3 +- lib/order.mli | 2 +- lib/replay.ml | 17 +- lib/risk.ml | 1 + lib/risk.mli | 1 + lib/scenario.ml | 201 +- lib/scenario.mli | 2 + lib/scenario_shape.ml | 27 +- lib/scenario_shape.mli | 1 + lib/scenario_validation.ml | 6 +- lib/strategy_protocol.ml | 86 +- lib/strategy_protocol.mli | 1 + mkdocs.yml | 4 +- scripts/check-deterministic-journals | 8 + scripts/check-documentation.py | 4 +- scripts/release_artifacts.py | 12 +- test/cli.t | 2 +- test/dune | 73 + test/test_boundary_failures.ml | 1 + test/test_diagnostic.ml | 2 +- test/test_engine.ml | 1 + test/test_financing.ml | 464 ++++ test/test_scenario.ml | 108 +- test/test_strategy_protocol.ml | 5 +- 67 files changed, 6506 insertions(+), 177 deletions(-) create mode 100644 contracts/strategy/v8/README.md create mode 100644 contracts/strategy/v8/dune create mode 100644 contracts/strategy/v8/fixtures/external.scenario.json create mode 100644 contracts/strategy/v8/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v8/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v8/message.schema.json create mode 100644 contracts/strategy/v8/transcript.schema.json create mode 100644 contracts/v10/README.md create mode 100644 contracts/v10/dune create mode 100644 contracts/v10/fixtures/demo.journal.jsonl create mode 100644 contracts/v10/fixtures/demo.scenario.json create mode 100644 contracts/v10/fixtures/demo.scenario.jsonl create mode 100644 contracts/v10/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v10/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v10/journal.schema.json create mode 100644 contracts/v10/scenario-stream.schema.json create mode 100644 contracts/v10/scenario.schema.json create mode 100644 lib/financing.ml create mode 100644 lib/financing.mli create mode 100644 test/test_financing.ml diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c8936..94ab8fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +- Add effective-time borrow availability, signed rates, locate clipping or rejection, recalls, + deterministic close-outs, and explicit missing-data behavior. +- Add effective-time currency credit/debit rates with Actual/365 or Actual/360 day count, simple or + daily compounding, deterministic cash-ledger entries, and realized P&L attribution. +- Publish scenario/journal contract v10 and external strategy protocol v8 while preserving v9 and + protocol v7 as frozen compatibility contracts. - Added instrument-aware, composable fee schedules with named fixed, notional, and per-unit components; explicit rounding; maker/taker applicability; per-fill minimums and caps; rebates; and deterministic multi-currency conversion. diff --git a/README.md b/README.md index 5db1024..fb4d30d 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,15 @@ scenario slices and scheduled or external intents - Explicit multi-currency cash ledgers and complete per-slice FX marks in a base currency - Explicit signed initial portfolios with cost basis, P&L and fee history, marks, and FX state - Split and cash-dividend processing before matching, including target and order adjustment -- Short borrow accrual, maintenance-margin calls, and deterministic liquidation orders +- Effective-time short locates, availability clipping, borrow-rate accrual, recalls, and + deterministic close-out orders +- Per-currency credit/debit cash rates with explicit day-count and compounding policies - Signed average-cost accounting, realized and unrealized P&L, and equity reconciliation - Per-currency cash and per-instrument quantity, mark, value, basis, P&L, aggregate fee, and named 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; v9 currently exposes `completed_bar_v1` configuration v2 + capability descriptors; v10 currently exposes `completed_bar_v1` configuration v2 - 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 @@ -88,7 +90,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v9/fixtures/demo.scenario.json \ + --input contracts/v10/fixtures/demo.scenario.json \ --validate-only ``` @@ -96,7 +98,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v9/fixtures/demo.scenario.json \ + --input contracts/v10/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -104,7 +106,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v9/fixtures/demo.scenario.jsonl \ + --input contracts/v10/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -113,7 +115,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v7/fixtures/external.scenario.json \ + --input contracts/strategy/v8/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -183,7 +185,11 @@ slice whose start is not earlier than its creation time. sells precede buys and FIFO creation order breaks ties within a side. - Corporate actions are applied before matching. Splits adjust positions, persistent targets, and active orders; cash dividends credit longs and debit shorts in the quote-currency ledger. -- Borrow fees accrue on open shorts for the slice interval before matching. +- Effective-time borrow observations control short availability and rates. New shorts are rejected + or clipped to their locate, recalls reject new shorts or create deterministic close-out orders, + and observed borrow charges accrue before matching. +- Effective-time currency observations credit positive cash and debit negative cash for the slice + interval under the configured day-count, compounding, and missing-data policies. - Proposed fills are clipped to the largest permitted fractional-lot quantity at the actual fill price and never exceed the maximum order quantity. Increasing exposure must satisfy position, gross-exposure, leverage, and initial-margin limits; exposure-reducing fills remain available. @@ -220,19 +226,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 v9 and conformance fixtures](contracts/v9/README.md) +- [Current contract v10 and conformance fixtures](contracts/v10/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v9/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v9/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v9/journal.schema.json) -- [External strategy protocol v7](contracts/strategy/v7/README.md) +- [Scenario JSON Schema](contracts/v10/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v10/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v10/journal.schema.json) +- [External strategy protocol v8](contracts/strategy/v8/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/v7/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v7/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v8/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v8/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/bench/benchmark_replay.py b/bench/benchmark_replay.py index e3a7cb8..899f4b3 100644 --- a/bench/benchmark_replay.py +++ b/bench/benchmark_replay.py @@ -23,7 +23,7 @@ ROOT = Path(__file__).resolve().parents[1] DEFAULT_EXECUTABLE = ROOT / "_build/default/bin/main.exe" DEFAULT_BASELINE = ROOT / "bench/baselines/linux-x86_64.json" -FIXTURE = ROOT / "contracts/v9/fixtures/demo.scenario.json" +FIXTURE = ROOT / "contracts/v10/fixtures/demo.scenario.json" STRATEGY = ROOT / "bench/latency_strategy.py" SUMMARY_PATTERN = re.compile( r"\baudits=(?P[0-9]+).*\bactive=(?P[0-9]+)" @@ -133,6 +133,24 @@ def build_scenario(case: BenchmarkCase) -> dict[str, object]: for instrument in instruments ], "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": instrument["instrument_id"], + "effective_at": timestamp(start), + "available_quantity": "1000000", + "annual_rate_bps": 0, + "recalled": False, + } + for instrument in instruments + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": timestamp(start), + "credit_rate_bps": 0, + "debit_rate_bps": 0, + } + ], } ) slices.append(market_slice) @@ -249,6 +267,7 @@ def stream_records(document: dict[str, object]) -> list[dict[str, object]]: "venue_calendars", "risk", "execution", + "financing", "max_internal_events", ) records = [ diff --git a/contracts/conformance/cases.json b/contracts/conformance/cases.json index 424e173..c578aed 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -719,6 +719,107 @@ "schema_expectation": "accept", "runtime_expectation": "accept", "rule": "structural" + }, + { + "name": "scenario-v10-valid", + "artifact": "scenario-v10", + "kind": "scenario", + "source": "v10/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-stream-v10-valid", + "artifact": "scenario-stream-v10", + "kind": "scenario_stream", + "source": "v10/fixtures/demo.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-ready-valid-v8", + "artifact": "strategy-message-v8", + "kind": "strategy_response", + "source": "strategy/v8/fixtures/external.strategy.jsonl", + "record": 2, + "extract": [ + "message" + ], + "expected_sequence": "1", + "protocol_version": "8", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-intents-valid-v8", + "artifact": "strategy-message-v8", + "kind": "strategy_response", + "source": "strategy/v8/fixtures/external.strategy.jsonl", + "record": 4, + "extract": [ + "message" + ], + "expected_sequence": "2", + "protocol_version": "8", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-stopped-valid-v8", + "artifact": "strategy-message-v8", + "kind": "strategy_response", + "source": "strategy/v8/fixtures/external.strategy.jsonl", + "record": 14, + "extract": [ + "message" + ], + "expected_sequence": "7", + "protocol_version": "8", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-error-valid-v8", + "artifact": "strategy-message-v8", + "kind": "strategy_response", + "source": "strategy/v8/fixtures/external.strategy.jsonl", + "record": 14, + "extract": [ + "message" + ], + "expected_sequence": "7", + "protocol_version": "8", + "mutations": [ + { + "op": "replace", + "path": [ + "message_type" + ], + "value": "error" + }, + { + "op": "replace", + "path": [ + "payload" + ], + "value": { + "message": "fixture failure" + } + } + ], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" } ], "schema_only_cases": [ @@ -953,6 +1054,36 @@ "mutations": [], "schema_expectation": "accept", "source": "strategy/v7/fixtures/external.strategy.jsonl" + }, + { + "name": "strategy-v8-rejected-response-branch", + "artifact": "strategy-transcript-v8", + "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", + "source": "strategy/v8/fixtures/external.strategy.jsonl" } ] } diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index df297da..8f33b36 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -629,6 +629,85 @@ "format": "jsonl" } ] + }, + { + "name": "scenario-v10", + "schema": "v10/scenario.schema.json", + "version_field": "contract_version", + "version": "10", + "sources": [ + { + "path": "v10/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v10/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "strategy/v8/fixtures/external.scenario.json", + "format": "json" + } + ] + }, + { + "name": "scenario-stream-v10", + "schema": "v10/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "10", + "sources": [ + { + "path": "v10/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v8/fixtures/external.scenario.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "journal-v10", + "schema": "v10/journal.schema.json", + "version_field": "contract_version", + "version": "10", + "sources": [ + { + "path": "v10/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v10/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "strategy-message-v8", + "schema": "strategy/v8/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "8", + "sources": [ + { + "path": "strategy/v8/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } + ] + }, + { + "name": "strategy-transcript-v8", + "schema": "strategy/v8/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "8", + "sources": [ + { + "path": "strategy/v8/fixtures/external.strategy.jsonl", + "format": "jsonl" + } + ] } ] } diff --git a/contracts/strategy/v8/README.md b/contracts/strategy/v8/README.md new file mode 100644 index 0000000..5669e19 --- /dev/null +++ b/contracts/strategy/v8/README.md @@ -0,0 +1,56 @@ +# External strategy protocol v8 + +Version 8 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 v7 remains available for scenario contract v9; earlier versions retain their frozen +shapes. + +Every message repeats `strategy_protocol_version: "8"` 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 v10 and includes the exact `initial_portfolio` +snapshot alongside the legacy cash projection. It also carries the complete versioned venue +calendars, nested execution configuration, and financing policy, so a strategy can construct DAY +orders and reject incompatible execution or financing 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, +fills, order updates, and rejected intents. Portfolio contexts include cash-interest attribution. +Response intents use the scenario v10 intent shapes, including recall-origin order snapshots. + +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/v8/dune b/contracts/strategy/v8/dune new file mode 100644 index 0000000..c613fc4 --- /dev/null +++ b/contracts/strategy/v8/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v8/message.schema.json) + (transcript.schema.json as contracts/strategy/v8/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v8/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v8/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v8/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v8/fixtures/external.scenario.json b/contracts/strategy/v8/fixtures/external.scenario.json new file mode 100644 index 0000000..e52dd61 --- /dev/null +++ b/contracts/strategy/v8/fixtures/external.scenario.json @@ -0,0 +1,271 @@ +{ + "contract_version": "10", + "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 + } + ] + }, + { + "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 + } + ] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + } +} diff --git a/contracts/strategy/v8/fixtures/external.scenario.jsonl b/contracts/strategy/v8/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..d89b702 --- /dev/null +++ b/contracts/strategy/v8/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"10","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"}}} +{"contract_version":"10","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}]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"10","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}]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"10","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v8/fixtures/external.strategy.jsonl b/contracts/strategy/v8/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..9622950 --- /dev/null +++ b/contracts/strategy/v8/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"8","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"10","scenario_sha256":"008026ed55d30ccabe95d2bb95a843b7631e00ddde2874155459717b53823c5f","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"},"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":"8","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"8","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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}]}}}}} +{"strategy_protocol_version":"8","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","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":"8","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":"8","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"8","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":"8","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"8","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":"8","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"8","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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}]}}}}} +{"strategy_protocol_version":"8","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"8","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"8","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v8/message.schema.json b/contracts/strategy/v8/message.schema.json new file mode 100644 index 0000000..dacc035 --- /dev/null +++ b/contracts/strategy/v8/message.schema.json @@ -0,0 +1,299 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v8/message.schema.json", + "title": "Trading Engine external strategy protocol v8 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": "8" }, + "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", "metadata"], + "properties": { + "engine_version": { "type": "string", "minLength": 1 }, + "scenario_contract_version": { "const": "10" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/venueCalendar" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/financing" }, + "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/v10/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/v10/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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/v10/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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/v10/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/v10/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/signedDecimal" } + ] + }, + "markedPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], + "properties": { + "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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/v10/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/v10/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/v10/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/v10/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/v8/transcript.schema.json b/contracts/strategy/v8/transcript.schema.json new file mode 100644 index 0000000..8c66867 --- /dev/null +++ b/contracts/strategy/v8/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/v8/transcript.schema.json", + "title": "Trading Engine external strategy protocol v8 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": "8" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v8/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/v10/README.md b/contracts/v10/README.md new file mode 100644 index 0000000..09a77f5 --- /dev/null +++ b/contracts/v10/README.md @@ -0,0 +1,62 @@ +# Trading Engine contract v10 + +This directory is the authoritative v10 process and file contract shared by Trading Engine and its +clients. Versions 9, 8, 7, 6, 5, 4, and 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 v10 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 v10 scenario, stream record, and journal record carries `"contract_version": "10"`. + +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. diff --git a/contracts/v10/dune b/contracts/v10/dune new file mode 100644 index 0000000..07b28ed --- /dev/null +++ b/contracts/v10/dune @@ -0,0 +1,18 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v10/journal.schema.json) + (scenario-stream.schema.json as contracts/v10/scenario-stream.schema.json) + (scenario.schema.json as contracts/v10/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v10/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v10/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl + as + contracts/v10/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v10/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v10/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v10/fixtures/demo.journal.jsonl b/contracts/v10/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..7d920e6 --- /dev/null +++ b/contracts/v10/fixtures/demo.journal.jsonl @@ -0,0 +1,26 @@ +{"contract_version":"10","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":"c129c13b6e4273c4d2a784c5b9aaa2630ad67ecbafd6da303da12b2fb6f253bc","execution_model":"completed_bar_v1"}} +{"contract_version":"10","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} +{"contract_version":"10","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} +{"contract_version":"10","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}]}} +{"contract_version":"10","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":"10","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":"10","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":"10","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":"10","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0.074201","margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052.074201","maintenance_excess":"10078.074201","margin_call":false},"group_exposures":[]}} +{"contract_version":"10","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":"12"}],"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}]}} +{"contract_version":"10","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":"10","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":"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","price":"103","notional":"618","fee":"0.868","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.618","quote_amount":"0.618"}]}} +{"contract_version":"10","engine_sequence":"13","event_id":"demo-event-000000000013","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","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"10","engine_sequence":"14","event_id":"demo-event-000000000014","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.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-000000000014","updated_event_id":"demo-event-000000000014","created_sequence":"14","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":"10","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.280402","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5.148402","unrealized_pnl":"40.132","equity":"10130.280402","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.280402","fx_rate":"1","base_value":"9381.280402","interest":"0.148402","base_interest":"0.148402"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618","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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}]}],"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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"cash_interest":"0.148402","margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.780402","maintenance_excess":"9943.030402","margin_call":false},"group_exposures":[]}} +{"contract_version":"10","engine_sequence":"16","event_id":"demo-event-000000000016","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}]}} +{"contract_version":"10","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":["demo-event-000000000016"],"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":"9381.280402","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.06961","closing_balance":"9381.350012"}} +{"contract_version":"10","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000014","demo-event-000000000016"],"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.715","price":"107","notional":"290.505","fee":"0.540505","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.290505","quote_amount":"0.290505"}]}} +{"contract_version":"10","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000016"],"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":"10","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000016","demo-event-000000000019"],"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-000000000020","updated_event_id":"demo-event-000000000020","created_sequence":"20","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":"10","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000016"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.304507","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5.218012","unrealized_pnl":"20.161495","equity":"10110.379507","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.304507","fx_rate":"1","base_value":"9090.304507","interest":"0.218012","base_interest":"0.218012"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505","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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}]}],"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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"cash_interest":"0.218012","margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.342007","maintenance_excess":"9855.360757","margin_call":false},"group_exposures":[]}} +{"contract_version":"10","engine_sequence":"22","event_id":"demo-event-000000000022","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}]}} +{"contract_version":"10","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000022"],"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.304507","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.067451","closing_balance":"9090.371958"}} +{"contract_version":"10","engine_sequence":"24","event_id":"demo-event-000000000024","causation_ids":["demo-event-000000000020","demo-event-000000000022"],"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":"105","notional":"757.575","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.757575","quote_amount":"0.757575"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_amount":"-0.007575"}]}} +{"contract_version":"10","engine_sequence":"25","event_id":"demo-event-000000000025","causation_ids":["demo-event-000000000022"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}]}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]}} +{"contract_version":"10","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000025"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"c129c13b6e4273c4d2a784c5b9aaa2630ad67ecbafd6da303da12b2fb6f253bc","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}]}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v10/fixtures/demo.scenario.json b/contracts/v10/fixtures/demo.scenario.json new file mode 100644 index 0000000..9f0332b --- /dev/null +++ b/contracts/v10/fixtures/demo.scenario.json @@ -0,0 +1,411 @@ +{ + "contract_version": "10", + "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_v1", + "configuration": { + "version": "2", + "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" + } + ] + } + ] + } + }, + "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 + } + ] + }, + { + "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": "12" + } + ], + "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 + } + ] + }, + { + "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 + } + ] + }, + { + "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 + } + ] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + } +} diff --git a/contracts/v10/fixtures/demo.scenario.jsonl b/contracts/v10/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..1ce27fc --- /dev/null +++ b/contracts/v10/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"10","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_v1","configuration":{"version":"2","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"}]}]}},"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"}}} +{"contract_version":"10","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}]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"10","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":"12"}],"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}]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"10","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}]}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"10","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}]}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"10","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v10/fixtures/fill-clipped.journal.jsonl b/contracts/v10/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..d363391 --- /dev/null +++ b/contracts/v10/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"10","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":"19f11d500905b44278fb098678e3a8467dedd02f5b98e126f0ed1a8e5773d53d","execution_model":"completed_bar_v1"}} +{"contract_version":"10","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"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} +{"contract_version":"10","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"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"10","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}]}} +{"contract_version":"10","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":"10","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":"10","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0.004081","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.004081","maintenance_excess":"550.004081","margin_call":false},"group_exposures":[]}} +{"contract_version":"10","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}]}} +{"contract_version":"10","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":"10","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":"10","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":"10","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0.008162","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]}} +{"contract_version":"10","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":"19f11d500905b44278fb098678e3a8467dedd02f5b98e126f0ed1a8e5773d53d","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0.008162","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/v10/fixtures/fill-clipped.scenario.json b/contracts/v10/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..a408f1f --- /dev/null +++ b/contracts/v10/fixtures/fill-clipped.scenario.json @@ -0,0 +1,236 @@ +{ + "contract_version": "10", + "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 + } + ] + }, + { + "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 + } + ] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + } +} diff --git a/contracts/v10/journal.schema.json b/contracts/v10/journal.schema.json new file mode 100644 index 0000000..fc11e1c --- /dev/null +++ b/contracts/v10/journal.schema.json @@ -0,0 +1,2193 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v10/journal.schema.json", + "title": "Trading Engine v10 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": "10" + }, + "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", + "order_adjusted", + "fill_applied", + "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": "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": "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": { + "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": { + "const": "completed_bar_v1" + } + } + }, + "initialState": { + "type": "object", + "additionalProperties": false, + "required": [ + "portfolio", + "valuation" + ], + "properties": { + "portfolio": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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" + } + } + } + ] + }, + "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" + ], + "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/v10/scenario.schema.json#/$defs/borrowObservation" + } + }, + "cash_rate_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/cashRateObservation" + } + } + } + }, + "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" + ] + }, + "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" + } + } + }, + "orderAdjusted": { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "action_id" + ], + "properties": { + "order": { + "$ref": "#/$defs/order" + }, + "action_id": { + "$ref": "#/$defs/identifier" + } + } + }, + "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" + } + } + }, + "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": "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/v10/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/v10/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/v10/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" + ], + "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" + } + } + }, + "positionAttribution": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "quote_currency", + "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" + }, + "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", + "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" + }, + "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": { + "const": "completed_bar_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/v10/scenario-stream.schema.json b/contracts/v10/scenario-stream.schema.json new file mode 100644 index 0000000..5f50c87 --- /dev/null +++ b/contracts/v10/scenario-stream.schema.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v10/scenario-stream.schema.json", + "title": "Trading Engine v10 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": "10" }, + "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": "10" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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": "10" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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", "max_internal_events"], + "properties": { + "metadata": { "type": "object" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/financing" }, + "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/v10/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v10/scenario.schema.json b/contracts/v10/scenario.schema.json new file mode 100644 index 0000000..c470212 --- /dev/null +++ b/contracts/v10/scenario.schema.json @@ -0,0 +1,510 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json", + "title": "Trading Engine v10 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", "max_internal_events", "schedule", "slices"], + "properties": { + "contract_version": { "const": "10" }, + "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" }, + "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": { + "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": { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + "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" } } + } + }, + "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"], + "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" } } + } + }, + "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" } + } + } + ] + } + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index 4e07468..843bbaa 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/v9/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v10/README.md) remain authoritative for process boundaries. diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 460101d..471767b 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -21,8 +21,8 @@ Every runtime cell replays the frozen v3 demo, v5 demo, and v5 risk-limited fill canonical fixtures. Standard output and standard error are captured separately because human diagnostics may contain platform-specific paths or process details and are not part of the journal contract. -The full test suite additionally validates and replays the current v9 batch, stream, journal, and -strategy-v7 fixtures, including fee-component attribution and the reconciled first valuation. +The full test suite additionally validates and replays the current v10 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 once against its full pinned commit; it is not repeated across dependency or operating-system diff --git a/docs/execution-model.md b/docs/execution-model.md index 8754523..12732c8 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,11 +1,11 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v9 advertises and accepts `completed_bar_v1`; embedders can inject another module through +Contract v10 advertises and accepts `completed_bar_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 v9 envelope separates selection from +Each compiled model owns a strict configuration contract. The v10 envelope separates selection from model-specific parameters: ```json @@ -143,13 +143,30 @@ order may exceed that maximum, but no individual fill may do so. A cash dividend multiplies the pre-match signed position by its per-unit amount. It credits a long or debits a short in the instrument's quote-currency ledger and records realized dividend P&L. -After actions, each open short accrues a quote-currency borrow fee from the slice open mark and the -exact `start_at`/`end_at` duration using a 365-day basis. Positive fees round upward to one money -micro-unit. +Contract v10 replaces the fixed legacy rate with effective-time borrow observations. Each +observation names an instrument, available quantity, annual rate in basis points, and recall state. +Observations become active no later than the slice start and remain active until superseded. A new +short either clips to the available locate or is rejected according to `locate_policy`; existing +short quantity consumes availability. A recall rejects further shorts and, under `close_out`, +cancels active sells and submits a priority IOC buy until the short is flat. `reject_new_shorts` +retains the position but prevents it from increasing. + +Before matching, each open short accrues its observed quote-currency charge from the slice open +mark and exact `start_at`/`end_at` duration. Missing observations follow `borrow_missing_data`: +`reject` fails the slice and `zero` applies no charge while still preventing an unlocated new short. +Signed rates support rebates. Charges use the scenario's explicit `actual_365` or `actual_360` +day-count and `simple` or `daily` compounding policy. + +Cash financing uses effective-time observations per currency with separate annual credit and debit +rates. Positive balances receive the credit rate; negative balances receive the debit rate. The +same explicit interval, day-count, compounding, and deterministic micro-unit rounding rules apply. +`cash_missing_data` either rejects a nonzero balance without an observation or treats its rate as +zero. Interest updates the native cash ledger and is reported separately and within aggregate +realized P&L; debit interest can therefore produce or deepen negative equity. ## Risk-limited fills and fees -Contract v9 selects exactly one fee schedule per instrument. A schedule composes named `fixed`, +Contract v10 selects exactly one fee schedule per instrument. A schedule composes named `fixed`, `notional_bps`, and `per_unit` components. Each component declares its currency, `up`, `down`, or `nearest` rounding, and `any`, `maker`, or `taker` applicability. A limit filled at its intrabar touch is maker liquidity; market orders and limits marketable at the open are takers. diff --git a/docs/persistra.md b/docs/persistra.md index 54d41d5..a881dee 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -17,6 +17,7 @@ The JSON scenario carries: - One explicit executable-instrument catalog - Signed position, exposure, leverage, margin, borrow, participation, and fee policies - Strictly increasing synchronized market slices with complete FX marks and corporate actions +- Effective-time borrow availability, recall, and per-currency credit/debit rate observations - Explicit signed initial cash and positions with accounting history, marks, and FX state - Optional scheduled full-portfolio signed weight or fractional quantity targets - Optional direct orders, cancellations, and metrics @@ -40,8 +41,8 @@ Persistra should: `run_completed`, and the retained manifest. 13. Reconcile every native/base position row and currency cash row to the aggregate account, exposure, fee, and margin values. -14. Reconcile split adjustments, dividends, borrow fees, risk-limited fills, and margin - liquidation against scenario and runtime state. +14. Reconcile split adjustments, dividends, borrow and cash financing, recalls, risk-limited fills, + and margin liquidation against scenario and runtime state. 15. For external replay, require an empty schedule, launch an explicit strategy argument vector, hash every declared strategy input, and validate the complete bidirectional transcript. 16. Reconcile external transcript intents to their journal outcomes. @@ -53,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 v9 while retaining v8 through v3 and +structural checks. The engine advertises current contract v10 while retaining v9 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 v7](../contracts/strategy/v7/README.md). Persistra's host turns protocol +[strategy protocol v8](../contracts/strategy/v8/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, @@ -78,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 v9 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v10 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 v7; a host must complete its exact initialization, event, + external boundary is strategy v8; 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 eef507f..48c774c 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 v9 demo](../contracts/v9/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v9/scenario.schema.json) provides structural validation. +Use [the v10 demo](../contracts/v10/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v10/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/v9/scenario-stream.schema.json) validates each line, -and [the v9 stream fixture](../contracts/v9/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v10/scenario-stream.schema.json) validates each line, +and [the v10 stream fixture](../contracts/v10/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; v9 is `"9"` | +| `contract_version` | Required string identifying this file contract; v10 is `"10"` | | `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 | @@ -51,6 +51,7 @@ retains current account, order, target, and latest-bar state required by executi | `venue_calendars` | Immutable venue/session policies covering every configured instrument | | `risk` | Signed position, exposure, leverage, margin, and borrow policy | | `execution` | Capacity and fee configuration | +| `financing` | Borrow/cash day-count, compounding, locate, recall, and missing-data policies | | `max_internal_events` | Positive reducer feedback cap, at most 100,000 | | `schedule` | Intents emitted after named slices, at most 4,096 per batch | | `slices` | Complete synchronized market observations | @@ -110,7 +111,7 @@ they may overlap and can constrain gross, long, short, absolute net, and gross-t concentration exposure. Admission and fill clipping include working-order reservations. Every applicable group is enforced, with group identity providing deterministic tie ordering. -Contract v9 execution contains a stable `model` and a model-owned `configuration`. For +Contract v10 execution contains a stable `model` and a model-owned `configuration`. For `completed_bar_v1`, configuration version `"2"` contains: - `version`, the strict model-configuration contract version @@ -168,7 +169,7 @@ checks run during parsing; position and outstanding-order checks run in the redu ## Market slices Each slice has common timing, one bar per configured instrument, a complete set of currency-to-base -FX marks, and zero or more corporate actions: +FX marks, zero or more corporate actions, and effective-time borrow and cash-rate observations: Timestamps use `YYYY-MM-DD[Tt]HH:MM:SS`, optional one-to-six fractional-second digits, and either `Z`/`z` or a colonized numeric offset such as `-05:00`. Seconds range from `00` through `59`. @@ -194,7 +195,13 @@ Audit timestamps use the same boundary. "fx_rates": [ { "currency": "USD", "rate": "1" } ], - "corporate_actions": [] + "corporate_actions": [], + "borrow_observations": [ + { "instrument_id": "asset-a", "effective_at": "2026-01-02T14:30:00Z", "available_quantity": "1000", "annual_rate_bps": 100, "recalled": false } + ], + "cash_rate_observations": [ + { "currency": "USD", "effective_at": "2026-01-02T14:30:00Z", "credit_rate_bps": 100, "debit_rate_bps": 200 } + ] } ``` @@ -206,6 +213,13 @@ their usual range relationships. Volume may be fractional but must align to the Each slice supplies exactly one positive FX rate for every scenario currency, and the base-currency rate is exactly one. +Financing observations are unique per instrument or currency within a slice, effective no later +than the slice start, and strictly advance the effective time for their key across slices. A recall +has zero available quantity. The latest observation remains active until replaced. The top-level +`financing` object selects `actual_365` or `actual_360`, `simple` or `daily`, `reject` or `zero` +missing-data handling, `reject_order` or `clip_fill` locate behavior, and +`reject_new_shorts` or `close_out` recall behavior. + Supported corporate actions are exact-ratio `split` and per-unit `cash_dividend` records. Action IDs are unique across the scenario. Actions are applied in canonical ID order before borrow fees and matching. A split rescales the position, persistent target, and active orders while preserving @@ -220,7 +234,7 @@ than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v9/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v10/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 @@ -235,11 +249,13 @@ basis, original weight when applicable, computed quantity, and sizing reference `eligible_after_slice_sequence`; fills use `slice_sequence`. `fill_clipped` records the proposed fill and the greatest lot-aligned permitted quantity. Its reason taxonomy version `1` names one of `max_order_quantity`, `max_long_position`, `max_short_position`, `max_gross_exposure`, -`max_leverage`, or `initial_margin` and carries a quantity, money, ratio, or basis-points threshold. +`max_leverage`, `initial_margin`, or `instrument_borrow_availability` and carries a quantity, money, +ratio, or basis-points threshold. Each order snapshot retains both creation and latest-update event IDs. -The journal also records split/dividend application, split-driven order adjustments, short borrow -fees, margin calls, liquidation-origin orders, and restoration. Every valuation contains complete +The journal also records split/dividend application, split-driven order adjustments, observed +borrow charges, recalls and close-outs, cash-interest entries, margin calls, liquidation-origin +orders, and restoration. Every valuation contains complete per-currency cash attribution, signed per-instrument native and base-currency attribution, long, short, net, and gross exposure, execution and borrow fees, and its initial/maintenance margin snapshot. Those rows reconcile exactly to the aggregate valuation. diff --git a/lib/account.ml b/lib/account.ml index 141fa2e..b52cff4 100644 --- a/lib/account.ml +++ b/lib/account.ml @@ -33,6 +33,8 @@ type cash_attribution = { amount : Scalar.Money.t; fx_rate : Scalar.Price.t; base_value : Scalar.Money.t; + interest : Scalar.Money.t; + base_interest : Scalar.Money.t; } type position_attribution = { @@ -64,6 +66,7 @@ type t = { base_currency : string; initial_cash : Scalar.Money.t Currency_map.t; cash : Scalar.Money.t Currency_map.t; + cash_interest : Scalar.Money.t Currency_map.t; positions : position Id.Instrument.Map.t; } @@ -81,6 +84,7 @@ type valuation = { dividend_pnl : Scalar.Money.t; execution_fees : Scalar.Money.t; borrow_fees : Scalar.Money.t; + cash_interest : Scalar.Money.t; total_fees : Scalar.Money.t; cash_balances : cash_attribution list; positions : position_attribution list; @@ -134,6 +138,7 @@ let create ~base_currency ~initial_cash = base_currency; initial_cash = balances; cash = balances; + cash_interest = Currency_map.map (fun _ -> Scalar.Money.zero) balances; positions = Id.Instrument.Map.empty; } @@ -165,6 +170,7 @@ let of_initial_portfolio (initial : Initial_portfolio.t) = base_currency = initial.base_currency; initial_cash = cash; cash; + cash_interest = Currency_map.map (fun _ -> Scalar.Money.zero) cash; positions; } @@ -387,8 +393,8 @@ let apply_borrow_fee (state : t) ~instrument_id ~quote_currency ~fee = let current = position state instrument_id in if not (Scalar.Quantity.is_negative current.quantity) then Error "borrow fees require an open short position" - else if Scalar.Money.compare fee Scalar.Money.zero <= 0 then - Error "borrow fee must be positive" + else if Scalar.Money.equal fee Scalar.Money.zero then + Error "borrow fee must be nonzero" else let* cash_delta = Scalar.Money.negate fee in let* state = adjust_cash state quote_currency cash_delta in @@ -401,6 +407,22 @@ let apply_borrow_fee (state : t) ~instrument_id ~quote_currency ~fee = positions = update_position state.positions instrument_id updated; } +let apply_cash_interest (state : t) ~currency ~interest = + if Scalar.Money.equal interest Scalar.Money.zero then Ok state + else + let* state = adjust_cash state currency interest in + let current = + Option.value + (Currency_map.find_opt currency state.cash_interest) + ~default:Scalar.Money.zero + in + let* total = Scalar.Money.add current interest in + Ok + { + state with + cash_interest = Currency_map.add currency total state.cash_interest; + } + let value (state : t) ~instruments ~marks ~fx_rates = let canonical_flat_mark = Scalar.Price.of_micros Scalar.Price.scale |> Result.get_ok @@ -430,7 +452,13 @@ let value (state : t) ~instruments ~marks ~fx_rates = let cash_attribution (currency, amount) = let* fx_rate = fx currency in let* base_value = Scalar.Money.convert amount ~rate:fx_rate in - Ok { currency; amount; fx_rate; base_value } + let interest = + Option.value + (Currency_map.find_opt currency state.cash_interest) + ~default:Scalar.Money.zero + in + let* base_interest = Scalar.Money.convert interest ~rate:fx_rate in + Ok { currency; amount; fx_rate; base_value; interest; base_interest } in let* cash_balances = Currency_map.bindings state.cash @@ -584,6 +612,13 @@ let value (state : t) ~instruments ~marks ~fx_rates = add total item.base_value) (Ok Scalar.Money.zero) cash_balances in + let* cash_interest = + List.fold_left + (fun result item -> + let* total = result in + add total item.base_interest) + (Ok Scalar.Money.zero) cash_balances + in let accumulate result item = let* ( net_market_value, long_market_value, @@ -650,6 +685,7 @@ let value (state : t) ~instruments ~marks ~fx_rates = Scalar.Money.zero )) positions in + let* realized_pnl = add realized_pnl cash_interest in let* gross_exposure = add long_market_value short_market_value in let* equity = add cash net_market_value in let add_attribution (components : execution_fee_component_attribution list) @@ -696,6 +732,7 @@ let value (state : t) ~instruments ~marks ~fx_rates = dividend_pnl; execution_fees; borrow_fees; + cash_interest; total_fees; cash_balances; positions; diff --git a/lib/account.mli b/lib/account.mli index ccf7985..ffe2a19 100644 --- a/lib/account.mli +++ b/lib/account.mli @@ -34,6 +34,8 @@ type cash_attribution = private { amount : Scalar.Money.t; fx_rate : Scalar.Price.t; base_value : Scalar.Money.t; + interest : Scalar.Money.t; + base_interest : Scalar.Money.t; } type position_attribution = private { @@ -77,6 +79,7 @@ type valuation = private { dividend_pnl : Scalar.Money.t; execution_fees : Scalar.Money.t; borrow_fees : Scalar.Money.t; + cash_interest : Scalar.Money.t; total_fees : Scalar.Money.t; cash_balances : cash_attribution list; positions : position_attribution list; @@ -119,6 +122,9 @@ val apply_borrow_fee : fee:Scalar.Money.t -> (t, string) result +val apply_cash_interest : + t -> currency:string -> interest:Scalar.Money.t -> (t, string) result + val value : t -> instruments:Instrument.t list -> diff --git a/lib/audit.ml b/lib/audit.ml index 5c02e64..887eaf6 100644 --- a/lib/audit.ml +++ b/lib/audit.ml @@ -7,6 +7,7 @@ type cancellation_reason = | Day_expired | Gtd_expired | Margin_call + | Borrow_recall type target_basis = Weights | Quantities @@ -76,6 +77,33 @@ type event = period_end : Ptime.t; fee : Scalar.Money.t; } + | Borrow_charge_applied of { + observation : Financing.borrow_observation; + quote_currency : string; + short_quantity : Scalar.Quantity.t; + reference_price : Scalar.Price.t; + day_count : Financing.day_count; + compounding : Financing.compounding; + period_start : Ptime.t; + period_end : Ptime.t; + amount : Scalar.Money.t; + } + | Borrow_recall_received of { + observation : Financing.borrow_observation; + short_quantity : Scalar.Quantity.t; + close_out_quantity : Scalar.Quantity.t; + } + | Cash_interest_applied of { + observation : Financing.cash_rate_observation; + opening_balance : Scalar.Money.t; + applied_rate_bps : int; + day_count : Financing.day_count; + compounding : Financing.compounding; + period_start : Ptime.t; + period_end : Ptime.t; + amount : Scalar.Money.t; + closing_balance : Scalar.Money.t; + } | Margin_call_triggered of valuation | Margin_restored of valuation | Intent_rejected of string @@ -123,6 +151,7 @@ let cancellation_reason_to_string = function | Day_expired -> "day_expired" | Gtd_expired -> "gtd_expired" | Margin_call -> "margin_call" + | Borrow_recall -> "borrow_recall" let target_basis_to_string = function | Weights -> "weights" @@ -144,6 +173,9 @@ let event_name = function | Margin_limited _ -> "margin_limited" | Fill_clipped _ -> "fill_clipped" | Borrow_fee_applied _ -> "borrow_fee_applied" + | Borrow_charge_applied _ -> "borrow_charge_applied" + | Borrow_recall_received _ -> "borrow_recall_received" + | Cash_interest_applied _ -> "cash_interest_applied" | Margin_call_triggered _ -> "margin_call" | Margin_restored _ -> "margin_restored" | Intent_rejected _ -> "intent_rejected" diff --git a/lib/audit.mli b/lib/audit.mli index 23e594a..0d715b7 100644 --- a/lib/audit.mli +++ b/lib/audit.mli @@ -9,6 +9,7 @@ type cancellation_reason = | Day_expired | Gtd_expired | Margin_call + | Borrow_recall type target_basis = Weights | Quantities @@ -78,6 +79,33 @@ type event = period_end : Ptime.t; fee : Scalar.Money.t; } + | Borrow_charge_applied of { + observation : Financing.borrow_observation; + quote_currency : string; + short_quantity : Scalar.Quantity.t; + reference_price : Scalar.Price.t; + day_count : Financing.day_count; + compounding : Financing.compounding; + period_start : Ptime.t; + period_end : Ptime.t; + amount : Scalar.Money.t; + } + | Borrow_recall_received of { + observation : Financing.borrow_observation; + short_quantity : Scalar.Quantity.t; + close_out_quantity : Scalar.Quantity.t; + } + | Cash_interest_applied of { + observation : Financing.cash_rate_observation; + opening_balance : Scalar.Money.t; + applied_rate_bps : int; + day_count : Financing.day_count; + compounding : Financing.compounding; + period_start : Ptime.t; + period_end : Ptime.t; + amount : Scalar.Money.t; + closing_balance : Scalar.Money.t; + } | Margin_call_triggered of valuation | Margin_restored of valuation | Intent_rejected of string diff --git a/lib/codec.ml b/lib/codec.ml index d258318..0ac4e94 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -133,6 +133,14 @@ let fill_limit_to_yojson = function ( "instrument_shorting_disabled", `Assoc [ ("instrument_id", instrument_id id); ("value", `Bool false) ] ) + | Risk.Instrument_borrow_availability (id, value) -> + ( "instrument_borrow_availability", + `Assoc + [ + ("instrument_id", instrument_id id); + ("unit", string "quantity"); + ("value", quantity value); + ] ) | Risk.Instrument_initial_margin (id, value) -> ( "instrument_initial_margin", `Assoc @@ -218,7 +226,26 @@ let corporate_action_to_yojson action = ((("type", string "cash_dividend") :: common) @ [ ("amount_per_unit", money amount_per_unit) ]) -let market_slice_to_yojson market_slice = +let borrow_observation_to_yojson observation = + `Assoc + [ + ("instrument_id", instrument_id observation.Financing.instrument_id); + ("effective_at", timestamp observation.effective_at); + ("available_quantity", quantity observation.available_quantity); + ("annual_rate_bps", `Int observation.annual_rate_bps); + ("recalled", `Bool observation.recalled); + ] + +let cash_rate_observation_to_yojson observation = + `Assoc + [ + ("currency", string observation.Financing.currency); + ("effective_at", timestamp observation.effective_at); + ("credit_rate_bps", `Int observation.credit_rate_bps); + ("debit_rate_bps", `Int observation.debit_rate_bps); + ] + +let versioned_market_slice_to_yojson ~contract_version market_slice = `Assoc [ ("slice_sequence", int64 market_slice.Market_slice.slice_sequence); @@ -233,6 +260,27 @@ let market_slice_to_yojson market_slice = (List.map corporate_action_to_yojson market_slice.corporate_actions) ); ] + |> function + | `Assoc fields when String.equal contract_version "10" -> + `Assoc + (fields + @ [ + ( "borrow_observations", + `List + (List.map borrow_observation_to_yojson + market_slice.Market_slice.borrow_observations) ); + ( "cash_rate_observations", + `List + (List.map cash_rate_observation_to_yojson + market_slice.Market_slice.cash_rate_observations) ); + ]) + | json -> json + +let market_slice_to_yojson market_slice = + versioned_market_slice_to_yojson ~contract_version:"9" market_slice + +let market_slice_to_yojson_v10 market_slice = + versioned_market_slice_to_yojson ~contract_version:"10" market_slice let request_fields request = let kind, limit_price = @@ -337,7 +385,7 @@ let order_to_yojson_v8 order = ]) let versioned_order_to_yojson ~contract_version order = - if List.mem contract_version [ "9"; "8" ] then order_to_yojson_v8 order + if List.mem contract_version [ "10"; "9"; "8" ] then order_to_yojson_v8 order else order_to_yojson order let fill_to_yojson fill = @@ -479,6 +527,17 @@ let cash_attribution_to_yojson cash = ("base_value", money cash.base_value); ] +let cash_attribution_to_yojson_v10 cash = + match cash_attribution_to_yojson cash with + | `Assoc fields -> + `Assoc + (fields + @ [ + ("interest", money cash.Account.interest); + ("base_interest", money cash.base_interest); + ]) + | _ -> assert false + let account_valuation_to_yojson ?(contract_version = "8") valuation = `Assoc [ @@ -497,17 +556,31 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ("borrow_fees", money valuation.borrow_fees); ("total_fees", money valuation.total_fees); ( "cash_balances", - `List (List.map cash_attribution_to_yojson valuation.cash_balances) ); + `List + (List.map + (if String.equal contract_version "10" then + cash_attribution_to_yojson_v10 + else cash_attribution_to_yojson) + valuation.cash_balances) ); ( "positions", `List (List.map - (if String.equal contract_version "9" then - position_attribution_to_yojson_v9 + (if + String.equal contract_version "9" + || String.equal contract_version "10" + then position_attribution_to_yojson_v9 else position_attribution_to_yojson) valuation.positions) ); ] |> function - | `Assoc fields when String.equal contract_version "9" -> + | `Assoc fields + when String.equal contract_version "9" || String.equal contract_version "10" + -> + let financing = + if String.equal contract_version "10" then + [ ("cash_interest", money valuation.Account.cash_interest) ] + else [] + in `Assoc (fields @ [ @@ -515,7 +588,8 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = `List (List.map execution_fee_component_attribution_to_yojson valuation.Account.execution_fee_components) ); - ]) + ] + @ financing) | json -> json let margin_to_yojson margin = @@ -547,7 +621,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 [ "9"; "8" ] then + if List.mem contract_version [ "10"; "9"; "8" ] then fields @ [ ( "group_exposures", @@ -594,7 +668,7 @@ let payload_to_yojson ~contract_version = function ("valuation", valuation_to_yojson ~contract_version valuation); ] | Audit.Market_slice_received market_slice -> - market_slice_to_yojson market_slice + versioned_market_slice_to_yojson ~contract_version market_slice | Audit.Target_portfolio_requested { basis; targets } -> `Assoc [ @@ -632,7 +706,8 @@ let payload_to_yojson ~contract_version = function ("action_id", string (Id.Corporate_action.to_string action_id)); ] | Audit.Fill_applied fill -> - if String.equal contract_version "9" then fill_to_yojson_v9 fill + if String.equal contract_version "9" || String.equal contract_version "10" + then fill_to_yojson_v9 fill else fill_to_yojson fill | Audit.Margin_limited { @@ -697,6 +772,62 @@ let payload_to_yojson ~contract_version = function ("period_end", timestamp period_end); ("fee", money fee); ] + | Audit.Borrow_charge_applied + { + observation; + quote_currency; + short_quantity; + reference_price; + day_count; + compounding; + period_start; + period_end; + amount; + } -> + `Assoc + [ + ("observation", borrow_observation_to_yojson observation); + ("quote_currency", string quote_currency); + ("short_quantity", quantity short_quantity); + ("reference_price", price reference_price); + ("day_count", string (Financing.day_count_to_string day_count)); + ("compounding", string (Financing.compounding_to_string compounding)); + ("period_start", timestamp period_start); + ("period_end", timestamp period_end); + ("amount", money amount); + ] + | Audit.Borrow_recall_received + { observation; short_quantity; close_out_quantity } -> + `Assoc + [ + ("observation", borrow_observation_to_yojson observation); + ("short_quantity", quantity short_quantity); + ("close_out_quantity", quantity close_out_quantity); + ] + | Audit.Cash_interest_applied + { + observation; + opening_balance; + applied_rate_bps; + day_count; + compounding; + period_start; + period_end; + amount; + closing_balance; + } -> + `Assoc + [ + ("observation", cash_rate_observation_to_yojson observation); + ("opening_balance", money opening_balance); + ("applied_rate_bps", `Int applied_rate_bps); + ("day_count", string (Financing.day_count_to_string day_count)); + ("compounding", string (Financing.compounding_to_string compounding)); + ("period_start", timestamp period_start); + ("period_end", timestamp period_end); + ("amount", money amount); + ("closing_balance", money closing_balance); + ] | Audit.Margin_call_triggered valuation | Audit.Margin_restored valuation -> valuation_to_yojson ~contract_version valuation | Audit.Intent_rejected reason -> `Assoc [ ("reason", string reason) ] diff --git a/lib/codec.mli b/lib/codec.mli index 8083351..30df6ba 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -4,6 +4,7 @@ val ptime_to_string : Ptime.t -> string val ptime_of_string : string -> (Ptime.t, string) result val bar_to_yojson : Bar.t -> Yojson.Safe.t val market_slice_to_yojson : Market_slice.t -> Yojson.Safe.t +val market_slice_to_yojson_v10 : 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 b015114..a60404f 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,13 +1,13 @@ -let version = "9" -let previous_version = "8" +let version = "10" +let previous_version = "9" let legacy_journal_version = "3" let supported_versions = - [ version; previous_version; "7"; "6"; "5"; "4"; legacy_journal_version ] + [ version; previous_version; "8"; "7"; "6"; "5"; "4"; legacy_journal_version ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "7" -let previous_strategy_protocol_version = "6" +let strategy_protocol_version = "8" +let previous_strategy_protocol_version = "7" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -26,6 +26,7 @@ let capabilities_to_yojson () = [ strategy_protocol_version; previous_strategy_protocol_version; + "6"; "5"; "4"; "3"; diff --git a/lib/engine.ml b/lib/engine.ml index 2c7a9f2..00f88e7 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -1,14 +1,17 @@ +module Currency_map = Map.Make (String) + type config = { contract_version : string; risk : Risk.t; venue_calendars : Venue_calendar.t list; execution_model : Execution_model.t; execution : Execution.t; + financing : Financing.policy option; max_internal_events : int; } let make_config ~venue_calendars ~contract_version ~risk ~execution_model - ~execution ~max_internal_events = + ~execution ~financing ~max_internal_events = if not (Contract.is_supported contract_version) then Error "engine contract version is unsupported" else if max_internal_events <= 0 then @@ -25,18 +28,24 @@ let make_config ~venue_calendars ~contract_version ~risk ~execution_model venue_calendars; execution_model; execution; + financing; max_internal_events; } let config ~contract_version ~risk ~execution_model ~execution ~max_internal_events = make_config ~venue_calendars:[] ~contract_version ~risk ~execution_model - ~execution ~max_internal_events + ~execution ~financing:None ~max_internal_events let config_v8 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~max_internal_events = make_config ~venue_calendars ~contract_version ~risk ~execution_model - ~execution ~max_internal_events + ~execution ~financing:None ~max_internal_events + +let config_v10 ~contract_version ~risk ~venue_calendars ~execution_model + ~execution ~financing ~max_internal_events = + make_config ~venue_calendars ~contract_version ~risk ~execution_model + ~execution ~financing:(Some financing) ~max_internal_events let valid_sha256 value = String.length value = 64 @@ -66,6 +75,8 @@ module Interactive = struct latest_bars : Bar.t Id.Instrument.Map.t; latest_marks : Scalar.Price.t Id.Instrument.Map.t; latest_fx_rates : (string * Scalar.Price.t) list; + latest_borrow : Financing.borrow_observation Id.Instrument.Map.t; + latest_cash_rates : Financing.cash_rate_observation Currency_map.t; initial_portfolio : Initial_portfolio.t option; applied_action_ids : Id.Corporate_action.Set.t; desired_targets : desired_targets option; @@ -127,6 +138,8 @@ module Interactive = struct latest_bars = Id.Instrument.Map.empty; latest_marks; latest_fx_rates; + latest_borrow = Id.Instrument.Map.empty; + latest_cash_rates = Currency_map.empty; initial_portfolio; applied_action_ids = Id.Corporate_action.Set.empty; desired_targets = None; @@ -374,6 +387,48 @@ module Interactive = struct let marks = Id.Instrument.Map.bindings reduction.state.latest_marks in + let* () = + match + ( reduction.state.config.financing, + request.Order.side, + Account.position_quantity reduction.state.account + request.instrument_id ) + with + | Some policy, Order.Sell, position + when policy.Financing.locate_policy = Financing.Reject_order + && not (Scalar.Quantity.is_positive position) -> + let available = + match + Id.Instrument.Map.find_opt request.instrument_id + reduction.state.latest_borrow + with + | Some observation when not observation.Financing.recalled + -> + observation.available_quantity + | None | Some _ -> Scalar.Quantity.zero + in + let* located = Scalar.Quantity.absolute position in + let* reserved = + Oms.active_for_instrument reduction.state.oms + request.instrument_id + |> List.fold_left + (fun result order -> + let* total = result in + if order.Order.request.side = Order.Sell then + Scalar.Quantity.add total + (Order.remaining_quantity order) + else Ok total) + (Ok Scalar.Quantity.zero) + in + let* requested = Scalar.Quantity.add located reserved in + let* requested = + Scalar.Quantity.add requested request.quantity + in + if Scalar.Quantity.compare requested available > 0 then + Error "order exceeds effective borrow availability" + else Ok () + | _ -> Ok () + in Risk.check reduction.state.config.risk ~account:reduction.state.account ~oms:reduction.state.oms ~marks ~fx_rates:reduction.state.latest_fx_rates request @@ -429,6 +484,38 @@ module Interactive = struct (enqueue reduction [ order_pending; rejection_pending ]))))) + let submit_recall_order reduction market_slice instrument quantity = + let* request = + Order.request_v8 ~instrument_id:instrument.Instrument.id ~side:Order.Buy + ~quantity ~kind:Order.Market ~time_in_force:Order.Ioc + ~origin:Order.Borrow_recall + in + let id = order_id reduction.state in + let* state = increment_order_number reduction.state in + let reduction = { reduction with state } in + let* order_sequence = next_sequence reduction.state.engine_sequence in + let created_event_id = + Audit.event_id ~run_id:reduction.state.run_id + ~engine_sequence:order_sequence + in + let eligible_after_slice_sequence = + Int64.pred market_slice.Market_slice.slice_sequence + in + let* oms, order = + Oms.accept reduction.state.oms ~id ~created_event_id + ~accepted_sequence:order_sequence ~created_at:market_slice.start_at + ~eligible_after_slice_sequence request + in + let reduction = { reduction with state = { reduction.state with oms } } in + let* reduction, event_id = + emit_with_id reduction (Audit.Order_accepted order) + in + let* pending = + notification reduction ~causation_ids:[ event_id ] + (Strategy.Order_updated order) + in + Ok (enqueue reduction [ pending ]) + let cancel_order reduction ~reason order_id = match Oms.cancel reduction.state.oms order_id with | Error message -> reject_intent reduction message @@ -676,7 +763,7 @@ module Interactive = struct if Z.fits_int64 fee then Ok (Scalar.Money.of_micros (Z.to_int64 fee)) else Error "short borrow fee overflow" - let apply_borrow_fees reduction market_slice = + let apply_legacy_borrow_fees reduction market_slice = let span = Ptime.diff market_slice.Market_slice.end_at market_slice.start_at in @@ -728,6 +815,201 @@ module Interactive = struct (Ok reduction) (configured_instruments reduction.state) + let apply_observed_borrow_fees reduction market_slice policy = + let span = + Ptime.diff market_slice.Market_slice.end_at market_slice.start_at + in + List.fold_left + (fun result instrument -> + let* reduction = result in + let quantity = + Account.position_quantity reduction.state.account + instrument.Instrument.id + in + if not (Scalar.Quantity.is_negative quantity) then Ok reduction + else + match + Id.Instrument.Map.find_opt instrument.id + reduction.state.latest_borrow + with + | None -> ( + match policy.Financing.borrow_missing_data with + | Financing.Zero -> Ok reduction + | Financing.Reject -> + Error + (Format.asprintf + "open short has no effective borrow observation for %a" + Id.Instrument.pp instrument.id)) + | Some observation -> + let* short_quantity = Scalar.Quantity.absolute quantity in + let* bar = + match Market_slice.bar market_slice instrument.id with + | Some value -> Ok value + | None -> Error "short position has no market slice bar" + in + let* notional = + Scalar.Money.notional bar.open_price short_quantity + in + let* amount = + Financing.accrue policy ~principal:notional + ~annual_rate_bps:observation.annual_rate_bps span + in + if Scalar.Money.equal amount Scalar.Money.zero then Ok reduction + else + let* account = + Account.apply_borrow_fee reduction.state.account + ~instrument_id:instrument.id + ~quote_currency:instrument.quote_currency ~fee:amount + in + let reduction = + { reduction with state = { reduction.state with account } } + in + emit + (with_causes reduction + (Option.to_list reduction.slice_event_id)) + (Audit.Borrow_charge_applied + { + observation; + quote_currency = instrument.quote_currency; + short_quantity; + reference_price = bar.open_price; + day_count = policy.day_count; + compounding = policy.compounding; + period_start = market_slice.start_at; + period_end = market_slice.end_at; + amount; + })) + (Ok reduction) + (configured_instruments reduction.state) + + let apply_cash_interest reduction market_slice policy = + let span = + Ptime.diff market_slice.Market_slice.end_at market_slice.start_at + in + List.fold_left + (fun result (currency, opening_balance) -> + let* reduction = result in + if Scalar.Money.equal opening_balance Scalar.Money.zero then + Ok reduction + else + match + Currency_map.find_opt currency reduction.state.latest_cash_rates + with + | None -> ( + match policy.Financing.cash_missing_data with + | Financing.Zero -> Ok reduction + | Financing.Reject -> + Error + ("nonzero cash balance has no effective rate for currency " + ^ currency)) + | Some observation -> + let debit = + Scalar.Money.compare opening_balance Scalar.Money.zero < 0 + in + let applied_rate_bps = + if debit then observation.debit_rate_bps + else observation.credit_rate_bps + in + let* principal = + if debit then Scalar.Money.negate opening_balance + else Ok opening_balance + in + let* accrued = + Financing.accrue policy ~principal + ~annual_rate_bps:applied_rate_bps span + in + let* amount = + if debit then Scalar.Money.negate accrued else Ok accrued + in + if Scalar.Money.equal amount Scalar.Money.zero then Ok reduction + else + let* account = + Account.apply_cash_interest reduction.state.account ~currency + ~interest:amount + in + let* closing_balance = + match Account.cash account currency with + | Some value -> Ok value + | None -> Error "cash interest removed its currency ledger" + in + let reduction = + { reduction with state = { reduction.state with account } } + in + emit + (with_causes reduction + (Option.to_list reduction.slice_event_id)) + (Audit.Cash_interest_applied + { + observation; + opening_balance; + applied_rate_bps; + day_count = policy.day_count; + compounding = policy.compounding; + period_start = market_slice.start_at; + period_end = market_slice.end_at; + amount; + closing_balance; + })) + (Ok reduction) + (Account.cash_balances reduction.state.account) + + let process_borrow_recalls reduction market_slice policy = + List.fold_left + (fun result instrument -> + let* reduction = result in + match + Id.Instrument.Map.find_opt instrument.Instrument.id + reduction.state.latest_borrow + with + | None | Some { Financing.recalled = false; _ } -> Ok reduction + | Some observation -> + let quantity = + Account.position_quantity reduction.state.account instrument.id + in + if not (Scalar.Quantity.is_negative quantity) then Ok reduction + else + let* short_quantity = Scalar.Quantity.absolute quantity in + let close_out_quantity = + match policy.Financing.recall_policy with + | Financing.Reject_new_shorts -> Scalar.Quantity.zero + | Financing.Close_out -> short_quantity + in + let* reduction, recall_event_id = + emit_with_id + (with_causes reduction + (Option.to_list reduction.slice_event_id)) + (Audit.Borrow_recall_received + { observation; short_quantity; close_out_quantity }) + in + let active_sells = + Oms.active_for_instrument reduction.state.oms instrument.id + |> List.filter_map (fun order -> + if order.Order.request.side = Order.Sell then Some order.id + else None) + in + let* reduction = + cancel_orders + (with_causes reduction [ recall_event_id ]) + ~reason:Audit.Borrow_recall active_sells + in + if Scalar.Quantity.is_zero close_out_quantity then Ok reduction + else + submit_recall_order + (with_causes reduction [ recall_event_id ]) + market_slice instrument close_out_quantity) + (Ok reduction) + (configured_instruments reduction.state) + + let apply_financing reduction market_slice = + match reduction.state.config.financing with + | None -> apply_legacy_borrow_fees reduction market_slice + | Some policy -> + let* reduction = process_borrow_recalls reduction market_slice policy in + let* reduction = + apply_observed_borrow_fees reduction market_slice policy + in + apply_cash_interest reduction market_slice policy + let validate_target_ids state ids = let expected = configured_instruments state @@ -983,6 +1265,36 @@ module Interactive = struct (Id.Corporate_action.Set.mem action.id state.applied_action_ids)) market_slice.corporate_actions in + let borrow_observations_valid = + List.for_all + (fun (observation : Financing.borrow_observation) -> + Option.is_some + (Risk.instrument state.config.risk observation.instrument_id) + && Ptime.compare observation.effective_at market_slice.start_at <= 0 + && + match + Id.Instrument.Map.find_opt observation.instrument_id + state.latest_borrow + with + | None -> true + | Some previous -> + Ptime.compare observation.effective_at previous.effective_at > 0) + market_slice.borrow_observations + in + let cash_observations_valid = + List.for_all + (fun (observation : Financing.cash_rate_observation) -> + List.mem observation.currency expected_currencies + && Ptime.compare observation.effective_at market_slice.start_at <= 0 + && + match + Currency_map.find_opt observation.currency state.latest_cash_rates + with + | None -> true + | Some previous -> + Ptime.compare observation.effective_at previous.effective_at > 0) + market_slice.cash_rate_observations + in if List.length ids <> List.length actual || actual <> expected then Error "market slice must contain each configured instrument exactly once" else if @@ -1000,6 +1312,10 @@ module Interactive = struct then Error "market slice base-currency FX rate must equal one" else if not actions_valid then Error "corporate action is unknown or was already applied" + else if not borrow_observations_valid then + Error "borrow observations must be known and advance effective time" + else if not cash_observations_valid then + Error "cash rate observations must be known and advance effective time" else match state.last_slice_sequence with | Some sequence @@ -1095,8 +1411,47 @@ module Interactive = struct | Some value -> Ok value | None -> Error "fill instrument has no risk policy" in + let* borrow_constraint = + match (state.config.financing, order.Order.request.side) with + | Some policy, Order.Sell + when not (Scalar.Quantity.is_positive before_position) -> + let available = + match + Id.Instrument.Map.find_opt instrument.id state.latest_borrow + with + | None -> Scalar.Quantity.zero + | Some observation when observation.Financing.recalled -> + Scalar.Quantity.zero + | Some observation -> observation.available_quantity + in + let* located = Scalar.Quantity.absolute before_position in + let remaining = + match Scalar.Quantity.subtract available located with + | Ok value -> value + | Error _ -> Scalar.Quantity.zero + in + let limit = + Risk.Instrument_borrow_availability (instrument.id, remaining) + in + Ok + (Some + ( remaining, + limit, + match policy.Financing.locate_policy with + | Financing.Reject_order -> true + | Financing.Clip_fill -> false )) + | _ -> Ok None + in let quantity_limit = - Scalar.Quantity.minimum proposed.quantity policy_order_limit + let risk_limit = + Scalar.Quantity.minimum proposed.quantity policy_order_limit + in + match borrow_constraint with + | None -> risk_limit + | Some (available, _, reject) -> + if reject && Scalar.Quantity.compare proposed.quantity available > 0 + then Scalar.Quantity.zero + else Scalar.Quantity.minimum risk_limit available in let requested_lots = Int64.div (Scalar.Quantity.to_micros quantity_limit) lot_value @@ -1121,7 +1476,11 @@ module Interactive = struct let* limit = if not clipped then Ok None else if Int64.equal lots requested_lots then - Ok (Some (Risk.Maximum_order_quantity policy_order_limit)) + match borrow_constraint with + | Some (available, limit, _) + when Scalar.Quantity.compare proposed.quantity available > 0 -> + Ok (Some limit) + | _ -> Ok (Some (Risk.Maximum_order_quantity policy_order_limit)) else let next_lots = Int64.succ lots in let next_quantity = @@ -1553,6 +1912,19 @@ module Interactive = struct Id.Instrument.Map.add bar.Bar.instrument_id bar.close_price marks) state.latest_marks market_slice.bars in + let latest_borrow = + List.fold_left + (fun observations (observation : Financing.borrow_observation) -> + Id.Instrument.Map.add observation.instrument_id observation + observations) + state.latest_borrow market_slice.borrow_observations + in + let latest_cash_rates = + List.fold_left + (fun observations (observation : Financing.cash_rate_observation) -> + Currency_map.add observation.currency observation observations) + state.latest_cash_rates market_slice.cash_rate_observations + in let reduction = { state; @@ -1578,6 +1950,8 @@ module Interactive = struct market_slice.fx_rates; latest_bars; latest_marks; + latest_borrow; + latest_cash_rates; applied_action_ids; } in @@ -1602,7 +1976,7 @@ module Interactive = struct end module Borrow_phase = struct - let run market_slice reduction = apply_borrow_fees reduction market_slice + let run market_slice reduction = apply_financing reduction market_slice end module Notifications_phase = struct diff --git a/lib/engine.mli b/lib/engine.mli index fc1d6f8..03095f6 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -19,6 +19,16 @@ val config_v8 : max_internal_events:int -> (config, string) result +val config_v10 : + contract_version:string -> + risk:Risk.t -> + venue_calendars:Venue_calendar.t list -> + execution_model:Execution_model.t -> + execution:Execution.t -> + financing:Financing.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 4b9f0d2..7291282 100644 --- a/lib/execution.ml +++ b/lib/execution.ml @@ -175,7 +175,11 @@ let validate_bar_prices instrument bar = else Ok () let compare_execution_order left right = - let origin_rank = function Order.Margin_liquidation -> 0 | _ -> 1 in + let origin_rank = function + | Order.Margin_liquidation -> 0 + | Order.Borrow_recall -> 1 + | Order.Direct | Order.Target_rebalance -> 2 + in let origin = Int.compare (origin_rank left.Order.request.origin) diff --git a/lib/execution_model.ml b/lib/execution_model.ml index 87d8643..44956ca 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -36,7 +36,7 @@ let completed_bar_v1_contract = { version = "2"; previous_versions = [ "1" ]; - scenario_contract_versions = [ "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; + scenario_contract_versions = [ "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" ]; diff --git a/lib/external_replay.ml b/lib/external_replay.ml index 559262d..b225255 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -69,6 +69,7 @@ let initialization_of_scenario ~scenario_sha256 (scenario : Scenario.t) = risk = scenario.risk; execution_model = scenario.execution_model; execution = scenario.execution; + financing = scenario.financing; } let initialization_of_header ~scenario_sha256 (header : Scenario.stream_header) @@ -87,14 +88,20 @@ let initialization_of_header ~scenario_sha256 (header : Scenario.stream_header) risk = header.risk; execution_model = header.execution_model; execution = header.execution; + financing = header.financing; } let create_runner ~contract_version ~run_id ~scenario_sha256 ~risk - ~venue_calendars ~execution_model ~execution ~max_internal_events + ~venue_calendars ~execution_model ~execution ~financing ~max_internal_events ~initial_cash ~initial_portfolio = let* config = - Engine.config_v8 ~contract_version ~risk ~venue_calendars ~execution_model - ~execution ~max_internal_events + (match financing with + | None -> + Engine.config_v8 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~max_internal_events + | Some financing -> + Engine.config_v10 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~max_internal_events) |> reducer_result in match initial_portfolio with @@ -181,6 +188,7 @@ let run ?(durability = Artifact_writer.Buffered) ~env ~scenario_sha256 ~run_id:scenario.run_id ~scenario_sha256 ~risk:scenario.risk ~venue_calendars:scenario.venue_calendars ~execution_model:scenario.execution_model ~execution:scenario.execution + ~financing:scenario.financing ~max_internal_events:scenario.max_internal_events ~initial_cash:scenario.initial_cash ~initial_portfolio:scenario.initial_portfolio @@ -237,6 +245,7 @@ let validate_stream_pass ~scenario_sha256 channel = ~run_id:header.Scenario.run_id ~scenario_sha256 ~risk:header.risk ~venue_calendars:header.venue_calendars ~execution_model:header.execution_model ~execution:header.execution + ~financing:header.financing ~max_internal_events:header.max_internal_events ~initial_cash:header.initial_cash ~initial_portfolio:header.initial_portfolio @@ -267,6 +276,7 @@ let replay_stream_pass ~scenario_sha256 ~journal ~session channel = ~run_id:header.Scenario.run_id ~scenario_sha256 ~risk:header.risk ~venue_calendars:header.venue_calendars ~execution_model:header.execution_model ~execution:header.execution + ~financing:header.financing ~max_internal_events:header.max_internal_events ~initial_cash:header.initial_cash ~initial_portfolio:header.initial_portfolio diff --git a/lib/financing.ml b/lib/financing.ml new file mode 100644 index 0000000..2c44f33 --- /dev/null +++ b/lib/financing.ml @@ -0,0 +1,164 @@ +type day_count = Actual_365 | Actual_360 +type compounding = Simple | Daily +type missing_data = Reject | Zero +type locate_policy = Reject_order | Clip_fill +type recall_policy = Reject_new_shorts | Close_out + +type policy = { + day_count : day_count; + compounding : compounding; + borrow_missing_data : missing_data; + cash_missing_data : missing_data; + locate_policy : locate_policy; + recall_policy : recall_policy; +} + +type borrow_observation = { + instrument_id : Id.Instrument.t; + effective_at : Ptime.t; + available_quantity : Scalar.Quantity.t; + annual_rate_bps : int; + recalled : bool; +} + +type cash_rate_observation = { + currency : string; + effective_at : Ptime.t; + credit_rate_bps : int; + debit_rate_bps : int; +} + +let policy ~day_count ~compounding ~borrow_missing_data ~cash_missing_data + ~locate_policy ~recall_policy = + { + day_count; + compounding; + borrow_missing_data; + cash_missing_data; + locate_policy; + recall_policy; + } + +let legacy_policy = + policy ~day_count:Actual_365 ~compounding:Simple ~borrow_missing_data:Zero + ~cash_missing_data:Zero ~locate_policy:Clip_fill + ~recall_policy:Reject_new_shorts + +let valid_currency value = + String.length value > 0 + && String.for_all + (fun character -> + let code = Char.code character in + code >= 0x21 && code <> 0x7f) + value + +let valid_rate value = value >= -1_000_000 && value <= 1_000_000 + +let borrow_observation ~instrument_id ~effective_at ~available_quantity + ~annual_rate_bps ~recalled = + if not (valid_rate annual_rate_bps) then + Error "borrow annual rate basis points must be between -1000000 and 1000000" + else if recalled && not (Scalar.Quantity.is_zero available_quantity) then + Error "recalled borrow availability must be zero" + else + Ok + { + instrument_id; + effective_at; + available_quantity; + annual_rate_bps; + recalled; + } + +let cash_rate_observation ~currency ~effective_at ~credit_rate_bps + ~debit_rate_bps = + if not (valid_currency currency) then + Error "cash rate currency must not be empty or contain whitespace" + else if not (valid_rate credit_rate_bps && valid_rate debit_rate_bps) then + Error "cash annual rate basis points must be between -1000000 and 1000000" + else Ok { currency; effective_at; credit_rate_bps; debit_rate_bps } + +let picoseconds_per_day = Z.of_string "86400000000000000" + +let span_picoseconds span = + let days, picoseconds = Ptime.Span.to_d_ps span in + Z.add (Z.mul (Z.of_int days) picoseconds_per_day) (Z.of_int64 picoseconds) + +let round_ratio numerator denominator = + let sign = Z.sign numerator in + if sign = 0 then Z.zero + else + let magnitude = Z.abs numerator in + let quotient, remainder = Z.ediv_rem magnitude denominator in + let rounded = + if Z.compare (Z.mul remainder (Z.of_int 2)) denominator >= 0 then + Z.succ quotient + else quotient + in + if sign < 0 then Z.neg rounded else rounded + +let year_days = function Actual_365 -> 365 | Actual_360 -> 360 + +let simple_micros policy ~principal_micros ~annual_rate_bps duration_ps = + let numerator = + Z.mul (Z.mul principal_micros (Z.of_int annual_rate_bps)) duration_ps + in + let denominator = + Z.mul + (Z.mul (Z.of_int 10_000) (Z.of_int (year_days policy.day_count))) + picoseconds_per_day + in + round_ratio numerator denominator + +let accrue policy ~principal ~annual_rate_bps span = + if not (valid_rate annual_rate_bps) then + Error "annual rate basis points must be between -1000000 and 1000000" + else + let duration_ps = span_picoseconds span in + if Z.sign duration_ps < 0 then + Error "financing accrual span must be nonnegative" + else + let principal_micros = Z.of_int64 (Scalar.Money.to_micros principal) in + let interest = + match policy.compounding with + | Simple -> + simple_micros policy ~principal_micros ~annual_rate_bps duration_ps + | Daily -> + let whole_days, remainder = + Z.ediv_rem duration_ps picoseconds_per_day + in + let rec compound remaining balance total = + if Z.equal remaining Z.zero then (balance, total) + else + let amount = + simple_micros policy ~principal_micros:balance + ~annual_rate_bps picoseconds_per_day + in + compound (Z.pred remaining) (Z.add balance amount) + (Z.add total amount) + in + let balance, full_interest = + compound whole_days principal_micros Z.zero + in + Z.add full_interest + (simple_micros policy ~principal_micros:balance ~annual_rate_bps + remainder) + in + if Z.fits_int64 interest then + Ok (Scalar.Money.of_micros (Z.to_int64 interest)) + else Error "financing accrual overflow" + +let day_count_to_string = function + | Actual_365 -> "actual_365" + | Actual_360 -> "actual_360" + +let compounding_to_string = function Simple -> "simple" | Daily -> "daily" +let missing_data_to_string = function Reject -> "reject" | Zero -> "zero" + +let locate_policy_to_string = function + | Reject_order -> "reject_order" + | Clip_fill -> "clip_fill" + +let recall_policy_to_string = function + | Reject_new_shorts -> "reject_new_shorts" + | Close_out -> "close_out" diff --git a/lib/financing.mli b/lib/financing.mli new file mode 100644 index 0000000..878072a --- /dev/null +++ b/lib/financing.mli @@ -0,0 +1,73 @@ +(** Effective-time borrow availability and multi-currency financing policy. *) + +type day_count = Actual_365 | Actual_360 +type compounding = Simple | Daily +type missing_data = Reject | Zero +type locate_policy = Reject_order | Clip_fill +type recall_policy = Reject_new_shorts | Close_out + +type policy = private { + day_count : day_count; + compounding : compounding; + borrow_missing_data : missing_data; + cash_missing_data : missing_data; + locate_policy : locate_policy; + recall_policy : recall_policy; +} + +type borrow_observation = private { + instrument_id : Id.Instrument.t; + effective_at : Ptime.t; + available_quantity : Scalar.Quantity.t; + annual_rate_bps : int; + recalled : bool; +} + +type cash_rate_observation = private { + currency : string; + effective_at : Ptime.t; + credit_rate_bps : int; + debit_rate_bps : int; +} + +val policy : + day_count:day_count -> + compounding:compounding -> + borrow_missing_data:missing_data -> + cash_missing_data:missing_data -> + locate_policy:locate_policy -> + recall_policy:recall_policy -> + policy + +val legacy_policy : policy + +val borrow_observation : + instrument_id:Id.Instrument.t -> + effective_at:Ptime.t -> + available_quantity:Scalar.Quantity.t -> + annual_rate_bps:int -> + recalled:bool -> + (borrow_observation, string) result + +val cash_rate_observation : + currency:string -> + effective_at:Ptime.t -> + credit_rate_bps:int -> + debit_rate_bps:int -> + (cash_rate_observation, string) result + +val accrue : + policy -> + principal:Scalar.Money.t -> + annual_rate_bps:int -> + Ptime.Span.t -> + (Scalar.Money.t, string) result +(** [accrue] returns signed interest. Rounding is nearest micro-unit with ties + away from zero. Daily compounding rounds and capitalizes after every full + day, then accrues a simple fractional-day remainder. *) + +val day_count_to_string : day_count -> string +val compounding_to_string : compounding -> string +val missing_data_to_string : missing_data -> string +val locate_policy_to_string : locate_policy -> string +val recall_policy_to_string : recall_policy -> string diff --git a/lib/market_slice.ml b/lib/market_slice.ml index 8281565..b0101e1 100644 --- a/lib/market_slice.ml +++ b/lib/market_slice.ml @@ -9,6 +9,8 @@ type 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; } let valid_currency value = @@ -27,8 +29,9 @@ let fx_mark ~currency ~rate = let compare_bar left right = Id.Instrument.compare left.Bar.instrument_id right.Bar.instrument_id -let create ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars - ~fx_rates ~corporate_actions = +let create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at + ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations = if Int64.compare slice_sequence 0L <= 0 then Error "market slice sequence must be positive" else if Ptime.compare start_at end_at >= 0 then @@ -66,6 +69,35 @@ let create ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars (not (Id.Corporate_action.equal left.Corporate_action.id right.id)) && unique_actions remaining in + let borrow_observations = + List.sort + (fun (left : Financing.borrow_observation) + (right : Financing.borrow_observation) -> + Id.Instrument.compare left.Financing.instrument_id + right.Financing.instrument_id) + borrow_observations + in + let rec unique_borrow = function + | [] | [ _ ] -> true + | left :: (right :: _ as remaining) -> + (not + (Id.Instrument.equal left.Financing.instrument_id + right.Financing.instrument_id)) + && unique_borrow remaining + in + let cash_rate_observations = + List.sort + (fun (left : Financing.cash_rate_observation) + (right : Financing.cash_rate_observation) -> + String.compare left.Financing.currency right.Financing.currency) + cash_rate_observations + in + let rec unique_cash_rate = function + | [] | [ _ ] -> true + | left :: (right :: _ as remaining) -> + (not (String.equal left.Financing.currency right.Financing.currency)) + && unique_cash_rate remaining + in if not (unique bars) then Error "market slice must contain one bar per instrument" else if fx_rates = [] then Error "market slice must contain FX rates" @@ -73,6 +105,10 @@ let create ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars Error "market slice must contain one FX rate per currency" else if not (unique_actions corporate_actions) then Error "market slice corporate action IDs must be unique" + else if not (unique_borrow borrow_observations) then + Error "market slice borrow observation instrument IDs must be unique" + else if not (unique_cash_rate cash_rate_observations) then + Error "market slice cash rate currencies must be unique" else Ok { @@ -84,8 +120,16 @@ let create ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars bars; fx_rates; corporate_actions; + borrow_observations; + cash_rate_observations; } +let create ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars + ~fx_rates ~corporate_actions = + create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars + ~fx_rates ~corporate_actions ~borrow_observations:[] + ~cash_rate_observations:[] + let bar state instrument_id = List.find_opt (fun bar -> Id.Instrument.equal bar.Bar.instrument_id instrument_id) @@ -101,7 +145,10 @@ let compare_replay_order left right = else Int64.compare left.slice_sequence right.slice_sequence let pp formatter state = - Format.fprintf formatter "slice[%Ld] bars=%d fx=%d actions=%d" + Format.fprintf formatter + "slice[%Ld] bars=%d fx=%d actions=%d borrow=%d cash_rates=%d" state.slice_sequence (List.length state.bars) (List.length state.fx_rates) (List.length state.corporate_actions) + (List.length state.borrow_observations) + (List.length state.cash_rate_observations) diff --git a/lib/market_slice.mli b/lib/market_slice.mli index 34cf4b5..84535f3 100644 --- a/lib/market_slice.mli +++ b/lib/market_slice.mli @@ -14,6 +14,8 @@ type t = private { 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; } val create : @@ -27,6 +29,19 @@ val create : corporate_actions:Corporate_action.t list -> (t, string) result +val create_v10 : + 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 -> + (t, string) result + val bar : t -> Id.Instrument.t -> Bar.t option val fx_rate : t -> string -> Scalar.Price.t option val compare_replay_order : t -> t -> int diff --git a/lib/order.ml b/lib/order.ml index 0475ff3..8301f1e 100644 --- a/lib/order.ml +++ b/lib/order.ml @@ -16,7 +16,7 @@ type time_in_force = | Day of { venue_id : Id.Venue.t; calendar_id : Id.Venue_calendar.t } | Gtd of Ptime.t -type origin = Direct | Target_rebalance | Margin_liquidation +type origin = Direct | Target_rebalance | Margin_liquidation | Borrow_recall type request = { instrument_id : Id.Instrument.t; @@ -273,6 +273,7 @@ let origin_to_string = function | Direct -> "direct" | Target_rebalance -> "target_rebalance" | Margin_liquidation -> "margin_liquidation" + | Borrow_recall -> "borrow_recall" let status_to_string = function | Working -> "working" diff --git a/lib/order.mli b/lib/order.mli index b10c9e6..482409b 100644 --- a/lib/order.mli +++ b/lib/order.mli @@ -18,7 +18,7 @@ type time_in_force = | Day of { venue_id : Id.Venue.t; calendar_id : Id.Venue_calendar.t } | Gtd of Ptime.t -type origin = Direct | Target_rebalance | Margin_liquidation +type origin = Direct | Target_rebalance | Margin_liquidation | Borrow_recall type request = private { instrument_id : Id.Instrument.t; diff --git a/lib/replay.ml b/lib/replay.ml index 6288f3a..4283bf1 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -68,15 +68,26 @@ let add_audit_count count events = Error (replay "audit event count is exhausted") else Ok (Int64.add count added) +let engine_config ~contract_version ~risk ~venue_calendars ~execution_model + ~execution ~financing ~max_internal_events = + match financing with + | None -> + Engine.config_v8 ~contract_version ~risk ~venue_calendars ~execution_model + ~execution ~max_internal_events + | Some financing -> + Engine.config_v10 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~max_internal_events + let run ~scenario_sha256 ?journal_path ?(durability = Artifact_writer.Buffered) scenario = let* strategy_state = Scripted_strategy.create scenario.Scenario.schedule |> reducer_result in let* config = - Engine.config_v8 ~contract_version:scenario.contract_version + engine_config ~contract_version:scenario.contract_version ~risk:scenario.risk ~venue_calendars:scenario.venue_calendars ~execution_model:scenario.execution_model ~execution:scenario.execution + ~financing:scenario.financing ~max_internal_events:scenario.max_internal_events |> reducer_result in @@ -153,10 +164,10 @@ let run_stream_pass ~scenario_sha256 ~journal channel = | Error _ as error -> error | Ok strategy_state -> ( match - Engine.config_v8 ~contract_version:header.contract_version + engine_config ~contract_version:header.contract_version ~risk:header.Scenario.risk ~venue_calendars:header.venue_calendars ~execution_model:header.execution_model - ~execution:header.execution + ~execution:header.execution ~financing:header.financing ~max_internal_events:header.max_internal_events |> reducer_result with diff --git a/lib/risk.ml b/lib/risk.ml index ae000f8..f2a293c 100644 --- a/lib/risk.ml +++ b/lib/risk.ml @@ -71,6 +71,7 @@ type fill_limit = | Instrument_maximum_short_position of Id.Instrument.t * Scalar.Quantity.t | Instrument_maximum_notional of Id.Instrument.t * Scalar.Money.t | Instrument_shorting_disabled of Id.Instrument.t + | Instrument_borrow_availability of Id.Instrument.t * Scalar.Quantity.t | Instrument_initial_margin of Id.Instrument.t * int | Group_maximum_gross of Id.Risk_group.t * Scalar.Money.t | Group_maximum_long of Id.Risk_group.t * Scalar.Money.t diff --git a/lib/risk.mli b/lib/risk.mli index 710ff91..db2df28 100644 --- a/lib/risk.mli +++ b/lib/risk.mli @@ -59,6 +59,7 @@ type fill_limit = | Instrument_maximum_short_position of Id.Instrument.t * Scalar.Quantity.t | Instrument_maximum_notional of Id.Instrument.t * Scalar.Money.t | Instrument_shorting_disabled of Id.Instrument.t + | Instrument_borrow_availability of Id.Instrument.t * Scalar.Quantity.t | Instrument_initial_margin of Id.Instrument.t * int | Group_maximum_gross of Id.Risk_group.t * Scalar.Money.t | Group_maximum_long of Id.Risk_group.t * Scalar.Money.t diff --git a/lib/scenario.ml b/lib/scenario.ml index 919b6d4..85d2a7c 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -10,6 +10,7 @@ type t = { risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; + financing : Financing.policy option; max_internal_events : int; schedule : (int64 * Strategy.intent list) list; slices : Market_slice.t list; @@ -27,6 +28,7 @@ type stream_header = { risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; + financing : Financing.policy option; max_internal_events : int; } @@ -550,7 +552,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 [ "9"; "8"; "7" ] then + if List.mem contract_version [ "10"; "9"; "8"; "7" ] then parse_v7_risk base_currency instruments json else parse_legacy_risk base_currency instruments json @@ -742,7 +744,7 @@ let parse_versioned_execution ~contract_version ~instruments json = Ok (execution_model, execution) let parse_execution ~contract_version ~instruments json = - if List.mem contract_version [ "9"; "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "10"; "9"; "8"; "7"; "6"; "5" ] then parse_versioned_execution ~contract_version ~instruments json else parse_legacy_execution ~contract_version json @@ -791,7 +793,7 @@ 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 [ "9"; "8" ] in + let versioned = List.mem contract_version [ "10"; "9"; "8" ] in let* fields = object_fields ~name:"submit_order intent" ~expected: @@ -1113,22 +1115,152 @@ let parse_corporate_action json = Corporate_action.cash_dividend ~id ~instrument_id ~amount_per_unit | _ -> Error "unsupported corporate action type" -let parse_slice json = +let parse_financing json = let* fields = - object_fields ~name:"market slice" + object_fields ~name:"financing policy" ~expected: [ - "slice_sequence"; - "start_at"; - "end_at"; - "available_at"; - "received_at"; - "bars"; - "fx_rates"; - "corporate_actions"; + "day_count"; + "compounding"; + "borrow_missing_data"; + "cash_missing_data"; + "locate_policy"; + "recall_policy"; ] json in + let text name = Result.bind (field fields name) (string ~name) in + let* day_count = + match text "day_count" with + | Ok "actual_365" -> Ok Financing.Actual_365 + | Ok "actual_360" -> Ok Financing.Actual_360 + | Ok _ -> Error "day_count must be actual_365 or actual_360" + | Error _ as error -> error + in + let* compounding = + match text "compounding" with + | Ok "simple" -> Ok Financing.Simple + | Ok "daily" -> Ok Financing.Daily + | Ok _ -> Error "compounding must be simple or daily" + | Error _ as error -> error + in + let missing name = + match text name with + | Ok "reject" -> Ok Financing.Reject + | Ok "zero" -> Ok Financing.Zero + | Ok _ -> Error (name ^ " must be reject or zero") + | Error _ as error -> error + in + let* borrow_missing_data = missing "borrow_missing_data" in + let* cash_missing_data = missing "cash_missing_data" in + let* locate_policy = + match text "locate_policy" with + | Ok "reject_order" -> Ok Financing.Reject_order + | Ok "clip_fill" -> Ok Financing.Clip_fill + | Ok _ -> Error "locate_policy must be reject_order or clip_fill" + | Error _ as error -> error + in + let* recall_policy = + match text "recall_policy" with + | Ok "reject_new_shorts" -> Ok Financing.Reject_new_shorts + | Ok "close_out" -> Ok Financing.Close_out + | Ok _ -> Error "recall_policy must be reject_new_shorts or close_out" + | Error _ as error -> error + in + Ok + (Financing.policy ~day_count ~compounding ~borrow_missing_data + ~cash_missing_data ~locate_policy ~recall_policy) + +let parse_borrow_observation json = + let* fields = + object_fields ~name:"borrow observation" + ~expected: + [ + "instrument_id"; + "effective_at"; + "available_quantity"; + "annual_rate_bps"; + "recalled"; + ] + json + in + let* instrument_id = + Result.bind + (field fields "instrument_id") + (parse_id Id.Instrument.of_string ~name:"borrow instrument_id") + in + let* effective_at = + Result.bind + (field fields "effective_at") + (parse_timestamp ~name:"borrow effective_at") + in + let* available_quantity = + Result.bind + (field fields "available_quantity") + (parse_quantity ~name:"borrow available_quantity") + in + let* annual_rate_bps = + Result.bind + (field fields "annual_rate_bps") + (integer ~name:"borrow annual_rate_bps") + in + let* recalled = + match field fields "recalled" with + | Ok (`Bool value) -> Ok value + | Ok _ -> Error "borrow recalled must be a boolean" + | Error _ as error -> error + in + Financing.borrow_observation ~instrument_id ~effective_at ~available_quantity + ~annual_rate_bps ~recalled + +let parse_cash_rate_observation json = + let* fields = + object_fields ~name:"cash rate observation" + ~expected: + [ "currency"; "effective_at"; "credit_rate_bps"; "debit_rate_bps" ] + json + in + let* currency = + Result.bind (field fields "currency") (string ~name:"cash rate currency") + in + let* effective_at = + Result.bind + (field fields "effective_at") + (parse_timestamp ~name:"cash rate effective_at") + in + let* credit_rate_bps = + Result.bind + (field fields "credit_rate_bps") + (integer ~name:"credit_rate_bps") + in + let* debit_rate_bps = + Result.bind (field fields "debit_rate_bps") (integer ~name:"debit_rate_bps") + in + Financing.cash_rate_observation ~currency ~effective_at ~credit_rate_bps + ~debit_rate_bps + +let parse_slice ~contract_version json = + let financing_fields = + if String.equal contract_version "10" then + [ "borrow_observations"; "cash_rate_observations" ] + else [] + in + let* fields = + object_fields ~name:"market slice" + ~expected: + ([ + "slice_sequence"; + "start_at"; + "end_at"; + "available_at"; + "received_at"; + "bars"; + "fx_rates"; + "corporate_actions"; + ] + @ financing_fields) + json + in let* sequence_json = field fields "slice_sequence" in let* slice_sequence = parse_int64 ~name:"slice_sequence" sequence_json in let* start_json = field fields "start_at" in @@ -1148,8 +1280,27 @@ let parse_slice 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 - Market_slice.create ~slice_sequence ~start_at ~end_at ~available_at - ~received_at ~bars ~fx_rates ~corporate_actions + if String.equal contract_version "10" then + let* borrow_json = + Result.bind + (field fields "borrow_observations") + (list ~name:"borrow_observations") + in + let* borrow_observations = map_list parse_borrow_observation borrow_json in + let* cash_json = + Result.bind + (field fields "cash_rate_observations") + (list ~name:"cash_rate_observations") + in + let* cash_rate_observations = + map_list parse_cash_rate_observation cash_json + in + Market_slice.create_v10 ~slice_sequence ~start_at ~end_at ~available_at + ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations + else + Market_slice.create ~slice_sequence ~start_at ~end_at ~available_at + ~received_at ~bars ~fx_rates ~corporate_actions let child root field = root ^ "." ^ field @@ -1182,7 +1333,7 @@ let construct_header ~root ~contract_path ~contract_version |> at (child root "base_currency") in let* initial_cash, initial_portfolio = - if List.mem contract_version [ "9"; "8"; "7"; "6" ] then + if List.mem contract_version [ "10"; "9"; "8"; "7"; "6" ] then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state |> at (child root "initial_portfolio") @@ -1242,6 +1393,16 @@ let construct_header ~root ~contract_path ~contract_version parse_execution ~contract_version ~instruments shape.execution |> at (child root "execution") in + let* financing = + match (contract_version, shape.financing) with + | "10", Some json -> parse_financing json |> at (child root "financing") + | "10", None -> + Error "missing financing policy" |> at (child root "financing") + | _, _ -> Ok Financing.legacy_policy + in + let financing = + if String.equal contract_version "10" then Some financing else None + in let header : stream_header = { contract_version; @@ -1255,6 +1416,7 @@ let construct_header ~root ~contract_path ~contract_version risk; execution_model; execution; + financing; max_internal_events; } in @@ -1278,7 +1440,9 @@ let construct_batch (shape : Scenario_shape.batch) = schedule_json in let* slices_json = list ~name:"slices" shape.slices |> at "$.slices" in - let* slices = map_list_at "$.slices" parse_slice slices_json in + let* slices = + map_list_at "$.slices" (parse_slice ~contract_version) slices_json + in let* () = Scenario_validation.batch ~root ~base_currency:header.base_currency ~currencies ~instruments:header.instruments ~risk:header.risk ~catalog @@ -1297,6 +1461,7 @@ let construct_batch (shape : Scenario_shape.batch) = risk = header.risk; execution_model = header.execution_model; execution = header.execution; + financing = header.financing; max_internal_events = header.max_internal_events; schedule; slices; @@ -1371,7 +1536,7 @@ let stream_item_of_yojson header ~previous json = Scenario_shape.stream_item json |> Result.map_error (diagnostic code) in let* market_slice = - parse_slice shape.market_slice + parse_slice ~contract_version:header.contract_version shape.market_slice |> at "$.payload.market_slice" |> Result.map_error (diagnostic code) in diff --git a/lib/scenario.mli b/lib/scenario.mli index 8a49b17..5272c88 100644 --- a/lib/scenario.mli +++ b/lib/scenario.mli @@ -12,6 +12,7 @@ type t = private { risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; + financing : Financing.policy option; max_internal_events : int; schedule : (int64 * Strategy.intent list) list; slices : Market_slice.t list; @@ -29,6 +30,7 @@ type stream_header = private { risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; + financing : Financing.policy option; max_internal_events : int; } diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index 3076a4a..393560d 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -9,6 +9,7 @@ type common = { venue_calendars : Yojson.Safe.t option; risk : Yojson.Safe.t; execution : Yojson.Safe.t; + financing : Yojson.Safe.t option; max_internal_events : Yojson.Safe.t; } @@ -68,18 +69,23 @@ 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 [ "9"; "8"; "7"; "6" ] then "initial_portfolio" + if List.mem contract_version [ "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 [ "9"; "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "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 String.equal contract_version "10" then List.assoc_opt "financing" fields + else None + in let* max_internal_events = field ~root fields "max_internal_events" in Ok { @@ -91,6 +97,7 @@ let common ~root ~contract_version fields = venue_calendars; risk; execution; + financing; max_internal_events; } @@ -105,12 +112,13 @@ let batch json = match preliminary with `String value -> value | _ -> "" in let calendar_fields = - if List.mem contract_version [ "9"; "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "9"; "8"; "7"; "6" ] then "initial_portfolio" + if List.mem contract_version [ "10"; "9"; "8"; "7"; "6" ] then + "initial_portfolio" else "initial_cash" in let* fields = @@ -129,7 +137,8 @@ let batch json = "schedule"; "slices"; ] - @ calendar_fields) + @ calendar_fields + @ if String.equal contract_version "10" then [ "financing" ] else []) json in let* contract_version_json = field ~root fields "contract_version" in @@ -141,12 +150,13 @@ let batch json = let stream_header ~contract_version json = let root = "$.payload" in let calendar_fields = - if List.mem contract_version [ "9"; "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "9"; "8"; "7"; "6" ] then "initial_portfolio" + if List.mem contract_version [ "10"; "9"; "8"; "7"; "6" ] then + "initial_portfolio" else "initial_cash" in let* fields = @@ -162,7 +172,8 @@ let stream_header ~contract_version json = "execution"; "max_internal_events"; ] - @ calendar_fields) + @ calendar_fields + @ if String.equal contract_version "10" then [ "financing" ] else []) json in common ~root ~contract_version fields diff --git a/lib/scenario_shape.mli b/lib/scenario_shape.mli index bdbd719..e0cc642 100644 --- a/lib/scenario_shape.mli +++ b/lib/scenario_shape.mli @@ -11,6 +11,7 @@ type common = { venue_calendars : Yojson.Safe.t option; risk : Yojson.Safe.t; execution : Yojson.Safe.t; + financing : Yojson.Safe.t option; max_internal_events : Yojson.Safe.t; } diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 8dea9b9..0ae5a44 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -48,7 +48,7 @@ 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 [ "9"; "8"; "7"; "6" ] then Ok () + if List.mem contract_version [ "10"; "9"; "8"; "7"; "6" ] then Ok () else Account.create ~base_currency ~initial_cash |> Result.map (fun _ -> ()) @@ -66,7 +66,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path:(child root "instruments") "instrument IDs must be unique" else let* () = - if List.mem contract_version [ "9"; "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "10"; "9"; "8"; "7"; "6"; "5" ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -84,7 +84,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path: (child root - (if List.mem contract_version [ "9"; "8"; "7"; "6" ] then + (if List.mem contract_version [ "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 c8f135b..9ea4d4e 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -14,6 +14,7 @@ type initialization = { risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; + financing : Financing.policy option; } type identity = { name : Id.Strategy.t; version : string option } @@ -101,7 +102,24 @@ let group_kind_to_string = function let nullable render = Option.fold ~none:`Null ~some:render let modern_protocol protocol_version = - List.mem protocol_version [ "7"; "6"; "5" ] + List.mem protocol_version [ "8"; "7"; "6"; "5" ] + +let financing_to_yojson policy = + `Assoc + [ + ( "day_count", + string (Financing.day_count_to_string policy.Financing.day_count) ); + ( "compounding", + string (Financing.compounding_to_string policy.compounding) ); + ( "borrow_missing_data", + string (Financing.missing_data_to_string policy.borrow_missing_data) ); + ( "cash_missing_data", + string (Financing.missing_data_to_string policy.cash_missing_data) ); + ( "locate_policy", + string (Financing.locate_policy_to_string policy.locate_policy) ); + ( "recall_policy", + string (Financing.recall_policy_to_string policy.recall_policy) ); + ] let instrument_policy_to_yojson (policy : Risk.instrument_policy) = `Assoc @@ -203,7 +221,7 @@ let execution_to_yojson ~protocol_version model execution = (Fee_schedule.components schedule)) ); ] in - if String.equal protocol_version "7" then + if List.mem protocol_version [ "8"; "7" ] then `Assoc [ ("model", string (Execution_model.name model)); @@ -241,13 +259,13 @@ let execution_to_yojson ~protocol_version model execution = ] let protocol_version initialization = - if String.equal initialization.scenario_contract_version Contract.version then - version - else if - String.equal initialization.scenario_contract_version - Contract.previous_version - then Contract.previous_strategy_protocol_version - else "3" + match initialization.scenario_contract_version with + | "10" -> "8" + | "9" -> "7" + | "8" -> "6" + | "7" -> "5" + | "6" -> "4" + | _ -> "3" let initialize_message ~sequence:message_sequence initialization = let protocol_version = protocol_version initialization in @@ -286,7 +304,7 @@ let initialize_message ~sequence:message_sequence initialization = ] in let fields = - if List.mem protocol_version [ "7"; "6" ] then + if List.mem protocol_version [ "8"; "7"; "6" ] then let initial_portfolio = Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson initialization.initial_portfolio @@ -299,6 +317,13 @@ let initialize_message ~sequence:message_sequence initialization = ( "venue_calendars", `List (List.map venue_calendar_to_yojson venue_calendars) ); ]; + (if String.equal protocol_version "8" then + [ + ( "financing", + Option.fold ~none:`Null ~some:financing_to_yojson + initialization.financing ); + ] + else []); List.drop 6 fields; ] else if modern_protocol protocol_version then @@ -317,14 +342,22 @@ let initialize_message ~sequence:message_sequence initialization = message ~protocol_version ~sequence:message_sequence ~message_type:"initialize" (`Assoc fields) -let cash_attribution_to_yojson (balance : Account.cash_attribution) = +let cash_attribution_to_yojson ~protocol_version + (balance : Account.cash_attribution) = `Assoc - [ - ("currency", string balance.currency); - ("amount", money balance.amount); - ("fx_rate", price balance.fx_rate); - ("base_value", money balance.base_value); - ] + ([ + ("currency", string balance.currency); + ("amount", money balance.amount); + ("fx_rate", price balance.fx_rate); + ("base_value", money balance.base_value); + ] + @ + if String.equal protocol_version "8" then + [ + ("interest", money balance.interest); + ("base_interest", money balance.base_interest); + ] + else []) let marked_position_to_yojson (position : Strategy.marked_position) = `Assoc @@ -386,7 +419,10 @@ let context_to_yojson ~protocol_version context = ("weights_available", `Bool (Option.is_some portfolio.cash_weight)); ("cash_weight", Option.fold ~none:`Null ~some:weight portfolio.cash_weight); ( "cash_balances", - `List (List.map cash_attribution_to_yojson cash_balances) ); + `List + (List.map + (cash_attribution_to_yojson ~protocol_version) + cash_balances) ); ("positions", `List (List.map marked_position_to_yojson positions)); ] in @@ -407,7 +443,7 @@ let context_to_yojson ~protocol_version context = ( "working_orders", `List (List.map - (if List.mem protocol_version [ "7"; "6" ] then + (if List.mem protocol_version [ "8"; "7"; "6" ] then Codec.order_to_yojson_v8 else Codec.order_to_yojson) working_orders) ); @@ -419,14 +455,17 @@ let event_to_yojson ~protocol_version = function `Assoc [ ("type", string "market_slice_closed"); - ("market_slice", Codec.market_slice_to_yojson market_slice); + ( "market_slice", + if String.equal protocol_version "8" then + Codec.market_slice_to_yojson_v10 market_slice + else Codec.market_slice_to_yojson market_slice ); ] | Strategy.Fill_received fill -> `Assoc [ ("type", string "fill_received"); ( "fill", - if String.equal protocol_version "7" then + if List.mem protocol_version [ "8"; "7" ] then Codec.fill_to_yojson_v9 fill else Codec.fill_to_yojson fill ); ] @@ -435,7 +474,7 @@ let event_to_yojson ~protocol_version = function [ ("type", string "order_updated"); ( "order", - if List.mem protocol_version [ "7"; "6" ] then + if List.mem protocol_version [ "8"; "7"; "6" ] then Codec.order_to_yojson_v8 order else Codec.order_to_yojson order ); ] @@ -523,7 +562,8 @@ let parse_intents_payload ~protocol_version json = let* intent = Scenario.intent_of_yojson ~contract_version: - (if String.equal protocol_version "7" then "9" + (if String.equal protocol_version "8" then "10" + else if String.equal protocol_version "7" then "9" else if String.equal protocol_version "6" then "8" else "7") value diff --git a/lib/strategy_protocol.mli b/lib/strategy_protocol.mli index 170e614..3fed934 100644 --- a/lib/strategy_protocol.mli +++ b/lib/strategy_protocol.mli @@ -16,6 +16,7 @@ type initialization = { risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; + financing : Financing.policy option; } type identity = private { name : Id.Strategy.t; version : string option } diff --git a/mkdocs.yml b/mkdocs.yml index f746a31..3e5940b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,14 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v9: contracts/v9/README.md + - Current v10: contracts/v10/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 v7: contracts/strategy/v7/README.md + - Current v8: contracts/strategy/v8/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 77e367d..97ef5af 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -66,3 +66,11 @@ compare_journal \ v9-fill-clipped \ contracts/v9/fixtures/fill-clipped.scenario.json \ contracts/v9/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v10-demo \ + contracts/v10/fixtures/demo.scenario.json \ + contracts/v10/fixtures/demo.journal.jsonl +compare_journal \ + v10-fill-clipped \ + contracts/v10/fixtures/fill-clipped.scenario.json \ + contracts/v10/fixtures/fill-clipped.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index 6ea8f8f..3efa7d1 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/v9/README.md", + "contracts/v10/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v7/README.md", + "contracts/strategy/v8/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 5226355..013e3c1 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/v9/scenario.schema.json", - "share/trading_engine/contracts/v9/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v10/scenario.schema.json", + "share/trading_engine/contracts/v10/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/v9/fixtures/demo.scenario.json", + "contracts/v10/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/v9/fixtures/demo.scenario.json", - "contracts/strategy/v7/message.schema.json", + "contracts/v10/fixtures/demo.scenario.json", + "contracts/strategy/v8/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v9/scenario.schema.json", + "contracts/v10/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index d195f29..63430fe 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":["9","8","7","6","5","4","3"],"journal_contract_versions":["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":["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":["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":["10","9","8","7","6","5","4","3"],"journal_contract_versions":["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":["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":["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 170c607..25b02b3 100644 --- a/test/dune +++ b/test/dune @@ -8,6 +8,7 @@ test_execution test_order_lifetimes test_fee_schedules + test_financing test_reducer test_reducer_properties test_checkpoint4 @@ -43,6 +44,14 @@ ../contracts/v9/journal.schema.json ../contracts/v9/scenario-stream.schema.json ../contracts/v9/scenario.schema.json + ../contracts/v10/fixtures/demo.journal.jsonl + ../contracts/v10/fixtures/demo.scenario.json + ../contracts/v10/fixtures/demo.scenario.jsonl + ../contracts/v10/fixtures/fill-clipped.journal.jsonl + ../contracts/v10/fixtures/fill-clipped.scenario.json + ../contracts/v10/journal.schema.json + ../contracts/v10/scenario-stream.schema.json + ../contracts/v10/scenario.schema.json ../contracts/v6/fixtures/demo.scenario.json ../contracts/v6/fixtures/demo.scenario.jsonl ../contracts/v5/fixtures/demo.scenario.json @@ -55,6 +64,7 @@ ../contracts/strategy/v5/fixtures/external.strategy.jsonl ../contracts/strategy/v6/fixtures/external.strategy.jsonl ../contracts/strategy/v7/fixtures/external.strategy.jsonl + ../contracts/strategy/v8/fixtures/external.strategy.jsonl ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries @@ -73,6 +83,48 @@ (modules fuzz_protocol) (libraries trading_engine yojson unix)) +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v10/fixtures/demo.journal.jsonl + ../contracts/v10/fixtures/demo.scenario.json + ../contracts/v10/fixtures/demo.scenario.jsonl + ../contracts/v10/journal.schema.json + ../contracts/v10/scenario-stream.schema.json + ../contracts/v10/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v10/scenario.schema.json} + %{dep:../contracts/v10/scenario-stream.schema.json} + %{dep:../contracts/v10/journal.schema.json} + %{dep:../contracts/v10/fixtures/demo.scenario.json} + %{dep:../contracts/v10/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v10/fixtures/demo.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v10/fixtures/fill-clipped.journal.jsonl + ../contracts/v10/fixtures/fill-clipped.scenario.json + ../contracts/v10/fixtures/demo.scenario.jsonl + ../contracts/v10/journal.schema.json + ../contracts/v10/scenario-stream.schema.json + ../contracts/v10/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v10/scenario.schema.json} + %{dep:../contracts/v10/scenario-stream.schema.json} + %{dep:../contracts/v10/journal.schema.json} + %{dep:../contracts/v10/fixtures/fill-clipped.scenario.json} + %{dep:../contracts/v10/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v10/fixtures/fill-clipped.journal.jsonl}))) + (rule (alias runtest) (deps @@ -133,6 +185,27 @@ ../contracts/v8/fixtures/demo.scenario.json ../contracts/v8/fixtures/demo.scenario.jsonl)) +(rule + (alias runtest) + (deps + validate_strategy_schema.py + ../contracts/v10/scenario.schema.json + ../contracts/v10/journal.schema.json + ../contracts/diagnostic/v1/diagnostic.schema.json + ../contracts/strategy/v8/message.schema.json + ../contracts/strategy/v8/transcript.schema.json + ../contracts/strategy/v8/fixtures/external.strategy.jsonl) + (action + (run + python3 + %{dep:validate_strategy_schema.py} + %{dep:../contracts/v10/scenario.schema.json} + %{dep:../contracts/v10/journal.schema.json} + %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} + %{dep:../contracts/strategy/v8/message.schema.json} + %{dep:../contracts/strategy/v8/transcript.schema.json} + %{dep:../contracts/strategy/v8/fixtures/external.strategy.jsonl}))) + (rule (alias runtest) (deps diff --git a/test/test_boundary_failures.ml b/test/test_boundary_failures.ml index dc9a4af..a099ea7 100644 --- a/test/test_boundary_failures.ml +++ b/test/test_boundary_failures.ml @@ -330,6 +330,7 @@ let initialization () = risk = risk ~instruments:[ instrument ] (); execution_model = T.Execution_model.find "completed_bar_v1" |> ok; execution = execution (); + financing = None; } let process_stages = diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index da1f2dc..3a1c4f5 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -118,7 +118,7 @@ let capabilities_describe_execution_contracts () = (strings "configuration_versions"); Alcotest.(check (list string)) "scenario contracts" - [ "9"; "8"; "7"; "6"; "5"; "4"; "3" ] + [ "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] (strings "scenario_contract_versions"); Alcotest.(check (list string)) "required fields" diff --git a/test/test_engine.ml b/test/test_engine.ml index 3de01c2..5a84020 100644 --- a/test/test_engine.ml +++ b/test/test_engine.ml @@ -7,6 +7,7 @@ let () = ("execution", Test_execution.tests); ("order-lifetimes", Test_order_lifetimes.tests); ("fee-schedules", Test_fee_schedules.tests); + ("financing", Test_financing.tests); ("reducer", Test_reducer.tests); ("reducer-properties", Test_reducer_properties.tests); ("checkpoint4", Test_checkpoint4.tests); diff --git a/test/test_financing.ml b/test/test_financing.ml new file mode 100644 index 0000000..f8908fd --- /dev/null +++ b/test/test_financing.ml @@ -0,0 +1,464 @@ +open Test_support +module T = Trading_engine +module Runner = T.Engine.Make (T.Scripted_strategy) + +let policy ?(day_count = T.Financing.Actual_360) + ?(compounding = T.Financing.Simple) + ?(borrow_missing_data = T.Financing.Reject) + ?(cash_missing_data = T.Financing.Reject) + ?(locate_policy = T.Financing.Clip_fill) + ?(recall_policy = T.Financing.Close_out) () = + T.Financing.policy ~day_count ~compounding ~borrow_missing_data + ~cash_missing_data ~locate_policy ~recall_policy + +let one_day = + Ptime.diff + (timestamp "2026-01-02T00:00:00Z") + (timestamp "2026-01-01T00:00:00Z") + +let explicit_accrual_policies () = + let principal = money "36000" in + let simple = + T.Financing.accrue (policy ()) ~principal ~annual_rate_bps:10_000 one_day + |> ok + in + Alcotest.check money_testable "actual/360 one-day interest" (money "100") + simple; + let two_days = Ptime.Span.add one_day one_day in + let daily = + T.Financing.accrue + (policy ~compounding:T.Financing.Daily ()) + ~principal ~annual_rate_bps:10_000 two_days + |> ok + in + Alcotest.check money_testable "daily capitalization" (money "200.277778") + daily; + let rebate = + T.Financing.accrue (policy ()) ~principal ~annual_rate_bps:(-1000) one_day + |> ok + in + Alcotest.check money_testable "negative rate" (money "-10") rebate + +let accrual_boundaries_and_policy_names () = + let actual_365 = + T.Financing.accrue + (policy ~day_count:T.Financing.Actual_365 ()) + ~principal:(money "36500") ~annual_rate_bps:10_000 one_day + |> ok + in + Alcotest.check money_testable "actual/365 one-day interest" (money "100") + actual_365; + Alcotest.(check bool) + "invalid accrual rate" true + (Result.is_error + (T.Financing.accrue (policy ()) ~principal:(money "1") + ~annual_rate_bps:1_000_001 one_day)); + Alcotest.(check bool) + "negative accrual interval" true + (Result.is_error + (T.Financing.accrue (policy ()) ~principal:(money "1") + ~annual_rate_bps:100 (Ptime.Span.neg one_day))); + let year = + Ptime.diff + (timestamp "2027-01-01T00:00:00Z") + (timestamp "2026-01-01T00:00:00Z") + in + Alcotest.(check bool) + "accrual overflow" true + (Result.is_error + (T.Financing.accrue + (policy ~day_count:T.Financing.Actual_365 ()) + ~principal:(T.Scalar.Money.of_micros Int64.max_int) + ~annual_rate_bps:1_000_000 year)); + Alcotest.(check (list string)) + "policy names" + [ + "actual_365"; + "actual_360"; + "simple"; + "daily"; + "reject"; + "zero"; + "reject_order"; + "clip_fill"; + "reject_new_shorts"; + "close_out"; + ] + [ + T.Financing.day_count_to_string T.Financing.Actual_365; + T.Financing.day_count_to_string T.Financing.Actual_360; + T.Financing.compounding_to_string T.Financing.Simple; + T.Financing.compounding_to_string T.Financing.Daily; + T.Financing.missing_data_to_string T.Financing.Reject; + T.Financing.missing_data_to_string T.Financing.Zero; + T.Financing.locate_policy_to_string T.Financing.Reject_order; + T.Financing.locate_policy_to_string T.Financing.Clip_fill; + T.Financing.recall_policy_to_string T.Financing.Reject_new_shorts; + T.Financing.recall_policy_to_string T.Financing.Close_out; + ] + +let cash_interest_is_ledger_attributed () = + let account = + test_account ~initial_cash:[ ("USD", money "100"); ("EUR", money "0") ] () + in + let account = + T.Account.apply_cash_interest account ~currency:"USD" ~interest:(money "1") + |> ok + in + let account = + T.Account.apply_cash_interest account ~currency:"EUR" + ~interest:(money "-0.5") + |> ok + in + let valuation = + account_value ~instruments:[] + ~fx_rates:[ ("USD", price "1"); ("EUR", price "2") ] + account ~marks:[] + in + Alcotest.check money_testable "base interest" (money "0") + valuation.cash_interest; + let eur = + List.find + (fun (row : T.Account.cash_attribution) -> + String.equal row.currency "EUR") + valuation.cash_balances + in + Alcotest.check money_testable "native debit interest" (money "-0.5") + eur.interest; + Alcotest.check money_testable "base debit interest" (money "-1") + eur.base_interest; + Alcotest.check money_testable "cash interest contributes to realized P&L" + (money "0") valuation.realized_pnl; + let negative_account = test_account ~initial_cash:[ ("USD", money "0") ] () in + let negative_account = + T.Account.apply_cash_interest negative_account ~currency:"USD" + ~interest:(money "-1") + |> ok + in + let negative_valuation = + account_value ~instruments:[] + ~fx_rates:[ ("USD", price "1") ] + negative_account ~marks:[] + in + Alcotest.check money_testable "debit interest can produce negative equity" + (money "-1") negative_valuation.equity; + Alcotest.check money_testable "negative equity retains realized attribution" + (money "-1") negative_valuation.realized_pnl + +let financing_slice ?(borrow_observations = []) ?(cash_rate_observations = []) + sequence = + let day = day sequence in + let start_at = timestamp (Printf.sprintf "2026-01-%02dT14:30:00Z" day) in + let end_at = timestamp (Printf.sprintf "2026-01-%02dT21:00:00Z" day) in + let available_at = timestamp (Printf.sprintf "2026-01-%02dT21:00:01Z" day) in + let received_at = timestamp (Printf.sprintf "2026-01-%02dT21:00:02Z" day) in + T.Market_slice.create_v10 ~slice_sequence:sequence ~start_at ~end_at + ~available_at ~received_at + ~bars:[ bar sequence ] + ~fx_rates:[ fx_mark () ] + ~corporate_actions:[] ~borrow_observations ~cash_rate_observations + |> ok + +let borrow_observation ?(available = "5") ?(rate = 3600) ?(recalled = false) + effective_at = + T.Financing.borrow_observation + ~instrument_id:(instrument_id "test-equity") + ~effective_at ~available_quantity:(quantity available) ~annual_rate_bps:rate + ~recalled + |> ok + +let cash_rate effective_at = + T.Financing.cash_rate_observation ~currency:"USD" ~effective_at + ~credit_rate_bps:0 ~debit_rate_bps:3600 + |> ok + +let financing_config financing = + T.Engine.config_v10 ~contract_version:T.Contract.version + ~risk:(risk ~short_borrow_bps:0 ()) + ~venue_calendars:[] + ~execution_model:(T.Execution_model.find "completed_bar_v1" |> ok) + ~execution:(execution ()) ~financing ~max_internal_events:1000 + |> ok + +let empty_strategy () = T.Scripted_strategy.create [] |> ok + +let missing_data_policies_are_explicit () = + let cash_state = + Runner.create ~run_id:(run_id "missing-cash") ~scenario_sha256 + ~config:(financing_config (policy ())) + ~initial_cash:[ ("USD", money "1000") ] + ~strategy_state:(empty_strategy ()) + |> ok + in + let cash_error = + Runner.process_slice cash_state (financing_slice 1L) |> error + in + Alcotest.(check string) + "missing cash rate rejected" + "nonzero cash balance has no effective rate for currency USD" cash_error; + let short = + T.Initial_portfolio.position + ~instrument_id:(instrument_id "test-equity") + ~quantity:(quantity "-1") ~cost_basis:(money "-100") + ~realized_pnl:T.Scalar.Money.zero ~dividend_pnl:T.Scalar.Money.zero + ~execution_fees:T.Scalar.Money.zero ~borrow_fees:T.Scalar.Money.zero + |> ok + in + let initial_portfolio = + T.Initial_portfolio.create ~base_currency:"USD" + ~cash:[ ("USD", money "1100") ] + ~positions:[ short ] + ~marks:[ (instrument_id "test-equity", price "100") ] + ~fx_rates:[ ("USD", price "1") ] + |> ok + in + let borrow_state = + Runner.create_with_portfolio ~run_id:(run_id "missing-borrow") + ~scenario_sha256 + ~config:(financing_config (policy ~cash_missing_data:T.Financing.Zero ())) + ~initial_portfolio ~strategy_state:(empty_strategy ()) + |> ok + in + let borrow_error = + Runner.process_slice borrow_state (financing_slice 1L) |> error + in + Alcotest.(check string) + "missing borrow observation rejected" + "open short has no effective borrow observation for test-equity" + borrow_error + +let reject_order_policy_uses_current_locate () = + let target = + T.Strategy.Target_quantities + [ + T.Strategy. + { + instrument_id = instrument_id "test-equity"; + quantity = quantity "-10"; + }; + ] + in + let strategy_state = T.Scripted_strategy.create [ (1L, [ target ]) ] |> ok in + let financing = + policy ~borrow_missing_data:T.Financing.Zero + ~cash_missing_data:T.Financing.Zero + ~locate_policy:T.Financing.Reject_order () + in + let state = + Runner.create ~run_id:(run_id "reject-locate") ~scenario_sha256 + ~config:(financing_config financing) + ~initial_cash:[ ("USD", money "1000") ] + ~strategy_state + |> ok + in + let start_at = timestamp "2026-01-02T14:30:00Z" in + let state, events = + Runner.process_slice state + (financing_slice ~borrow_observations:[ borrow_observation start_at ] 1L) + |> ok + in + Alcotest.check quantity_testable "rejected order leaves position flat" + T.Scalar.Quantity.zero + (T.Account.position_quantity (Runner.account state) + (instrument_id "test-equity")); + Alcotest.(check bool) + "oversized locate order rejected" true + (List.exists + (fun event -> + match event.T.Audit.event with + | T.Audit.Order_rejected { status = T.Order.Rejected reason; _ } -> + String.equal reason "order exceeds effective borrow availability" + | _ -> false) + events) + +let zero_missing_data_and_recall_retention () = + let short = + T.Initial_portfolio.position + ~instrument_id:(instrument_id "test-equity") + ~quantity:(quantity "-1") ~cost_basis:(money "-100") + ~realized_pnl:T.Scalar.Money.zero ~dividend_pnl:T.Scalar.Money.zero + ~execution_fees:T.Scalar.Money.zero ~borrow_fees:T.Scalar.Money.zero + |> ok + in + let initial_portfolio = + T.Initial_portfolio.create ~base_currency:"USD" + ~cash:[ ("USD", money "1100") ] + ~positions:[ short ] + ~marks:[ (instrument_id "test-equity", price "100") ] + ~fx_rates:[ ("USD", price "1") ] + |> ok + in + let financing = + policy ~borrow_missing_data:T.Financing.Zero + ~cash_missing_data:T.Financing.Zero + ~recall_policy:T.Financing.Reject_new_shorts () + in + let state = + Runner.create_with_portfolio ~run_id:(run_id "retain-recall") + ~scenario_sha256 + ~config:(financing_config financing) + ~initial_portfolio ~strategy_state:(empty_strategy ()) + |> ok + in + let state, _ = Runner.process_slice state (financing_slice 1L) |> ok in + let recall_at = timestamp "2026-01-03T14:30:00Z" in + let cash_observation = + T.Financing.cash_rate_observation ~currency:"USD" ~effective_at:recall_at + ~credit_rate_bps:0 ~debit_rate_bps:0 + |> ok + in + let state, events = + Runner.process_slice state + (financing_slice + ~borrow_observations: + [ + borrow_observation ~available:"0" ~rate:(-100) ~recalled:true + recall_at; + ] + ~cash_rate_observations:[ cash_observation ] 2L) + |> ok + in + Alcotest.check quantity_testable "reject-new-shorts retains recalled position" + (quantity "-1") + (T.Account.position_quantity (Runner.account state) + (instrument_id "test-equity")); + Alcotest.(check bool) + "retained recall audited without close-out" true + (List.exists + (fun event -> + match event.T.Audit.event with + | T.Audit.Borrow_recall_received { close_out_quantity; _ } -> + T.Scalar.Quantity.is_zero close_out_quantity + | _ -> false) + events) + +let availability_clips_and_recall_closes () = + let target = + T.Strategy.Target_quantities + [ + T.Strategy. + { + instrument_id = instrument_id "test-equity"; + quantity = quantity "-10"; + }; + ] + in + let strategy_state = T.Scripted_strategy.create [ (1L, [ target ]) ] |> ok in + let configured_risk = risk ~short_borrow_bps:0 () in + let financing = policy () in + let config = + T.Engine.config_v10 ~contract_version:T.Contract.version + ~risk:configured_risk ~venue_calendars:[] + ~execution_model:(T.Execution_model.find "completed_bar_v1" |> ok) + ~execution:(execution ()) ~financing ~max_internal_events:1000 + |> ok + in + let state = + Runner.create ~run_id:(run_id "financing") ~scenario_sha256 ~config + ~initial_cash:[ ("USD", money "1000") ] + ~strategy_state + |> ok + in + let first_start = timestamp "2026-01-02T14:30:00Z" in + let state, _ = + Runner.process_slice state + (financing_slice + ~borrow_observations:[ borrow_observation first_start ] + ~cash_rate_observations:[ cash_rate first_start ] + 1L) + |> ok + in + let state, events = Runner.process_slice state (financing_slice 2L) |> ok in + let encoded = List.map T.Codec.audit_to_string events in + Alcotest.(check bool) + "availability audits serialize" true + (List.for_all (fun value -> String.length value > 0) encoded); + Alcotest.check quantity_testable "locate-limited short" (quantity "-5") + (T.Account.position_quantity (Runner.account state) + (instrument_id "test-equity")); + Alcotest.(check bool) + "availability clipping audited" true + (List.exists + (fun event -> + match event.T.Audit.event with + | T.Audit.Fill_clipped + { limit = T.Risk.Instrument_borrow_availability _; _ } -> + true + | _ -> false) + events); + let recall_start = timestamp "2026-01-04T14:30:00Z" in + let recall = borrow_observation ~available:"0" ~recalled:true recall_start in + let state, events = + Runner.process_slice state + (financing_slice ~borrow_observations:[ recall ] 3L) + |> ok + in + let encoded = List.map T.Codec.audit_to_string events in + Alcotest.(check bool) + "recall and financing audits serialize" true + (List.for_all (fun value -> String.length value > 0) encoded); + Alcotest.check quantity_testable "recalled short closed" + T.Scalar.Quantity.zero + (T.Account.position_quantity (Runner.account state) + (instrument_id "test-equity")); + Alcotest.(check bool) + "recall and observed charge audited" true + (List.exists + (fun event -> + match event.T.Audit.event with + | T.Audit.Borrow_recall_received _ -> true + | _ -> false) + events + && List.exists + (fun event -> + match event.T.Audit.event with + | T.Audit.Borrow_charge_applied _ -> true + | _ -> false) + events) + +let constructors_reject_ambiguous_observations () = + let effective_at = timestamp "2026-01-01T00:00:00Z" in + Alcotest.(check bool) + "borrow rate bounds enforced" true + (Result.is_error + (T.Financing.borrow_observation + ~instrument_id:(instrument_id "test-equity") + ~effective_at ~available_quantity:(quantity "1") + ~annual_rate_bps:(-1_000_001) ~recalled:false)); + Alcotest.(check bool) + "recall cannot retain availability" true + (Result.is_error + (T.Financing.borrow_observation + ~instrument_id:(instrument_id "test-equity") + ~effective_at ~available_quantity:(quantity "1") ~annual_rate_bps:100 + ~recalled:true)); + Alcotest.(check bool) + "rate bounds enforced" true + (Result.is_error + (T.Financing.cash_rate_observation ~currency:"USD" ~effective_at + ~credit_rate_bps:1_000_001 ~debit_rate_bps:0)); + Alcotest.(check bool) + "currency validation enforced" true + (Result.is_error + (T.Financing.cash_rate_observation ~currency:"" ~effective_at + ~credit_rate_bps:0 ~debit_rate_bps:0)) + +let tests = + [ + Alcotest.test_case "explicit accrual policies" `Quick + explicit_accrual_policies; + Alcotest.test_case "accrual boundaries and names" `Quick + accrual_boundaries_and_policy_names; + Alcotest.test_case "cash interest attribution" `Quick + cash_interest_is_ledger_attributed; + Alcotest.test_case "explicit missing data" `Quick + missing_data_policies_are_explicit; + Alcotest.test_case "locate order rejection" `Quick + reject_order_policy_uses_current_locate; + Alcotest.test_case "zero missing data and retained recall" `Quick + zero_missing_data_and_recall_retention; + Alcotest.test_case "availability and recall" `Quick + availability_clips_and_recall_closes; + Alcotest.test_case "observation validation" `Quick + constructors_reject_ambiguous_observations; + ] diff --git a/test/test_scenario.ml b/test/test_scenario.ml index 0b56434..b4a6701 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/v9/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v10/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/v9/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v10/fixtures/demo.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -62,8 +62,21 @@ let write_large_stream path slice_count = for index = 1 to slice_count do let offset = (index - 1) * 4 in let market_slice = - T.Market_slice.create ~slice_sequence:(Int64.of_int index) - ~start_at:(add_seconds base offset) + let start_at = add_seconds base offset in + let borrow_observation = + T.Financing.borrow_observation + ~instrument_id:(instrument_id "demo-equity-acme") + ~effective_at:start_at ~available_quantity:(quantity "1000") + ~annual_rate_bps:100 ~recalled:false + |> ok + in + let cash_rate = + T.Financing.cash_rate_observation ~currency:"USD" + ~effective_at:start_at ~credit_rate_bps:0 ~debit_rate_bps:0 + |> ok + in + T.Market_slice.create_v10 ~slice_sequence:(Int64.of_int index) + ~start_at ~end_at:(add_seconds base (offset + 1)) ~available_at:(add_seconds base (offset + 2)) ~received_at:(add_seconds base (offset + 3)) @@ -74,13 +87,14 @@ let write_large_stream path slice_count = (Int64.of_int index); ] ~fx_rates:[ fx_mark () ] - ~corporate_actions:[] + ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] + ~cash_rate_observations:[ cash_rate ] |> ok in let payload = `Assoc [ - ("market_slice", T.Codec.market_slice_to_yojson market_slice); + ("market_slice", T.Codec.market_slice_to_yojson_v10 market_slice); ("intents", `List []); ] in @@ -125,9 +139,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v9/scenario.schema.json"; - check_schema "../contracts/v9/scenario-stream.schema.json"; - check_schema "../contracts/v9/journal.schema.json" + check_schema "../contracts/v10/scenario.schema.json"; + check_schema "../contracts/v10/scenario-stream.schema.json"; + check_schema "../contracts/v10/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -189,8 +203,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 9, 8, 7, 6, \ - 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 10, 9, 8, 7, \ + 6, 5, 4, 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -327,8 +341,20 @@ let dense_schedule_document slice_count = List.init slice_count (fun offset -> let index = offset + 1 in let time_offset = offset * 4 in - T.Market_slice.create ~slice_sequence:(Int64.of_int index) - ~start_at:(add_seconds base time_offset) + let start_at = add_seconds base time_offset in + let borrow_observation = + T.Financing.borrow_observation + ~instrument_id:(instrument_id "demo-equity-acme") + ~effective_at:start_at ~available_quantity:(quantity "1000") + ~annual_rate_bps:100 ~recalled:false + |> ok + in + let cash_rate_observation = + T.Financing.cash_rate_observation ~currency:"USD" + ~effective_at:start_at ~credit_rate_bps:100 ~debit_rate_bps:200 + |> ok + in + T.Market_slice.create_v10 ~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)) @@ -339,8 +365,9 @@ let dense_schedule_document slice_count = (Int64.of_int index); ] ~fx_rates:[ fx_mark () ] - ~corporate_actions:[] - |> ok |> T.Codec.market_slice_to_yojson) + ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] + ~cash_rate_observations:[ cash_rate_observation ] + |> ok |> T.Codec.market_slice_to_yojson_v10) in let schedule = List.init slice_count (fun offset -> @@ -822,24 +849,24 @@ let audit_ids_are_deterministic_and_causal () = in Alcotest.(check (list string)) "external slice has no engine cause" [] - (cause_strings (event 9L)); + (cause_strings (event 10L)); Alcotest.(check (list string)) "target order cites slice and target request" - [ "demo-event-000000000004"; "demo-event-000000000005" ] - (cause_strings (event 7L)); + [ "demo-event-000000000004"; "demo-event-000000000006" ] + (cause_strings (event 8L)); Alcotest.(check (list string)) "fill cites order creation and executable slice" - [ "demo-event-000000000007"; "demo-event-000000000009" ] - (cause_strings (event 10L)); + [ "demo-event-000000000008"; "demo-event-000000000010" ] + (cause_strings (event 12L)); Alcotest.(check (list string)) "completion cites terminal valuation" - [ "demo-event-000000000021" ] - (cause_strings (event 22L)); - match (event 7L).event with + [ "demo-event-000000000025" ] + (cause_strings (event 26L)); + match (event 8L).event with | T.Audit.Order_accepted order -> Alcotest.(check string) "order snapshot retains creation event" - (T.Id.Event.to_string (event 7L).event_id) + (T.Id.Event.to_string (event 8L).event_id) (T.Id.Event.to_string order.created_event_id) | _ -> Alcotest.fail "expected accepted order" @@ -872,7 +899,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v9/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v10/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -900,7 +927,8 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v9/fixtures/fill-clipped.scenario.json" In_channel.input_all + "../contracts/v10/fixtures/fill-clipped.scenario.json" + In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in let result = @@ -913,7 +941,8 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v9/fixtures/fill-clipped.journal.jsonl" In_channel.input_all + "../contracts/v10/fixtures/fill-clipped.journal.jsonl" + In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual @@ -1012,8 +1041,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) "twenty-two audits" 22L result.audit_count; - Alcotest.check money_testable "same equity" (money "10111.661495") + Alcotest.(check int64) "twenty-six audits" 26L result.audit_count; + Alcotest.check money_testable "same equity" (money "10111.946958") result.valuation.equity; Alcotest.(check string) "stream and batch journals agree" expected @@ -1079,8 +1108,25 @@ let stream_with_second_slice_start start_at = map_field "payload" (change_field "intents" (`List [])) record else if index = 2 then map_field "payload" - (map_field "market_slice" - (change_field "start_at" (`String start_at))) + (map_field "market_slice" (fun market_slice -> + market_slice + |> change_field "start_at" (`String start_at) + |> map_field "borrow_observations" (function + | `List [ observation ] -> + `List + [ + change_field "effective_at" (`String start_at) + observation; + ] + | value -> value) + |> map_field "cash_rate_observations" (function + | `List [ observation ] -> + `List + [ + change_field "effective_at" (`String start_at) + observation; + ] + | value -> value))) record else record in diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index a6eeea2..f1ceddd 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -32,6 +32,7 @@ let initialization () = T.Execution.create_v2 ~participation_bps:10_000 ~fee_schedules:[ fee_schedule ] |> ok; + financing = Some T.Financing.legacy_policy; } let field name = function @@ -43,7 +44,7 @@ let initialize_message_is_complete () = T.Strategy_protocol.initialize_message ~sequence:1L (initialization ()) in Alcotest.(check string) - "protocol version" "7" + "protocol version" "8" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -220,7 +221,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "7"); + ("strategy_protocol_version", `String "8"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); From 7a8a70a1a23388160b151013ae651a9993b07064 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 14:47:16 -0400 Subject: [PATCH 45/57] feat: model deterministic trade settlement --- CHANGELOG.md | 5 + README.md | 24 +- contracts/conformance/cases.json | 106 + contracts/conformance/manifest.json | 49 + contracts/strategy/v9/README.md | 58 + contracts/strategy/v9/dune | 15 + .../v9/fixtures/external.scenario.json | 302 +++ .../v9/fixtures/external.scenario.jsonl | 4 + .../v9/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v9/message.schema.json | 302 +++ contracts/strategy/v9/transcript.schema.json | 82 + contracts/v11/README.md | 73 + contracts/v11/dune | 18 + contracts/v11/fixtures/demo.journal.jsonl | 31 + contracts/v11/fixtures/demo.scenario.json | 444 ++++ contracts/v11/fixtures/demo.scenario.jsonl | 6 + .../v11/fixtures/fill-clipped.journal.jsonl | 13 + .../v11/fixtures/fill-clipped.scenario.json | 267 ++ contracts/v11/journal.schema.json | 2314 +++++++++++++++++ contracts/v11/scenario-stream.schema.json | 78 + contracts/v11/scenario.schema.json | 552 ++++ docs/api-reference.md | 2 +- docs/continuous-integration.md | 2 +- docs/execution-model.md | 4 +- docs/persistra.md | 6 +- docs/scenario.md | 24 +- lib/account.ml | 128 +- lib/account.mli | 12 + lib/audit.ml | 6 + lib/audit.mli | 3 + lib/codec.ml | 126 +- lib/codec.mli | 1 + lib/contract.ml | 21 +- lib/engine.ml | 194 +- lib/engine.mli | 11 + lib/execution_model.ml | 3 +- lib/external_replay.ml | 25 +- lib/market_slice.ml | 31 +- lib/market_slice.mli | 15 + lib/replay.ml | 15 +- lib/risk.ml | 2 + lib/risk.mli | 2 + lib/scenario.ml | 155 +- lib/scenario.mli | 2 + lib/scenario_shape.ml | 30 +- lib/scenario_shape.mli | 1 + lib/scenario_validation.ml | 10 +- lib/settlement.ml | 215 ++ lib/settlement.mli | 77 + lib/strategy.ml | 4 + lib/strategy.mli | 2 + lib/strategy_protocol.ml | 102 +- lib/strategy_protocol.mli | 1 + mkdocs.yml | 4 +- scripts/check-deterministic-journals | 8 + scripts/check-documentation.py | 4 +- scripts/release_artifacts.py | 12 +- test/cli.t | 2 +- test/dune | 73 + test/test_boundary_failures.ml | 1 + test/test_diagnostic.ml | 2 +- test/test_engine.ml | 1 + test/test_scenario.ml | 33 +- test/test_settlement.ml | 377 +++ test/test_strategy_protocol.ml | 5 +- 65 files changed, 6362 insertions(+), 149 deletions(-) create mode 100644 contracts/strategy/v9/README.md create mode 100644 contracts/strategy/v9/dune create mode 100644 contracts/strategy/v9/fixtures/external.scenario.json create mode 100644 contracts/strategy/v9/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v9/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v9/message.schema.json create mode 100644 contracts/strategy/v9/transcript.schema.json create mode 100644 contracts/v11/README.md create mode 100644 contracts/v11/dune create mode 100644 contracts/v11/fixtures/demo.journal.jsonl create mode 100644 contracts/v11/fixtures/demo.scenario.json create mode 100644 contracts/v11/fixtures/demo.scenario.jsonl create mode 100644 contracts/v11/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v11/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v11/journal.schema.json create mode 100644 contracts/v11/scenario-stream.schema.json create mode 100644 contracts/v11/scenario.schema.json create mode 100644 lib/settlement.ml create mode 100644 lib/settlement.mli create mode 100644 test/test_settlement.ml diff --git a/CHANGELOG.md b/CHANGELOG.md index 94ab8fd..dc311aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Add deterministic trade-date and settlement-date accounting, versioned business-date settlement + calendars, settled and unsettled cash and position attribution, explicit settlement buying-power + policies, and auditable settlement completion and failure events. +- Publish scenario/journal contract v11 and external strategy protocol v9 while preserving v10 and + protocol v8 as frozen compatibility contracts. - Add effective-time borrow availability, signed rates, locate clipping or rejection, recalls, deterministic close-outs, and explicit missing-data behavior. - Add effective-time currency credit/debit rates with Actual/365 or Actual/360 day count, simple or diff --git a/README.md b/README.md index fb4d30d..692ca38 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ 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; v10 currently exposes `completed_bar_v1` configuration v2 + capability descriptors; v11 currently exposes `completed_bar_v1` configuration v2 - 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 @@ -90,7 +90,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v10/fixtures/demo.scenario.json \ + --input contracts/v11/fixtures/demo.scenario.json \ --validate-only ``` @@ -98,7 +98,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v10/fixtures/demo.scenario.json \ + --input contracts/v11/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -106,7 +106,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v10/fixtures/demo.scenario.jsonl \ + --input contracts/v11/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -115,7 +115,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v8/fixtures/external.scenario.json \ + --input contracts/strategy/v9/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -226,19 +226,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 v10 and conformance fixtures](contracts/v10/README.md) +- [Current contract v11 and conformance fixtures](contracts/v11/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v10/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v10/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v10/journal.schema.json) -- [External strategy protocol v8](contracts/strategy/v8/README.md) +- [Scenario JSON Schema](contracts/v11/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v11/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v11/journal.schema.json) +- [External strategy protocol v9](contracts/strategy/v9/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/v8/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v8/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v9/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v9/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 c578aed..4e603e2 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -820,6 +820,85 @@ "schema_expectation": "accept", "runtime_expectation": "accept", "rule": "structural" + }, + { + "name": "scenario-v11-valid", + "artifact": "scenario-v11", + "kind": "scenario", + "source": "v11/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-stream-v11-valid", + "artifact": "scenario-stream-v11", + "kind": "scenario_stream", + "source": "v11/fixtures/demo.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-ready-valid-v9", + "artifact": "strategy-message-v9", + "kind": "strategy_response", + "source": "strategy/v9/fixtures/external.strategy.jsonl", + "record": 2, + "extract": ["message"], + "expected_sequence": "1", + "protocol_version": "9", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-intents-valid-v9", + "artifact": "strategy-message-v9", + "kind": "strategy_response", + "source": "strategy/v9/fixtures/external.strategy.jsonl", + "record": 4, + "extract": ["message"], + "expected_sequence": "2", + "protocol_version": "9", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-stopped-valid-v9", + "artifact": "strategy-message-v9", + "kind": "strategy_response", + "source": "strategy/v9/fixtures/external.strategy.jsonl", + "record": 14, + "extract": ["message"], + "expected_sequence": "7", + "protocol_version": "9", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-error-valid-v9", + "artifact": "strategy-message-v9", + "kind": "strategy_response", + "source": "strategy/v9/fixtures/external.strategy.jsonl", + "record": 14, + "extract": ["message"], + "expected_sequence": "7", + "protocol_version": "9", + "mutations": [ + { "op": "replace", "path": ["message_type"], "value": "error" }, + { "op": "replace", "path": ["payload"], "value": { "message": "fixture failure" } } + ], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" } ], "schema_only_cases": [ @@ -1084,6 +1163,33 @@ "mutations": [], "schema_expectation": "accept", "source": "strategy/v8/fixtures/external.strategy.jsonl" + }, + { + "name": "strategy-v9-rejected-response-branch", + "artifact": "strategy-transcript-v9", + "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", + "source": "strategy/v9/fixtures/external.strategy.jsonl" } ] } diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index 8f33b36..59f555a 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -708,6 +708,55 @@ "format": "jsonl" } ] + }, + { + "name": "scenario-v11", + "schema": "v11/scenario.schema.json", + "version_field": "contract_version", + "version": "11", + "sources": [ + { "path": "v11/fixtures/demo.scenario.json", "format": "json" }, + { "path": "v11/fixtures/fill-clipped.scenario.json", "format": "json" }, + { "path": "strategy/v9/fixtures/external.scenario.json", "format": "json" } + ] + }, + { + "name": "scenario-stream-v11", + "schema": "v11/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "11", + "sources": [ + { "path": "v11/fixtures/demo.scenario.jsonl", "format": "jsonl" }, + { "path": "strategy/v9/fixtures/external.scenario.jsonl", "format": "jsonl" } + ] + }, + { + "name": "journal-v11", + "schema": "v11/journal.schema.json", + "version_field": "contract_version", + "version": "11", + "sources": [ + { "path": "v11/fixtures/demo.journal.jsonl", "format": "jsonl" }, + { "path": "v11/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" } + ] + }, + { + "name": "strategy-message-v9", + "schema": "strategy/v9/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "9", + "sources": [ + { "path": "strategy/v9/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"] } + ] + }, + { + "name": "strategy-transcript-v9", + "schema": "strategy/v9/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "9", + "sources": [ + { "path": "strategy/v9/fixtures/external.strategy.jsonl", "format": "jsonl" } + ] } ] } diff --git a/contracts/strategy/v9/README.md b/contracts/strategy/v9/README.md new file mode 100644 index 0000000..c449032 --- /dev/null +++ b/contracts/strategy/v9/README.md @@ -0,0 +1,58 @@ +# External strategy protocol v9 + +Version 9 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 v8 remains available for scenario contract v10; earlier versions retain their frozen +shapes. + +Every message repeats `strategy_protocol_version: "9"` 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 v11 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 v11 intent shapes, including recall-origin order snapshots. + +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/v9/dune b/contracts/strategy/v9/dune new file mode 100644 index 0000000..8354cc5 --- /dev/null +++ b/contracts/strategy/v9/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v9/message.schema.json) + (transcript.schema.json as contracts/strategy/v9/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v9/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v9/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v9/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v9/fixtures/external.scenario.json b/contracts/strategy/v9/fixtures/external.scenario.json new file mode 100644 index 0000000..7d1a4a8 --- /dev/null +++ b/contracts/strategy/v9/fixtures/external.scenario.json @@ -0,0 +1,302 @@ +{ + "contract_version": "11", + "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": [] + }, + { + "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": [] + } + ], + "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/v9/fixtures/external.scenario.jsonl b/contracts/strategy/v9/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..605761a --- /dev/null +++ b/contracts/strategy/v9/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"11","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":"11","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":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"11","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":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"11","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v9/fixtures/external.strategy.jsonl b/contracts/strategy/v9/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..0875b1b --- /dev/null +++ b/contracts/strategy/v9/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"9","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"11","scenario_sha256":"d9b389a906984af1e3170e94863884a938b5c8b86025b1ccc1708e9896520278","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":"9","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"9","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","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":[]}}}}} +{"strategy_protocol_version":"9","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","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":"9","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","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":"9","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"9","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","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":"9","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"9","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","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":"9","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"9","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","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":[]}}}}} +{"strategy_protocol_version":"9","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"9","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"9","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v9/message.schema.json b/contracts/strategy/v9/message.schema.json new file mode 100644 index 0000000..3962824 --- /dev/null +++ b/contracts/strategy/v9/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/v9/message.schema.json", + "title": "Trading Engine external strategy protocol v9 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": "9" }, + "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": "11" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/venueCalendar" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/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/v11/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/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/v11/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/signedDecimal" }, + "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/signedDecimal" }, + "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/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/v11/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/v11/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/v11/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/v9/transcript.schema.json b/contracts/strategy/v9/transcript.schema.json new file mode 100644 index 0000000..8b8fc95 --- /dev/null +++ b/contracts/strategy/v9/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/v9/transcript.schema.json", + "title": "Trading Engine external strategy protocol v9 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": "9" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v9/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/v11/README.md b/contracts/v11/README.md new file mode 100644 index 0000000..cd3f5ae --- /dev/null +++ b/contracts/v11/README.md @@ -0,0 +1,73 @@ +# Trading Engine contract v11 + +This directory is the authoritative v11 process and file contract shared by Trading Engine and its +clients. Versions 10 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 v11 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 v11 scenario, stream record, and journal record carries `"contract_version": "11"`. + +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. diff --git a/contracts/v11/dune b/contracts/v11/dune new file mode 100644 index 0000000..a0608ce --- /dev/null +++ b/contracts/v11/dune @@ -0,0 +1,18 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v11/journal.schema.json) + (scenario-stream.schema.json as contracts/v11/scenario-stream.schema.json) + (scenario.schema.json as contracts/v11/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v11/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v11/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl + as + contracts/v11/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v11/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v11/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v11/fixtures/demo.journal.jsonl b/contracts/v11/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..d8fd545 --- /dev/null +++ b/contracts/v11/fixtures/demo.journal.jsonl @@ -0,0 +1,31 @@ +{"contract_version":"11","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":"eb93b72479e9a6dc49111ba26dce90e04e98538fffc2717095a3f1d98be2a8c1","execution_model":"completed_bar_v1"}} +{"contract_version":"11","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":"11","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":"11","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":[]}} +{"contract_version":"11","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":"11","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":"11","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":"11","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":"11","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":"11","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":"12"}],"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":[]}} +{"contract_version":"11","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":"11","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":"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","price":"103","notional":"618","fee":"0.868","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.618","quote_amount":"0.618"}]}} +{"contract_version":"11","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":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000001-settlement","fill_id":"demo-fill-000000000001","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-618.868","position_movement":"6","trade_date":"2026-01-05","due_date":"2026-01-06","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"11","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","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"11","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.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-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":"11","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":"9381.280402","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5.148402","unrealized_pnl":"40.132","equity":"10130.280402","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.280402","fx_rate":"1","base_value":"9381.280402","interest":"0.148402","base_interest":"0.148402","settled_amount":"10000.148402","unsettled_amount":"-618.868","base_settled_value":"10000.148402","base_unsettled_value":"-618.868"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618","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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"settled_quantity":"1","unsettled_quantity":"6"}],"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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"cash_interest":"0.148402","settled_cash":"10000.148402","unsettled_cash":"-618.868","margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.780402","maintenance_excess":"9943.030402","margin_call":false},"group_exposures":[]}} +{"contract_version":"11","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":[]}} +{"contract_version":"11","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":"settlement_completed","payload":{"instruction_id":"demo-fill-000000000001-settlement","fill_id":"demo-fill-000000000001","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-618.868","position_movement":"6","trade_date":"2026-01-05","due_date":"2026-01-06","status":"settled","settled_at":"2026-01-06T14:30:00.000000Z","failed_at":null,"failure_reason":null}} +{"contract_version":"11","engine_sequence":"19","event_id":"demo-event-000000000019","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":"9381.280402","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.06961","closing_balance":"9381.350012"}} +{"contract_version":"11","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"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.715","price":"107","notional":"290.505","fee":"0.540505","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.290505","quote_amount":"0.290505"}]}} +{"contract_version":"11","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000020"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000002-settlement","fill_id":"demo-fill-000000000002","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-291.045505","position_movement":"2.715","trade_date":"2026-01-06","due_date":"2026-01-07","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"11","engine_sequence":"22","event_id":"demo-event-000000000022","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":"11","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000017","demo-event-000000000022"],"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-000000000023","updated_event_id":"demo-event-000000000023","created_sequence":"23","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":"11","engine_sequence":"24","event_id":"demo-event-000000000024","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.304507","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5.218012","unrealized_pnl":"20.161495","equity":"10110.379507","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.304507","fx_rate":"1","base_value":"9090.304507","interest":"0.218012","base_interest":"0.218012","settled_amount":"9381.350012","unsettled_amount":"-291.045505","base_settled_value":"9381.350012","base_unsettled_value":"-291.045505"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505","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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"settled_quantity":"7","unsettled_quantity":"2.715"}],"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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"cash_interest":"0.218012","settled_cash":"9381.350012","unsettled_cash":"-291.045505","margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.342007","maintenance_excess":"9855.360757","margin_call":false},"group_exposures":[]}} +{"contract_version":"11","engine_sequence":"25","event_id":"demo-event-000000000025","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":[]}} +{"contract_version":"11","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000025"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"settlement_completed","payload":{"instruction_id":"demo-fill-000000000002-settlement","fill_id":"demo-fill-000000000002","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-291.045505","position_movement":"2.715","trade_date":"2026-01-06","due_date":"2026-01-07","status":"settled","settled_at":"2026-01-07T14:30:00.000000Z","failed_at":null,"failure_reason":null}} +{"contract_version":"11","engine_sequence":"27","event_id":"demo-event-000000000027","causation_ids":["demo-event-000000000025"],"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.304507","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.067451","closing_balance":"9090.371958"}} +{"contract_version":"11","engine_sequence":"28","event_id":"demo-event-000000000028","causation_ids":["demo-event-000000000023","demo-event-000000000025"],"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":"105","notional":"757.575","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.757575","quote_amount":"0.757575"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_amount":"-0.007575"}]}} +{"contract_version":"11","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":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000003-settlement","fill_id":"demo-fill-000000000003","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"756.575","position_movement":"-7.215","trade_date":"2026-01-07","due_date":"2026-01-08","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"11","engine_sequence":"30","event_id":"demo-event-000000000030","causation_ids":["demo-event-000000000025"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463","settled_amount":"9090.371958","unsettled_amount":"756.575","base_settled_value":"9090.371958","base_unsettled_value":"756.575"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"settled_quantity":"9.715","unsettled_quantity":"-7.215"}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","settled_cash":"9090.371958","unsettled_cash":"756.575","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]}} +{"contract_version":"11","engine_sequence":"31","event_id":"demo-event-000000000031","causation_ids":["demo-event-000000000030"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"eb93b72479e9a6dc49111ba26dce90e04e98538fffc2717095a3f1d98be2a8c1","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463","settled_amount":"9090.371958","unsettled_amount":"756.575","base_settled_value":"9090.371958","base_unsettled_value":"756.575"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"settled_quantity":"9.715","unsettled_quantity":"-7.215"}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","settled_cash":"9090.371958","unsettled_cash":"756.575","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v11/fixtures/demo.scenario.json b/contracts/v11/fixtures/demo.scenario.json new file mode 100644 index 0000000..0747373 --- /dev/null +++ b/contracts/v11/fixtures/demo.scenario.json @@ -0,0 +1,444 @@ +{ + "contract_version": "11", + "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_v1", + "configuration": { + "version": "2", + "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" + } + ] + } + ] + } + }, + "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": [] + }, + { + "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": "12" + } + ], + "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": [] + }, + { + "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": [] + }, + { + "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": [] + } + ], + "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/v11/fixtures/demo.scenario.jsonl b/contracts/v11/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..bc7d5d0 --- /dev/null +++ b/contracts/v11/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"11","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_v1","configuration":{"version":"2","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"}]}]}},"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":"11","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":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"11","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":"12"}],"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":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"11","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":[]}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"11","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":[]}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"11","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v11/fixtures/fill-clipped.journal.jsonl b/contracts/v11/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..9ad7d70 --- /dev/null +++ b/contracts/v11/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"11","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":"8235142ca2e31e8db6f25372b1079adbb218b8b310bfd7a95672f766f43e6808","execution_model":"completed_bar_v1"}} +{"contract_version":"11","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":"11","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":"11","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":[]}} +{"contract_version":"11","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":"11","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":"11","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":"11","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":[]}} +{"contract_version":"11","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":"11","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":"11","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":"11","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":"11","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":"8235142ca2e31e8db6f25372b1079adbb218b8b310bfd7a95672f766f43e6808","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/v11/fixtures/fill-clipped.scenario.json b/contracts/v11/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..5729f37 --- /dev/null +++ b/contracts/v11/fixtures/fill-clipped.scenario.json @@ -0,0 +1,267 @@ +{ + "contract_version": "11", + "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": [] + }, + { + "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": [] + } + ], + "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/v11/journal.schema.json b/contracts/v11/journal.schema.json new file mode 100644 index 0000000..d0e9c2d --- /dev/null +++ b/contracts/v11/journal.schema.json @@ -0,0 +1,2314 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v11/journal.schema.json", + "title": "Trading Engine v11 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": "11" + }, + "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", + "order_adjusted", + "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": "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": "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": { + "const": "completed_bar_v1" + } + } + }, + "initialState": { + "type": "object", + "additionalProperties": false, + "required": [ + "portfolio", + "valuation" + ], + "properties": { + "portfolio": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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" + } + } + } + ] + }, + "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" + ], + "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/v11/scenario.schema.json#/$defs/borrowObservation" + } + }, + "cash_rate_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/cashRateObservation" + } + }, + "settlement_failures": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/settlementFailure" + } + } + } + }, + "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" + ] + }, + "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" + } + } + }, + "orderAdjusted": { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "action_id" + ], + "properties": { + "order": { + "$ref": "#/$defs/order" + }, + "action_id": { + "$ref": "#/$defs/identifier" + } + } + }, + "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/v11/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/v11/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/v11/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": { + "const": "completed_bar_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/v11/scenario-stream.schema.json b/contracts/v11/scenario-stream.schema.json new file mode 100644 index 0000000..ede8140 --- /dev/null +++ b/contracts/v11/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/v11/scenario-stream.schema.json", + "title": "Trading Engine v11 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": "11" }, + "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": "11" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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": "11" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v11/scenario.schema.json b/contracts/v11/scenario.schema.json new file mode 100644 index 0000000..8198197 --- /dev/null +++ b/contracts/v11/scenario.schema.json @@ -0,0 +1,552 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json", + "title": "Trading Engine v11 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": "11" }, + "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": { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + "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" } } + } + }, + "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"], + "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" } } + } + }, + "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" } + } + } + ] + } + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index 843bbaa..3734265 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/v10/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v11/README.md) remain authoritative for process boundaries. diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 471767b..c179de0 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 v10 batch, stream, journal, and +The full test suite additionally validates and replays the current v11 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 12732c8..b07c337 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,11 +1,11 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v10 advertises and accepts `completed_bar_v1`; embedders can inject another module through +Contract v11 advertises and accepts `completed_bar_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 v10 envelope separates selection from +Each compiled model owns a strict configuration contract. The v11 envelope separates selection from model-specific parameters: ```json diff --git a/docs/persistra.md b/docs/persistra.md index a881dee..4b66b78 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 v10 while retaining v9 through v3 and +structural checks. The engine advertises current contract v11 while retaining v10 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 v8](../contracts/strategy/v8/README.md). Persistra's host turns protocol +[strategy protocol v9](../contracts/strategy/v9/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,7 +79,7 @@ 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 v10 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v11 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, diff --git a/docs/scenario.md b/docs/scenario.md index 48c774c..613f761 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 v10 demo](../contracts/v10/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v10/scenario.schema.json) provides structural validation. +Use [the v11 demo](../contracts/v11/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v11/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/v10/scenario-stream.schema.json) validates each line, -and [the v10 stream fixture](../contracts/v10/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v11/scenario-stream.schema.json) validates each line, +and [the v11 stream fixture](../contracts/v11/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; v10 is `"10"` | +| `contract_version` | Required string identifying this file contract; v11 is `"11"` | | `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 | @@ -52,6 +52,7 @@ retains current account, order, target, and latest-bar state required by executi | `risk` | Signed position, exposure, leverage, margin, and borrow policy | | `execution` | Capacity and fee configuration | | `financing` | Borrow/cash day-count, compounding, locate, recall, and missing-data policies | +| `settlement` | Business-date calendars, per-instrument lags, and cash/position availability policies | | `max_internal_events` | Positive reducer feedback cap, at most 100,000 | | `schedule` | Intents emitted after named slices, at most 4,096 per batch | | `slices` | Complete synchronized market observations | @@ -201,7 +202,8 @@ Audit timestamps use the same boundary. ], "cash_rate_observations": [ { "currency": "USD", "effective_at": "2026-01-02T14:30:00Z", "credit_rate_bps": 100, "debit_rate_bps": 200 } - ] + ], + "settlement_failures": [] } ``` @@ -220,6 +222,14 @@ 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 v11 `settlement` object selects `total_cash` or `settled_cash` buying power and +`total_positions` or `settled_positions` availability. Its immutable calendars contain ordered +canonical business dates, and each instrument has exactly one calendar and a lag from zero through +30 business days. A fill updates economic accounting immediately and creates a deterministic +instruction. Pending cash and quantity appear as unsettled attribution until the first slice on or +after the due date. A due instruction named in that slice's `settlement_failures` becomes failed +instead, retains its unsettled balances, and records the supplied reason. + Supported corporate actions are exact-ratio `split` and per-unit `cash_dividend` records. Action IDs are unique across the scenario. Actions are applied in canonical ID order before borrow fees and matching. A split rescales the position, persistent target, and active orders while preserving @@ -234,7 +244,7 @@ than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v10/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v11/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/account.ml b/lib/account.ml index b52cff4..2d4b2af 100644 --- a/lib/account.ml +++ b/lib/account.ml @@ -31,8 +31,12 @@ type execution_fee_component_attribution = { type cash_attribution = { currency : string; amount : Scalar.Money.t; + settled_amount : Scalar.Money.t; + unsettled_amount : Scalar.Money.t; fx_rate : Scalar.Price.t; base_value : Scalar.Money.t; + base_settled_value : Scalar.Money.t; + base_unsettled_value : Scalar.Money.t; interest : Scalar.Money.t; base_interest : Scalar.Money.t; } @@ -41,6 +45,8 @@ type position_attribution = { instrument_id : Id.Instrument.t; quote_currency : string; quantity : Scalar.Quantity.t; + settled_quantity : Scalar.Quantity.t; + unsettled_quantity : Scalar.Quantity.t; mark : Scalar.Price.t; fx_rate : Scalar.Price.t; market_value : Scalar.Money.t; @@ -66,13 +72,17 @@ type t = { base_currency : string; initial_cash : Scalar.Money.t Currency_map.t; cash : Scalar.Money.t Currency_map.t; + settled_cash : Scalar.Money.t Currency_map.t; cash_interest : Scalar.Money.t Currency_map.t; positions : position Id.Instrument.Map.t; + settled_positions : Scalar.Quantity.t Id.Instrument.Map.t; } type valuation = { base_currency : string; cash : Scalar.Money.t; + settled_cash : Scalar.Money.t; + unsettled_cash : Scalar.Money.t; net_market_value : Scalar.Money.t; long_market_value : Scalar.Money.t; short_market_value : Scalar.Money.t; @@ -138,8 +148,10 @@ let create ~base_currency ~initial_cash = base_currency; initial_cash = balances; cash = balances; + settled_cash = balances; cash_interest = Currency_map.map (fun _ -> Scalar.Money.zero) balances; positions = Id.Instrument.Map.empty; + settled_positions = Id.Instrument.Map.empty; } let of_initial_portfolio (initial : Initial_portfolio.t) = @@ -170,8 +182,13 @@ let of_initial_portfolio (initial : Initial_portfolio.t) = base_currency = initial.base_currency; initial_cash = cash; cash; + settled_cash = cash; cash_interest = Currency_map.map (fun _ -> Scalar.Money.zero) cash; positions; + settled_positions = + Id.Instrument.Map.map + (fun (value : position) -> value.quantity) + positions; } let base_currency (state : t) = state.base_currency @@ -179,6 +196,9 @@ let initial_cash (state : t) = Currency_map.bindings state.initial_cash let cash_balances (state : t) = Currency_map.bindings state.cash let cash (state : t) currency = Currency_map.find_opt currency state.cash +let settled_cash (state : t) currency = + Currency_map.find_opt currency state.settled_cash + let position (state : t) instrument_id = Option.value (Id.Instrument.Map.find_opt instrument_id state.positions) @@ -187,6 +207,11 @@ let position (state : t) instrument_id = let position_quantity (state : t) instrument_id = (position state instrument_id).quantity +let settled_position_quantity (state : t) instrument_id = + Option.value + (Id.Instrument.Map.find_opt instrument_id state.settled_positions) + ~default:Scalar.Quantity.zero + let positions (state : t) = Id.Instrument.Map.bindings state.positions let total_fees (position : position) = @@ -212,6 +237,27 @@ let adjust_cash (state : t) currency delta = let* amount = Scalar.Money.add current delta in Ok { state with cash = Currency_map.add currency amount state.cash } +let adjust_settled_cash (state : t) currency delta = + match Currency_map.find_opt currency state.settled_cash with + | None -> Error ("missing settled cash ledger for currency " ^ currency) + | Some current -> + let* amount = Scalar.Money.add current delta in + Ok + { + state with + settled_cash = Currency_map.add currency amount state.settled_cash; + } + +let adjust_settled_position (state : t) instrument_id delta = + let current = settled_position_quantity state instrument_id in + let* quantity = Scalar.Quantity.add current delta in + let settled_positions = + if Scalar.Quantity.is_zero quantity then + Id.Instrument.Map.remove instrument_id state.settled_positions + else Id.Instrument.Map.add instrument_id quantity state.settled_positions + in + Ok { state with settled_positions } + let add_fee_component (components : execution_fee_component list) (component : Fee_schedule.calculated_component) = let rec add prefix = function @@ -346,7 +392,7 @@ let apply_close_short (state : t) fill (current : position) projected = positions = update_position state.positions fill.instrument_id updated; } -let apply_fill (state : t) fill = +let apply_unsettled_fill (state : t) fill = let current = position state fill.Fill.instrument_id in match fill.side with | Order.Buy -> @@ -361,6 +407,30 @@ let apply_fill (state : t) fill = apply_close_long state fill current projected else apply_open_short state fill current projected +let settlement_movements (fill : Fill.t) = + match fill.side with + | Order.Buy -> + let* debit = Scalar.Money.add fill.notional fill.fee in + let* cash = Scalar.Money.negate debit in + Ok (cash, fill.quantity) + | Order.Sell -> + let* cash = Scalar.Money.subtract fill.notional fill.fee in + let* position = Scalar.Quantity.negate fill.quantity in + Ok (cash, position) + +let apply_settlement (state : t) (instruction : Settlement.instruction) = + let* state = + adjust_settled_cash state instruction.currency instruction.cash_movement + in + adjust_settled_position state instruction.instrument_id + instruction.position_movement + +let apply_fill (state : t) fill = + let* state = apply_unsettled_fill state fill in + let* cash_movement, position_movement = settlement_movements fill in + let* state = adjust_settled_cash state fill.quote_currency cash_movement in + adjust_settled_position state fill.instrument_id position_movement + let apply_split (state : t) ~instrument_id ~numerator ~denominator = let current = position state instrument_id in if Scalar.Quantity.is_zero current.quantity then Ok state @@ -371,7 +441,16 @@ let apply_split (state : t) ~instrument_id ~numerator ~denominator = let positions = update_position state.positions instrument_id { current with quantity } in - Ok { state with positions } + let settled = settled_position_quantity state instrument_id in + let* settled = + Scalar.Quantity.scale_ratio_exact settled ~numerator ~denominator + in + let settled_positions = + if Scalar.Quantity.is_zero settled then + Id.Instrument.Map.remove instrument_id state.settled_positions + else Id.Instrument.Map.add instrument_id settled state.settled_positions + in + Ok { state with positions; settled_positions } let apply_cash_dividend (state : t) ~instrument_id ~quote_currency ~amount_per_unit = @@ -380,6 +459,7 @@ let apply_cash_dividend (state : t) ~instrument_id ~quote_currency else let* amount = Scalar.Money.for_quantity amount_per_unit current.quantity in let* state = adjust_cash state quote_currency amount in + let* state = adjust_settled_cash state quote_currency amount in let* realized_pnl = Scalar.Money.add current.realized_pnl amount in let* dividend_pnl = Scalar.Money.add current.dividend_pnl amount in let updated = { current with realized_pnl; dividend_pnl } in @@ -398,6 +478,7 @@ let apply_borrow_fee (state : t) ~instrument_id ~quote_currency ~fee = else let* cash_delta = Scalar.Money.negate fee in let* state = adjust_cash state quote_currency cash_delta in + let* state = adjust_settled_cash state quote_currency cash_delta in let* realized_pnl = Scalar.Money.add current.realized_pnl cash_delta in let* borrow_fees = Scalar.Money.add current.borrow_fees fee in let updated = { current with realized_pnl; borrow_fees } in @@ -411,6 +492,7 @@ let apply_cash_interest (state : t) ~currency ~interest = if Scalar.Money.equal interest Scalar.Money.zero then Ok state else let* state = adjust_cash state currency interest in + let* state = adjust_settled_cash state currency interest in let current = Option.value (Currency_map.find_opt currency state.cash_interest) @@ -452,13 +534,37 @@ let value (state : t) ~instruments ~marks ~fx_rates = let cash_attribution (currency, amount) = let* fx_rate = fx currency in let* base_value = Scalar.Money.convert amount ~rate:fx_rate in + let settled_amount = + Option.value + (Currency_map.find_opt currency state.settled_cash) + ~default:Scalar.Money.zero + in + let* unsettled_amount = Scalar.Money.subtract amount settled_amount in + let* base_settled_value = + Scalar.Money.convert settled_amount ~rate:fx_rate + in + let* base_unsettled_value = + Scalar.Money.convert unsettled_amount ~rate:fx_rate + in let interest = Option.value (Currency_map.find_opt currency state.cash_interest) ~default:Scalar.Money.zero in let* base_interest = Scalar.Money.convert interest ~rate:fx_rate in - Ok { currency; amount; fx_rate; base_value; interest; base_interest } + Ok + { + currency; + amount; + settled_amount; + unsettled_amount; + fx_rate; + base_value; + base_settled_value; + base_unsettled_value; + interest; + base_interest; + } in let* cash_balances = Currency_map.bindings state.cash @@ -473,6 +579,10 @@ let value (state : t) ~instruments ~marks ~fx_rates = let position_attribution instrument = let instrument_id = instrument.Instrument.id in let current = position state instrument_id in + let settled_quantity = settled_position_quantity state instrument_id in + let* unsettled_quantity = + Scalar.Quantity.subtract current.quantity settled_quantity + in let* mark = match Id.Instrument.Map.find_opt instrument_id mark_map with | Some value -> Ok value @@ -525,6 +635,8 @@ let value (state : t) ~instruments ~marks ~fx_rates = instrument_id; quote_currency = instrument.quote_currency; quantity = current.quantity; + settled_quantity; + unsettled_quantity; mark; fx_rate; market_value; @@ -612,6 +724,14 @@ let value (state : t) ~instruments ~marks ~fx_rates = add total item.base_value) (Ok Scalar.Money.zero) cash_balances in + let* settled_cash = + List.fold_left + (fun result item -> + let* total = result in + add total item.base_settled_value) + (Ok Scalar.Money.zero) cash_balances + in + let* unsettled_cash = Scalar.Money.subtract cash settled_cash in let* cash_interest = List.fold_left (fun result item -> @@ -721,6 +841,8 @@ let value (state : t) ~instruments ~marks ~fx_rates = { base_currency = state.base_currency; cash; + settled_cash; + unsettled_cash; net_market_value; long_market_value; short_market_value; diff --git a/lib/account.mli b/lib/account.mli index ffe2a19..bfbcc8f 100644 --- a/lib/account.mli +++ b/lib/account.mli @@ -32,8 +32,12 @@ type position = private { type cash_attribution = private { currency : string; amount : Scalar.Money.t; + settled_amount : Scalar.Money.t; + unsettled_amount : Scalar.Money.t; fx_rate : Scalar.Price.t; base_value : Scalar.Money.t; + base_settled_value : Scalar.Money.t; + base_unsettled_value : Scalar.Money.t; interest : Scalar.Money.t; base_interest : Scalar.Money.t; } @@ -42,6 +46,8 @@ type position_attribution = private { instrument_id : Id.Instrument.t; quote_currency : string; quantity : Scalar.Quantity.t; + settled_quantity : Scalar.Quantity.t; + unsettled_quantity : Scalar.Quantity.t; mark : Scalar.Price.t; fx_rate : Scalar.Price.t; market_value : Scalar.Money.t; @@ -68,6 +74,8 @@ type t type valuation = private { base_currency : string; cash : Scalar.Money.t; + settled_cash : Scalar.Money.t; + unsettled_cash : Scalar.Money.t; net_market_value : Scalar.Money.t; long_market_value : Scalar.Money.t; short_market_value : Scalar.Money.t; @@ -96,10 +104,14 @@ val base_currency : t -> string val initial_cash : t -> (string * Scalar.Money.t) list val cash_balances : t -> (string * Scalar.Money.t) list val cash : t -> string -> Scalar.Money.t option +val settled_cash : t -> string -> Scalar.Money.t option val position : t -> Id.Instrument.t -> position val position_quantity : t -> Id.Instrument.t -> Scalar.Quantity.t +val settled_position_quantity : t -> Id.Instrument.t -> Scalar.Quantity.t val positions : t -> (Id.Instrument.t * position) list val apply_fill : t -> Fill.t -> (t, string) result +val apply_unsettled_fill : t -> Fill.t -> (t, string) result +val apply_settlement : t -> Settlement.instruction -> (t, string) result val apply_split : t -> diff --git a/lib/audit.ml b/lib/audit.ml index 887eaf6..ddcfe9c 100644 --- a/lib/audit.ml +++ b/lib/audit.ml @@ -52,6 +52,9 @@ type event = } | Order_adjusted of { order : Order.t; action_id : Id.Corporate_action.t } | Fill_applied of Fill.t + | Settlement_instruction_created of Settlement.instruction + | Settlement_completed of Settlement.instruction + | Settlement_failed of Settlement.instruction | Margin_limited of { order_id : Id.Order.t; instrument_id : Id.Instrument.t; @@ -170,6 +173,9 @@ let event_name = function | Cash_dividend_applied _ -> "cash_dividend_applied" | Order_adjusted _ -> "order_adjusted" | Fill_applied _ -> "fill_applied" + | Settlement_instruction_created _ -> "settlement_instruction_created" + | Settlement_completed _ -> "settlement_completed" + | Settlement_failed _ -> "settlement_failed" | Margin_limited _ -> "margin_limited" | Fill_clipped _ -> "fill_clipped" | Borrow_fee_applied _ -> "borrow_fee_applied" diff --git a/lib/audit.mli b/lib/audit.mli index 0d715b7..c016e4e 100644 --- a/lib/audit.mli +++ b/lib/audit.mli @@ -54,6 +54,9 @@ type event = } | Order_adjusted of { order : Order.t; action_id : Id.Corporate_action.t } | Fill_applied of Fill.t + | Settlement_instruction_created of Settlement.instruction + | Settlement_completed of Settlement.instruction + | Settlement_failed of Settlement.instruction | Margin_limited of { order_id : Id.Order.t; instrument_id : Id.Instrument.t; diff --git a/lib/codec.ml b/lib/codec.ml index 0ac4e94..6d48fca 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -141,6 +141,22 @@ let fill_limit_to_yojson = function ("unit", string "quantity"); ("value", quantity value); ] ) + | Risk.Settlement_cash_buying_power (currency, value) -> + ( "settlement_cash_buying_power", + `Assoc + [ + ("currency", string currency); + ("unit", string "money"); + ("value", money value); + ] ) + | Risk.Settlement_position_availability (id, value) -> + ( "settlement_position_availability", + `Assoc + [ + ("instrument_id", instrument_id id); + ("unit", string "quantity"); + ("value", quantity value); + ] ) | Risk.Instrument_initial_margin (id, value) -> ( "instrument_initial_margin", `Assoc @@ -245,6 +261,13 @@ let cash_rate_observation_to_yojson observation = ("debit_rate_bps", `Int observation.debit_rate_bps); ] +let settlement_failure_to_yojson failure = + `Assoc + [ + ("instruction_id", string failure.Settlement.instruction_id); + ("reason", string failure.reason); + ] + let versioned_market_slice_to_yojson ~contract_version market_slice = `Assoc [ @@ -261,7 +284,17 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ); ] |> function - | `Assoc fields when String.equal contract_version "10" -> + | `Assoc fields when List.mem contract_version [ "11"; "10" ] -> + let settlement = + if String.equal contract_version "11" then + [ + ( "settlement_failures", + `List + (List.map settlement_failure_to_yojson + market_slice.Market_slice.settlement_failures) ); + ] + else [] + in `Assoc (fields @ [ @@ -273,7 +306,8 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = `List (List.map cash_rate_observation_to_yojson market_slice.Market_slice.cash_rate_observations) ); - ]) + ] + @ settlement) | json -> json let market_slice_to_yojson market_slice = @@ -282,6 +316,9 @@ let market_slice_to_yojson market_slice = let market_slice_to_yojson_v10 market_slice = versioned_market_slice_to_yojson ~contract_version:"10" market_slice +let market_slice_to_yojson_v11 market_slice = + versioned_market_slice_to_yojson ~contract_version:"11" market_slice + let request_fields request = let kind, limit_price = match request.Order.kind with @@ -385,7 +422,8 @@ let order_to_yojson_v8 order = ]) let versioned_order_to_yojson ~contract_version order = - if List.mem contract_version [ "10"; "9"; "8" ] then order_to_yojson_v8 order + if List.mem contract_version [ "11"; "10"; "9"; "8" ] then + order_to_yojson_v8 order else order_to_yojson order let fill_to_yojson fill = @@ -518,6 +556,17 @@ let position_attribution_to_yojson_v9 position = ]) | _ -> assert false +let position_attribution_to_yojson_v11 position = + match position_attribution_to_yojson_v9 position with + | `Assoc fields -> + `Assoc + (fields + @ [ + ("settled_quantity", quantity position.Account.settled_quantity); + ("unsettled_quantity", quantity position.unsettled_quantity); + ]) + | _ -> assert false + let cash_attribution_to_yojson cash = `Assoc [ @@ -538,6 +587,19 @@ let cash_attribution_to_yojson_v10 cash = ]) | _ -> assert false +let cash_attribution_to_yojson_v11 cash = + match cash_attribution_to_yojson_v10 cash with + | `Assoc fields -> + `Assoc + (fields + @ [ + ("settled_amount", money cash.Account.settled_amount); + ("unsettled_amount", money cash.unsettled_amount); + ("base_settled_value", money cash.base_settled_value); + ("base_unsettled_value", money cash.base_unsettled_value); + ]) + | _ -> assert false + let account_valuation_to_yojson ?(contract_version = "8") valuation = `Assoc [ @@ -558,14 +620,18 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "cash_balances", `List (List.map - (if String.equal contract_version "10" then + (if String.equal contract_version "11" then + cash_attribution_to_yojson_v11 + else if String.equal contract_version "10" then cash_attribution_to_yojson_v10 else cash_attribution_to_yojson) valuation.cash_balances) ); ( "positions", `List (List.map - (if + (if String.equal contract_version "11" then + position_attribution_to_yojson_v11 + else if String.equal contract_version "9" || String.equal contract_version "10" then position_attribution_to_yojson_v9 @@ -573,14 +639,20 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = valuation.positions) ); ] |> function - | `Assoc fields - when String.equal contract_version "9" || String.equal contract_version "10" - -> + | `Assoc fields when List.mem contract_version [ "11"; "10"; "9" ] -> let financing = - if String.equal contract_version "10" then + if List.mem contract_version [ "11"; "10" ] then [ ("cash_interest", money valuation.Account.cash_interest) ] else [] in + let settlement = + if String.equal contract_version "11" then + [ + ("settled_cash", money valuation.Account.settled_cash); + ("unsettled_cash", money valuation.unsettled_cash); + ] + else [] + in `Assoc (fields @ [ @@ -589,7 +661,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = (List.map execution_fee_component_attribution_to_yojson valuation.Account.execution_fee_components) ); ] - @ financing) + @ financing @ settlement) | json -> json let margin_to_yojson margin = @@ -621,7 +693,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 [ "10"; "9"; "8" ] then + if List.mem contract_version [ "11"; "10"; "9"; "8" ] then fields @ [ ( "group_exposures", @@ -644,6 +716,30 @@ let order_counts_to_yojson counts = ("cancelled", `Int counts.cancelled); ] +let settlement_instruction_to_yojson instruction = + let settled_at, failed_at, failure_reason = + match instruction.Settlement.status with + | Settlement.Pending -> (`Null, `Null, `Null) + | Settlement.Settled value -> (timestamp value, `Null, `Null) + | Settlement.Failed { failed_at; reason } -> + (`Null, timestamp failed_at, string reason) + in + `Assoc + [ + ("instruction_id", string instruction.instruction_id); + ("fill_id", string (Id.Fill.to_string instruction.fill_id)); + ("instrument_id", instrument_id instruction.instrument_id); + ("currency", string instruction.currency); + ("cash_movement", money instruction.cash_movement); + ("position_movement", quantity instruction.position_movement); + ("trade_date", string instruction.trade_date); + ("due_date", string instruction.due_date); + ("status", string (Settlement.status_to_string instruction.status)); + ("settled_at", settled_at); + ("failed_at", failed_at); + ("failure_reason", failure_reason); + ] + let requested_target_to_yojson target = `Assoc [ @@ -706,9 +802,13 @@ let payload_to_yojson ~contract_version = function ("action_id", string (Id.Corporate_action.to_string action_id)); ] | Audit.Fill_applied fill -> - if String.equal contract_version "9" || String.equal contract_version "10" - then fill_to_yojson_v9 fill + if List.mem contract_version [ "11"; "10"; "9" ] then + fill_to_yojson_v9 fill else fill_to_yojson fill + | Audit.Settlement_instruction_created instruction + | Audit.Settlement_completed instruction + | Audit.Settlement_failed instruction -> + settlement_instruction_to_yojson instruction | Audit.Margin_limited { order_id = id; diff --git a/lib/codec.mli b/lib/codec.mli index 30df6ba..0c31a15 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -5,6 +5,7 @@ val ptime_of_string : string -> (Ptime.t, string) result val bar_to_yojson : Bar.t -> Yojson.Safe.t 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 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 a60404f..ff2231d 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,13 +1,23 @@ -let version = "10" -let previous_version = "9" +let version = "11" +let previous_version = "10" let legacy_journal_version = "3" let supported_versions = - [ version; previous_version; "8"; "7"; "6"; "5"; "4"; legacy_journal_version ] + [ + version; + previous_version; + "9"; + "8"; + "7"; + "6"; + "5"; + "4"; + legacy_journal_version; + ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "8" -let previous_strategy_protocol_version = "7" +let strategy_protocol_version = "9" +let previous_strategy_protocol_version = "8" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -26,6 +36,7 @@ let capabilities_to_yojson () = [ strategy_protocol_version; previous_strategy_protocol_version; + "7"; "6"; "5"; "4"; diff --git a/lib/engine.ml b/lib/engine.ml index 00f88e7..2eb590e 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -7,11 +7,12 @@ type config = { execution_model : Execution_model.t; execution : Execution.t; financing : Financing.policy option; + settlement : Settlement.policy option; max_internal_events : int; } let make_config ~venue_calendars ~contract_version ~risk ~execution_model - ~execution ~financing ~max_internal_events = + ~execution ~financing ~settlement ~max_internal_events = if not (Contract.is_supported contract_version) then Error "engine contract version is unsupported" else if max_internal_events <= 0 then @@ -29,23 +30,30 @@ let make_config ~venue_calendars ~contract_version ~risk ~execution_model execution_model; execution; financing; + settlement; max_internal_events; } let config ~contract_version ~risk ~execution_model ~execution ~max_internal_events = make_config ~venue_calendars:[] ~contract_version ~risk ~execution_model - ~execution ~financing:None ~max_internal_events + ~execution ~financing:None ~max_internal_events ~settlement:None let config_v8 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~max_internal_events = make_config ~venue_calendars ~contract_version ~risk ~execution_model - ~execution ~financing:None ~max_internal_events + ~execution ~financing:None ~max_internal_events ~settlement:None let config_v10 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~max_internal_events = make_config ~venue_calendars ~contract_version ~risk ~execution_model - ~execution ~financing:(Some financing) ~max_internal_events + ~execution ~financing:(Some financing) ~max_internal_events ~settlement:None + +let config_v11 ~contract_version ~risk ~venue_calendars ~execution_model + ~execution ~financing ~settlement ~max_internal_events = + make_config ~venue_calendars ~contract_version ~risk ~execution_model + ~execution ~financing:(Some financing) ~settlement:(Some settlement) + ~max_internal_events let valid_sha256 value = String.length value = 64 @@ -77,6 +85,7 @@ module Interactive = struct latest_fx_rates : (string * Scalar.Price.t) list; latest_borrow : Financing.borrow_observation Id.Instrument.Map.t; latest_cash_rates : Financing.cash_rate_observation Currency_map.t; + settlement_instructions : Settlement.instruction list; initial_portfolio : Initial_portfolio.t option; applied_action_ids : Id.Corporate_action.Set.t; desired_targets : desired_targets option; @@ -140,6 +149,7 @@ module Interactive = struct latest_fx_rates; latest_borrow = Id.Instrument.Map.empty; latest_cash_rates = Currency_map.empty; + settlement_instructions = []; initial_portfolio; applied_action_ids = Id.Corporate_action.Set.empty; desired_targets = None; @@ -1010,6 +1020,77 @@ module Interactive = struct in apply_cash_interest reduction market_slice policy + let process_settlements reduction (market_slice : Market_slice.t) = + match reduction.state.config.settlement with + | None -> Ok reduction + | Some _ -> + List.fold_left + (fun result (instruction : Settlement.instruction) -> + let* reduction = result in + match instruction.status with + | Settlement.Settled _ | Settlement.Failed _ -> Ok reduction + | Settlement.Pending -> + if not (Settlement.is_due instruction market_slice.start_at) + then Ok reduction + else + let failure = + List.find_opt + (fun (failure : Settlement.failure) -> + String.equal failure.instruction_id + instruction.instruction_id) + market_slice.Market_slice.settlement_failures + in + let* instruction, account, event = + match failure with + | Some failure -> + let* instruction = + Settlement.fail instruction + ~failed_at:market_slice.start_at + ~reason:failure.reason + in + Ok + ( instruction, + reduction.state.account, + Audit.Settlement_failed instruction ) + | None -> + let* account = + Account.apply_settlement reduction.state.account + instruction + in + let* instruction = + Settlement.settle instruction + ~settled_at:market_slice.start_at + in + Ok + ( instruction, + account, + Audit.Settlement_completed instruction ) + in + let settlement_instructions = + List.map + (fun (current : Settlement.instruction) -> + if + String.equal current.instruction_id + instruction.instruction_id + then instruction + else current) + reduction.state.settlement_instructions + in + emit + (with_causes + { + reduction with + state = + { + reduction.state with + account; + settlement_instructions; + }; + } + (Option.to_list reduction.slice_event_id)) + event) + (Ok reduction) reduction.state.settlement_instructions + let validate_target_ids state ids = let expected = configured_instruments state @@ -1295,6 +1376,20 @@ module Interactive = struct Ptime.compare observation.effective_at previous.effective_at > 0) market_slice.cash_rate_observations in + let settlement_failures_valid = + match state.config.settlement with + | None -> market_slice.settlement_failures = [] + | Some _ -> + List.for_all + (fun (failure : Settlement.failure) -> + List.exists + (fun (instruction : Settlement.instruction) -> + String.equal instruction.instruction_id failure.instruction_id + && instruction.status = Settlement.Pending + && Settlement.is_due instruction market_slice.start_at) + state.settlement_instructions) + market_slice.settlement_failures + in if List.length ids <> List.length actual || actual <> expected then Error "market slice must contain each configured instrument exactly once" else if @@ -1316,6 +1411,8 @@ module Interactive = struct Error "borrow observations must be known and advance effective time" else if not cash_observations_valid then Error "cash rate observations must be known and advance effective time" + else if not settlement_failures_valid then + Error "settlement failures must reference due pending instructions" else match state.last_slice_sequence with | Some sequence @@ -1379,7 +1476,11 @@ module Interactive = struct ~executed_at:proposed.executed_at ~slice_sequence:market_slice.Market_slice.slice_sequence in - let* account = Account.apply_fill state.account fill in + let* account = + match state.config.settlement with + | None -> Account.apply_fill state.account fill + | Some _ -> Account.apply_unsettled_fill state.account fill + in let after_position = Account.position_quantity account instrument.id in let* after = Account.value account ~instruments ~marks @@ -1391,6 +1492,59 @@ module Interactive = struct | Error message -> Error (`Invalid message) | Ok (fee_components, fee, account, after_position, after) -> ( let checked = + let* () = + match (state.config.settlement, order.Order.request.side) with + | Some settlement, Order.Buy -> ( + let available = + match settlement.Settlement.cash_buying_power with + | Settlement.Total_cash -> + Account.cash state.account instrument.quote_currency + | Settlement.Settled_cash -> + Account.settled_cash state.account + instrument.quote_currency + in + let available = + Option.value available ~default:Scalar.Money.zero + in + let available = + if Scalar.Money.compare available Scalar.Money.zero > 0 then + available + else Scalar.Money.zero + in + match + let* notional = + Scalar.Money.notional proposed.Execution.price quantity + in + Scalar.Money.add notional fee + with + | Error message -> Error (Risk.Invalid message) + | Ok cost -> + if Scalar.Money.compare cost available > 0 then + Error + (Risk.Limit + (Risk.Settlement_cash_buying_power + (instrument.quote_currency, available))) + else Ok ()) + | Some settlement, Order.Sell + when settlement.position_availability + = Settlement.Settled_positions + && Scalar.Quantity.is_positive before_position -> + let available = + Account.settled_position_quantity state.account + instrument.id + in + let available = + if Scalar.Quantity.is_positive available then available + else Scalar.Quantity.zero + in + if Scalar.Quantity.compare quantity available > 0 then + Error + (Risk.Limit + (Risk.Settlement_position_availability + (instrument.id, available))) + else Ok () + | None, _ | Some _, _ -> Ok () + in let* () = Risk.check_post_fill_for state.config.risk ~instrument_id:instrument.id ~before_position ~after_position @@ -1531,7 +1685,12 @@ module Interactive = struct Error "newly allocated fill ID was duplicated" | Ok (oms, Oms.Applied order) -> ( match - Account.apply_fill reduction.state.account fill + match reduction.state.config.settlement with + | None -> + Account.apply_fill reduction.state.account fill + | Some _ -> + Account.apply_unsettled_fill + reduction.state.account fill with | Error _ as error -> error | Ok account -> ( @@ -1542,6 +1701,28 @@ module Interactive = struct with | Error _ as error -> error | Ok (reduction, event_id) -> + let* reduction = + match reduction.state.config.settlement with + | None -> Ok reduction + | Some policy -> + let* instruction = + Settlement.instruction policy fill + in + let state = + { + reduction.state with + settlement_instructions = + reduction.state + .settlement_instructions + @ [ instruction ]; + } + in + emit + (with_causes { reduction with state } + [ event_id ]) + (Audit.Settlement_instruction_created + instruction) + in let* fill_pending = notification reduction ~causation_ids:[ event_id ] @@ -1971,6 +2152,7 @@ module Interactive = struct module Actions_phase = struct let run market_slice reduction = let* reduction = cancel_expired_gtd reduction market_slice in + let* reduction = process_settlements reduction market_slice in apply_corporate_actions reduction market_slice.Market_slice.corporate_actions end diff --git a/lib/engine.mli b/lib/engine.mli index 03095f6..3483edd 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -29,6 +29,17 @@ val config_v10 : max_internal_events:int -> (config, string) result +val config_v11 : + 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_model.ml b/lib/execution_model.ml index 44956ca..c4c39d3 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -36,7 +36,8 @@ let completed_bar_v1_contract = { version = "2"; previous_versions = [ "1" ]; - scenario_contract_versions = [ "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; + scenario_contract_versions = + [ "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" ]; diff --git a/lib/external_replay.ml b/lib/external_replay.ml index b225255..d76f87e 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -70,6 +70,7 @@ let initialization_of_scenario ~scenario_sha256 (scenario : Scenario.t) = execution_model = scenario.execution_model; execution = scenario.execution; financing = scenario.financing; + settlement = scenario.settlement; } let initialization_of_header ~scenario_sha256 (header : Scenario.stream_header) @@ -89,19 +90,25 @@ let initialization_of_header ~scenario_sha256 (header : Scenario.stream_header) execution_model = header.execution_model; execution = header.execution; financing = header.financing; + settlement = header.settlement; } let create_runner ~contract_version ~run_id ~scenario_sha256 ~risk - ~venue_calendars ~execution_model ~execution ~financing ~max_internal_events - ~initial_cash ~initial_portfolio = + ~venue_calendars ~execution_model ~execution ~financing ~settlement + ~max_internal_events ~initial_cash ~initial_portfolio = let* config = - (match financing with - | None -> + (match (financing, settlement) with + | None, None -> Engine.config_v8 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~max_internal_events - | Some financing -> + | Some financing, None -> Engine.config_v10 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~financing ~max_internal_events) + ~execution_model ~execution ~financing ~max_internal_events + | Some financing, Some settlement -> + Engine.config_v11 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + | None, Some _ -> Error "settlement requires financing configuration") |> reducer_result in match initial_portfolio with @@ -188,7 +195,7 @@ let run ?(durability = Artifact_writer.Buffered) ~env ~scenario_sha256 ~run_id:scenario.run_id ~scenario_sha256 ~risk:scenario.risk ~venue_calendars:scenario.venue_calendars ~execution_model:scenario.execution_model ~execution:scenario.execution - ~financing:scenario.financing + ~financing:scenario.financing ~settlement:scenario.settlement ~max_internal_events:scenario.max_internal_events ~initial_cash:scenario.initial_cash ~initial_portfolio:scenario.initial_portfolio @@ -245,7 +252,7 @@ let validate_stream_pass ~scenario_sha256 channel = ~run_id:header.Scenario.run_id ~scenario_sha256 ~risk:header.risk ~venue_calendars:header.venue_calendars ~execution_model:header.execution_model ~execution:header.execution - ~financing:header.financing + ~financing:header.financing ~settlement:header.settlement ~max_internal_events:header.max_internal_events ~initial_cash:header.initial_cash ~initial_portfolio:header.initial_portfolio @@ -276,7 +283,7 @@ let replay_stream_pass ~scenario_sha256 ~journal ~session channel = ~run_id:header.Scenario.run_id ~scenario_sha256 ~risk:header.risk ~venue_calendars:header.venue_calendars ~execution_model:header.execution_model ~execution:header.execution - ~financing:header.financing + ~financing:header.financing ~settlement:header.settlement ~max_internal_events:header.max_internal_events ~initial_cash:header.initial_cash ~initial_portfolio:header.initial_portfolio diff --git a/lib/market_slice.ml b/lib/market_slice.ml index b0101e1..36e2d07 100644 --- a/lib/market_slice.ml +++ b/lib/market_slice.ml @@ -11,6 +11,7 @@ type t = { 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; } let valid_currency value = @@ -29,9 +30,9 @@ let fx_mark ~currency ~rate = let compare_bar left right = Id.Instrument.compare left.Bar.instrument_id right.Bar.instrument_id -let create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at +let create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations = + ~cash_rate_observations ~settlement_failures = if Int64.compare slice_sequence 0L <= 0 then Error "market slice sequence must be positive" else if Ptime.compare start_at end_at >= 0 then @@ -98,6 +99,19 @@ let create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at (not (String.equal left.Financing.currency right.Financing.currency)) && unique_cash_rate remaining in + let settlement_failures = + List.sort + (fun (left : Settlement.failure) (right : Settlement.failure) -> + String.compare left.instruction_id right.instruction_id) + settlement_failures + in + let rec unique_failure = function + | [] | [ _ ] -> true + | left :: (right :: _ as remaining) -> + (not + (String.equal left.Settlement.instruction_id right.instruction_id)) + && unique_failure remaining + in if not (unique bars) then Error "market slice must contain one bar per instrument" else if fx_rates = [] then Error "market slice must contain FX rates" @@ -109,6 +123,8 @@ let create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at Error "market slice borrow observation instrument IDs must be unique" else if not (unique_cash_rate cash_rate_observations) then Error "market slice cash rate currencies must be unique" + else if not (unique_failure settlement_failures) then + Error "market slice settlement failure instruction IDs must be unique" else Ok { @@ -122,8 +138,16 @@ let create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at corporate_actions; borrow_observations; cash_rate_observations; + settlement_failures; } +let create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at + ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations = + create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars + ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations + ~settlement_failures:[] + let create ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions = create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars @@ -146,9 +170,10 @@ let compare_replay_order left right = let pp formatter state = Format.fprintf formatter - "slice[%Ld] bars=%d fx=%d actions=%d borrow=%d cash_rates=%d" + "slice[%Ld] bars=%d fx=%d actions=%d borrow=%d cash_rates=%d failures=%d" state.slice_sequence (List.length state.bars) (List.length state.fx_rates) (List.length state.corporate_actions) (List.length state.borrow_observations) (List.length state.cash_rate_observations) + (List.length state.settlement_failures) diff --git a/lib/market_slice.mli b/lib/market_slice.mli index 84535f3..037690a 100644 --- a/lib/market_slice.mli +++ b/lib/market_slice.mli @@ -16,6 +16,7 @@ type t = private { 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; } val create : @@ -42,6 +43,20 @@ val create_v10 : cash_rate_observations:Financing.cash_rate_observation list -> (t, string) result +val create_v11 : + 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 -> + (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 4283bf1..f0c6332 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -69,14 +69,18 @@ let add_audit_count count events = else Ok (Int64.add count added) let engine_config ~contract_version ~risk ~venue_calendars ~execution_model - ~execution ~financing ~max_internal_events = - match financing with - | None -> + ~execution ~financing ~settlement ~max_internal_events = + match (financing, settlement) with + | None, None -> Engine.config_v8 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~max_internal_events - | Some financing -> + | Some financing, None -> Engine.config_v10 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~max_internal_events + | Some financing, Some settlement -> + Engine.config_v11 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement ~max_internal_events + | None, Some _ -> Error "settlement requires financing configuration" let run ~scenario_sha256 ?journal_path ?(durability = Artifact_writer.Buffered) scenario = @@ -87,7 +91,7 @@ let run ~scenario_sha256 ?journal_path ?(durability = Artifact_writer.Buffered) engine_config ~contract_version:scenario.contract_version ~risk:scenario.risk ~venue_calendars:scenario.venue_calendars ~execution_model:scenario.execution_model ~execution:scenario.execution - ~financing:scenario.financing + ~financing:scenario.financing ~settlement:scenario.settlement ~max_internal_events:scenario.max_internal_events |> reducer_result in @@ -168,6 +172,7 @@ let run_stream_pass ~scenario_sha256 ~journal channel = ~risk:header.Scenario.risk ~venue_calendars:header.venue_calendars ~execution_model:header.execution_model ~execution:header.execution ~financing:header.financing + ~settlement:header.settlement ~max_internal_events:header.max_internal_events |> reducer_result with diff --git a/lib/risk.ml b/lib/risk.ml index f2a293c..13378b9 100644 --- a/lib/risk.ml +++ b/lib/risk.ml @@ -72,6 +72,8 @@ type fill_limit = | Instrument_maximum_notional of Id.Instrument.t * Scalar.Money.t | Instrument_shorting_disabled of Id.Instrument.t | Instrument_borrow_availability of Id.Instrument.t * Scalar.Quantity.t + | Settlement_cash_buying_power of string * Scalar.Money.t + | Settlement_position_availability of Id.Instrument.t * Scalar.Quantity.t | Instrument_initial_margin of Id.Instrument.t * int | Group_maximum_gross of Id.Risk_group.t * Scalar.Money.t | Group_maximum_long of Id.Risk_group.t * Scalar.Money.t diff --git a/lib/risk.mli b/lib/risk.mli index db2df28..52d6455 100644 --- a/lib/risk.mli +++ b/lib/risk.mli @@ -60,6 +60,8 @@ type fill_limit = | Instrument_maximum_notional of Id.Instrument.t * Scalar.Money.t | Instrument_shorting_disabled of Id.Instrument.t | Instrument_borrow_availability of Id.Instrument.t * Scalar.Quantity.t + | Settlement_cash_buying_power of string * Scalar.Money.t + | Settlement_position_availability of Id.Instrument.t * Scalar.Quantity.t | Instrument_initial_margin of Id.Instrument.t * int | Group_maximum_gross of Id.Risk_group.t * Scalar.Money.t | Group_maximum_long of Id.Risk_group.t * Scalar.Money.t diff --git a/lib/scenario.ml b/lib/scenario.ml index 85d2a7c..babd019 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -11,6 +11,7 @@ type t = { execution_model : Execution_model.t; execution : Execution.t; financing : Financing.policy option; + settlement : Settlement.policy option; max_internal_events : int; schedule : (int64 * Strategy.intent list) list; slices : Market_slice.t list; @@ -29,6 +30,7 @@ type stream_header = { execution_model : Execution_model.t; execution : Execution.t; financing : Financing.policy option; + settlement : Settlement.policy option; max_internal_events : int; } @@ -552,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 [ "10"; "9"; "8"; "7" ] then + if List.mem contract_version [ "11"; "10"; "9"; "8"; "7" ] then parse_v7_risk base_currency instruments json else parse_legacy_risk base_currency instruments json @@ -744,7 +746,7 @@ let parse_versioned_execution ~contract_version ~instruments json = Ok (execution_model, execution) let parse_execution ~contract_version ~instruments json = - if List.mem contract_version [ "10"; "9"; "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then parse_versioned_execution ~contract_version ~instruments json else parse_legacy_execution ~contract_version json @@ -793,7 +795,7 @@ 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 [ "10"; "9"; "8" ] in + let versioned = List.mem contract_version [ "11"; "10"; "9"; "8" ] in let* fields = object_fields ~name:"submit_order intent" ~expected: @@ -1171,6 +1173,105 @@ let parse_financing json = (Financing.policy ~day_count ~compounding ~borrow_missing_data ~cash_missing_data ~locate_policy ~recall_policy) +let parse_settlement_calendar json = + let* fields = + object_fields ~name:"settlement calendar" + ~expected:[ "calendar_id"; "version"; "business_dates" ] + json + in + let* calendar_id = + Result.bind + (field fields "calendar_id") + (string ~name:"settlement calendar_id") + in + let* version = + Result.bind (field fields "version") + (string ~name:"settlement calendar version") + in + let* dates_json = + Result.bind + (field fields "business_dates") + (list ~name:"settlement business_dates") + in + let* business_dates = + map_list (string ~name:"settlement business date") dates_json + in + Settlement.calendar ~calendar_id ~version ~business_dates + +let parse_settlement_rule json = + let* fields = + object_fields ~name:"settlement rule" + ~expected:[ "instrument_id"; "calendar_id"; "lag_business_days" ] + json + in + let* instrument_id = + Result.bind + (field fields "instrument_id") + (parse_id Id.Instrument.of_string ~name:"settlement instrument_id") + in + let* calendar_id = + Result.bind + (field fields "calendar_id") + (string ~name:"settlement calendar_id") + in + let* lag_business_days = + Result.bind + (field fields "lag_business_days") + (integer ~name:"lag_business_days") + in + Settlement.rule ~instrument_id ~calendar_id ~lag_business_days + +let parse_settlement json = + let* fields = + object_fields ~name:"settlement policy" + ~expected: + [ "cash_buying_power"; "position_availability"; "calendars"; "rules" ] + json + in + let text name = Result.bind (field fields name) (string ~name) in + let* cash_buying_power = + match text "cash_buying_power" with + | Ok "total_cash" -> Ok Settlement.Total_cash + | Ok "settled_cash" -> Ok Settlement.Settled_cash + | Ok _ -> Error "cash_buying_power must be total_cash or settled_cash" + | Error _ as error -> error + in + let* position_availability = + match text "position_availability" with + | Ok "total_positions" -> Ok Settlement.Total_positions + | Ok "settled_positions" -> Ok Settlement.Settled_positions + | Ok _ -> + Error + "position_availability must be total_positions or settled_positions" + | Error _ as error -> error + in + let* calendars_json = + Result.bind (field fields "calendars") (list ~name:"settlement calendars") + in + let* calendars = map_list parse_settlement_calendar calendars_json in + let* rules_json = + Result.bind (field fields "rules") (list ~name:"settlement rules") + in + let* rules = map_list parse_settlement_rule rules_json in + Settlement.policy ~cash_buying_power ~position_availability ~calendars ~rules + +let parse_settlement_failure json = + let* fields = + object_fields ~name:"settlement failure" + ~expected:[ "instruction_id"; "reason" ] + json + in + let* instruction_id = + Result.bind + (field fields "instruction_id") + (string ~name:"settlement instruction_id") + in + let* reason = + Result.bind (field fields "reason") + (string ~name:"settlement failure reason") + in + Settlement.failure ~instruction_id ~reason + let parse_borrow_observation json = let* fields = object_fields ~name:"borrow observation" @@ -1241,10 +1342,13 @@ let parse_cash_rate_observation json = let parse_slice ~contract_version json = let financing_fields = - if String.equal contract_version "10" then + if List.mem contract_version [ "11"; "10" ] then [ "borrow_observations"; "cash_rate_observations" ] else [] in + let settlement_fields = + if String.equal contract_version "11" then [ "settlement_failures" ] else [] + in let* fields = object_fields ~name:"market slice" ~expected: @@ -1258,7 +1362,7 @@ let parse_slice ~contract_version json = "fx_rates"; "corporate_actions"; ] - @ financing_fields) + @ financing_fields @ settlement_fields) json in let* sequence_json = field fields "slice_sequence" in @@ -1280,7 +1384,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 String.equal contract_version "10" then + if List.mem contract_version [ "11"; "10" ] then let* borrow_json = Result.bind (field fields "borrow_observations") @@ -1295,9 +1399,22 @@ let parse_slice ~contract_version json = let* cash_rate_observations = map_list parse_cash_rate_observation cash_json in - Market_slice.create_v10 ~slice_sequence ~start_at ~end_at ~available_at - ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations + if String.equal contract_version "11" then + let* failures_json = + Result.bind + (field fields "settlement_failures") + (list ~name:"settlement_failures") + in + let* settlement_failures = + map_list parse_settlement_failure failures_json + in + Market_slice.create_v11 ~slice_sequence ~start_at ~end_at ~available_at + ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations ~settlement_failures + else + Market_slice.create_v10 ~slice_sequence ~start_at ~end_at ~available_at + ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations else Market_slice.create ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions @@ -1333,7 +1450,7 @@ let construct_header ~root ~contract_path ~contract_version |> at (child root "base_currency") in let* initial_cash, initial_portfolio = - if List.mem contract_version [ "10"; "9"; "8"; "7"; "6" ] then + if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state |> at (child root "initial_portfolio") @@ -1395,13 +1512,23 @@ let construct_header ~root ~contract_path ~contract_version in let* financing = match (contract_version, shape.financing) with - | "10", Some json -> parse_financing json |> at (child root "financing") - | "10", None -> + | ("11" | "10"), Some json -> + parse_financing json |> at (child root "financing") + | ("11" | "10"), None -> Error "missing financing policy" |> at (child root "financing") | _, _ -> Ok Financing.legacy_policy in let financing = - if String.equal contract_version "10" then Some financing else None + if List.mem contract_version [ "11"; "10" ] then Some financing else None + in + let* settlement = + match (contract_version, shape.settlement) with + | "11", Some json -> + let* policy = parse_settlement json |> at (child root "settlement") in + Ok (Some policy) + | "11", None -> + Error "missing settlement policy" |> at (child root "settlement") + | _, _ -> Ok None in let header : stream_header = { @@ -1417,6 +1544,7 @@ let construct_header ~root ~contract_path ~contract_version execution_model; execution; financing; + settlement; max_internal_events; } in @@ -1462,6 +1590,7 @@ let construct_batch (shape : Scenario_shape.batch) = execution_model = header.execution_model; execution = header.execution; financing = header.financing; + settlement = header.settlement; max_internal_events = header.max_internal_events; schedule; slices; diff --git a/lib/scenario.mli b/lib/scenario.mli index 5272c88..245281c 100644 --- a/lib/scenario.mli +++ b/lib/scenario.mli @@ -13,6 +13,7 @@ type t = private { execution_model : Execution_model.t; execution : Execution.t; financing : Financing.policy option; + settlement : Settlement.policy option; max_internal_events : int; schedule : (int64 * Strategy.intent list) list; slices : Market_slice.t list; @@ -31,6 +32,7 @@ type stream_header = private { execution_model : Execution_model.t; execution : Execution.t; financing : Financing.policy option; + settlement : Settlement.policy option; max_internal_events : int; } diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index 393560d..c58e96c 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -10,6 +10,7 @@ type common = { risk : Yojson.Safe.t; execution : Yojson.Safe.t; financing : Yojson.Safe.t option; + settlement : Yojson.Safe.t option; max_internal_events : Yojson.Safe.t; } @@ -69,21 +70,27 @@ 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 [ "10"; "9"; "8"; "7"; "6" ] then + if List.mem contract_version [ "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 [ "10"; "9"; "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "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 String.equal contract_version "10" then List.assoc_opt "financing" fields + if List.mem contract_version [ "11"; "10" ] then + List.assoc_opt "financing" fields + else None + in + let settlement = + if String.equal contract_version "11" then + List.assoc_opt "settlement" fields else None in let* max_internal_events = field ~root fields "max_internal_events" in @@ -98,6 +105,7 @@ let common ~root ~contract_version fields = risk; execution; financing; + settlement; max_internal_events; } @@ -112,12 +120,12 @@ let batch json = match preliminary with `String value -> value | _ -> "" in let calendar_fields = - if List.mem contract_version [ "10"; "9"; "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "10"; "9"; "8"; "7"; "6" ] then + if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -138,7 +146,9 @@ let batch json = "slices"; ] @ calendar_fields - @ if String.equal contract_version "10" then [ "financing" ] else []) + @ (if List.mem contract_version [ "11"; "10" ] then [ "financing" ] + else []) + @ if String.equal contract_version "11" then [ "settlement" ] else []) json in let* contract_version_json = field ~root fields "contract_version" in @@ -150,12 +160,12 @@ let batch json = let stream_header ~contract_version json = let root = "$.payload" in let calendar_fields = - if List.mem contract_version [ "10"; "9"; "8"; "7"; "6"; "5" ] then + if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "10"; "9"; "8"; "7"; "6" ] then + if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -173,7 +183,9 @@ let stream_header ~contract_version json = "max_internal_events"; ] @ calendar_fields - @ if String.equal contract_version "10" then [ "financing" ] else []) + @ (if List.mem contract_version [ "11"; "10" ] then [ "financing" ] + else []) + @ if String.equal contract_version "11" then [ "settlement" ] else []) json in common ~root ~contract_version fields diff --git a/lib/scenario_shape.mli b/lib/scenario_shape.mli index e0cc642..dda3d07 100644 --- a/lib/scenario_shape.mli +++ b/lib/scenario_shape.mli @@ -12,6 +12,7 @@ type common = { risk : Yojson.Safe.t; execution : Yojson.Safe.t; financing : Yojson.Safe.t option; + settlement : Yojson.Safe.t option; max_internal_events : Yojson.Safe.t; } diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 0ae5a44..9f4dc91 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -48,7 +48,7 @@ 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 [ "10"; "9"; "8"; "7"; "6" ] then Ok () + if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then Ok () else Account.create ~base_currency ~initial_cash |> Result.map (fun _ -> ()) @@ -66,8 +66,8 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path:(child root "instruments") "instrument IDs must be unique" else let* () = - if List.mem contract_version [ "10"; "9"; "8"; "7"; "6"; "5" ] then - validate_venue_calendars ~root catalog venue_calendars + if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + then validate_venue_calendars ~root catalog venue_calendars else Ok () in let currencies = @@ -84,8 +84,8 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path: (child root - (if List.mem contract_version [ "10"; "9"; "8"; "7"; "6" ] then - "initial_portfolio.cash" + (if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6" ] + then "initial_portfolio.cash" else "initial_cash")) "initial cash must contain every scenario currency exactly once" else if max_internal_events <= 0 then diff --git a/lib/settlement.ml b/lib/settlement.ml new file mode 100644 index 0000000..0066c30 --- /dev/null +++ b/lib/settlement.ml @@ -0,0 +1,215 @@ +type cash_buying_power = Total_cash | Settled_cash +type position_availability = Total_positions | Settled_positions + +type calendar = { + calendar_id : string; + version : string; + business_dates : string list; +} + +type rule = { + instrument_id : Id.Instrument.t; + calendar_id : string; + lag_business_days : int; +} + +type policy = { + cash_buying_power : cash_buying_power; + position_availability : position_availability; + calendars : calendar list; + rules : rule list; +} + +type status = + | Pending + | Settled of Ptime.t + | Failed of { failed_at : Ptime.t; reason : string } + +type instruction = { + instruction_id : string; + fill_id : Id.Fill.t; + instrument_id : Id.Instrument.t; + currency : string; + cash_movement : Scalar.Money.t; + position_movement : Scalar.Quantity.t; + trade_date : string; + due_date : string; + status : status; +} + +type failure = { instruction_id : string; reason : string } + +let ( let* ) result function_ = + match result with Ok value -> function_ value | Error _ as error -> error + +let valid_token value = + String.length value > 0 + && String.for_all + (fun character -> + let code = Char.code character in + code >= 0x21 && code <> 0x7f) + value + +let valid_text value = + String.length value > 0 + && String.equal value (String.trim value) + && String.for_all + (fun character -> + let code = Char.code character in + code >= 0x20 && code <> 0x7f) + value + +let valid_date value = + String.length value = 10 + && value.[4] = '-' + && value.[7] = '-' + && Result.is_ok (Ptime.of_rfc3339 (value ^ "T00:00:00Z")) + +let calendar ~calendar_id ~version ~business_dates = + if not (valid_token calendar_id) then + Error "settlement calendar_id must not be empty or contain whitespace" + else if not (String.equal version "1") then + Error (Printf.sprintf "unsupported settlement calendar version %S" version) + else if business_dates = [] then + Error "settlement calendar must define at least one business date" + else if not (List.for_all valid_date business_dates) then + Error "settlement business dates must use canonical YYYY-MM-DD dates" + else if List.sort_uniq String.compare business_dates <> business_dates then + Error "settlement business dates must be unique and increasing" + else Ok { calendar_id; version; business_dates } + +let rule ~instrument_id ~calendar_id ~lag_business_days = + if not (valid_token calendar_id) then + Error "settlement rule calendar_id must not be empty or contain whitespace" + else if lag_business_days < 0 || lag_business_days > 30 then + Error "settlement lag_business_days must be between zero and 30" + else Ok { instrument_id; calendar_id; lag_business_days } + +let policy ~cash_buying_power ~position_availability + ~(calendars : calendar list) ~(rules : rule list) = + if calendars = [] then Error "settlement policy must define calendars" + else if rules = [] then Error "settlement policy must define instrument rules" + else + let calendar_ids = + List.map (fun (value : calendar) -> value.calendar_id) calendars + in + let instruments = + List.map (fun (value : rule) -> value.instrument_id) rules + in + if + List.sort_uniq String.compare calendar_ids + <> List.sort String.compare calendar_ids + then Error "settlement calendar IDs must be unique" + else if + List.sort_uniq Id.Instrument.compare instruments + <> List.sort Id.Instrument.compare instruments + then Error "settlement rules must name unique instruments" + else if + not + (List.for_all + (fun (value : rule) -> List.mem value.calendar_id calendar_ids) + rules) + then Error "settlement rule refers to an unknown calendar" + else Ok { cash_buying_power; position_availability; calendars; rules } + +let date_of_timestamp timestamp = + let year, month, day = Ptime.to_date timestamp in + Printf.sprintf "%04d-%02d-%02d" year month day + +let instruction policy (fill : Fill.t) = + let* rule = + match + List.find_opt + (fun (rule : rule) -> + Id.Instrument.equal rule.instrument_id fill.instrument_id) + policy.rules + with + | Some value -> Ok value + | None -> Error "fill instrument has no settlement rule" + in + let* calendar = + match + List.find_opt + (fun (calendar : calendar) -> + String.equal calendar.calendar_id rule.calendar_id) + policy.calendars + with + | Some value -> Ok value + | None -> Error "settlement rule calendar is unavailable" + in + let trade_date = date_of_timestamp fill.executed_at in + let* trade_index = + let rec find index = function + | [] -> Error "fill trade date is absent from its settlement calendar" + | date :: remaining -> + if String.equal date trade_date then Ok index + else find (index + 1) remaining + in + find 0 calendar.business_dates + in + let* due_date = + match + List.nth_opt calendar.business_dates (trade_index + rule.lag_business_days) + with + | Some value -> Ok value + | None -> + Error "settlement calendar does not cover the instruction due date" + in + let* cash_movement, position_movement = + match fill.side with + | Order.Buy -> + let* debit = Scalar.Money.add fill.notional fill.fee in + let* cash = Scalar.Money.negate debit in + Ok (cash, fill.quantity) + | Order.Sell -> + let* cash = Scalar.Money.subtract fill.notional fill.fee in + let* position = Scalar.Quantity.negate fill.quantity in + Ok (cash, position) + in + Ok + { + instruction_id = Id.Fill.to_string fill.id ^ "-settlement"; + fill_id = fill.id; + instrument_id = fill.instrument_id; + currency = fill.quote_currency; + cash_movement; + position_movement; + trade_date; + due_date; + status = Pending; + } + +let failure ~instruction_id ~reason = + if not (valid_token instruction_id) then + Error + "settlement failure instruction_id must not be empty or contain \ + whitespace" + else if not (valid_text reason) then + Error "settlement failure reason must be nonempty, trimmed text" + else Ok { instruction_id; reason } + +let settle instruction ~settled_at = + match instruction.status with + | Pending -> Ok { instruction with status = Settled settled_at } + | Settled _ | Failed _ -> Error "settlement instruction is already terminal" + +let fail instruction ~failed_at ~reason = + match instruction.status with + | Pending -> Ok { instruction with status = Failed { failed_at; reason } } + | Settled _ | Failed _ -> Error "settlement instruction is already terminal" + +let is_due instruction timestamp = + String.compare (date_of_timestamp timestamp) instruction.due_date >= 0 + +let cash_buying_power_to_string = function + | Total_cash -> "total_cash" + | Settled_cash -> "settled_cash" + +let position_availability_to_string = function + | Total_positions -> "total_positions" + | Settled_positions -> "settled_positions" + +let status_to_string = function + | Pending -> "pending" + | Settled _ -> "settled" + | Failed _ -> "failed" diff --git a/lib/settlement.mli b/lib/settlement.mli new file mode 100644 index 0000000..9d97a5a --- /dev/null +++ b/lib/settlement.mli @@ -0,0 +1,77 @@ +(** Deterministic trade-settlement calendars, policies, and instructions. *) + +type cash_buying_power = Total_cash | Settled_cash +type position_availability = Total_positions | Settled_positions + +type calendar = private { + calendar_id : string; + version : string; + business_dates : string list; +} + +type rule = private { + instrument_id : Id.Instrument.t; + calendar_id : string; + lag_business_days : int; +} + +type policy = private { + cash_buying_power : cash_buying_power; + position_availability : position_availability; + calendars : calendar list; + rules : rule list; +} + +type status = + | Pending + | Settled of Ptime.t + | Failed of { failed_at : Ptime.t; reason : string } + +type instruction = private { + instruction_id : string; + fill_id : Id.Fill.t; + instrument_id : Id.Instrument.t; + currency : string; + cash_movement : Scalar.Money.t; + position_movement : Scalar.Quantity.t; + trade_date : string; + due_date : string; + status : status; +} + +type failure = private { instruction_id : string; reason : string } + +val calendar : + calendar_id:string -> + version:string -> + business_dates:string list -> + (calendar, string) result + +val rule : + instrument_id:Id.Instrument.t -> + calendar_id:string -> + lag_business_days:int -> + (rule, string) result + +val policy : + cash_buying_power:cash_buying_power -> + position_availability:position_availability -> + calendars:calendar list -> + rules:rule list -> + (policy, string) result + +val instruction : policy -> Fill.t -> (instruction, string) result +val failure : instruction_id:string -> reason:string -> (failure, string) result +val settle : instruction -> settled_at:Ptime.t -> (instruction, string) result + +val fail : + instruction -> + failed_at:Ptime.t -> + reason:string -> + (instruction, string) result + +val date_of_timestamp : Ptime.t -> string +val is_due : instruction -> Ptime.t -> bool +val cash_buying_power_to_string : cash_buying_power -> string +val position_availability_to_string : position_availability -> string +val status_to_string : status -> string diff --git a/lib/strategy.ml b/lib/strategy.ml index 62681e1..8cdd8b5 100644 --- a/lib/strategy.ml +++ b/lib/strategy.ml @@ -8,6 +8,8 @@ type context = { and marked_position = { instrument_id : Id.Instrument.t; quantity : Scalar.Quantity.t; + settled_quantity : Scalar.Quantity.t; + unsettled_quantity : Scalar.Quantity.t; mark : Scalar.Price.t; base_market_value : Scalar.Money.t; weight : Scalar.Weight.t option; @@ -71,6 +73,8 @@ let context ~now ~(valuation : Account.valuation) ~group_exposures ({ instrument_id = position.instrument_id; quantity = position.quantity; + settled_quantity = position.settled_quantity; + unsettled_quantity = position.unsettled_quantity; mark = position.mark; base_market_value = position.base_market_value; weight = position_weight; diff --git a/lib/strategy.mli b/lib/strategy.mli index 8b66edf..071e23e 100644 --- a/lib/strategy.mli +++ b/lib/strategy.mli @@ -5,6 +5,8 @@ type context type marked_position = private { instrument_id : Id.Instrument.t; quantity : Scalar.Quantity.t; + settled_quantity : Scalar.Quantity.t; + unsettled_quantity : Scalar.Quantity.t; mark : Scalar.Price.t; base_market_value : Scalar.Money.t; weight : Scalar.Weight.t option; diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index 9ea4d4e..1daaf2c 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -15,6 +15,7 @@ type initialization = { execution_model : Execution_model.t; execution : Execution.t; financing : Financing.policy option; + settlement : Settlement.policy option; } type identity = { name : Id.Strategy.t; version : string option } @@ -102,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 [ "8"; "7"; "6"; "5" ] + List.mem protocol_version [ "9"; "8"; "7"; "6"; "5" ] let financing_to_yojson policy = `Assoc @@ -121,6 +122,36 @@ let financing_to_yojson policy = string (Financing.recall_policy_to_string policy.recall_policy) ); ] +let settlement_to_yojson (policy : Settlement.policy) = + let calendar (calendar : Settlement.calendar) = + `Assoc + [ + ("calendar_id", string calendar.calendar_id); + ("version", string calendar.version); + ("business_dates", `List (List.map string calendar.business_dates)); + ] + in + let rule (rule : Settlement.rule) = + `Assoc + [ + ("instrument_id", instrument_id rule.instrument_id); + ("calendar_id", string rule.calendar_id); + ("lag_business_days", `Int rule.lag_business_days); + ] + in + `Assoc + [ + ( "cash_buying_power", + string (Settlement.cash_buying_power_to_string policy.cash_buying_power) + ); + ( "position_availability", + string + (Settlement.position_availability_to_string + policy.position_availability) ); + ("calendars", `List (List.map calendar policy.calendars)); + ("rules", `List (List.map rule policy.rules)); + ] + let instrument_policy_to_yojson (policy : Risk.instrument_policy) = `Assoc [ @@ -221,7 +252,7 @@ let execution_to_yojson ~protocol_version model execution = (Fee_schedule.components schedule)) ); ] in - if List.mem protocol_version [ "8"; "7" ] then + if List.mem protocol_version [ "9"; "8"; "7" ] then `Assoc [ ("model", string (Execution_model.name model)); @@ -260,6 +291,7 @@ let execution_to_yojson ~protocol_version model execution = let protocol_version initialization = match initialization.scenario_contract_version with + | "11" -> "9" | "10" -> "8" | "9" -> "7" | "8" -> "6" @@ -304,7 +336,7 @@ let initialize_message ~sequence:message_sequence initialization = ] in let fields = - if List.mem protocol_version [ "8"; "7"; "6" ] then + if List.mem protocol_version [ "9"; "8"; "7"; "6" ] then let initial_portfolio = Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson initialization.initial_portfolio @@ -317,13 +349,20 @@ let initialize_message ~sequence:message_sequence initialization = ( "venue_calendars", `List (List.map venue_calendar_to_yojson venue_calendars) ); ]; - (if String.equal protocol_version "8" then + (if List.mem protocol_version [ "9"; "8" ] then [ ( "financing", Option.fold ~none:`Null ~some:financing_to_yojson initialization.financing ); ] else []); + (if String.equal protocol_version "9" then + [ + ( "settlement", + Option.fold ~none:`Null ~some:settlement_to_yojson + initialization.settlement ); + ] + else []); List.drop 6 fields; ] else if modern_protocol protocol_version then @@ -351,23 +390,39 @@ let cash_attribution_to_yojson ~protocol_version ("fx_rate", price balance.fx_rate); ("base_value", money balance.base_value); ] + @ (if List.mem protocol_version [ "9"; "8" ] then + [ + ("interest", money balance.interest); + ("base_interest", money balance.base_interest); + ] + else []) @ - if String.equal protocol_version "8" then + if String.equal protocol_version "9" then [ - ("interest", money balance.interest); - ("base_interest", money balance.base_interest); + ("settled_amount", money balance.settled_amount); + ("unsettled_amount", money balance.unsettled_amount); + ("base_settled_value", money balance.base_settled_value); + ("base_unsettled_value", money balance.base_unsettled_value); ] else []) -let marked_position_to_yojson (position : Strategy.marked_position) = +let marked_position_to_yojson ~protocol_version + (position : Strategy.marked_position) = `Assoc - [ - ("instrument_id", instrument_id position.instrument_id); - ("quantity", quantity position.quantity); - ("mark", price position.mark); - ("base_market_value", money position.base_market_value); - ("weight", Option.fold ~none:`Null ~some:weight position.weight); - ] + ([ + ("instrument_id", instrument_id position.instrument_id); + ("quantity", quantity position.quantity); + ("mark", price position.mark); + ("base_market_value", money position.base_market_value); + ("weight", Option.fold ~none:`Null ~some:weight position.weight); + ] + @ + if String.equal protocol_version "9" then + [ + ("settled_quantity", quantity position.settled_quantity); + ("unsettled_quantity", quantity position.unsettled_quantity); + ] + else []) let group_exposure_to_yojson (exposure : Risk.group_exposure) = `Assoc @@ -423,7 +478,9 @@ let context_to_yojson ~protocol_version context = (List.map (cash_attribution_to_yojson ~protocol_version) cash_balances) ); - ("positions", `List (List.map marked_position_to_yojson positions)); + ( "positions", + `List (List.map (marked_position_to_yojson ~protocol_version) positions) + ); ] in let portfolio_fields = @@ -443,7 +500,7 @@ let context_to_yojson ~protocol_version context = ( "working_orders", `List (List.map - (if List.mem protocol_version [ "8"; "7"; "6" ] then + (if List.mem protocol_version [ "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 else Codec.order_to_yojson) working_orders) ); @@ -456,7 +513,9 @@ let event_to_yojson ~protocol_version = function [ ("type", string "market_slice_closed"); ( "market_slice", - if String.equal protocol_version "8" then + if String.equal protocol_version "9" then + Codec.market_slice_to_yojson_v11 market_slice + else if String.equal protocol_version "8" then Codec.market_slice_to_yojson_v10 market_slice else Codec.market_slice_to_yojson market_slice ); ] @@ -465,7 +524,7 @@ let event_to_yojson ~protocol_version = function [ ("type", string "fill_received"); ( "fill", - if List.mem protocol_version [ "8"; "7" ] then + if List.mem protocol_version [ "9"; "8"; "7" ] then Codec.fill_to_yojson_v9 fill else Codec.fill_to_yojson fill ); ] @@ -474,7 +533,7 @@ let event_to_yojson ~protocol_version = function [ ("type", string "order_updated"); ( "order", - if List.mem protocol_version [ "8"; "7"; "6" ] then + if List.mem protocol_version [ "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 order else Codec.order_to_yojson order ); ] @@ -562,7 +621,8 @@ let parse_intents_payload ~protocol_version json = let* intent = Scenario.intent_of_yojson ~contract_version: - (if String.equal protocol_version "8" then "10" + (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" else if String.equal protocol_version "6" then "8" else "7") diff --git a/lib/strategy_protocol.mli b/lib/strategy_protocol.mli index 3fed934..ab15974 100644 --- a/lib/strategy_protocol.mli +++ b/lib/strategy_protocol.mli @@ -17,6 +17,7 @@ type initialization = { execution_model : Execution_model.t; execution : Execution.t; financing : Financing.policy option; + settlement : Settlement.policy option; } type identity = private { name : Id.Strategy.t; version : string option } diff --git a/mkdocs.yml b/mkdocs.yml index 3e5940b..c11b345 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,14 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v10: contracts/v10/README.md + - Current v11: contracts/v11/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 v8: contracts/strategy/v8/README.md + - Current v9: contracts/strategy/v9/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 97ef5af..a9cd805 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -74,3 +74,11 @@ compare_journal \ v10-fill-clipped \ contracts/v10/fixtures/fill-clipped.scenario.json \ contracts/v10/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v11-demo \ + contracts/v11/fixtures/demo.scenario.json \ + contracts/v11/fixtures/demo.journal.jsonl +compare_journal \ + v11-fill-clipped \ + contracts/v11/fixtures/fill-clipped.scenario.json \ + contracts/v11/fixtures/fill-clipped.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index 3efa7d1..c134f22 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/v10/README.md", + "contracts/v11/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v8/README.md", + "contracts/strategy/v9/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 013e3c1..6b28226 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/v10/scenario.schema.json", - "share/trading_engine/contracts/v10/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v11/scenario.schema.json", + "share/trading_engine/contracts/v11/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/v10/fixtures/demo.scenario.json", + "contracts/v11/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/v10/fixtures/demo.scenario.json", - "contracts/strategy/v8/message.schema.json", + "contracts/v11/fixtures/demo.scenario.json", + "contracts/strategy/v9/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v10/scenario.schema.json", + "contracts/v11/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index 63430fe..f0497fe 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":["10","9","8","7","6","5","4","3"],"journal_contract_versions":["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":["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":["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":["11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["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":["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":["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 25b02b3..4662ab7 100644 --- a/test/dune +++ b/test/dune @@ -9,6 +9,7 @@ test_order_lifetimes test_fee_schedules test_financing + test_settlement test_reducer test_reducer_properties test_checkpoint4 @@ -52,6 +53,14 @@ ../contracts/v10/journal.schema.json ../contracts/v10/scenario-stream.schema.json ../contracts/v10/scenario.schema.json + ../contracts/v11/fixtures/demo.journal.jsonl + ../contracts/v11/fixtures/demo.scenario.json + ../contracts/v11/fixtures/demo.scenario.jsonl + ../contracts/v11/fixtures/fill-clipped.journal.jsonl + ../contracts/v11/fixtures/fill-clipped.scenario.json + ../contracts/v11/journal.schema.json + ../contracts/v11/scenario-stream.schema.json + ../contracts/v11/scenario.schema.json ../contracts/v6/fixtures/demo.scenario.json ../contracts/v6/fixtures/demo.scenario.jsonl ../contracts/v5/fixtures/demo.scenario.json @@ -65,6 +74,7 @@ ../contracts/strategy/v6/fixtures/external.strategy.jsonl ../contracts/strategy/v7/fixtures/external.strategy.jsonl ../contracts/strategy/v8/fixtures/external.strategy.jsonl + ../contracts/strategy/v9/fixtures/external.strategy.jsonl ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries @@ -83,6 +93,69 @@ (modules fuzz_protocol) (libraries trading_engine yojson unix)) +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v11/fixtures/demo.journal.jsonl + ../contracts/v11/fixtures/demo.scenario.json + ../contracts/v11/fixtures/demo.scenario.jsonl + ../contracts/v11/journal.schema.json + ../contracts/v11/scenario-stream.schema.json + ../contracts/v11/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v11/scenario.schema.json} + %{dep:../contracts/v11/scenario-stream.schema.json} + %{dep:../contracts/v11/journal.schema.json} + %{dep:../contracts/v11/fixtures/demo.scenario.json} + %{dep:../contracts/v11/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v11/fixtures/demo.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v11/fixtures/fill-clipped.journal.jsonl + ../contracts/v11/fixtures/fill-clipped.scenario.json + ../contracts/v11/fixtures/demo.scenario.jsonl + ../contracts/v11/journal.schema.json + ../contracts/v11/scenario-stream.schema.json + ../contracts/v11/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v11/scenario.schema.json} + %{dep:../contracts/v11/scenario-stream.schema.json} + %{dep:../contracts/v11/journal.schema.json} + %{dep:../contracts/v11/fixtures/fill-clipped.scenario.json} + %{dep:../contracts/v11/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v11/fixtures/fill-clipped.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_strategy_schema.py + ../contracts/v11/scenario.schema.json + ../contracts/v11/journal.schema.json + ../contracts/diagnostic/v1/diagnostic.schema.json + ../contracts/strategy/v9/message.schema.json + ../contracts/strategy/v9/transcript.schema.json + ../contracts/strategy/v9/fixtures/external.strategy.jsonl) + (action + (run + python3 + %{dep:validate_strategy_schema.py} + %{dep:../contracts/v11/scenario.schema.json} + %{dep:../contracts/v11/journal.schema.json} + %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} + %{dep:../contracts/strategy/v9/message.schema.json} + %{dep:../contracts/strategy/v9/transcript.schema.json} + %{dep:../contracts/strategy/v9/fixtures/external.strategy.jsonl}))) + (rule (alias runtest) (deps diff --git a/test/test_boundary_failures.ml b/test/test_boundary_failures.ml index a099ea7..9319621 100644 --- a/test/test_boundary_failures.ml +++ b/test/test_boundary_failures.ml @@ -331,6 +331,7 @@ let initialization () = execution_model = T.Execution_model.find "completed_bar_v1" |> ok; execution = execution (); financing = None; + settlement = None; } let process_stages = diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 3a1c4f5..1024b5d 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -118,7 +118,7 @@ let capabilities_describe_execution_contracts () = (strings "configuration_versions"); Alcotest.(check (list string)) "scenario contracts" - [ "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] + [ "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] (strings "scenario_contract_versions"); Alcotest.(check (list string)) "required fields" diff --git a/test/test_engine.ml b/test/test_engine.ml index 5a84020..2227ae0 100644 --- a/test/test_engine.ml +++ b/test/test_engine.ml @@ -8,6 +8,7 @@ let () = ("order-lifetimes", Test_order_lifetimes.tests); ("fee-schedules", Test_fee_schedules.tests); ("financing", Test_financing.tests); + ("settlement", Test_settlement.tests); ("reducer", Test_reducer.tests); ("reducer-properties", Test_reducer_properties.tests); ("checkpoint4", Test_checkpoint4.tests); diff --git a/test/test_scenario.ml b/test/test_scenario.ml index b4a6701..4fb37d1 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/v10/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v11/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/v10/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v11/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_v10 ~slice_sequence:(Int64.of_int index) + T.Market_slice.create_v11 ~slice_sequence:(Int64.of_int index) ~start_at ~end_at:(add_seconds base (offset + 1)) ~available_at:(add_seconds base (offset + 2)) @@ -88,13 +88,13 @@ let write_large_stream path slice_count = ] ~fx_rates:[ fx_mark () ] ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] - ~cash_rate_observations:[ cash_rate ] + ~cash_rate_observations:[ cash_rate ] ~settlement_failures:[] |> ok in let payload = `Assoc [ - ("market_slice", T.Codec.market_slice_to_yojson_v10 market_slice); + ("market_slice", T.Codec.market_slice_to_yojson_v11 market_slice); ("intents", `List []); ] in @@ -139,9 +139,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v10/scenario.schema.json"; - check_schema "../contracts/v10/scenario-stream.schema.json"; - check_schema "../contracts/v10/journal.schema.json" + check_schema "../contracts/v11/scenario.schema.json"; + check_schema "../contracts/v11/scenario-stream.schema.json"; + check_schema "../contracts/v11/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -203,8 +203,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 10, 9, 8, 7, \ - 6, 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 11, 10, 9, \ + 8, 7, 6, 5, 4, 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -354,7 +354,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_v10 ~slice_sequence:(Int64.of_int index) ~start_at + T.Market_slice.create_v11 ~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)) @@ -367,7 +367,8 @@ let dense_schedule_document slice_count = ~fx_rates:[ fx_mark () ] ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] ~cash_rate_observations:[ cash_rate_observation ] - |> ok |> T.Codec.market_slice_to_yojson_v10) + ~settlement_failures:[] + |> ok |> T.Codec.market_slice_to_yojson_v11) in let schedule = List.init slice_count (fun offset -> @@ -899,7 +900,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v10/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v11/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -927,7 +928,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v10/fixtures/fill-clipped.scenario.json" + "../contracts/v11/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in @@ -941,7 +942,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v10/fixtures/fill-clipped.journal.jsonl" + "../contracts/v11/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual @@ -1041,7 +1042,7 @@ 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) "twenty-six audits" 26L result.audit_count; + Alcotest.(check int64) "thirty-one audits" 31L result.audit_count; Alcotest.check money_testable "same equity" (money "10111.946958") result.valuation.equity; Alcotest.(check string) diff --git a/test/test_settlement.ml b/test/test_settlement.ml new file mode 100644 index 0000000..eb7fe85 --- /dev/null +++ b/test/test_settlement.ml @@ -0,0 +1,377 @@ +open Test_support +module T = Trading_engine +module Runner = T.Engine.Make (T.Scripted_strategy) + +let settlement_policy ?(cash_buying_power = T.Settlement.Total_cash) + ?(position_availability = T.Settlement.Total_positions) ?(lag = 1) () = + let calendar = + T.Settlement.calendar ~calendar_id:"test-settlement" ~version:"1" + ~business_dates:[ "2026-01-02"; "2026-01-03"; "2026-01-04"; "2026-01-05" ] + |> ok + in + let rule = + T.Settlement.rule + ~instrument_id:(instrument_id "test-equity") + ~calendar_id:"test-settlement" ~lag_business_days:lag + |> ok + in + T.Settlement.policy ~cash_buying_power ~position_availability + ~calendars:[ calendar ] ~rules:[ rule ] + |> ok + +let buy_fill () = + let order = request ~quantity_value:"2" () |> accepted_order in + fill ~quantity_value:"2" ~fee_value:"1" + ~executed_at:(timestamp "2026-01-03T21:00:00Z") + order + +let calendar_and_trade_date_accounting () = + let policy = settlement_policy () in + let fill = buy_fill () in + let instruction = T.Settlement.instruction policy fill |> ok in + Alcotest.(check string) "trade date" "2026-01-03" instruction.trade_date; + Alcotest.(check string) "due date" "2026-01-04" instruction.due_date; + Alcotest.check money_testable "cash movement" (money "-201") + instruction.cash_movement; + Alcotest.check quantity_testable "position movement" (quantity "2") + instruction.position_movement; + let account = test_account ~initial_cash:[ ("USD", money "1000") ] () in + let account = T.Account.apply_unsettled_fill account fill |> ok in + Alcotest.check money_testable "economic cash" (money "799") + (account_cash account); + Alcotest.check money_testable "settled cash unchanged" (money "1000") + (T.Account.settled_cash account "USD" |> Option.get); + Alcotest.check quantity_testable "economic position" (quantity "2") + (T.Account.position_quantity account (instrument_id "test-equity")); + Alcotest.check quantity_testable "settled position unchanged" (quantity "0") + (T.Account.settled_position_quantity account (instrument_id "test-equity")); + let valuation = + account_value account ~marks:[ (instrument_id "test-equity", price "100") ] + in + Alcotest.check money_testable "unsettled cash valuation" (money "-201") + valuation.unsettled_cash; + Alcotest.check quantity_testable "unsettled position valuation" (quantity "2") + (List.hd valuation.positions).unsettled_quantity; + let account = T.Account.apply_settlement account instruction |> ok in + Alcotest.check money_testable "settled cash reconciled" (money "799") + (T.Account.settled_cash account "USD" |> Option.get); + Alcotest.check quantity_testable "settled position reconciled" (quantity "2") + (T.Account.settled_position_quantity account (instrument_id "test-equity")) + +let slice ?(settlement_failures = []) sequence = + let date = match sequence with 1L -> "02" | 2L -> "03" | _ -> "04" in + T.Market_slice.create_v11 ~slice_sequence:sequence + ~start_at:(timestamp ("2026-01-" ^ date ^ "T14:30:00Z")) + ~end_at:(timestamp ("2026-01-" ^ date ^ "T21:00:00Z")) + ~available_at:(timestamp ("2026-01-" ^ date ^ "T21:00:01Z")) + ~received_at:(timestamp ("2026-01-" ^ date ^ "T21:00:02Z")) + ~bars:[ bar sequence ] + ~fx_rates:[ fx_mark () ] + ~corporate_actions:[] ~borrow_observations:[] ~cash_rate_observations:[] + ~settlement_failures + |> ok + +let runner ?(initial_cash = "1000") ?schedule policy run = + let config = + T.Engine.config_v11 ~contract_version:"11" ~risk:(risk ()) + ~venue_calendars:[] + ~execution_model:(T.Execution_model.find "completed_bar_v1" |> ok) + ~execution:(execution ()) ~financing:T.Financing.legacy_policy + ~settlement:policy ~max_internal_events:1000 + |> ok + in + let schedule = + Option.value schedule + ~default: + [ + ( 1L, + [ + T.Strategy.Target_quantities + [ + { + instrument_id = instrument_id "test-equity"; + quantity = quantity "2"; + }; + ]; + ] ); + ] + in + let strategy_state = T.Scripted_strategy.create schedule |> ok in + Runner.create ~run_id:(run_id run) ~scenario_sha256 ~config + ~initial_cash:[ ("USD", money initial_cash) ] + ~strategy_state + |> ok + +let find_instruction events = + List.find_map + (fun (event : T.Audit.t) -> + match event.event with + | T.Audit.Settlement_instruction_created instruction -> Some instruction + | _ -> None) + events + |> Option.get + +let engine_settles_due_instruction () = + let state, _ = + Runner.process_slice (runner (settlement_policy ()) "settle") (slice 1L) + |> ok + in + let state, events = Runner.process_slice state (slice 2L) |> ok in + let instruction = find_instruction events in + Alcotest.(check string) + "pending instruction" "pending" + (T.Settlement.status_to_string instruction.status); + Alcotest.check quantity_testable "trade-date position" (quantity "2") + (T.Account.position_quantity (Runner.account state) + (instrument_id "test-equity")); + Alcotest.check quantity_testable "not yet settled" (quantity "0") + (T.Account.settled_position_quantity (Runner.account state) + (instrument_id "test-equity")); + let state, events = Runner.process_slice state (slice 3L) |> ok in + Alcotest.(check bool) + "completion event" true + (List.exists + (fun (event : T.Audit.t) -> + match event.event with + | T.Audit.Settlement_completed _ -> true + | _ -> false) + events); + Alcotest.check quantity_testable "settled position" (quantity "2") + (T.Account.settled_position_quantity (Runner.account state) + (instrument_id "test-equity")) + +let engine_records_settlement_failure () = + let state, _ = + Runner.process_slice (runner (settlement_policy ()) "failure") (slice 1L) + |> ok + in + let state, events = Runner.process_slice state (slice 2L) |> ok in + let instruction = find_instruction events in + let failure = + T.Settlement.failure ~instruction_id:instruction.instruction_id + ~reason:"counterparty default" + |> ok + in + let state, events = + match + Runner.process_slice state (slice ~settlement_failures:[ failure ] 3L) + with + | Ok value -> value + | Error message -> Alcotest.fail message + in + Alcotest.(check bool) + "failure event" true + (List.exists + (fun (event : T.Audit.t) -> + match event.event with + | T.Audit.Settlement_failed _ -> true + | _ -> false) + events); + Alcotest.check quantity_testable "failed position remains unsettled" + (quantity "0") + (T.Account.settled_position_quantity (Runner.account state) + (instrument_id "test-equity")) + +let constructors_reject_ambiguous_inputs () = + let invalid_calendar ?(id = "calendar") ?(version = "1") dates = + T.Settlement.calendar ~calendar_id:id ~version ~business_dates:dates + in + List.iter + (fun result -> + Alcotest.(check bool) "invalid calendar" true (Result.is_error result)) + [ + invalid_calendar ~id:"" [ "2026-01-02" ]; + invalid_calendar ~version:"2" [ "2026-01-02" ]; + invalid_calendar []; + invalid_calendar [ "2026/01/02" ]; + invalid_calendar [ "2026-01-03"; "2026-01-02" ]; + ]; + let id = instrument_id "test-equity" in + Alcotest.(check bool) + "empty rule calendar" true + (Result.is_error + (T.Settlement.rule ~instrument_id:id ~calendar_id:"" ~lag_business_days:1)); + Alcotest.(check bool) + "invalid lag" true + (Result.is_error + (T.Settlement.rule ~instrument_id:id ~calendar_id:"calendar" + ~lag_business_days:31)); + Alcotest.(check bool) + "negative lag" true + (Result.is_error + (T.Settlement.rule ~instrument_id:id ~calendar_id:"calendar" + ~lag_business_days:(-1))); + let calendar = invalid_calendar [ "2026-01-03" ] |> ok in + let rule = + T.Settlement.rule ~instrument_id:id ~calendar_id:"calendar" + ~lag_business_days:1 + |> ok + in + let make_policy calendars rules = + T.Settlement.policy ~cash_buying_power:T.Settlement.Total_cash + ~position_availability:T.Settlement.Total_positions ~calendars ~rules + in + List.iter + (fun result -> + Alcotest.(check bool) "invalid policy" true (Result.is_error result)) + [ + make_policy [] [ rule ]; + make_policy [ calendar ] []; + make_policy [ calendar; calendar ] [ rule ]; + make_policy [ calendar ] [ rule; rule ]; + make_policy [ calendar ] + [ + T.Settlement.rule ~instrument_id:id ~calendar_id:"unknown" + ~lag_business_days:1 + |> ok; + ]; + ]; + let policy = make_policy [ calendar ] [ rule ] |> ok in + Alcotest.(check bool) + "calendar misses due date" true + (Result.is_error (T.Settlement.instruction policy (buy_fill ()))); + let absent_trade_calendar = + invalid_calendar [ "2026-01-02"; "2026-01-04" ] |> ok + in + let absent_trade_policy = + make_policy [ absent_trade_calendar ] [ rule ] |> ok + in + Alcotest.(check bool) + "calendar misses trade date" true + (Result.is_error + (T.Settlement.instruction absent_trade_policy (buy_fill ()))); + let other_order = + request ~instrument:(instrument_id "other") () |> accepted_order + in + Alcotest.(check bool) + "missing instrument rule" true + (Result.is_error (T.Settlement.instruction policy (fill other_order))); + Alcotest.(check bool) + "failure instruction required" true + (Result.is_error (T.Settlement.failure ~instruction_id:"" ~reason:"reason")); + Alcotest.(check bool) + "failure reason trimmed" true + (Result.is_error + (T.Settlement.failure ~instruction_id:"instruction" ~reason:" bad ")); + let instruction = + T.Settlement.instruction (settlement_policy ()) (buy_fill ()) |> ok + in + let settled = + T.Settlement.settle instruction + ~settled_at:(timestamp "2026-01-04T21:00:00Z") + |> ok + in + Alcotest.(check string) + "settled status" "settled" + (T.Settlement.status_to_string settled.status); + Alcotest.(check bool) + "settled instruction terminal" true + (Result.is_error + (T.Settlement.settle settled + ~settled_at:(timestamp "2026-01-05T21:00:00Z"))); + let failed = + T.Settlement.fail instruction + ~failed_at:(timestamp "2026-01-04T21:00:00Z") + ~reason:"default" + |> ok + in + Alcotest.(check string) + "failed status" "failed" + (T.Settlement.status_to_string failed.status); + Alcotest.(check bool) + "failed instruction terminal" true + (Result.is_error + (T.Settlement.fail failed + ~failed_at:(timestamp "2026-01-05T21:00:00Z") + ~reason:"again")); + Alcotest.(check bool) + "failed instruction cannot settle" true + (Result.is_error + (T.Settlement.settle failed + ~settled_at:(timestamp "2026-01-05T21:00:00Z"))); + Alcotest.(check bool) + "settled instruction cannot fail" true + (Result.is_error + (T.Settlement.fail settled + ~failed_at:(timestamp "2026-01-05T21:00:00Z") + ~reason:"again")); + Alcotest.(check string) + "total buying power name" "total_cash" + (T.Settlement.cash_buying_power_to_string T.Settlement.Total_cash); + Alcotest.(check string) + "settled buying power name" "settled_cash" + (T.Settlement.cash_buying_power_to_string T.Settlement.Settled_cash); + Alcotest.(check string) + "total position name" "total_positions" + (T.Settlement.position_availability_to_string T.Settlement.Total_positions); + Alcotest.(check string) + "settled position name" "settled_positions" + (T.Settlement.position_availability_to_string T.Settlement.Settled_positions) + +let settlement_limits_are_explicit () = + let unknown_failure = + T.Settlement.failure ~instruction_id:"unknown-settlement" ~reason:"default" + |> ok + in + let state = runner (settlement_policy ()) "unknown-failure" in + let state, _ = Runner.process_slice state (slice 1L) |> ok in + Alcotest.(check bool) + "unknown settlement failure rejected" true + (Result.is_error + (Runner.process_slice state + (slice ~settlement_failures:[ unknown_failure ] 2L))); + let state = runner ~initial_cash:"150" (settlement_policy ()) "cash-limit" in + let state, _ = Runner.process_slice state (slice 1L) |> ok in + let _, events = Runner.process_slice state (slice 2L) |> ok in + Alcotest.(check bool) + "cash buying-power limit" true + (List.exists + (fun (event : T.Audit.t) -> + match event.event with + | T.Audit.Fill_clipped + { limit = T.Risk.Settlement_cash_buying_power _; _ } -> + true + | _ -> false) + events); + let target value = + T.Strategy.Target_quantities + [ + { + instrument_id = instrument_id "test-equity"; + quantity = quantity value; + }; + ] + in + let schedule = [ (1L, [ target "2" ]); (2L, [ target "0" ]) ] in + let policy = + settlement_policy ~position_availability:T.Settlement.Settled_positions + ~lag:2 () + in + let state = runner ~schedule policy "position-limit" in + let state, _ = Runner.process_slice state (slice 1L) |> ok in + let state, _ = Runner.process_slice state (slice 2L) |> ok in + let _, events = Runner.process_slice state (slice 3L) |> ok in + Alcotest.(check bool) + "settled-position limit" true + (List.exists + (fun (event : T.Audit.t) -> + match event.event with + | T.Audit.Fill_clipped + { limit = T.Risk.Settlement_position_availability _; _ } -> + true + | _ -> false) + events) + +let tests = + [ + Alcotest.test_case "calendar and trade-date accounting" `Quick + calendar_and_trade_date_accounting; + Alcotest.test_case "engine settles due instruction" `Quick + engine_settles_due_instruction; + Alcotest.test_case "engine records settlement failure" `Quick + engine_records_settlement_failure; + Alcotest.test_case "constructors reject ambiguous inputs" `Quick + constructors_reject_ambiguous_inputs; + Alcotest.test_case "settlement limits are explicit" `Quick + settlement_limits_are_explicit; + ] diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index f1ceddd..b05ac12 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -33,6 +33,7 @@ let initialization () = ~fee_schedules:[ fee_schedule ] |> ok; financing = Some T.Financing.legacy_policy; + settlement = None; } let field name = function @@ -44,7 +45,7 @@ let initialize_message_is_complete () = T.Strategy_protocol.initialize_message ~sequence:1L (initialization ()) in Alcotest.(check string) - "protocol version" "8" + "protocol version" "9" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -221,7 +222,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "8"); + ("strategy_protocol_version", `String "9"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); From 81b6ba223dfe7177dbe91f83e7b3ae4c2327f600 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 15:31:08 -0400 Subject: [PATCH 46/57] feat: expand corporate actions and instrument lifecycle --- CHANGELOG.md | 8 + README.md | 33 +- contracts/conformance/cases.json | 84 + contracts/conformance/manifest.json | 49 + contracts/strategy/v10/README.md | 59 + contracts/strategy/v10/dune | 15 + .../v10/fixtures/external.scenario.json | 302 +++ .../v10/fixtures/external.scenario.jsonl | 4 + .../v10/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v10/message.schema.json | 302 +++ contracts/strategy/v10/transcript.schema.json | 82 + contracts/v12/README.md | 85 + contracts/v12/dune | 18 + contracts/v12/fixtures/demo.journal.jsonl | 31 + contracts/v12/fixtures/demo.scenario.json | 444 +++ contracts/v12/fixtures/demo.scenario.jsonl | 6 + .../v12/fixtures/fill-clipped.journal.jsonl | 13 + .../v12/fixtures/fill-clipped.scenario.json | 267 ++ contracts/v12/journal.schema.json | 2394 +++++++++++++++++ contracts/v12/scenario-stream.schema.json | 78 + contracts/v12/scenario.schema.json | 669 +++++ docs/api-reference.md | 2 +- docs/architecture.md | 3 +- docs/continuous-integration.md | 2 +- docs/execution-model.md | 12 +- docs/persistra.md | 6 +- docs/scenario.md | 30 +- lib/account.ml | 169 ++ lib/account.mli | 27 + lib/audit.ml | 16 + lib/audit.mli | 12 + lib/codec.ml | 146 +- lib/codec.mli | 1 + lib/contract.ml | 10 +- lib/corporate_action.ml | 73 + lib/corporate_action.mli | 26 + lib/engine.ml | 261 +- lib/engine.mli | 11 + lib/execution_model.ml | 2 +- lib/external_replay.ml | 11 +- lib/instrument_lifecycle.ml | 136 + lib/instrument_lifecycle.mli | 47 + lib/market_slice.ml | 29 +- lib/market_slice.mli | 16 + lib/replay.ml | 10 +- lib/scenario.ml | 242 +- lib/scenario_shape.ml | 36 +- lib/scenario_validation.ml | 58 +- lib/strategy_protocol.ml | 30 +- mkdocs.yml | 4 +- scripts/check-deterministic-journals | 8 + scripts/check-documentation.py | 4 +- scripts/release_artifacts.py | 12 +- test/cli.t | 2 +- test/dune | 73 + test/test_corporate_lifecycle.ml | 507 ++++ test/test_diagnostic.ml | 2 +- test/test_engine.ml | 1 + test/test_scenario.ml | 260 +- test/test_strategy_protocol.ml | 4 +- 60 files changed, 7117 insertions(+), 141 deletions(-) create mode 100644 contracts/strategy/v10/README.md create mode 100644 contracts/strategy/v10/dune create mode 100644 contracts/strategy/v10/fixtures/external.scenario.json create mode 100644 contracts/strategy/v10/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v10/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v10/message.schema.json create mode 100644 contracts/strategy/v10/transcript.schema.json create mode 100644 contracts/v12/README.md create mode 100644 contracts/v12/dune create mode 100644 contracts/v12/fixtures/demo.journal.jsonl create mode 100644 contracts/v12/fixtures/demo.scenario.json create mode 100644 contracts/v12/fixtures/demo.scenario.jsonl create mode 100644 contracts/v12/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v12/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v12/journal.schema.json create mode 100644 contracts/v12/scenario-stream.schema.json create mode 100644 contracts/v12/scenario.schema.json create mode 100644 lib/instrument_lifecycle.ml create mode 100644 lib/instrument_lifecycle.mli create mode 100644 test/test_corporate_lifecycle.ml diff --git a/CHANGELOG.md b/CHANGELOG.md index dc311aa..d64a099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +- 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. +- Add stable-identity instrument lifecycle state for halt, resume, identifier/provider remapping, + expiration, and delisting with deterministic order cancellation and explicit terminal hold or + cash-out policy. +- Publish scenario/journal contract v12 and external strategy protocol v10 while preserving v11 + and protocol v9 as frozen compatibility contracts. - Add deterministic trade-date and settlement-date accounting, versioned business-date settlement calendars, settled and unsettled cash and position attribution, explicit settlement buying-power policies, and auditable settlement completion and failure events. diff --git a/README.md b/README.md index 692ca38..50dbf26 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ scenario slices and scheduled or external intents maker/taker applicability, minimums, caps, rebates, and deterministic FX conversion - Explicit multi-currency cash ledgers and complete per-slice FX marks in a base currency - Explicit signed initial portfolios with cost basis, P&L and fee history, marks, and FX state -- Split and cash-dividend processing before matching, including target and order adjustment +- Splits, dividends, rights, spin-offs, fractional cash-in-lieu, and exact basis allocation +- Stable instrument identity with halt/resume, identifier changes, expiration, and delisting - Effective-time short locates, availability clipping, borrow-rate accrual, recalls, and deterministic close-out orders - Per-currency credit/debit cash rates with explicit day-count and compounding policies @@ -60,7 +61,7 @@ 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; v11 currently exposes `completed_bar_v1` configuration v2 + capability descriptors; v12 currently exposes `completed_bar_v1` configuration v2 - 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 @@ -90,7 +91,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v11/fixtures/demo.scenario.json \ + --input contracts/v12/fixtures/demo.scenario.json \ --validate-only ``` @@ -98,7 +99,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v11/fixtures/demo.scenario.json \ + --input contracts/v12/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -106,7 +107,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v11/fixtures/demo.scenario.jsonl \ + --input contracts/v12/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -115,7 +116,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v9/fixtures/external.scenario.json \ + --input contracts/strategy/v10/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -184,7 +185,11 @@ slice whose start is not earlier than its creation time. - Eligible liquidation orders consume capacity before other orders. Within each origin class, sells precede buys and FIFO creation order breaks ties within a side. - Corporate actions are applied before matching. Splits adjust positions, persistent targets, and - active orders; cash dividends credit longs and debit shorts in the quote-currency ledger. + active orders; distributions allocate basis and fractional cash exactly; cash dividends credit + longs and debit shorts in the quote-currency ledger. +- Lifecycle events update symbols and provider mappings without changing instrument identity. + Halts and terminal events cancel orders; expiration and delisting follow an explicit hold or + cash-out policy. - Effective-time borrow observations control short availability and rates. New shorts are rejected or clipped to their locate, recalls reject new shorts or create deterministic close-out orders, and observed borrow charges accrue before matching. @@ -226,19 +231,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 v11 and conformance fixtures](contracts/v11/README.md) +- [Current contract v12 and conformance fixtures](contracts/v12/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v11/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v11/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v11/journal.schema.json) -- [External strategy protocol v9](contracts/strategy/v9/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) - [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/v9/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v9/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v10/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v10/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 4e603e2..c36acb5 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -1190,6 +1190,90 @@ "mutations": [], "schema_expectation": "accept", "source": "strategy/v9/fixtures/external.strategy.jsonl" + }, + { + "name": "scenario-v12-valid", + "artifact": "scenario-v12", + "kind": "scenario", + "source": "v12/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept" + }, + { + "name": "scenario-stream-v12-valid", + "artifact": "scenario-stream-v12", + "kind": "scenario_stream", + "source": "v12/fixtures/demo.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept" + }, + { + "name": "strategy-ready-valid-v10", + "artifact": "strategy-message-v10", + "instance": { + "strategy_protocol_version": "10", + "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-v10", + "artifact": "strategy-message-v10", + "instance": { + "strategy_protocol_version": "10", + "strategy_sequence": "2", + "message_type": "intents", + "payload": { "intents": [] } + }, + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept", + "parser_expected": "intents" + }, + { + "name": "strategy-error-valid-v10", + "artifact": "strategy-message-v10", + "instance": { + "strategy_protocol_version": "10", + "strategy_sequence": "7", + "message_type": "error", + "payload": { "message": "fixture failure" } + }, + "mutations": [], + "schema_expectation": "accept" + }, + { + "name": "strategy-v10-rejected-response-branch", + "artifact": "strategy-transcript-v10", + "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 59f555a..e61644d 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -757,6 +757,55 @@ "sources": [ { "path": "strategy/v9/fixtures/external.strategy.jsonl", "format": "jsonl" } ] + }, + { + "name": "scenario-v12", + "schema": "v12/scenario.schema.json", + "version_field": "contract_version", + "version": "12", + "sources": [ + { "path": "v12/fixtures/demo.scenario.json", "format": "json" }, + { "path": "v12/fixtures/fill-clipped.scenario.json", "format": "json" }, + { "path": "strategy/v10/fixtures/external.scenario.json", "format": "json" } + ] + }, + { + "name": "scenario-stream-v12", + "schema": "v12/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "12", + "sources": [ + { "path": "v12/fixtures/demo.scenario.jsonl", "format": "jsonl" }, + { "path": "strategy/v10/fixtures/external.scenario.jsonl", "format": "jsonl" } + ] + }, + { + "name": "journal-v12", + "schema": "v12/journal.schema.json", + "version_field": "contract_version", + "version": "12", + "sources": [ + { "path": "v12/fixtures/demo.journal.jsonl", "format": "jsonl" }, + { "path": "v12/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" } + ] + }, + { + "name": "strategy-message-v10", + "schema": "strategy/v10/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "10", + "sources": [ + { "path": "strategy/v10/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"] } + ] + }, + { + "name": "strategy-transcript-v10", + "schema": "strategy/v10/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "10", + "sources": [ + { "path": "strategy/v10/fixtures/external.strategy.jsonl", "format": "jsonl" } + ] } ] } diff --git a/contracts/strategy/v10/README.md b/contracts/strategy/v10/README.md new file mode 100644 index 0000000..17c89a1 --- /dev/null +++ b/contracts/strategy/v10/README.md @@ -0,0 +1,59 @@ +# External strategy protocol v10 + +Version 10 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 v9 remains available for scenario contract v11; earlier versions retain their frozen +shapes. + +Every message repeats `strategy_protocol_version: "10"` 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 v12 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 v12 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/v10/dune b/contracts/strategy/v10/dune new file mode 100644 index 0000000..7fa7432 --- /dev/null +++ b/contracts/strategy/v10/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v10/message.schema.json) + (transcript.schema.json as contracts/strategy/v10/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v10/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v10/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v10/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v10/fixtures/external.scenario.json b/contracts/strategy/v10/fixtures/external.scenario.json new file mode 100644 index 0000000..c37e23b --- /dev/null +++ b/contracts/strategy/v10/fixtures/external.scenario.json @@ -0,0 +1,302 @@ +{ + "contract_version": "12", + "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/v10/fixtures/external.scenario.jsonl b/contracts/strategy/v10/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..e428012 --- /dev/null +++ b/contracts/strategy/v10/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"12","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":"12","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":"12","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":"12","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v10/fixtures/external.strategy.jsonl b/contracts/strategy/v10/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..0b07034 --- /dev/null +++ b/contracts/strategy/v10/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"10","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"12","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":"10","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"10","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"10","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"10","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"10","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"10","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"10","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v10/message.schema.json b/contracts/strategy/v10/message.schema.json new file mode 100644 index 0000000..cf722e9 --- /dev/null +++ b/contracts/strategy/v10/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/v10/message.schema.json", + "title": "Trading Engine external strategy protocol v10 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": "10" }, + "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": "12" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/venueCalendar" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/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/v12/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/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/v12/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/signedDecimal" }, + "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/signedDecimal" }, + "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/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/v12/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/v12/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/v12/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/v10/transcript.schema.json b/contracts/strategy/v10/transcript.schema.json new file mode 100644 index 0000000..02d3ca0 --- /dev/null +++ b/contracts/strategy/v10/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/v10/transcript.schema.json", + "title": "Trading Engine external strategy protocol v10 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": "10" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v10/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/v12/README.md b/contracts/v12/README.md new file mode 100644 index 0000000..970097d --- /dev/null +++ b/contracts/v12/README.md @@ -0,0 +1,85 @@ +# Trading Engine contract v12 + +This directory is the authoritative v12 process and file contract shared by Trading Engine and its +clients. Versions 11 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 v12 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 v12 scenario, stream record, and journal record carries `"contract_version": "12"`. + +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. diff --git a/contracts/v12/dune b/contracts/v12/dune new file mode 100644 index 0000000..5cff266 --- /dev/null +++ b/contracts/v12/dune @@ -0,0 +1,18 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v12/journal.schema.json) + (scenario-stream.schema.json as contracts/v12/scenario-stream.schema.json) + (scenario.schema.json as contracts/v12/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v12/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v12/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl + as + contracts/v12/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v12/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v12/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v12/fixtures/demo.journal.jsonl b/contracts/v12/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..204d4df --- /dev/null +++ b/contracts/v12/fixtures/demo.journal.jsonl @@ -0,0 +1,31 @@ +{"contract_version":"12","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":"ee80423182d458afa2458803c30af1d18f0a8d873bbfe4e16c510920a6aee7d3","execution_model":"completed_bar_v1"}} +{"contract_version":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12"}],"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":"12","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":"12","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":"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","price":"103","notional":"618","fee":"0.868","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.618","quote_amount":"0.618"}]}} +{"contract_version":"12","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":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000001-settlement","fill_id":"demo-fill-000000000001","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-618.868","position_movement":"6","trade_date":"2026-01-05","due_date":"2026-01-06","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"12","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","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"12","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.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-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":"12","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":"9381.280402","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5.148402","unrealized_pnl":"40.132","equity":"10130.280402","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.280402","fx_rate":"1","base_value":"9381.280402","interest":"0.148402","base_interest":"0.148402","settled_amount":"10000.148402","unsettled_amount":"-618.868","base_settled_value":"10000.148402","base_unsettled_value":"-618.868"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618","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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"settled_quantity":"1","unsettled_quantity":"6"}],"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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"cash_interest":"0.148402","settled_cash":"10000.148402","unsettled_cash":"-618.868","margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.780402","maintenance_excess":"9943.030402","margin_call":false},"group_exposures":[]}} +{"contract_version":"12","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":"12","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":"settlement_completed","payload":{"instruction_id":"demo-fill-000000000001-settlement","fill_id":"demo-fill-000000000001","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-618.868","position_movement":"6","trade_date":"2026-01-05","due_date":"2026-01-06","status":"settled","settled_at":"2026-01-06T14:30:00.000000Z","failed_at":null,"failure_reason":null}} +{"contract_version":"12","engine_sequence":"19","event_id":"demo-event-000000000019","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":"9381.280402","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.06961","closing_balance":"9381.350012"}} +{"contract_version":"12","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"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.715","price":"107","notional":"290.505","fee":"0.540505","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.290505","quote_amount":"0.290505"}]}} +{"contract_version":"12","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000020"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000002-settlement","fill_id":"demo-fill-000000000002","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-291.045505","position_movement":"2.715","trade_date":"2026-01-06","due_date":"2026-01-07","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"12","engine_sequence":"22","event_id":"demo-event-000000000022","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":"12","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000017","demo-event-000000000022"],"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-000000000023","updated_event_id":"demo-event-000000000023","created_sequence":"23","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":"12","engine_sequence":"24","event_id":"demo-event-000000000024","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.304507","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5.218012","unrealized_pnl":"20.161495","equity":"10110.379507","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.304507","fx_rate":"1","base_value":"9090.304507","interest":"0.218012","base_interest":"0.218012","settled_amount":"9381.350012","unsettled_amount":"-291.045505","base_settled_value":"9381.350012","base_unsettled_value":"-291.045505"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505","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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"settled_quantity":"7","unsettled_quantity":"2.715"}],"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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"cash_interest":"0.218012","settled_cash":"9381.350012","unsettled_cash":"-291.045505","margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.342007","maintenance_excess":"9855.360757","margin_call":false},"group_exposures":[]}} +{"contract_version":"12","engine_sequence":"25","event_id":"demo-event-000000000025","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":"12","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000025"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"settlement_completed","payload":{"instruction_id":"demo-fill-000000000002-settlement","fill_id":"demo-fill-000000000002","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-291.045505","position_movement":"2.715","trade_date":"2026-01-06","due_date":"2026-01-07","status":"settled","settled_at":"2026-01-07T14:30:00.000000Z","failed_at":null,"failure_reason":null}} +{"contract_version":"12","engine_sequence":"27","event_id":"demo-event-000000000027","causation_ids":["demo-event-000000000025"],"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.304507","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.067451","closing_balance":"9090.371958"}} +{"contract_version":"12","engine_sequence":"28","event_id":"demo-event-000000000028","causation_ids":["demo-event-000000000023","demo-event-000000000025"],"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":"105","notional":"757.575","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.757575","quote_amount":"0.757575"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_amount":"-0.007575"}]}} +{"contract_version":"12","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":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000003-settlement","fill_id":"demo-fill-000000000003","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"756.575","position_movement":"-7.215","trade_date":"2026-01-07","due_date":"2026-01-08","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"12","engine_sequence":"30","event_id":"demo-event-000000000030","causation_ids":["demo-event-000000000025"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463","settled_amount":"9090.371958","unsettled_amount":"756.575","base_settled_value":"9090.371958","base_unsettled_value":"756.575"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"settled_quantity":"9.715","unsettled_quantity":"-7.215"}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","settled_cash":"9090.371958","unsettled_cash":"756.575","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]}} +{"contract_version":"12","engine_sequence":"31","event_id":"demo-event-000000000031","causation_ids":["demo-event-000000000030"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"ee80423182d458afa2458803c30af1d18f0a8d873bbfe4e16c510920a6aee7d3","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463","settled_amount":"9090.371958","unsettled_amount":"756.575","base_settled_value":"9090.371958","base_unsettled_value":"756.575"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"settled_quantity":"9.715","unsettled_quantity":"-7.215"}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","settled_cash":"9090.371958","unsettled_cash":"756.575","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v12/fixtures/demo.scenario.json b/contracts/v12/fixtures/demo.scenario.json new file mode 100644 index 0000000..cbabef0 --- /dev/null +++ b/contracts/v12/fixtures/demo.scenario.json @@ -0,0 +1,444 @@ +{ + "contract_version": "12", + "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_v1", + "configuration": { + "version": "2", + "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" + } + ] + } + ] + } + }, + "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": "12" + } + ], + "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/v12/fixtures/demo.scenario.jsonl b/contracts/v12/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..d4e5fbb --- /dev/null +++ b/contracts/v12/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"12","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_v1","configuration":{"version":"2","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"}]}]}},"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":"12","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":"12","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":"12"}],"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":"12","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":"12","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":"12","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v12/fixtures/fill-clipped.journal.jsonl b/contracts/v12/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..168bcc2 --- /dev/null +++ b/contracts/v12/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"12","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":"24baff6bd716f87e9bb1aab521527ba056c089390c442e6e650ea193df12abfe","execution_model":"completed_bar_v1"}} +{"contract_version":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"24baff6bd716f87e9bb1aab521527ba056c089390c442e6e650ea193df12abfe","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/v12/fixtures/fill-clipped.scenario.json b/contracts/v12/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..ff34f55 --- /dev/null +++ b/contracts/v12/fixtures/fill-clipped.scenario.json @@ -0,0 +1,267 @@ +{ + "contract_version": "12", + "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/v12/journal.schema.json b/contracts/v12/journal.schema.json new file mode 100644 index 0000000..210f8e3 --- /dev/null +++ b/contracts/v12/journal.schema.json @@ -0,0 +1,2394 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v12/journal.schema.json", + "title": "Trading Engine v12 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": "12" + }, + "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", + "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": "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": { + "const": "completed_bar_v1" + } + } + }, + "initialState": { + "type": "object", + "additionalProperties": false, + "required": [ + "portfolio", + "valuation" + ], + "properties": { + "portfolio": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/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/v12/scenario.schema.json#/$defs/borrowObservation" + } + }, + "cash_rate_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/cashRateObservation" + } + }, + "settlement_failures": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/settlementFailure" + } + }, + "lifecycle_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/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" + } + } + }, + "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/v12/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/v12/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/v12/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": { + "const": "completed_bar_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/v12/scenario-stream.schema.json b/contracts/v12/scenario-stream.schema.json new file mode 100644 index 0000000..e6aa4dd --- /dev/null +++ b/contracts/v12/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/v12/scenario-stream.schema.json", + "title": "Trading Engine v12 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": "12" }, + "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": "12" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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": "12" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v12/scenario.schema.json b/contracts/v12/scenario.schema.json new file mode 100644 index 0000000..82c0c8c --- /dev/null +++ b/contracts/v12/scenario.schema.json @@ -0,0 +1,669 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json", + "title": "Trading Engine v12 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": "12" }, + "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": { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + "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" } } + } + }, + "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 3734265..b93d25d 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/v11/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v12/README.md) remain authoritative for process boundaries. diff --git a/docs/architecture.md b/docs/architecture.md index b1b1bbf..8f6357f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -55,7 +55,8 @@ For each synchronized market slice, the engine: 1. Validates catalog coverage, slice order, receipt order, and market time. 2. Stores the synchronized closes and complete FX vector, emits `run_started` once, and then emits `market_slice_received`. -3. Applies splits and dividends, adjusting signed positions, persistent targets, and active orders. +3. Applies corporate distributions and lifecycle events, adjusting positions, basis, targets, + listings, and active orders. 4. Accrues borrow fees on open shorts for the slice interval. 5. Fixes the priority sequence of orders that became eligible after an earlier slice. 6. Offers each instrument's remaining capacity to liquidation orders first, then applies diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index c179de0..b635fb7 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 v11 batch, stream, journal, and +The full test suite additionally validates and replays the current v12 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 b07c337..e20c710 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,11 +1,11 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v11 advertises and accepts `completed_bar_v1`; embedders can inject another module through +Contract v12 advertises and accepts `completed_bar_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 v11 envelope separates selection from +Each compiled model owns a strict configuration contract. The v12 envelope separates selection from model-specific parameters: ```json @@ -143,6 +143,14 @@ order may exceed that maximum, but no individual fill may do so. A cash dividend multiplies the pre-match signed position by its per-unit amount. It credits a long or debits a short in the instrument's quote-currency ledger and records realized dividend P&L. +Stock dividends, rights, and spin-offs deliver a lot-aligned exact-ratio entitlement. Their payload +allocates basis explicitly and either rejects fractions or converts them at a declared +quote-currency price. Stock dividends also scale persistent targets and eligible working orders. + +Lifecycle events follow corporate actions and precede matching. Identifier changes preserve the +stable instrument ID while updating the symbol and named provider mapping. Halts and terminal +events cancel active orders. Expiration and delisting use an explicit hold or cash-out policy and +cannot be resumed. Contract v10 replaces the fixed legacy rate with effective-time borrow observations. Each observation names an instrument, available quantity, annual rate in basis points, and recall state. Observations become active no later than the slice start and remain active until superseded. A new diff --git a/docs/persistra.md b/docs/persistra.md index 4b66b78..285b155 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 v11 while retaining v10 through v3 and +structural checks. The engine advertises current contract v12 while retaining v11 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 v9](../contracts/strategy/v9/README.md). Persistra's host turns protocol +[strategy protocol v10](../contracts/strategy/v10/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,7 +79,7 @@ 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 v11 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v12 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, diff --git a/docs/scenario.md b/docs/scenario.md index 613f761..663ccf6 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 v11 demo](../contracts/v11/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v11/scenario.schema.json) provides structural validation. +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. 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/v11/scenario-stream.schema.json) validates each line, -and [the v11 stream fixture](../contracts/v11/fixtures/demo.scenario.jsonl) is the canonical example. +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 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; v11 is `"11"` | +| `contract_version` | Required string identifying this file contract; v12 is `"12"` | | `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 | @@ -222,7 +222,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 v11 `settlement` object selects `total_cash` or `settled_cash` buying power and +The v12 `settlement` object selects `total_cash` or `settled_cash` buying power and `total_positions` or `settled_positions` availability. Its immutable calendars contain ordered canonical business dates, and each instrument has exactly one calendar and a lag from zero through 30 business days. A fill updates economic accounting immediately and creates a deterministic @@ -230,7 +230,11 @@ instruction. Pending cash and quantity appear as unsettled attribution until the after the due date. A due instruction named in that slice's `settlement_failures` becomes failed instead, retains its unsettled balances, and records the supplied reason. -Supported corporate actions are exact-ratio `split` and per-unit `cash_dividend` records. Action +Supported corporate actions are exact-ratio `split`, per-unit `cash_dividend`, `stock_dividend`, +`rights`, and `spin_off` records. Distribution records name a destination instrument, entitlement +ratio, basis allocation in basis points, and a fractional policy. `reject` fails on a non-lot +entitlement; `cash_in_lieu` requires an explicit destination-quote-currency price and journals the +delivered quantity, fractional quantity, allocated basis, fractional basis, and cash amount. Action IDs are unique across the scenario. Actions are applied in canonical ID order before borrow fees and matching. A split rescales the position, persistent target, and active orders while preserving basis; it does not rescale unit-based risk limits. Split-adjusted positions and targets are @@ -239,12 +243,19 @@ but each fill is bounded by `max_order_quantity`; GTC limit remainders may fill while market IOC remainders are cancelled. A dividend changes the quote-currency cash ledger and realized dividend P&L, crediting a long and debiting a short. +Version 12 slices also carry `lifecycle_events`. Stable `instrument_id` never changes. An +`identifier_change` updates the current symbol and one named provider mapping with provenance; +`halt` and `resume` control whether new exposure is accepted. `expiration` and `delisting` are +terminal and require either `hold` or an explicit quote-currency `cash_out` price. Halts and +terminal events cancel active orders. Terminal events set persistent target exposure to zero and +cash-out clears the position with exact realized-P&L attribution. + For causal next-open execution, an order-changing schedule entry's anchor `received_at` is no later than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v11/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v12/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 @@ -263,7 +274,8 @@ fill and the greatest lot-aligned permitted quantity. Its reason taxonomy versio ratio, or basis-points threshold. Each order snapshot retains both creation and latest-update event IDs. -The journal also records split/dividend application, split-driven order adjustments, observed +The journal also records split/dividend/distribution application, lifecycle transitions, +action-driven order adjustments, observed borrow charges, recalls and close-outs, cash-interest entries, margin calls, liquidation-origin orders, and restoration. Every valuation contains complete per-currency cash attribution, signed per-instrument native and base-currency attribution, long, diff --git a/lib/account.ml b/lib/account.ml index 2d4b2af..ecfffbf 100644 --- a/lib/account.ml +++ b/lib/account.ml @@ -469,6 +469,175 @@ let apply_cash_dividend (state : t) ~instrument_id ~quote_currency positions = update_position state.positions instrument_id updated; } +type distribution_result = { + source_quantity : Scalar.Quantity.t; + destination_quantity : Scalar.Quantity.t; + fractional_quantity : Scalar.Quantity.t; + allocated_basis : Scalar.Money.t; + fractional_basis : Scalar.Money.t; + cash_in_lieu : Scalar.Money.t; +} + +let money_bps_toward_zero value bps = + let numerator = + Z.mul (Z.of_int64 (Scalar.Money.to_micros value)) (Z.of_int bps) + in + let result = Z.div numerator (Z.of_int 10_000) in + if Z.fits_int64 result then Ok (Scalar.Money.of_micros (Z.to_int64 result)) + else Error "distribution basis allocation overflow" + +let apply_distribution (state : t) ~source_instrument_id + ~destination_instrument_id ~destination_lot_size ~numerator ~denominator + ~basis_allocation_bps ~fractional_policy = + let source = position state source_instrument_id in + let destination = position state destination_instrument_id in + let same_instrument = + Id.Instrument.equal source_instrument_id destination_instrument_id + in + let* () = + if + (not same_instrument) + && (not (Scalar.Quantity.is_zero source.quantity)) + && (not (Scalar.Quantity.is_zero destination.quantity)) + && Scalar.Quantity.is_positive source.quantity + <> Scalar.Quantity.is_positive destination.quantity + then Error "distribution cannot cross an opposite destination position" + else Ok () + in + let* entitlement = + Scalar.Quantity.scale_ratio_exact source.quantity ~numerator ~denominator + in + let* delivered = + Scalar.Quantity.round_toward_zero_to_multiple entitlement + ~multiple:destination_lot_size + in + let* fractional = Scalar.Quantity.subtract entitlement delivered in + let has_fractional = not (Scalar.Quantity.is_zero fractional) in + let* cash_in_lieu = + match (has_fractional, fractional_policy) with + | false, _ -> Ok Scalar.Money.zero + | true, Corporate_action.Reject_fractional -> + Error "distribution produces a fractional entitlement" + | true, Cash_in_lieu { price; _ } -> Scalar.Money.notional price fractional + in + let* allocated_basis = + money_bps_toward_zero source.cost_basis basis_allocation_bps + in + let* source_cost_basis = + Scalar.Money.subtract source.cost_basis allocated_basis + in + let* absolute_entitlement = Scalar.Quantity.absolute entitlement in + let* absolute_delivered = Scalar.Quantity.absolute delivered in + let* delivered_basis = + if Scalar.Quantity.is_zero absolute_entitlement then Ok Scalar.Money.zero + else + Scalar.Money.proportion_toward_zero allocated_basis + ~numerator:absolute_delivered ~denominator:absolute_entitlement + in + let* fractional_basis = + Scalar.Money.subtract allocated_basis delivered_basis + in + let* source_realized = + if has_fractional then + let* delta = Scalar.Money.subtract cash_in_lieu fractional_basis in + Scalar.Money.add source.realized_pnl delta + else Ok source.realized_pnl + in + let* destination_quantity = + Scalar.Quantity.add destination.quantity delivered + in + let* destination_basis = + Scalar.Money.add destination.cost_basis delivered_basis + in + let positions = + if same_instrument then + update_position state.positions source_instrument_id + { + source with + quantity = destination_quantity; + cost_basis = destination_basis; + realized_pnl = source_realized; + } + else + update_position state.positions source_instrument_id + { + source with + cost_basis = source_cost_basis; + realized_pnl = source_realized; + } + |> fun positions -> + update_position positions destination_instrument_id + { + destination with + quantity = destination_quantity; + cost_basis = destination_basis; + } + in + let settled_source = settled_position_quantity state source_instrument_id in + let* settled_entitlement = + Scalar.Quantity.scale_ratio_exact settled_source ~numerator ~denominator + in + let* settled_delivered = + Scalar.Quantity.round_toward_zero_to_multiple settled_entitlement + ~multiple:destination_lot_size + in + let settled_destination = + settled_position_quantity state destination_instrument_id + in + let* settled_destination = + Scalar.Quantity.add settled_destination settled_delivered + in + let settled_positions = + if Scalar.Quantity.is_zero settled_destination then + Id.Instrument.Map.remove destination_instrument_id state.settled_positions + else + Id.Instrument.Map.add destination_instrument_id settled_destination + state.settled_positions + in + let state = { state with positions; settled_positions } in + let* state = + match fractional_policy with + | Corporate_action.Cash_in_lieu { currency; _ } when has_fractional -> + let* state = adjust_cash state currency cash_in_lieu in + adjust_settled_cash state currency cash_in_lieu + | Reject_fractional | Cash_in_lieu _ -> Ok state + in + Ok + ( state, + { + source_quantity = source.quantity; + destination_quantity = delivered; + fractional_quantity = fractional; + allocated_basis; + fractional_basis; + cash_in_lieu; + } ) + +let cash_out_position (state : t) ~instrument_id ~currency ~price = + let current = position state instrument_id in + let* proceeds = Scalar.Money.notional price current.quantity in + let* state = adjust_cash state currency proceeds in + let* state = adjust_settled_cash state currency proceeds in + let* realized_delta = Scalar.Money.subtract proceeds current.cost_basis in + let* realized_pnl = Scalar.Money.add current.realized_pnl realized_delta in + let updated = + { + current with + quantity = Scalar.Quantity.zero; + cost_basis = Scalar.Money.zero; + realized_pnl; + } + in + Ok + ( { + state with + positions = update_position state.positions instrument_id updated; + settled_positions = + Id.Instrument.Map.remove instrument_id state.settled_positions; + }, + current.quantity, + proceeds ) + let apply_borrow_fee (state : t) ~instrument_id ~quote_currency ~fee = let current = position state instrument_id in if not (Scalar.Quantity.is_negative current.quantity) then diff --git a/lib/account.mli b/lib/account.mli index bfbcc8f..ac4a1f5 100644 --- a/lib/account.mli +++ b/lib/account.mli @@ -127,6 +127,33 @@ val apply_cash_dividend : amount_per_unit:Scalar.Money.t -> (t, string) result +type distribution_result = { + source_quantity : Scalar.Quantity.t; + destination_quantity : Scalar.Quantity.t; + fractional_quantity : Scalar.Quantity.t; + allocated_basis : Scalar.Money.t; + fractional_basis : Scalar.Money.t; + cash_in_lieu : Scalar.Money.t; +} + +val apply_distribution : + t -> + source_instrument_id:Id.Instrument.t -> + destination_instrument_id:Id.Instrument.t -> + destination_lot_size:Scalar.Quantity.t -> + numerator:int64 -> + denominator:int64 -> + basis_allocation_bps:int -> + fractional_policy:Corporate_action.fractional_policy -> + (t * distribution_result, string) result + +val cash_out_position : + t -> + instrument_id:Id.Instrument.t -> + currency:string -> + price:Scalar.Price.t -> + (t * Scalar.Quantity.t * Scalar.Money.t, string) result + val apply_borrow_fee : t -> instrument_id:Id.Instrument.t -> diff --git a/lib/audit.ml b/lib/audit.ml index ddcfe9c..a57d669 100644 --- a/lib/audit.ml +++ b/lib/audit.ml @@ -8,6 +8,8 @@ type cancellation_reason = | Gtd_expired | Margin_call | Borrow_recall + | Instrument_halt + | Instrument_terminal type target_basis = Weights | Quantities @@ -50,6 +52,16 @@ type event = quantity : Scalar.Quantity.t; cash_amount : Scalar.Money.t; } + | Distribution_applied of { + action : Corporate_action.t; + result : Account.distribution_result; + } + | Lifecycle_applied of { + lifecycle_event : Instrument_lifecycle.event; + listing : Instrument_lifecycle.listing; + liquidated_quantity : Scalar.Quantity.t; + cash_amount : Scalar.Money.t; + } | Order_adjusted of { order : Order.t; action_id : Id.Corporate_action.t } | Fill_applied of Fill.t | Settlement_instruction_created of Settlement.instruction @@ -155,6 +167,8 @@ let cancellation_reason_to_string = function | Gtd_expired -> "gtd_expired" | Margin_call -> "margin_call" | Borrow_recall -> "borrow_recall" + | Instrument_halt -> "instrument_halt" + | Instrument_terminal -> "instrument_terminal" let target_basis_to_string = function | Weights -> "weights" @@ -171,6 +185,8 @@ let event_name = function | Order_cancelled _ -> "order_cancelled" | Split_applied _ -> "split_applied" | Cash_dividend_applied _ -> "cash_dividend_applied" + | Distribution_applied _ -> "distribution_applied" + | Lifecycle_applied _ -> "lifecycle_applied" | Order_adjusted _ -> "order_adjusted" | Fill_applied _ -> "fill_applied" | Settlement_instruction_created _ -> "settlement_instruction_created" diff --git a/lib/audit.mli b/lib/audit.mli index c016e4e..6866ce7 100644 --- a/lib/audit.mli +++ b/lib/audit.mli @@ -10,6 +10,8 @@ type cancellation_reason = | Gtd_expired | Margin_call | Borrow_recall + | Instrument_halt + | Instrument_terminal type target_basis = Weights | Quantities @@ -52,6 +54,16 @@ type event = quantity : Scalar.Quantity.t; cash_amount : Scalar.Money.t; } + | Distribution_applied of { + action : Corporate_action.t; + result : Account.distribution_result; + } + | Lifecycle_applied of { + lifecycle_event : Instrument_lifecycle.event; + listing : Instrument_lifecycle.listing; + liquidated_quantity : Scalar.Quantity.t; + cash_amount : Scalar.Money.t; + } | Order_adjusted of { order : Order.t; action_id : Id.Corporate_action.t } | Fill_applied of Fill.t | Settlement_instruction_created of Settlement.instruction diff --git a/lib/codec.ml b/lib/codec.ml index 6d48fca..10e85c3 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -241,6 +241,97 @@ let corporate_action_to_yojson action = `Assoc ((("type", string "cash_dividend") :: common) @ [ ("amount_per_unit", money amount_per_unit) ]) + | Corporate_action.Distribution + { + distribution_type; + destination_instrument_id; + numerator; + denominator; + basis_allocation_bps; + fractional_policy; + } -> + let fractional_policy = + match fractional_policy with + | Corporate_action.Reject_fractional -> + `Assoc [ ("policy", string "reject") ] + | Cash_in_lieu { price = value; currency } -> + `Assoc + [ + ("policy", string "cash_in_lieu"); + ("price", price value); + ("currency", string currency); + ] + in + `Assoc + (( "type", + string + (Corporate_action.distribution_type_to_string distribution_type) ) + :: common + @ [ + ( "destination_instrument_id", + instrument_id destination_instrument_id ); + ("numerator", int64 numerator); + ("denominator", int64 denominator); + ("basis_allocation_bps", `Int basis_allocation_bps); + ("fractional_policy", fractional_policy); + ]) + +let terminal_policy_to_yojson = function + | Instrument_lifecycle.Hold -> `Assoc [ ("policy", string "hold") ] + | Cash_out { price = value; currency } -> + `Assoc + [ + ("policy", string "cash_out"); + ("price", price value); + ("currency", string currency); + ] + +let lifecycle_event_to_yojson event = + let common = + [ + ( "event_id", + string (Id.Corporate_action.to_string event.Instrument_lifecycle.id) ); + ("instrument_id", instrument_id event.instrument_id); + ] + in + match event.kind with + | Instrument_lifecycle.Halt { reason } -> + `Assoc (("type", string "halt") :: ("reason", string reason) :: common) + | Resume -> `Assoc (("type", string "resume") :: common) + | Identifier_change { symbol; provider; provider_instrument_id } -> + `Assoc + ((("type", string "identifier_change") :: common) + @ [ + ("symbol", string symbol); + ("provider", string provider); + ("provider_instrument_id", string provider_instrument_id); + ]) + | Expiration { terminal_policy } -> + `Assoc + ((("type", string "expiration") :: common) + @ [ ("terminal_policy", terminal_policy_to_yojson terminal_policy) ]) + | Delisting { terminal_policy; reason } -> + `Assoc + ((("type", string "delisting") :: ("reason", string reason) :: common) + @ [ ("terminal_policy", terminal_policy_to_yojson terminal_policy) ]) + +let lifecycle_listing_to_yojson listing = + `Assoc + [ + ("instrument_id", instrument_id listing.Instrument_lifecycle.instrument_id); + ("symbol", string listing.symbol); + ("status", string (Instrument_lifecycle.status_to_string listing.status)); + ( "provider_mappings", + `List + (List.map + (fun (provider, provider_instrument_id) -> + `Assoc + [ + ("provider", string provider); + ("provider_instrument_id", string provider_instrument_id); + ]) + listing.provider_mappings) ); + ] let borrow_observation_to_yojson observation = `Assoc @@ -284,9 +375,9 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ); ] |> function - | `Assoc fields when List.mem contract_version [ "11"; "10" ] -> + | `Assoc fields when List.mem contract_version [ "12"; "11"; "10" ] -> let settlement = - if String.equal contract_version "11" then + if List.mem contract_version [ "12"; "11" ] then [ ( "settlement_failures", `List @@ -295,6 +386,16 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ] else [] in + let lifecycle = + if String.equal contract_version "12" then + [ + ( "lifecycle_events", + `List + (List.map lifecycle_event_to_yojson + market_slice.Market_slice.lifecycle_events) ); + ] + else [] + in `Assoc (fields @ [ @@ -307,7 +408,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = (List.map cash_rate_observation_to_yojson market_slice.Market_slice.cash_rate_observations) ); ] - @ settlement) + @ settlement @ lifecycle) | json -> json let market_slice_to_yojson market_slice = @@ -319,6 +420,9 @@ let market_slice_to_yojson_v10 market_slice = let market_slice_to_yojson_v11 market_slice = versioned_market_slice_to_yojson ~contract_version:"11" market_slice +let market_slice_to_yojson_v12 market_slice = + versioned_market_slice_to_yojson ~contract_version:"12" market_slice + let request_fields request = let kind, limit_price = match request.Order.kind with @@ -422,7 +526,7 @@ let order_to_yojson_v8 order = ]) let versioned_order_to_yojson ~contract_version order = - if List.mem contract_version [ "11"; "10"; "9"; "8" ] then + if List.mem contract_version [ "12"; "11"; "10"; "9"; "8" ] then order_to_yojson_v8 order else order_to_yojson order @@ -620,7 +724,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "cash_balances", `List (List.map - (if String.equal contract_version "11" then + (if List.mem contract_version [ "12"; "11" ] then cash_attribution_to_yojson_v11 else if String.equal contract_version "10" then cash_attribution_to_yojson_v10 @@ -629,7 +733,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "positions", `List (List.map - (if String.equal contract_version "11" then + (if List.mem contract_version [ "12"; "11" ] then position_attribution_to_yojson_v11 else if String.equal contract_version "9" @@ -639,14 +743,14 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = valuation.positions) ); ] |> function - | `Assoc fields when List.mem contract_version [ "11"; "10"; "9" ] -> + | `Assoc fields when List.mem contract_version [ "12"; "11"; "10"; "9" ] -> let financing = - if List.mem contract_version [ "11"; "10" ] then + if List.mem contract_version [ "12"; "11"; "10" ] then [ ("cash_interest", money valuation.Account.cash_interest) ] else [] in let settlement = - if String.equal contract_version "11" then + if List.mem contract_version [ "12"; "11" ] then [ ("settled_cash", money valuation.Account.settled_cash); ("unsettled_cash", money valuation.unsettled_cash); @@ -693,7 +797,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 [ "11"; "10"; "9"; "8" ] then + if List.mem contract_version [ "12"; "11"; "10"; "9"; "8" ] then fields @ [ ( "group_exposures", @@ -795,6 +899,26 @@ let payload_to_yojson ~contract_version = function ("quantity", quantity held); ("cash_amount", money cash_amount); ] + | Audit.Distribution_applied { action; result } -> + `Assoc + [ + ("action", corporate_action_to_yojson action); + ("source_quantity", quantity result.Account.source_quantity); + ("destination_quantity", quantity result.destination_quantity); + ("fractional_quantity", quantity result.fractional_quantity); + ("allocated_basis", money result.allocated_basis); + ("fractional_basis", money result.fractional_basis); + ("cash_in_lieu", money result.cash_in_lieu); + ] + | Audit.Lifecycle_applied + { lifecycle_event; listing; liquidated_quantity; cash_amount } -> + `Assoc + [ + ("lifecycle_event", lifecycle_event_to_yojson lifecycle_event); + ("listing", lifecycle_listing_to_yojson listing); + ("liquidated_quantity", quantity liquidated_quantity); + ("cash_amount", money cash_amount); + ] | Audit.Order_adjusted { order; action_id } -> `Assoc [ @@ -802,7 +926,7 @@ let payload_to_yojson ~contract_version = function ("action_id", string (Id.Corporate_action.to_string action_id)); ] | Audit.Fill_applied fill -> - if List.mem contract_version [ "11"; "10"; "9" ] then + if List.mem contract_version [ "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 0c31a15..497651a 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -6,6 +6,7 @@ val bar_to_yojson : Bar.t -> Yojson.Safe.t 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 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 ff2231d..0bad3aa 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,11 +1,12 @@ -let version = "11" -let previous_version = "10" +let version = "12" +let previous_version = "11" let legacy_journal_version = "3" let supported_versions = [ version; previous_version; + "10"; "9"; "8"; "7"; @@ -16,8 +17,8 @@ let supported_versions = ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "9" -let previous_strategy_protocol_version = "8" +let strategy_protocol_version = "10" +let previous_strategy_protocol_version = "9" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -36,6 +37,7 @@ let capabilities_to_yojson () = [ strategy_protocol_version; previous_strategy_protocol_version; + "8"; "7"; "6"; "5"; diff --git a/lib/corporate_action.ml b/lib/corporate_action.ml index 3e75b9d..68c7a98 100644 --- a/lib/corporate_action.ml +++ b/lib/corporate_action.ml @@ -1,6 +1,20 @@ type kind = | Split of { numerator : int64; denominator : int64 } | Cash_dividend of { amount_per_unit : Scalar.Money.t } + | Distribution of { + distribution_type : distribution_type; + destination_instrument_id : Id.Instrument.t; + numerator : int64; + denominator : int64; + basis_allocation_bps : int; + fractional_policy : fractional_policy; + } + +and distribution_type = Stock_dividend | Rights | Spin_off + +and fractional_policy = + | Reject_fractional + | Cash_in_lieu of { price : Scalar.Price.t; currency : string } type t = { id : Id.Corporate_action.t; @@ -20,6 +34,61 @@ let cash_dividend ~id ~instrument_id ~amount_per_unit = Error "cash dividend amount per unit must be positive" else Ok { id; instrument_id; kind = Cash_dividend { amount_per_unit } } +let valid_label value = + String.length value > 0 + && String.for_all + (fun character -> + let code = Char.code character in + code >= 0x21 && code <> 0x7f) + value + +let distribution ~id ~instrument_id ~distribution_type + ~destination_instrument_id ~numerator ~denominator ~basis_allocation_bps + ~fractional_policy = + if Int64.compare numerator 0L <= 0 || Int64.compare denominator 0L <= 0 then + Error "distribution numerator and denominator must be positive" + else if basis_allocation_bps < 0 || basis_allocation_bps > 10_000 then + Error "distribution basis allocation must be between 0 and 10000 bps" + else if + distribution_type = Stock_dividend + && not (Id.Instrument.equal instrument_id destination_instrument_id) + then Error "stock dividend destination must be its source instrument" + else if distribution_type = Stock_dividend && basis_allocation_bps <> 0 then + Error "stock dividend basis allocation must be zero" + else if + distribution_type = Stock_dividend + && Int64.compare numerator (Int64.sub Int64.max_int denominator) > 0 + then Error "stock dividend total ratio overflows" + else if + distribution_type <> Stock_dividend + && Id.Instrument.equal instrument_id destination_instrument_id + then Error "rights and spin-off destinations must differ from their source" + else + match fractional_policy with + | Cash_in_lieu { currency; _ } when not (valid_label currency) -> + Error "cash-in-lieu currency must not be empty or contain whitespace" + | Reject_fractional | Cash_in_lieu _ -> + Ok + { + id; + instrument_id; + kind = + Distribution + { + distribution_type; + destination_instrument_id; + numerator; + denominator; + basis_allocation_bps; + fractional_policy; + }; + } + +let distribution_type_to_string = function + | Stock_dividend -> "stock_dividend" + | Rights -> "rights" + | Spin_off -> "spin_off" + let compare left right = Id.Corporate_action.compare left.id right.id let pp formatter action = @@ -29,6 +98,10 @@ let pp formatter action = Printf.sprintf "split %Ld:%Ld" numerator denominator | Cash_dividend { amount_per_unit } -> "dividend " ^ Scalar.Money.to_decimal_string amount_per_unit + | Distribution { distribution_type; numerator; denominator; _ } -> + Printf.sprintf "%s %Ld:%Ld" + (distribution_type_to_string distribution_type) + numerator denominator in Format.fprintf formatter "%a %s %a" Id.Corporate_action.pp action.id kind Id.Instrument.pp action.instrument_id diff --git a/lib/corporate_action.mli b/lib/corporate_action.mli index 9d974c5..6ccb831 100644 --- a/lib/corporate_action.mli +++ b/lib/corporate_action.mli @@ -4,6 +4,20 @@ type kind = | Split of { numerator : int64; denominator : int64 } | Cash_dividend of { amount_per_unit : Scalar.Money.t } + | Distribution of { + distribution_type : distribution_type; + destination_instrument_id : Id.Instrument.t; + numerator : int64; + denominator : int64; + basis_allocation_bps : int; + fractional_policy : fractional_policy; + } + +and distribution_type = Stock_dividend | Rights | Spin_off + +and fractional_policy = + | Reject_fractional + | Cash_in_lieu of { price : Scalar.Price.t; currency : string } type t = private { id : Id.Corporate_action.t; @@ -24,5 +38,17 @@ val cash_dividend : amount_per_unit:Scalar.Money.t -> (t, string) result +val distribution : + id:Id.Corporate_action.t -> + instrument_id:Id.Instrument.t -> + distribution_type:distribution_type -> + destination_instrument_id:Id.Instrument.t -> + numerator:int64 -> + denominator:int64 -> + basis_allocation_bps:int -> + fractional_policy:fractional_policy -> + (t, string) result + +val distribution_type_to_string : distribution_type -> string val compare : t -> t -> int val pp : Format.formatter -> t -> unit diff --git a/lib/engine.ml b/lib/engine.ml index 2eb590e..f1e69fc 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -55,6 +55,8 @@ let config_v11 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing:(Some financing) ~settlement:(Some settlement) ~max_internal_events +let config_v12 = config_v11 + let valid_sha256 value = String.length value = 64 && String.for_all @@ -88,6 +90,7 @@ module Interactive = struct settlement_instructions : Settlement.instruction list; initial_portfolio : Initial_portfolio.t option; applied_action_ids : Id.Corporate_action.Set.t; + lifecycle : Instrument_lifecycle.t; desired_targets : desired_targets option; liquidation_pending : bool; account : Account.t; @@ -133,6 +136,9 @@ module Interactive = struct let create_state ~run_id ~scenario_sha256 ~config ~account ~latest_marks ~latest_fx_rates ~initial_portfolio = + let* lifecycle = + Instrument_lifecycle.create (Risk.instruments config.risk) + in Ok { run_id; @@ -152,6 +158,7 @@ module Interactive = struct settlement_instructions = []; initial_portfolio; applied_action_ids = Id.Corporate_action.Set.empty; + lifecycle; desired_targets = None; liquidation_pending = false; account; @@ -370,6 +377,13 @@ module Interactive = struct ~engine_sequence:order_sequence in match + let* () = + if + Instrument_lifecycle.is_tradable reduction.state.lifecycle + request.Order.instrument_id + then Ok () + else Error "instrument is not tradable" + in let* () = match request.Order.time_in_force with | Order.Day { venue_id; calendar_id } -> ( @@ -612,7 +626,8 @@ module Interactive = struct | None -> Error "split target refers to an unknown instrument" | Some quantity -> ( match action.kind with - | Corporate_action.Cash_dividend _ -> Ok desired + | Corporate_action.Cash_dividend _ | Corporate_action.Distribution _ -> + Ok desired | Corporate_action.Split { numerator; denominator } -> let* quantity = Scalar.Quantity.scale_ratio_exact quantity ~numerator ~denominator @@ -731,6 +746,95 @@ module Interactive = struct emit reduction (Audit.Cash_dividend_applied { action; quantity; cash_amount }) + let apply_distribution_action reduction action distribution_type + destination_instrument_id numerator denominator basis_allocation_bps + fractional_policy = + let* destination = + match + Risk.instrument reduction.state.config.risk destination_instrument_id + with + | Some value -> Ok value + | None -> Error "distribution refers to an unknown destination instrument" + in + let* () = + match fractional_policy with + | Corporate_action.Reject_fractional -> Ok () + | Cash_in_lieu { currency; _ } -> + if String.equal currency destination.quote_currency then Ok () + else + Error "cash-in-lieu currency must equal destination quote currency" + in + let* account, result = + Account.apply_distribution reduction.state.account + ~source_instrument_id:action.Corporate_action.instrument_id + ~destination_instrument_id ~destination_lot_size:destination.lot_size + ~numerator ~denominator ~basis_allocation_bps ~fractional_policy + in + let reduction = + { reduction with state = { reduction.state with account } } + in + let* reduction, distribution_event_id = + emit_with_id reduction (Audit.Distribution_applied { action; result }) + in + match distribution_type with + | Corporate_action.Stock_dividend -> + let total_numerator = Int64.add numerator denominator in + let* desired_targets = + match reduction.state.desired_targets with + | None -> Ok None + | Some desired -> ( + match + Id.Instrument.Map.find_opt action.instrument_id + desired.quantities + with + | None -> + Error "stock-dividend target refers to an unknown instrument" + | Some quantity -> + let* entitlement = + Scalar.Quantity.scale_ratio_exact quantity ~numerator + ~denominator + in + let* delivered = + Scalar.Quantity.round_toward_zero_to_multiple entitlement + ~multiple:destination.lot_size + in + let* quantity = Scalar.Quantity.add quantity delivered in + Ok + (Some + { + quantities = + Id.Instrument.Map.add action.instrument_id quantity + desired.quantities; + cause_ids = distribution_event_id :: desired.cause_ids; + })) + in + let active = + Oms.active_for_instrument reduction.state.oms action.instrument_id + in + let* updated_event_ids = + event_ids_after reduction.state (List.length active) + in + let* oms, adjusted = + Oms.adjust_for_split reduction.state.oms + ~instrument_id:action.instrument_id ~updated_event_ids + ~numerator:total_numerator ~denominator + in + let reduction = + { + reduction with + state = { reduction.state with oms; desired_targets }; + } + in + List.fold_left + (fun result order -> + let* reduction = result in + emit + (with_causes reduction + [ order.Order.created_event_id; distribution_event_id ]) + (Audit.Order_adjusted { order; action_id = action.id })) + (Ok reduction) adjusted + | Rights | Spin_off -> Ok reduction + let apply_corporate_actions reduction actions = List.fold_left (fun result action -> @@ -741,9 +845,116 @@ module Interactive = struct | Split { numerator; denominator } -> apply_split_action reduction action numerator denominator | Cash_dividend { amount_per_unit } -> - apply_dividend_action reduction action amount_per_unit) + apply_dividend_action reduction action amount_per_unit + | Distribution + { + distribution_type; + destination_instrument_id; + numerator; + denominator; + basis_allocation_bps; + fractional_policy; + } -> + apply_distribution_action reduction action distribution_type + destination_instrument_id numerator denominator + basis_allocation_bps fractional_policy) (Ok reduction) actions + let replace_desired_quantity state instrument_id quantity cause_id = + match state.desired_targets with + | None -> state + | Some desired -> + { + state with + desired_targets = + Some + { + quantities = + Id.Instrument.Map.add instrument_id quantity + desired.quantities; + cause_ids = cause_id :: desired.cause_ids; + }; + } + + let apply_lifecycle_event reduction + (lifecycle_event : Instrument_lifecycle.event) = + let instrument_id = lifecycle_event.Instrument_lifecycle.instrument_id in + let* lifecycle = + Instrument_lifecycle.apply reduction.state.lifecycle lifecycle_event + in + let* instrument = + match Risk.instrument reduction.state.config.risk instrument_id with + | Some instrument -> Ok instrument + | None -> Error "lifecycle event refers to an unknown instrument" + in + let* account, liquidated_quantity, cash_amount = + match lifecycle_event.kind with + | Instrument_lifecycle.Expiration { terminal_policy } + | Delisting { terminal_policy; _ } -> ( + match terminal_policy with + | Instrument_lifecycle.Hold -> + Ok + ( reduction.state.account, + Scalar.Quantity.zero, + Scalar.Money.zero ) + | Cash_out { price; currency } -> + if not (String.equal currency instrument.quote_currency) then + Error + "terminal cash-out currency must equal instrument quote \ + currency" + else + Account.cash_out_position reduction.state.account ~instrument_id + ~currency ~price) + | Halt _ | Resume | Identifier_change _ -> + Ok (reduction.state.account, Scalar.Quantity.zero, Scalar.Money.zero) + in + let listing = + Instrument_lifecycle.listing lifecycle instrument_id |> Option.get + in + let reduction = + { reduction with state = { reduction.state with lifecycle; account } } + in + let* reduction, lifecycle_event_id = + emit_with_id reduction + (Audit.Lifecycle_applied + { lifecycle_event; listing; liquidated_quantity; cash_amount }) + in + let cancellation_reason, target_quantity = + match lifecycle_event.kind with + | Instrument_lifecycle.Halt _ -> + ( Some Audit.Instrument_halt, + Account.position_quantity account instrument_id ) + | Expiration _ | Delisting _ -> + ( Some Audit.Instrument_terminal, + Account.position_quantity account instrument_id ) + | Resume | Identifier_change _ -> (None, Scalar.Quantity.zero) + in + let state = + match cancellation_reason with + | None -> reduction.state + | Some _ -> + replace_desired_quantity reduction.state instrument_id target_quantity + lifecycle_event_id + in + let reduction = { reduction with state } in + match cancellation_reason with + | None -> Ok reduction + | Some reason -> + let ids = + Oms.active_for_instrument reduction.state.oms instrument_id + |> List.map (fun order -> order.Order.id) + in + cancel_orders (with_causes reduction [ lifecycle_event_id ]) ~reason ids + + let apply_lifecycle_events reduction events = + List.fold_left + (fun result lifecycle_event -> + let* reduction = result in + apply_lifecycle_event + (with_causes reduction (Option.to_list reduction.slice_event_id)) + lifecycle_event) + (Ok reduction) events + let borrow_fee ~notional ~bps span = if bps = 0 || Scalar.Money.equal notional Scalar.Money.zero then Ok Scalar.Money.zero @@ -1125,6 +1336,12 @@ module Interactive = struct (Scalar.Quantity.is_multiple target.quantity ~lot:instrument.Instrument.lot_size) then Error "target quantity is not aligned to its instrument lot" + else if + (not + (Instrument_lifecycle.is_tradable state.lifecycle + target.instrument_id)) + && not (Scalar.Quantity.is_zero target.quantity) + then Error "non-tradable instrument target must be zero" else let* () = Risk.check_position_for state.config.risk target.instrument_id @@ -1189,6 +1406,14 @@ module Interactive = struct ~weight:target.weight ~price:bar.close_price ~lot_size:instrument.lot_size in + let* () = + if + Instrument_lifecycle.is_tradable state.lifecycle + target.instrument_id + || Scalar.Quantity.is_zero quantity + then Ok () + else Error "non-tradable instrument target must be zero" + in let* () = Risk.check_position_for state.config.risk target.instrument_id quantity @@ -1346,6 +1571,23 @@ module Interactive = struct (Id.Corporate_action.Set.mem action.id state.applied_action_ids)) market_slice.corporate_actions in + let lifecycle_valid = + List.for_all + (fun (lifecycle_event : Instrument_lifecycle.event) -> + Option.is_some + (Risk.instrument state.config.risk + lifecycle_event.Instrument_lifecycle.instrument_id) + && (not + (Id.Corporate_action.Set.mem lifecycle_event.id + state.applied_action_ids)) + && not + (List.exists + (fun action -> + Id.Corporate_action.equal action.Corporate_action.id + lifecycle_event.id) + market_slice.corporate_actions)) + market_slice.lifecycle_events + in let borrow_observations_valid = List.for_all (fun (observation : Financing.borrow_observation) -> @@ -1407,6 +1649,8 @@ module Interactive = struct then Error "market slice base-currency FX rate must equal one" else if not actions_valid then Error "corporate action is unknown or was already applied" + else if not lifecycle_valid then + Error "lifecycle event is unknown, duplicated, or was already applied" else if not borrow_observations_valid then Error "borrow observations must be known and advance effective time" else if not cash_observations_valid then @@ -2081,6 +2325,12 @@ module Interactive = struct (fun ids action -> Id.Corporate_action.Set.add action.Corporate_action.id ids) state.applied_action_ids market_slice.Market_slice.corporate_actions + |> fun ids -> + List.fold_left + (fun ids (lifecycle_event : Instrument_lifecycle.event) -> + Id.Corporate_action.Set.add lifecycle_event.Instrument_lifecycle.id + ids) + ids market_slice.lifecycle_events in let latest_bars = List.fold_left @@ -2153,8 +2403,11 @@ module Interactive = struct let run market_slice reduction = let* reduction = cancel_expired_gtd reduction market_slice in let* reduction = process_settlements reduction market_slice in - apply_corporate_actions reduction - market_slice.Market_slice.corporate_actions + let* reduction = + apply_corporate_actions reduction + market_slice.Market_slice.corporate_actions + in + apply_lifecycle_events reduction market_slice.lifecycle_events end module Borrow_phase = struct diff --git a/lib/engine.mli b/lib/engine.mli index 3483edd..50c9eaa 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -40,6 +40,17 @@ val config_v11 : max_internal_events:int -> (config, string) result +val config_v12 : + 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_model.ml b/lib/execution_model.ml index c4c39d3..ad8097c 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -37,7 +37,7 @@ let completed_bar_v1_contract = version = "2"; previous_versions = [ "1" ]; scenario_contract_versions = - [ "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; + [ "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" ]; diff --git a/lib/external_replay.ml b/lib/external_replay.ml index d76f87e..e72a720 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -105,9 +105,14 @@ 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 -> - Engine.config_v11 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~financing ~settlement - ~max_internal_events + if String.equal contract_version "12" then + Engine.config_v12 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else + Engine.config_v11 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events | None, Some _ -> Error "settlement requires financing configuration") |> reducer_result in diff --git a/lib/instrument_lifecycle.ml b/lib/instrument_lifecycle.ml new file mode 100644 index 0000000..756aeb1 --- /dev/null +++ b/lib/instrument_lifecycle.ml @@ -0,0 +1,136 @@ +type terminal_policy = + | Hold + | Cash_out of { price : Scalar.Price.t; currency : string } + +type kind = + | Halt of { reason : string } + | Resume + | Identifier_change of { + symbol : string; + provider : string; + provider_instrument_id : string; + } + | Expiration of { terminal_policy : terminal_policy } + | Delisting of { terminal_policy : terminal_policy; reason : string } + +type event = { + id : Id.Corporate_action.t; + instrument_id : Id.Instrument.t; + kind : kind; +} + +type status = Tradable | Halted | Expired | Delisted + +type listing = { + instrument_id : Id.Instrument.t; + symbol : string; + provider_mappings : (string * string) list; + status : status; +} + +type t = listing Id.Instrument.Map.t + +let valid_label value = + String.length value > 0 + && String.for_all + (fun character -> + let code = Char.code character in + code >= 0x21 && code <> 0x7f) + value + +let validate_terminal_policy = function + | Hold -> Ok () + | Cash_out { currency; _ } -> + if valid_label currency then Ok () + else + Error + "terminal cash-out currency must not be empty or contain whitespace" + +let create_event ~id ~instrument_id ~kind = + match kind with + | (Halt { reason } | Delisting { reason; _ }) when not (valid_label reason) -> + Error "lifecycle reason must not be empty or contain whitespace" + | Identifier_change { symbol; provider; provider_instrument_id } + when not + (valid_label symbol && valid_label provider + && valid_label provider_instrument_id) -> + Error "identifier-change values must not be empty or contain whitespace" + | Expiration { terminal_policy } | Delisting { terminal_policy; _ } -> + Result.map + (fun () -> { id; instrument_id; kind }) + (validate_terminal_policy terminal_policy) + | Halt _ | Resume | Identifier_change _ -> Ok { id; instrument_id; kind } + +let compare_event left right = Id.Corporate_action.compare left.id right.id + +let create instruments = + List.fold_left + (fun result (instrument : Instrument.t) -> + Result.bind result (fun state -> + if Id.Instrument.Map.mem instrument.id state then + Error "lifecycle catalog instrument IDs must be unique" + else + Ok + (Id.Instrument.Map.add instrument.id + { + instrument_id = instrument.id; + symbol = instrument.symbol; + provider_mappings = []; + status = Tradable; + } + state))) + (Ok Id.Instrument.Map.empty) instruments + +let listing state instrument_id = Id.Instrument.Map.find_opt instrument_id state + +let is_tradable state instrument_id = + match listing state instrument_id with + | Some { status = Tradable; _ } -> true + | Some _ | None -> false + +let apply state (event : event) = + match listing state event.instrument_id with + | None -> Error "lifecycle event refers to an unknown instrument" + | Some current -> + let result = + match (current.status, event.kind) with + | (Expired | Delisted), _ -> + Error "terminal instrument cannot accept another lifecycle event" + | Tradable, Halt _ -> Ok { current with status = Halted } + | Halted, Resume -> Ok { current with status = Tradable } + | Halted, Halt _ -> Error "halted instrument cannot be halted again" + | Tradable, Resume -> Error "tradable instrument cannot be resumed" + | (Tradable | Halted), Identifier_change change -> + let mappings = + (change.provider, change.provider_instrument_id) + :: List.remove_assoc change.provider current.provider_mappings + |> List.sort (fun (left, _) (right, _) -> + String.compare left right) + in + Ok + { + current with + symbol = change.symbol; + provider_mappings = mappings; + } + | (Tradable | Halted), Expiration _ -> + Ok { current with status = Expired } + | (Tradable | Halted), Delisting _ -> + Ok { current with status = Delisted } + in + Result.map + (fun updated -> Id.Instrument.Map.add event.instrument_id updated state) + result + +let status_to_string = function + | Tradable -> "tradable" + | Halted -> "halted" + | Expired -> "expired" + | Delisted -> "delisted" + +let kind_to_string = function + | Halt _ -> "halt" + | Resume -> "resume" + | Identifier_change _ -> "identifier_change" + | Expiration _ -> "expiration" + | Delisting _ -> "delisting" diff --git a/lib/instrument_lifecycle.mli b/lib/instrument_lifecycle.mli new file mode 100644 index 0000000..8bb75be --- /dev/null +++ b/lib/instrument_lifecycle.mli @@ -0,0 +1,47 @@ +(** Deterministic mutable-listing state keyed by stable instrument identity. *) + +type terminal_policy = + | Hold + | Cash_out of { price : Scalar.Price.t; currency : string } + +type kind = + | Halt of { reason : string } + | Resume + | Identifier_change of { + symbol : string; + provider : string; + provider_instrument_id : string; + } + | Expiration of { terminal_policy : terminal_policy } + | Delisting of { terminal_policy : terminal_policy; reason : string } + +type event = private { + id : Id.Corporate_action.t; + instrument_id : Id.Instrument.t; + kind : kind; +} + +type status = Tradable | Halted | Expired | Delisted + +type listing = private { + instrument_id : Id.Instrument.t; + symbol : string; + provider_mappings : (string * string) list; + status : status; +} + +type t + +val create_event : + id:Id.Corporate_action.t -> + instrument_id:Id.Instrument.t -> + kind:kind -> + (event, string) result + +val compare_event : event -> event -> int +val create : Instrument.t list -> (t, string) result +val listing : t -> Id.Instrument.t -> listing option +val is_tradable : t -> Id.Instrument.t -> bool +val apply : t -> event -> (t, string) result +val status_to_string : status -> string +val kind_to_string : kind -> string diff --git a/lib/market_slice.ml b/lib/market_slice.ml index 36e2d07..9f3fdbe 100644 --- a/lib/market_slice.ml +++ b/lib/market_slice.ml @@ -9,6 +9,7 @@ type t = { bars : Bar.t list; fx_rates : fx_mark list; corporate_actions : Corporate_action.t list; + lifecycle_events : Instrument_lifecycle.event list; borrow_observations : Financing.borrow_observation list; cash_rate_observations : Financing.cash_rate_observation list; settlement_failures : Settlement.failure list; @@ -30,9 +31,9 @@ let fx_mark ~currency ~rate = let compare_bar left right = Id.Instrument.compare left.Bar.instrument_id right.Bar.instrument_id -let create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at +let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations ~settlement_failures = + ~cash_rate_observations ~settlement_failures ~lifecycle_events = if Int64.compare slice_sequence 0L <= 0 then Error "market slice sequence must be positive" else if Ptime.compare start_at end_at >= 0 then @@ -64,6 +65,16 @@ let create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at let corporate_actions = List.sort Corporate_action.compare corporate_actions in + let lifecycle_events = + List.sort Instrument_lifecycle.compare_event lifecycle_events + in + let rec unique_lifecycle = function + | [] | [ _ ] -> true + | left :: (right :: _ as remaining) -> + (not + (Id.Corporate_action.equal left.Instrument_lifecycle.id right.id)) + && unique_lifecycle remaining + in let rec unique_actions = function | [] | [ _ ] -> true | left :: (right :: _ as remaining) -> @@ -119,6 +130,8 @@ let create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at Error "market slice must contain one FX rate per currency" else if not (unique_actions corporate_actions) then Error "market slice corporate action IDs must be unique" + else if not (unique_lifecycle lifecycle_events) then + Error "market slice lifecycle event IDs must be unique" else if not (unique_borrow borrow_observations) then Error "market slice borrow observation instrument IDs must be unique" else if not (unique_cash_rate cash_rate_observations) then @@ -136,11 +149,19 @@ let create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at bars; fx_rates; corporate_actions; + lifecycle_events; borrow_observations; cash_rate_observations; settlement_failures; } +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 = + create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars + ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations + ~settlement_failures ~lifecycle_events:[] + let create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations = @@ -170,10 +191,12 @@ let compare_replay_order left right = let pp formatter state = Format.fprintf formatter - "slice[%Ld] bars=%d fx=%d actions=%d borrow=%d cash_rates=%d failures=%d" + "slice[%Ld] bars=%d fx=%d actions=%d lifecycle=%d borrow=%d cash_rates=%d \ + failures=%d" state.slice_sequence (List.length state.bars) (List.length state.fx_rates) (List.length state.corporate_actions) + (List.length state.lifecycle_events) (List.length state.borrow_observations) (List.length state.cash_rate_observations) (List.length state.settlement_failures) diff --git a/lib/market_slice.mli b/lib/market_slice.mli index 037690a..329ecd1 100644 --- a/lib/market_slice.mli +++ b/lib/market_slice.mli @@ -14,6 +14,7 @@ type t = private { bars : Bar.t list; fx_rates : fx_mark list; corporate_actions : Corporate_action.t list; + lifecycle_events : Instrument_lifecycle.event list; borrow_observations : Financing.borrow_observation list; cash_rate_observations : Financing.cash_rate_observation list; settlement_failures : Settlement.failure list; @@ -57,6 +58,21 @@ val create_v11 : settlement_failures:Settlement.failure list -> (t, string) result +val create_v12 : + 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 f0c6332..baff101 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -78,8 +78,14 @@ 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 -> - Engine.config_v11 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~financing ~settlement ~max_internal_events + if String.equal contract_version "12" then + Engine.config_v12 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else + Engine.config_v11 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events | None, Some _ -> Error "settlement requires financing configuration" let run ~scenario_sha256 ?journal_path ?(durability = Artifact_writer.Buffered) diff --git a/lib/scenario.ml b/lib/scenario.ml index babd019..92fd715 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 [ "11"; "10"; "9"; "8"; "7" ] then + if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7" ] then parse_v7_risk base_currency instruments json else parse_legacy_risk base_currency instruments json @@ -746,8 +746,8 @@ let parse_versioned_execution ~contract_version ~instruments json = Ok (execution_model, execution) let parse_execution ~contract_version ~instruments json = - if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then - parse_versioned_execution ~contract_version ~instruments json + if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + then parse_versioned_execution ~contract_version ~instruments json else parse_legacy_execution ~contract_version json let parse_side json = @@ -795,7 +795,7 @@ 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 [ "11"; "10"; "9"; "8" ] in + let versioned = List.mem contract_version [ "12"; "11"; "10"; "9"; "8" ] in let* fields = object_fields ~name:"submit_order intent" ~expected: @@ -1115,8 +1115,198 @@ let parse_corporate_action json = let* amount_json = field fields "amount_per_unit" in let* amount_per_unit = parse_money ~name:"amount_per_unit" amount_json in Corporate_action.cash_dividend ~id ~instrument_id ~amount_per_unit + | ("stock_dividend" | "rights" | "spin_off") as distribution_name -> + let* () = + object_fields ~name:"distribution corporate action" + ~expected: + [ + "type"; + "action_id"; + "instrument_id"; + "destination_instrument_id"; + "numerator"; + "denominator"; + "basis_allocation_bps"; + "fractional_policy"; + ] + json + |> Result.map (fun _ -> ()) + in + let* destination_json = field fields "destination_instrument_id" in + let* destination_instrument_id = + parse_id Id.Instrument.of_string ~name:"destination_instrument_id" + destination_json + in + let* numerator = + Result.bind (field fields "numerator") + (parse_int64 ~name:"distribution numerator") + in + let* denominator = + Result.bind + (field fields "denominator") + (parse_int64 ~name:"distribution denominator") + in + let* basis_allocation_bps = + Result.bind + (field fields "basis_allocation_bps") + (integer ~name:"basis_allocation_bps") + in + let* fractional_json = field fields "fractional_policy" in + let* fractional_fields = + match fractional_json with + | `Assoc fields -> Ok fields + | _ -> Error "fractional_policy must be a JSON object" + in + let* policy_name = + Result.bind + (field fractional_fields "policy") + (string ~name:"fractional policy") + in + let* fractional_policy = + match policy_name with + | "reject" -> + object_fields ~name:"reject fractional policy" + ~expected:[ "policy" ] fractional_json + |> Result.map (fun _ -> Corporate_action.Reject_fractional) + | "cash_in_lieu" -> + let* () = + object_fields ~name:"cash-in-lieu fractional policy" + ~expected:[ "policy"; "price"; "currency" ] + fractional_json + |> Result.map (fun _ -> ()) + in + let* price = + Result.bind + (field fractional_fields "price") + (parse_price ~name:"cash-in-lieu price") + in + let* currency = + Result.bind + (field fractional_fields "currency") + (string ~name:"cash-in-lieu currency") + in + Ok (Corporate_action.Cash_in_lieu { price; currency }) + | _ -> Error "fractional policy must be reject or cash_in_lieu" + in + let distribution_type = + match distribution_name with + | "stock_dividend" -> Corporate_action.Stock_dividend + | "rights" -> Rights + | "spin_off" -> Spin_off + | _ -> assert false + in + Corporate_action.distribution ~id ~instrument_id ~distribution_type + ~destination_instrument_id ~numerator ~denominator ~basis_allocation_bps + ~fractional_policy | _ -> Error "unsupported corporate action type" +let parse_terminal_policy json = + let* fields = + match json with + | `Assoc fields -> Ok fields + | _ -> Error "terminal_policy must be a JSON object" + in + let* policy = + Result.bind (field fields "policy") (string ~name:"terminal policy") + in + match policy with + | "hold" -> + object_fields ~name:"hold terminal policy" ~expected:[ "policy" ] json + |> Result.map (fun _ -> Instrument_lifecycle.Hold) + | "cash_out" -> + let* () = + object_fields ~name:"cash-out terminal policy" + ~expected:[ "policy"; "price"; "currency" ] + json + |> Result.map (fun _ -> ()) + in + let* price = + Result.bind (field fields "price") (parse_price ~name:"terminal price") + in + let* currency = + Result.bind (field fields "currency") (string ~name:"terminal currency") + in + Ok (Instrument_lifecycle.Cash_out { price; currency }) + | _ -> Error "terminal policy must be hold or cash_out" + +let parse_lifecycle_event json = + let* fields = + match json with + | `Assoc fields -> Ok fields + | _ -> Error "lifecycle event must be a JSON object" + in + let* kind_name = + Result.bind (field fields "type") (string ~name:"lifecycle event type") + in + let* id = + Result.bind (field fields "event_id") + (parse_id Id.Corporate_action.of_string ~name:"event_id") + in + let* instrument_id = + Result.bind + (field fields "instrument_id") + (parse_id Id.Instrument.of_string ~name:"instrument_id") + in + let* kind = + match kind_name with + | "halt" -> + let* () = + object_fields ~name:"halt lifecycle event" + ~expected:[ "type"; "event_id"; "instrument_id"; "reason" ] + json + |> Result.map (fun _ -> ()) + in + Result.bind (field fields "reason") (string ~name:"halt reason") + |> Result.map (fun reason -> Instrument_lifecycle.Halt { reason }) + | "resume" -> + object_fields ~name:"resume lifecycle event" + ~expected:[ "type"; "event_id"; "instrument_id" ] + json + |> Result.map (fun _ -> Instrument_lifecycle.Resume) + | "identifier_change" -> + let* () = + object_fields ~name:"identifier-change lifecycle event" + ~expected: + [ + "type"; + "event_id"; + "instrument_id"; + "symbol"; + "provider"; + "provider_instrument_id"; + ] + json + |> Result.map (fun _ -> ()) + in + let text name = Result.bind (field fields name) (string ~name) in + let* symbol = text "symbol" in + let* provider = text "provider" in + let* provider_instrument_id = text "provider_instrument_id" in + Ok + (Instrument_lifecycle.Identifier_change + { symbol; provider; provider_instrument_id }) + | "expiration" | "delisting" -> + let delisting = String.equal kind_name "delisting" in + let expected = + [ "type"; "event_id"; "instrument_id"; "terminal_policy" ] + @ if delisting then [ "reason" ] else [] + in + let* () = + object_fields ~name:"terminal lifecycle event" ~expected json + |> Result.map (fun _ -> ()) + in + let* terminal_policy = + Result.bind (field fields "terminal_policy") parse_terminal_policy + in + if delisting then + Result.bind (field fields "reason") (string ~name:"delisting reason") + |> Result.map (fun reason -> + Instrument_lifecycle.Delisting { terminal_policy; reason }) + else Ok (Instrument_lifecycle.Expiration { terminal_policy }) + | _ -> Error "unsupported lifecycle event type" + in + Instrument_lifecycle.create_event ~id ~instrument_id ~kind + let parse_financing json = let* fields = object_fields ~name:"financing policy" @@ -1342,12 +1532,16 @@ let parse_cash_rate_observation json = let parse_slice ~contract_version json = let financing_fields = - if List.mem contract_version [ "11"; "10" ] then + if List.mem contract_version [ "12"; "11"; "10" ] then [ "borrow_observations"; "cash_rate_observations" ] else [] in let settlement_fields = - if String.equal contract_version "11" then [ "settlement_failures" ] else [] + if List.mem contract_version [ "12"; "11" ] then [ "settlement_failures" ] + else [] + in + let lifecycle_fields = + if String.equal contract_version "12" then [ "lifecycle_events" ] else [] in let* fields = object_fields ~name:"market slice" @@ -1362,7 +1556,7 @@ let parse_slice ~contract_version json = "fx_rates"; "corporate_actions"; ] - @ financing_fields @ settlement_fields) + @ financing_fields @ settlement_fields @ lifecycle_fields) json in let* sequence_json = field fields "slice_sequence" in @@ -1384,7 +1578,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 [ "11"; "10" ] then + if List.mem contract_version [ "12"; "11"; "10" ] then let* borrow_json = Result.bind (field fields "borrow_observations") @@ -1399,7 +1593,7 @@ let parse_slice ~contract_version json = let* cash_rate_observations = map_list parse_cash_rate_observation cash_json in - if String.equal contract_version "11" then + if List.mem contract_version [ "12"; "11" ] then let* failures_json = Result.bind (field fields "settlement_failures") @@ -1408,9 +1602,20 @@ let parse_slice ~contract_version json = let* settlement_failures = map_list parse_settlement_failure failures_json in - Market_slice.create_v11 ~slice_sequence ~start_at ~end_at ~available_at - ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations ~settlement_failures + if String.equal contract_version "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 + ~cash_rate_observations ~settlement_failures ~lifecycle_events + else + Market_slice.create_v11 ~slice_sequence ~start_at ~end_at ~available_at + ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations ~settlement_failures else Market_slice.create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations @@ -1450,7 +1655,7 @@ let construct_header ~root ~contract_path ~contract_version |> at (child root "base_currency") in let* initial_cash, initial_portfolio = - if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then + if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state |> at (child root "initial_portfolio") @@ -1512,21 +1717,22 @@ let construct_header ~root ~contract_path ~contract_version in let* financing = match (contract_version, shape.financing) with - | ("11" | "10"), Some json -> + | ("12" | "11" | "10"), Some json -> parse_financing json |> at (child root "financing") - | ("11" | "10"), None -> + | ("12" | "11" | "10"), None -> Error "missing financing policy" |> at (child root "financing") | _, _ -> Ok Financing.legacy_policy in let financing = - if List.mem contract_version [ "11"; "10" ] then Some financing else None + if List.mem contract_version [ "12"; "11"; "10" ] then Some financing + else None in let* settlement = match (contract_version, shape.settlement) with - | "11", Some json -> + | ("12" | "11"), Some json -> let* policy = parse_settlement json |> at (child root "settlement") in Ok (Some policy) - | "11", None -> + | ("12" | "11"), None -> Error "missing settlement policy" |> at (child root "settlement") | _, _ -> Ok None in diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index c58e96c..e385c8d 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -70,26 +70,26 @@ 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 [ "11"; "10"; "9"; "8"; "7"; "6" ] then + if List.mem contract_version [ "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 [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then - List.assoc_opt "venue_calendars" fields + if List.mem contract_version [ "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 [ "11"; "10" ] then + if List.mem contract_version [ "12"; "11"; "10" ] then List.assoc_opt "financing" fields else None in let settlement = - if String.equal contract_version "11" then + if List.mem contract_version [ "12"; "11" ] then List.assoc_opt "settlement" fields else None in @@ -120,12 +120,12 @@ let batch json = match preliminary with `String value -> value | _ -> "" in let calendar_fields = - if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then - [ "venue_calendars" ] + if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then + if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -146,9 +146,12 @@ let batch json = "slices"; ] @ calendar_fields - @ (if List.mem contract_version [ "11"; "10" ] then [ "financing" ] + @ (if List.mem contract_version [ "12"; "11"; "10" ] then + [ "financing" ] else []) - @ if String.equal contract_version "11" then [ "settlement" ] else []) + @ + if List.mem contract_version [ "12"; "11" ] then [ "settlement" ] + else []) json in let* contract_version_json = field ~root fields "contract_version" in @@ -160,12 +163,12 @@ let batch json = let stream_header ~contract_version json = let root = "$.payload" in let calendar_fields = - if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then - [ "venue_calendars" ] + if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then + if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -183,9 +186,12 @@ let stream_header ~contract_version json = "max_internal_events"; ] @ calendar_fields - @ (if List.mem contract_version [ "11"; "10" ] then [ "financing" ] + @ (if List.mem contract_version [ "12"; "11"; "10" ] then + [ "financing" ] else []) - @ if String.equal contract_version "11" then [ "settlement" ] else []) + @ + if List.mem contract_version [ "12"; "11" ] then [ "settlement" ] + else []) json in common ~root ~contract_version fields diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 9f4dc91..21b9668 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -48,7 +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 [ "11"; "10"; "9"; "8"; "7"; "6" ] then Ok () + if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then + Ok () else Account.create ~base_currency ~initial_cash |> Result.map (fun _ -> ()) @@ -66,7 +67,9 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path:(child root "instruments") "instrument IDs must be unique" else let* () = - if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + if + List.mem contract_version + [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -84,7 +87,9 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments fail ~json_path: (child root - (if List.mem contract_version [ "11"; "10"; "9"; "8"; "7"; "6" ] + (if + List.mem contract_version + [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio.cash" else "initial_cash")) "initial cash must contain every scenario currency exactly once" @@ -327,14 +332,33 @@ let validate_slices_at ~paths ~base_currency ~currencies ~instruments slices = List.for_all (fun action -> Id.Instrument.Set.mem action.Corporate_action.instrument_id - catalog) + catalog + && + match action.kind with + | Corporate_action.Distribution { destination_instrument_id; _ } + -> + Id.Instrument.Set.mem destination_instrument_id catalog + | Split _ | Cash_dividend _ -> true) market_slice.corporate_actions in + let lifecycle_valid = + List.for_all + (fun (event : Instrument_lifecycle.event) -> + Id.Instrument.Set.mem event.instrument_id catalog) + market_slice.lifecycle_events + in let duplicate_action = + let current_ids = + List.map + (fun action -> action.Corporate_action.id) + market_slice.corporate_actions + @ List.map + (fun (event : Instrument_lifecycle.event) -> event.id) + market_slice.lifecycle_events + in List.find_opt - (fun action -> - Id.Corporate_action.Set.mem action.Corporate_action.id action_ids) - market_slice.corporate_actions + (fun id -> Id.Corporate_action.Set.mem id action_ids) + current_ids in let bars_aligned = List.for_all @@ -378,6 +402,10 @@ let validate_slices_at ~paths ~base_currency ~currencies ~instruments slices = fail ~json_path:(child root "corporate_actions") "corporate action refers to an unknown instrument" + else if not lifecycle_valid then + fail + ~json_path:(child root "lifecycle_events") + "lifecycle event refers to an unknown instrument" else if Option.is_some duplicate_action then fail ~json_path:(child root "corporate_actions") @@ -415,6 +443,11 @@ let validate_slices_at ~paths ~base_currency ~currencies ~instruments slices = (fun ids action -> Id.Corporate_action.Set.add action.Corporate_action.id ids) action_ids market_slice.corporate_actions + |> fun ids -> + List.fold_left + (fun ids (event : Instrument_lifecycle.event) -> + Id.Corporate_action.Set.add event.id ids) + ids market_slice.lifecycle_events in validate (index + 1) (Some market_slice.slice_sequence) (Some market_slice.end_at) (Some market_slice.received_at) @@ -517,6 +550,17 @@ let stream_item ~root ~base_currency ~instruments ~risk ~previous_slice else Ok (Id.Corporate_action.Set.add action.id ids)) (Ok prior_action_ids) market_slice.corporate_actions in + let* action_ids = + List.fold_left + (fun result (event : Instrument_lifecycle.event) -> + let* ids = result in + if Id.Corporate_action.Set.mem event.id ids then + fail + ~json_path:(child current_slice_path "lifecycle_events") + "action and lifecycle IDs must be unique across the scenario stream" + else Ok (Id.Corporate_action.Set.add event.id ids)) + (Ok action_ids) market_slice.lifecycle_events + in let currencies = base_currency :: List.map diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index 1daaf2c..5f0790a 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 [ "9"; "8"; "7"; "6"; "5" ] + List.mem protocol_version [ "10"; "9"; "8"; "7"; "6"; "5" ] let financing_to_yojson policy = `Assoc @@ -252,7 +252,7 @@ let execution_to_yojson ~protocol_version model execution = (Fee_schedule.components schedule)) ); ] in - if List.mem protocol_version [ "9"; "8"; "7" ] then + if List.mem protocol_version [ "10"; "9"; "8"; "7" ] then `Assoc [ ("model", string (Execution_model.name model)); @@ -291,6 +291,7 @@ let execution_to_yojson ~protocol_version model execution = let protocol_version initialization = match initialization.scenario_contract_version with + | "12" -> "10" | "11" -> "9" | "10" -> "8" | "9" -> "7" @@ -336,7 +337,7 @@ let initialize_message ~sequence:message_sequence initialization = ] in let fields = - if List.mem protocol_version [ "9"; "8"; "7"; "6" ] then + if List.mem protocol_version [ "10"; "9"; "8"; "7"; "6" ] then let initial_portfolio = Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson initialization.initial_portfolio @@ -349,14 +350,14 @@ let initialize_message ~sequence:message_sequence initialization = ( "venue_calendars", `List (List.map venue_calendar_to_yojson venue_calendars) ); ]; - (if List.mem protocol_version [ "9"; "8" ] then + (if List.mem protocol_version [ "10"; "9"; "8" ] then [ ( "financing", Option.fold ~none:`Null ~some:financing_to_yojson initialization.financing ); ] else []); - (if String.equal protocol_version "9" then + (if List.mem protocol_version [ "10"; "9" ] then [ ( "settlement", Option.fold ~none:`Null ~some:settlement_to_yojson @@ -390,14 +391,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 [ "9"; "8" ] then + @ (if List.mem protocol_version [ "10"; "9"; "8" ] then [ ("interest", money balance.interest); ("base_interest", money balance.base_interest); ] else []) @ - if String.equal protocol_version "9" then + if List.mem protocol_version [ "10"; "9" ] then [ ("settled_amount", money balance.settled_amount); ("unsettled_amount", money balance.unsettled_amount); @@ -417,7 +418,7 @@ let marked_position_to_yojson ~protocol_version ("weight", Option.fold ~none:`Null ~some:weight position.weight); ] @ - if String.equal protocol_version "9" then + if List.mem protocol_version [ "10"; "9" ] then [ ("settled_quantity", quantity position.settled_quantity); ("unsettled_quantity", quantity position.unsettled_quantity); @@ -500,7 +501,7 @@ let context_to_yojson ~protocol_version context = ( "working_orders", `List (List.map - (if List.mem protocol_version [ "9"; "8"; "7"; "6" ] then + (if List.mem protocol_version [ "10"; "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 else Codec.order_to_yojson) working_orders) ); @@ -513,7 +514,9 @@ let event_to_yojson ~protocol_version = function [ ("type", string "market_slice_closed"); ( "market_slice", - if String.equal protocol_version "9" then + if String.equal protocol_version "10" then + Codec.market_slice_to_yojson_v12 market_slice + else if String.equal protocol_version "9" then Codec.market_slice_to_yojson_v11 market_slice else if String.equal protocol_version "8" then Codec.market_slice_to_yojson_v10 market_slice @@ -524,7 +527,7 @@ let event_to_yojson ~protocol_version = function [ ("type", string "fill_received"); ( "fill", - if List.mem protocol_version [ "9"; "8"; "7" ] then + if List.mem protocol_version [ "10"; "9"; "8"; "7" ] then Codec.fill_to_yojson_v9 fill else Codec.fill_to_yojson fill ); ] @@ -533,7 +536,7 @@ let event_to_yojson ~protocol_version = function [ ("type", string "order_updated"); ( "order", - if List.mem protocol_version [ "9"; "8"; "7"; "6" ] then + if List.mem protocol_version [ "10"; "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 order else Codec.order_to_yojson order ); ] @@ -621,7 +624,8 @@ let parse_intents_payload ~protocol_version json = let* intent = Scenario.intent_of_yojson ~contract_version: - (if String.equal protocol_version "9" then "11" + (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" else if String.equal protocol_version "6" then "8" diff --git a/mkdocs.yml b/mkdocs.yml index c11b345..c9145e4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,14 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v11: contracts/v11/README.md + - Current v12: contracts/v12/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 v9: contracts/strategy/v9/README.md + - Current v10: contracts/strategy/v10/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 a9cd805..a3d533b 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -82,3 +82,11 @@ compare_journal \ v11-fill-clipped \ contracts/v11/fixtures/fill-clipped.scenario.json \ contracts/v11/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v12-demo \ + contracts/v12/fixtures/demo.scenario.json \ + contracts/v12/fixtures/demo.journal.jsonl +compare_journal \ + v12-fill-clipped \ + contracts/v12/fixtures/fill-clipped.scenario.json \ + contracts/v12/fixtures/fill-clipped.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index c134f22..bbe4a58 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/v11/README.md", + "contracts/v12/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v9/README.md", + "contracts/strategy/v10/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 6b28226..3915dfe 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/v11/scenario.schema.json", - "share/trading_engine/contracts/v11/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v12/scenario.schema.json", + "share/trading_engine/contracts/v12/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/v11/fixtures/demo.scenario.json", + "contracts/v12/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/v11/fixtures/demo.scenario.json", - "contracts/strategy/v9/message.schema.json", + "contracts/v12/fixtures/demo.scenario.json", + "contracts/strategy/v10/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v11/scenario.schema.json", + "contracts/v12/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index f0497fe..a77dbbd 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":["11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["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":["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":["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":["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}} $ ../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 4662ab7..a0c266d 100644 --- a/test/dune +++ b/test/dune @@ -10,6 +10,7 @@ test_fee_schedules test_financing test_settlement + test_corporate_lifecycle test_reducer test_reducer_properties test_checkpoint4 @@ -61,6 +62,14 @@ ../contracts/v11/journal.schema.json ../contracts/v11/scenario-stream.schema.json ../contracts/v11/scenario.schema.json + ../contracts/v12/fixtures/demo.journal.jsonl + ../contracts/v12/fixtures/demo.scenario.json + ../contracts/v12/fixtures/demo.scenario.jsonl + ../contracts/v12/fixtures/fill-clipped.journal.jsonl + ../contracts/v12/fixtures/fill-clipped.scenario.json + ../contracts/v12/journal.schema.json + ../contracts/v12/scenario-stream.schema.json + ../contracts/v12/scenario.schema.json ../contracts/v6/fixtures/demo.scenario.json ../contracts/v6/fixtures/demo.scenario.jsonl ../contracts/v5/fixtures/demo.scenario.json @@ -75,6 +84,7 @@ ../contracts/strategy/v7/fixtures/external.strategy.jsonl ../contracts/strategy/v8/fixtures/external.strategy.jsonl ../contracts/strategy/v9/fixtures/external.strategy.jsonl + ../contracts/strategy/v10/fixtures/external.strategy.jsonl ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries @@ -93,6 +103,69 @@ (modules fuzz_protocol) (libraries trading_engine yojson unix)) +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v12/fixtures/demo.journal.jsonl + ../contracts/v12/fixtures/demo.scenario.json + ../contracts/v12/fixtures/demo.scenario.jsonl + ../contracts/v12/journal.schema.json + ../contracts/v12/scenario-stream.schema.json + ../contracts/v12/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v12/scenario.schema.json} + %{dep:../contracts/v12/scenario-stream.schema.json} + %{dep:../contracts/v12/journal.schema.json} + %{dep:../contracts/v12/fixtures/demo.scenario.json} + %{dep:../contracts/v12/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v12/fixtures/demo.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v12/fixtures/fill-clipped.journal.jsonl + ../contracts/v12/fixtures/fill-clipped.scenario.json + ../contracts/v12/fixtures/demo.scenario.jsonl + ../contracts/v12/journal.schema.json + ../contracts/v12/scenario-stream.schema.json + ../contracts/v12/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v12/scenario.schema.json} + %{dep:../contracts/v12/scenario-stream.schema.json} + %{dep:../contracts/v12/journal.schema.json} + %{dep:../contracts/v12/fixtures/fill-clipped.scenario.json} + %{dep:../contracts/v12/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v12/fixtures/fill-clipped.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_strategy_schema.py + ../contracts/v12/scenario.schema.json + ../contracts/v12/journal.schema.json + ../contracts/diagnostic/v1/diagnostic.schema.json + ../contracts/strategy/v10/message.schema.json + ../contracts/strategy/v10/transcript.schema.json + ../contracts/strategy/v10/fixtures/external.strategy.jsonl) + (action + (run + python3 + %{dep:validate_strategy_schema.py} + %{dep:../contracts/v12/scenario.schema.json} + %{dep:../contracts/v12/journal.schema.json} + %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} + %{dep:../contracts/strategy/v10/message.schema.json} + %{dep:../contracts/strategy/v10/transcript.schema.json} + %{dep:../contracts/strategy/v10/fixtures/external.strategy.jsonl}))) + (rule (alias runtest) (deps diff --git a/test/test_corporate_lifecycle.ml b/test/test_corporate_lifecycle.ml new file mode 100644 index 0000000..afd7d58 --- /dev/null +++ b/test/test_corporate_lifecycle.ml @@ -0,0 +1,507 @@ +open Test_support +module T = Trading_engine +module Runner = T.Engine.Make (T.Scripted_strategy) + +let action_id value = T.Id.Corporate_action.of_string_exn value + +let distribution_allocates_basis_and_fractional_cash () = + let source = instrument ~id:"source" ~symbol:"SRC" () in + let child = instrument ~id:"child" ~symbol:"CHD" () in + let account = test_account ~initial_cash:[ ("USD", money "1000") ] () in + let order = + request ~instrument:source.id ~quantity_value:"3" () |> accepted_order + in + let account = + T.Account.apply_fill account + (fill ~price_value:"100" ~quantity_value:"3" order) + |> ok + in + let fractional_policy = + T.Corporate_action.Cash_in_lieu { price = price "20"; currency = "USD" } + in + let account, result = + T.Account.apply_distribution account ~source_instrument_id:source.id + ~destination_instrument_id:child.id ~destination_lot_size:child.lot_size + ~numerator:1L ~denominator:2L ~basis_allocation_bps:2000 + ~fractional_policy + |> ok + in + Alcotest.check quantity_testable "delivered child units" (quantity "1") + result.destination_quantity; + Alcotest.check quantity_testable "fractional child units" (quantity "0.5") + result.fractional_quantity; + Alcotest.check money_testable "allocated source basis" (money "60") + result.allocated_basis; + Alcotest.check money_testable "fractional basis" (money "20") + result.fractional_basis; + Alcotest.check money_testable "cash in lieu" (money "10") result.cash_in_lieu; + Alcotest.check money_testable "source basis retained" (money "240") + (T.Account.position account source.id).cost_basis; + Alcotest.check money_testable "child basis delivered" (money "40") + (T.Account.position account child.id).cost_basis; + Alcotest.check money_testable "cash credited in declared currency" + (money "710") (account_cash account) + +let fractional_policy_is_explicit () = + let source = instrument ~id:"source" ~symbol:"SRC" () in + let child = instrument ~id:"child" ~symbol:"CHD" () in + let account = test_account () in + let order = + request ~instrument:source.id ~quantity_value:"3" () |> accepted_order + in + let account = + T.Account.apply_fill account + (fill ~price_value:"100" ~quantity_value:"3" order) + |> ok + in + Alcotest.(check bool) + "fractional entitlement rejected" true + (Result.is_error + (T.Account.apply_distribution account ~source_instrument_id:source.id + ~destination_instrument_id:child.id + ~destination_lot_size:child.lot_size ~numerator:1L ~denominator:2L + ~basis_allocation_bps:2000 + ~fractional_policy:T.Corporate_action.Reject_fractional)) + +let lifecycle_preserves_identity_and_terminal_state () = + let configured = instrument ~id:"stable-id" ~symbol:"OLD" () in + let state = T.Instrument_lifecycle.create [ configured ] |> ok in + let event name kind = + T.Instrument_lifecycle.create_event ~id:(action_id name) + ~instrument_id:configured.id ~kind + |> ok + in + let state = + T.Instrument_lifecycle.apply state + (event "rename" + (T.Instrument_lifecycle.Identifier_change + { + symbol = "NEW"; + provider = "sip"; + provider_instrument_id = "NEW.X"; + })) + |> ok + in + let listing = + T.Instrument_lifecycle.listing state configured.id |> Option.get + in + Alcotest.(check string) + "stable identity" "stable-id" + (T.Id.Instrument.to_string listing.instrument_id); + Alcotest.(check string) "new symbol" "NEW" listing.symbol; + Alcotest.(check (list (pair string string))) + "provider provenance" + [ ("sip", "NEW.X") ] + listing.provider_mappings; + let state = + T.Instrument_lifecycle.apply state + (event "halt" (T.Instrument_lifecycle.Halt { reason = "volatility" })) + |> ok + in + Alcotest.(check bool) + "halt is not tradable" false + (T.Instrument_lifecycle.is_tradable state configured.id); + let state = + T.Instrument_lifecycle.apply state + (event "resume" T.Instrument_lifecycle.Resume) + |> ok + in + Alcotest.(check bool) + "resume is tradable" true + (T.Instrument_lifecycle.is_tradable state configured.id); + let state = + T.Instrument_lifecycle.apply state + (event "expire" + (T.Instrument_lifecycle.Expiration + { terminal_policy = T.Instrument_lifecycle.Hold })) + |> ok + in + Alcotest.(check bool) + "expiration is terminal" false + (T.Instrument_lifecycle.is_tradable state configured.id); + Alcotest.(check bool) + "terminal event rejects resume" true + (Result.is_error + (T.Instrument_lifecycle.apply state + (event "late-resume" T.Instrument_lifecycle.Resume))) + +let constructors_reject_ambiguous_policies () = + let id = action_id "distribution" in + let source = instrument_id "source" in + let child = instrument_id "child" in + Alcotest.(check bool) + "stock destination must be source" true + (Result.is_error + (T.Corporate_action.distribution ~id ~instrument_id:source + ~distribution_type:T.Corporate_action.Stock_dividend + ~destination_instrument_id:child ~numerator:1L ~denominator:10L + ~basis_allocation_bps:0 + ~fractional_policy:T.Corporate_action.Reject_fractional)); + Alcotest.(check bool) + "basis allocation bounded" true + (Result.is_error + (T.Corporate_action.distribution ~id ~instrument_id:source + ~distribution_type:T.Corporate_action.Spin_off + ~destination_instrument_id:child ~numerator:1L ~denominator:10L + ~basis_allocation_bps:10_001 + ~fractional_policy:T.Corporate_action.Reject_fractional)) + +let policy_and_transition_boundaries () = + let id = action_id "boundary-event" in + let source = instrument_id "source" in + let child = instrument_id "child" in + let distribution ?(distribution_type = T.Corporate_action.Spin_off) + ?(destination = child) ?(numerator = 1L) ?(denominator = 2L) + ?(basis = 1000) + ?(fractional_policy = T.Corporate_action.Reject_fractional) () = + T.Corporate_action.distribution ~id ~instrument_id:source ~distribution_type + ~destination_instrument_id:destination ~numerator ~denominator + ~basis_allocation_bps:basis ~fractional_policy + in + List.iter + (fun (name, result) -> + Alcotest.(check bool) name true (Result.is_error result)) + [ + ("zero numerator", distribution ~numerator:0L ()); + ("zero denominator", distribution ~denominator:0L ()); + ("negative basis", distribution ~basis:(-1) ()); + ( "stock basis must be zero", + distribution ~distribution_type:T.Corporate_action.Stock_dividend + ~destination:source () ); + ( "stock ratio overflow", + distribution ~distribution_type:T.Corporate_action.Stock_dividend + ~destination:source ~basis:0 ~numerator:Int64.max_int () ); + ("spin-off destination differs", distribution ~destination:source ()); + ( "cash currency label", + distribution + ~fractional_policy: + (T.Corporate_action.Cash_in_lieu + { price = price "1"; currency = "bad currency" }) + () ); + ]; + Alcotest.(check (list string)) + "distribution labels" + [ "stock_dividend"; "rights"; "spin_off" ] + (List.map T.Corporate_action.distribution_type_to_string + [ + T.Corporate_action.Stock_dividend; + T.Corporate_action.Rights; + T.Corporate_action.Spin_off; + ]); + Alcotest.(check string) + "distribution formatting" "boundary-event spin_off 1:2 source" + (Format.asprintf "%a" T.Corporate_action.pp (distribution () |> ok)); + let configured = instrument ~id:"stable" ~symbol:"OLD" () in + Alcotest.(check bool) + "duplicate lifecycle catalog" true + (Result.is_error (T.Instrument_lifecycle.create [ configured; configured ])); + let state = T.Instrument_lifecycle.create [ configured ] |> ok in + Alcotest.(check bool) + "unknown instrument is not tradable" false + (T.Instrument_lifecycle.is_tradable state (instrument_id "unknown")); + let create ?(instrument_id = configured.id) name kind = + T.Instrument_lifecycle.create_event ~id:(action_id name) ~instrument_id + ~kind + in + List.iter + (fun (name, result) -> + Alcotest.(check bool) name true (Result.is_error result)) + [ + ( "invalid halt reason", + create "bad-halt" (T.Instrument_lifecycle.Halt { reason = "" }) ); + ( "invalid delisting reason", + create "bad-delist" + (T.Instrument_lifecycle.Delisting + { + terminal_policy = T.Instrument_lifecycle.Hold; + reason = "bad reason"; + }) ); + ( "invalid identifier mapping", + create "bad-id" + (T.Instrument_lifecycle.Identifier_change + { symbol = ""; provider = "sip"; provider_instrument_id = "x" }) ); + ( "invalid terminal currency", + create "bad-terminal" + (T.Instrument_lifecycle.Expiration + { + terminal_policy = + T.Instrument_lifecycle.Cash_out + { price = price "1"; currency = "" }; + }) ); + ]; + let unknown = + create ~instrument_id:(instrument_id "unknown") "unknown-event" + (T.Instrument_lifecycle.Halt { reason = "halt" }) + |> ok + in + Alcotest.(check bool) + "unknown lifecycle instrument" true + (Result.is_error (T.Instrument_lifecycle.apply state unknown)); + let resume = create "early-resume" T.Instrument_lifecycle.Resume |> ok in + Alcotest.(check bool) + "tradable cannot resume" true + (Result.is_error (T.Instrument_lifecycle.apply state resume)); + let halt = + create "first-halt" (T.Instrument_lifecycle.Halt { reason = "halt" }) |> ok + in + let halted = T.Instrument_lifecycle.apply state halt |> ok in + let second_halt = + create "second-halt" (T.Instrument_lifecycle.Halt { reason = "halt" }) |> ok + in + Alcotest.(check bool) + "halted cannot halt" true + (Result.is_error (T.Instrument_lifecycle.apply halted second_halt)); + Alcotest.(check (list string)) + "status labels" + [ "tradable"; "halted"; "expired"; "delisted" ] + (List.map T.Instrument_lifecycle.status_to_string + [ + T.Instrument_lifecycle.Tradable; + T.Instrument_lifecycle.Halted; + T.Instrument_lifecycle.Expired; + T.Instrument_lifecycle.Delisted; + ]); + let kinds = + [ + T.Instrument_lifecycle.Halt { reason = "halt" }; + T.Instrument_lifecycle.Resume; + T.Instrument_lifecycle.Identifier_change + { symbol = "NEW"; provider = "sip"; provider_instrument_id = "NEW.X" }; + T.Instrument_lifecycle.Expiration + { terminal_policy = T.Instrument_lifecycle.Hold }; + T.Instrument_lifecycle.Delisting + { + terminal_policy = T.Instrument_lifecycle.Hold; + reason = "acquisition"; + }; + ] + in + Alcotest.(check (list string)) + "kind labels" + [ "halt"; "resume"; "identifier_change"; "expiration"; "delisting" ] + (List.map T.Instrument_lifecycle.kind_to_string kinds); + let renamed = + T.Instrument_lifecycle.apply state + (create "provider-b" + (T.Instrument_lifecycle.Identifier_change + { symbol = "NEW"; provider = "b"; provider_instrument_id = "2" }) + |> ok) + |> ok + in + let renamed = + T.Instrument_lifecycle.apply renamed + (create "provider-a" + (T.Instrument_lifecycle.Identifier_change + { symbol = "NEW"; provider = "a"; provider_instrument_id = "1" }) + |> ok) + |> ok + in + let delisting = + create "valid-delisting" + (T.Instrument_lifecycle.Delisting + { + terminal_policy = T.Instrument_lifecycle.Hold; + reason = "acquisition"; + }) + |> ok + in + let delisted = T.Instrument_lifecycle.apply renamed delisting |> ok in + Alcotest.(check bool) + "delisted is terminal" true + (Result.is_error (T.Instrument_lifecycle.apply delisted resume)); + let expiration = + create "halted-expiration" + (T.Instrument_lifecycle.Expiration + { terminal_policy = T.Instrument_lifecycle.Hold }) + |> ok + in + ignore (T.Instrument_lifecycle.apply halted expiration |> ok) + +let lifecycle_slice ?(corporate_actions = []) ?(lifecycle_events = []) sequence + = + let date = Int64.to_int sequence + 1 in + T.Market_slice.create_v12 ~slice_sequence:sequence + ~start_at:(timestamp (Printf.sprintf "2026-03-%02dT14:30:00Z" date)) + ~end_at:(timestamp (Printf.sprintf "2026-03-%02dT21:00:00Z" date)) + ~available_at:(timestamp (Printf.sprintf "2026-03-%02dT21:00:01Z" date)) + ~received_at:(timestamp (Printf.sprintf "2026-03-%02dT21:00:02Z" date)) + ~bars:[ bar sequence ] + ~fx_rates:[ fx_mark () ] + ~corporate_actions ~borrow_observations:[] ~cash_rate_observations:[] + ~settlement_failures:[] ~lifecycle_events + |> ok + +let lifecycle_runner schedule run = + let config = engine_config ~contract_version:"12" () in + let strategy_state = T.Scripted_strategy.create schedule |> ok in + Runner.create ~run_id:(run_id run) ~scenario_sha256 ~config + ~initial_cash:[ ("USD", money "10000") ] + ~strategy_state + |> ok + +let halt_cancels_orders_and_rejects_new_exposure () = + let working = + request ~kind:(T.Order.Limit (price "50")) ~quantity_value:"2" () + in + let schedule = + [ + (1L, [ T.Strategy.Submit_order working ]); + (2L, [ T.Strategy.Submit_order working ]); + ] + in + let state, _ = + Runner.process_slice + (lifecycle_runner schedule "halt-run") + (lifecycle_slice 1L) + |> ok + in + let halt = + T.Instrument_lifecycle.create_event ~id:(action_id "halt-event") + ~instrument_id:(instrument_id "test-equity") + ~kind:(T.Instrument_lifecycle.Halt { reason = "regulatory" }) + |> ok + in + let state, events = + Runner.process_slice state (lifecycle_slice ~lifecycle_events:[ halt ] 2L) + |> ok + in + Alcotest.(check bool) + "halt audit" true + (List.exists + (fun (audit : T.Audit.t) -> + match audit.event with + | T.Audit.Lifecycle_applied _ -> true + | _ -> false) + events); + Alcotest.(check bool) + "working order cancelled" true + (List.exists + (fun (audit : T.Audit.t) -> + match audit.event with + | T.Audit.Order_cancelled { reason = T.Audit.Instrument_halt; _ } -> + true + | _ -> false) + events); + Alcotest.(check bool) + "same-slice new order rejected" true + (List.exists + (fun (audit : T.Audit.t) -> + match audit.event with + | T.Audit.Order_rejected order -> + order.status = T.Order.Rejected "instrument is not tradable" + | _ -> false) + events); + Alcotest.(check int) + "no active orders" 0 + (List.length (T.Oms.active_orders (Runner.oms state))) + +let terminal_cash_out_is_auditable () = + let target = + T.Strategy.Target_quantities + [ + { instrument_id = instrument_id "test-equity"; quantity = quantity "3" }; + ] + in + let state = lifecycle_runner [ (1L, [ target ]) ] "terminal-run" in + let state, _ = Runner.process_slice state (lifecycle_slice 1L) |> ok in + let state, _ = Runner.process_slice state (lifecycle_slice 2L) |> ok in + let expiration = + T.Instrument_lifecycle.create_event + ~id:(action_id "expiration-event") + ~instrument_id:(instrument_id "test-equity") + ~kind: + (T.Instrument_lifecycle.Expiration + { + terminal_policy = + T.Instrument_lifecycle.Cash_out + { price = price "90"; currency = "USD" }; + }) + |> ok + in + let state, events = + Runner.process_slice state + (lifecycle_slice ~lifecycle_events:[ expiration ] 3L) + |> ok + in + Alcotest.check quantity_testable "terminal position cleared" (quantity "0") + (T.Account.position_quantity (Runner.account state) + (instrument_id "test-equity")); + Alcotest.(check bool) + "terminal attribution" true + (List.exists + (fun (audit : T.Audit.t) -> + match audit.event with + | T.Audit.Lifecycle_applied + { liquidated_quantity; cash_amount; listing; _ } -> + T.Scalar.Quantity.equal liquidated_quantity (quantity "3") + && T.Scalar.Money.equal cash_amount (money "270") + && listing.status = T.Instrument_lifecycle.Expired + | _ -> false) + events) + +let stock_dividend_adjusts_account_and_target () = + let target = + T.Strategy.Target_quantities + [ + { instrument_id = instrument_id "test-equity"; quantity = quantity "3" }; + ] + in + let state = lifecycle_runner [ (1L, [ target ]) ] "stock-dividend-run" in + let state, _ = Runner.process_slice state (lifecycle_slice 1L) |> ok in + let state, _ = Runner.process_slice state (lifecycle_slice 2L) |> ok in + let action = + T.Corporate_action.distribution + ~id:(action_id "stock-dividend") + ~instrument_id:(instrument_id "test-equity") + ~distribution_type:T.Corporate_action.Stock_dividend + ~destination_instrument_id:(instrument_id "test-equity") + ~numerator:1L ~denominator:2L ~basis_allocation_bps:0 + ~fractional_policy: + (T.Corporate_action.Cash_in_lieu + { price = price "20"; currency = "USD" }) + |> ok + in + let state, events = + Runner.process_slice state + (lifecycle_slice ~corporate_actions:[ action ] 3L) + |> ok + in + Alcotest.check quantity_testable "lot-aligned stock entitlement" + (quantity "4") + (T.Account.position_quantity (Runner.account state) + (instrument_id "test-equity")); + Alcotest.(check bool) + "distribution attribution" true + (List.exists + (fun (audit : T.Audit.t) -> + match audit.event with + | T.Audit.Distribution_applied { result; _ } -> + T.Scalar.Quantity.equal result.destination_quantity (quantity "1") + && T.Scalar.Quantity.equal result.fractional_quantity + (quantity "0.5") + && T.Scalar.Money.equal result.cash_in_lieu (money "10") + | _ -> false) + events); + Alcotest.(check bool) + "fraction does not create a target order" true + (T.Oms.active_orders (Runner.oms state) = []) + +let tests = + [ + Alcotest.test_case "distribution basis and fractional cash" `Quick + distribution_allocates_basis_and_fractional_cash; + Alcotest.test_case "fractional policy is explicit" `Quick + fractional_policy_is_explicit; + Alcotest.test_case "lifecycle identity and terminal state" `Quick + lifecycle_preserves_identity_and_terminal_state; + Alcotest.test_case "constructors reject ambiguous policies" `Quick + constructors_reject_ambiguous_policies; + Alcotest.test_case "policy and transition boundaries" `Quick + policy_and_transition_boundaries; + Alcotest.test_case "halt cancels and rejects exposure" `Quick + halt_cancels_orders_and_rejects_new_exposure; + Alcotest.test_case "terminal cash-out is auditable" `Quick + terminal_cash_out_is_auditable; + Alcotest.test_case "stock dividend adjusts account and target" `Quick + stock_dividend_adjusts_account_and_target; + ] diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 1024b5d..4e7a482 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -118,7 +118,7 @@ let capabilities_describe_execution_contracts () = (strings "configuration_versions"); Alcotest.(check (list string)) "scenario contracts" - [ "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] + [ "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_engine.ml b/test/test_engine.ml index 2227ae0..fd1bd50 100644 --- a/test/test_engine.ml +++ b/test/test_engine.ml @@ -9,6 +9,7 @@ let () = ("fee-schedules", Test_fee_schedules.tests); ("financing", Test_financing.tests); ("settlement", Test_settlement.tests); + ("corporate-lifecycle", Test_corporate_lifecycle.tests); ("reducer", Test_reducer.tests); ("reducer-properties", Test_reducer_properties.tests); ("checkpoint4", Test_checkpoint4.tests); diff --git a/test/test_scenario.ml b/test/test_scenario.ml index 4fb37d1..c3c91ad 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/v11/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v12/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/v11/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v12/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_v11 ~slice_sequence:(Int64.of_int index) + T.Market_slice.create_v12 ~slice_sequence:(Int64.of_int index) ~start_at ~end_at:(add_seconds base (offset + 1)) ~available_at:(add_seconds base (offset + 2)) @@ -89,12 +89,13 @@ let write_large_stream path slice_count = ~fx_rates:[ fx_mark () ] ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] ~cash_rate_observations:[ cash_rate ] ~settlement_failures:[] + ~lifecycle_events:[] |> ok in let payload = `Assoc [ - ("market_slice", T.Codec.market_slice_to_yojson_v11 market_slice); + ("market_slice", T.Codec.market_slice_to_yojson_v12 market_slice); ("intents", `List []); ] in @@ -139,9 +140,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v11/scenario.schema.json"; - check_schema "../contracts/v11/scenario-stream.schema.json"; - check_schema "../contracts/v11/journal.schema.json" + check_schema "../contracts/v12/scenario.schema.json"; + check_schema "../contracts/v12/scenario-stream.schema.json"; + check_schema "../contracts/v12/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -178,6 +179,233 @@ let map_root change = | `Assoc fields -> `Assoc (change fields) | _ -> Alcotest.fail "demo must be an object" +let replace_assoc name value fields = + (name, value) :: List.remove_assoc name fields + +let v12_distributions_and_lifecycle_parse () = + let source = instrument_id "demo-equity-acme" in + let child = instrument_id "demo-equity-child" in + let action name distribution_type destination fractional_policy = + T.Corporate_action.distribution + ~id:(T.Id.Corporate_action.of_string_exn name) + ~instrument_id:source ~distribution_type + ~destination_instrument_id:destination ~numerator:1L ~denominator:2L + ~basis_allocation_bps: + (if distribution_type = T.Corporate_action.Stock_dividend then 0 + else 2500) + ~fractional_policy + |> ok + in + let event name kind = + T.Instrument_lifecycle.create_event + ~id:(T.Id.Corporate_action.of_string_exn name) + ~instrument_id:source ~kind + |> ok + in + let market_slice = + T.Market_slice.create_v12 ~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") + ~received_at:(timestamp "2026-01-02T21:00:01Z") + ~bars:[ bar ~instrument:source 1L; bar ~instrument:child 1L ] + ~fx_rates:[ fx_mark () ] + ~corporate_actions: + [ + action "stock-action" T.Corporate_action.Stock_dividend source + T.Corporate_action.Reject_fractional; + action "rights-action" T.Corporate_action.Rights child + (T.Corporate_action.Cash_in_lieu + { price = price "12.5"; currency = "USD" }); + action "spinoff-action" T.Corporate_action.Spin_off child + T.Corporate_action.Reject_fractional; + ] + ~borrow_observations:[] ~cash_rate_observations:[] ~settlement_failures:[] + ~lifecycle_events: + [ + event "rename-event" + (T.Instrument_lifecycle.Identifier_change + { + symbol = "ACME2"; + provider = "sip"; + provider_instrument_id = "ACME.X"; + }); + event "halt-event" + (T.Instrument_lifecycle.Halt { reason = "regulatory" }); + event "resume-event" T.Instrument_lifecycle.Resume; + event "expiration-event" + (T.Instrument_lifecycle.Expiration + { terminal_policy = T.Instrument_lifecycle.Hold }); + event "delisting-event" + (T.Instrument_lifecycle.Delisting + { + terminal_policy = + T.Instrument_lifecycle.Cash_out + { price = price "9"; currency = "USD" }; + reason = "acquisition"; + }); + ] + |> ok + in + let document = + map_root (fun fields -> + let instruments = + match List.assoc "instruments" fields with + | `List (`Assoc configured :: rest) -> + let child_instrument = + configured + |> replace_assoc "instrument_id" (`String "demo-equity-child") + |> replace_assoc "symbol" (`String "CHILD") + in + `List (`Assoc configured :: `Assoc child_instrument :: rest) + | _ -> Alcotest.fail "demo instruments must be a list" + in + let risk = + match List.assoc "risk" fields with + | `Assoc risk_fields -> + let policies = + match List.assoc "instrument_policies" risk_fields with + | `List (`Assoc configured :: rest) -> + let child_policy = + replace_assoc "instrument_id" + (`String "demo-equity-child") configured + in + `List (`Assoc configured :: `Assoc child_policy :: rest) + | _ -> Alcotest.fail "demo risk policies must be a list" + in + `Assoc (replace_assoc "instrument_policies" policies risk_fields) + | _ -> Alcotest.fail "demo risk must be an object" + in + let venue_calendars = + match List.assoc "venue_calendars" fields with + | `List [ `Assoc calendar ] -> + `List + [ + `Assoc + (replace_assoc "instrument_ids" + (`List + [ + `String "demo-equity-acme"; + `String "demo-equity-child"; + ]) + calendar); + ] + | _ -> Alcotest.fail "demo venue calendars must be a singleton" + in + let execution = + match List.assoc "execution" fields with + | `Assoc execution_fields -> ( + match List.assoc "configuration" execution_fields with + | `Assoc configuration -> + let schedules = + match List.assoc "fee_schedules" configuration with + | `List (`Assoc configured :: rest) -> + let child_schedule = + configured + |> replace_assoc "schedule_id" + (`String "demo-child-fees-v1") + |> replace_assoc "instrument_id" + (`String "demo-equity-child") + in + `List + (`Assoc configured :: `Assoc child_schedule :: rest) + | _ -> Alcotest.fail "demo fee schedules must be a list" + in + `Assoc + (replace_assoc "configuration" + (`Assoc + (replace_assoc "fee_schedules" schedules configuration)) + execution_fields) + | _ -> + Alcotest.fail "demo execution configuration must be an object" + ) + | _ -> Alcotest.fail "demo execution must be an object" + in + let slices = + match List.assoc "slices" fields with + | `List (_ :: rest) -> + let add_child_bar = function + | `Assoc slice_fields -> ( + match List.assoc "bars" slice_fields with + | `List (`Assoc configured :: bars) -> + let child_bar = + replace_assoc "instrument_id" + (`String "demo-equity-child") configured + in + `Assoc + (replace_assoc "bars" + (`List + (`Assoc configured :: `Assoc child_bar :: bars)) + slice_fields) + | _ -> Alcotest.fail "demo slice bars must be nonempty") + | _ -> Alcotest.fail "demo slice must be an object" + in + `List + (T.Codec.market_slice_to_yojson_v12 market_slice + :: List.map add_child_bar rest) + | _ -> Alcotest.fail "demo slices must be nonempty" + in + let schedule = + let add_child_target = function + | `Assoc intent_fields as intent -> ( + match List.assoc_opt "targets" intent_fields with + | Some (`List (`Assoc configured :: targets)) -> + let child_target = + configured + |> replace_assoc "instrument_id" + (`String "demo-equity-child") + |> fun fields -> + if List.mem_assoc "weight" fields then + replace_assoc "weight" (`String "0") fields + else replace_assoc "quantity" (`String "0") fields + in + `Assoc + (replace_assoc "targets" + (`List + (`Assoc configured :: `Assoc child_target :: targets)) + intent_fields) + | _ -> intent) + | json -> json + in + match List.assoc "schedule" fields with + | `List entries -> + `List + (List.map + (function + | `Assoc entry_fields -> ( + match List.assoc "intents" entry_fields with + | `List intents -> + `Assoc + (replace_assoc "intents" + (`List (List.map add_child_target intents)) + entry_fields) + | _ -> Alcotest.fail "schedule intents must be a list") + | _ -> Alcotest.fail "schedule entry must be an object") + entries) + | _ -> Alcotest.fail "demo schedule must be a list" + in + fields + |> replace_assoc "instruments" instruments + |> replace_assoc "risk" risk + |> replace_assoc "venue_calendars" venue_calendars + |> replace_assoc "execution" execution + |> replace_assoc "slices" slices + |> replace_assoc "schedule" schedule) + |> Yojson.Safe.to_string + in + let parsed = + match T.Scenario.of_string document with + | Ok value -> value + | Error diagnostic -> Alcotest.fail (T.Diagnostic.to_human diagnostic) + in + let first = List.hd parsed.slices in + Alcotest.(check int) + "all distribution variants" 3 + (List.length first.corporate_actions); + Alcotest.(check int) + "all lifecycle variants" 5 + (List.length first.lifecycle_events) + let unknown_fields_are_rejected () = let changed = map_root (fun fields -> ("unexpected", `Bool true) :: fields) in Alcotest.(check bool) @@ -203,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 11, 10, 9, \ - 8, 7, 6, 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 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" @@ -354,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_v11 ~slice_sequence:(Int64.of_int index) ~start_at + T.Market_slice.create_v12 ~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)) @@ -367,8 +595,8 @@ let dense_schedule_document slice_count = ~fx_rates:[ fx_mark () ] ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] ~cash_rate_observations:[ cash_rate_observation ] - ~settlement_failures:[] - |> ok |> T.Codec.market_slice_to_yojson_v11) + ~settlement_failures:[] ~lifecycle_events:[] + |> ok |> T.Codec.market_slice_to_yojson_v12) in let schedule = List.init slice_count (fun offset -> @@ -900,7 +1128,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v11/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v12/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -928,7 +1156,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v11/fixtures/fill-clipped.scenario.json" + "../contracts/v12/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in @@ -942,7 +1170,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v11/fixtures/fill-clipped.journal.jsonl" + "../contracts/v12/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual @@ -1245,6 +1473,8 @@ let large_stream_replay_does_not_retain_audit_history () = let tests = [ Alcotest.test_case "demo contract parses" `Quick demo_contract_parses; + Alcotest.test_case "v12 distributions and lifecycle parse" `Quick + v12_distributions_and_lifecycle_parse; Alcotest.test_case "schema artifacts parse" `Quick schema_artifacts_parse; Alcotest.test_case "timestamp precision is bounded" `Quick timestamp_precision_is_bounded; diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index b05ac12..9eab6ed 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" "9" + "protocol version" "10" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -222,7 +222,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "9"); + ("strategy_protocol_version", `String "10"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); From a595ab3d4b8fb6874f9e5360272dd9e7fb3c4a47 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 16:10:54 -0400 Subject: [PATCH 47/57] feat: add conservative bar execution models --- CHANGELOG.md | 6 + README.md | 27 +- contracts/conformance/cases.json | 84 + contracts/conformance/manifest.json | 49 + contracts/strategy/v11/README.md | 59 + contracts/strategy/v11/dune | 15 + .../v11/fixtures/external.scenario.json | 304 +++ .../v11/fixtures/external.scenario.jsonl | 4 + .../v11/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v11/message.schema.json | 302 +++ contracts/strategy/v11/transcript.schema.json | 82 + contracts/v13/README.md | 93 + contracts/v13/dune | 18 + contracts/v13/fixtures/demo.journal.jsonl | 29 + contracts/v13/fixtures/demo.scenario.json | 453 ++++ contracts/v13/fixtures/demo.scenario.jsonl | 6 + .../v13/fixtures/fill-clipped.journal.jsonl | 13 + .../v13/fixtures/fill-clipped.scenario.json | 267 ++ contracts/v13/journal.schema.json | 2413 +++++++++++++++++ contracts/v13/scenario-stream.schema.json | 78 + contracts/v13/scenario.schema.json | 713 +++++ docs/api-reference.md | 2 +- docs/continuous-integration.md | 2 +- docs/execution-model.md | 23 +- docs/persistra.md | 8 +- docs/scenario.md | 26 +- lib/audit.ml | 7 + lib/audit.mli | 6 + lib/codec.ml | 38 +- lib/codec.mli | 1 + lib/contract.ml | 10 +- lib/engine.ml | 29 + lib/engine.mli | 11 + lib/execution.ml | 307 ++- lib/execution.mli | 38 + lib/execution_model.ml | 61 +- lib/external_replay.ml | 6 +- lib/market_slice.ml | 2 + lib/market_slice.mli | 15 + lib/replay.ml | 6 +- lib/scenario.ml | 107 +- lib/scenario_shape.ml | 36 +- lib/scenario_validation.ml | 8 +- lib/strategy_protocol.ml | 66 +- mkdocs.yml | 4 +- scripts/check-deterministic-journals | 8 + scripts/check-documentation.py | 4 +- scripts/release_artifacts.py | 12 +- test/cli.t | 2 +- test/dune | 72 + test/test_diagnostic.ml | 25 +- test/test_execution.ml | 264 ++ test/test_reducer.ml | 28 + test/test_scenario.ml | 104 +- test/test_strategy_protocol.ml | 49 +- 55 files changed, 6217 insertions(+), 199 deletions(-) create mode 100644 contracts/strategy/v11/README.md create mode 100644 contracts/strategy/v11/dune create mode 100644 contracts/strategy/v11/fixtures/external.scenario.json create mode 100644 contracts/strategy/v11/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v11/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v11/message.schema.json create mode 100644 contracts/strategy/v11/transcript.schema.json create mode 100644 contracts/v13/README.md create mode 100644 contracts/v13/dune create mode 100644 contracts/v13/fixtures/demo.journal.jsonl create mode 100644 contracts/v13/fixtures/demo.scenario.json create mode 100644 contracts/v13/fixtures/demo.scenario.jsonl create mode 100644 contracts/v13/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v13/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v13/journal.schema.json create mode 100644 contracts/v13/scenario-stream.schema.json create mode 100644 contracts/v13/scenario.schema.json 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 From 7990830e29918b82170400b8a70d03ea075c8462 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 16:40:51 -0400 Subject: [PATCH 48/57] feat: add causal quote and trade replay --- CHANGELOG.md | 4 + README.md | 28 +- contracts/conformance/cases.json | 178 +- contracts/conformance/manifest.json | 235 +- contracts/strategy/v12/README.md | 59 + contracts/strategy/v12/dune | 15 + .../v12/fixtures/external.scenario.json | 306 +++ .../v12/fixtures/external.scenario.jsonl | 4 + .../v12/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v12/message.schema.json | 302 ++ contracts/strategy/v12/transcript.schema.json | 82 + contracts/v14/README.md | 102 + contracts/v14/dune | 27 + contracts/v14/fixtures/demo.journal.jsonl | 29 + contracts/v14/fixtures/demo.scenario.json | 461 ++++ contracts/v14/fixtures/demo.scenario.jsonl | 6 + .../v14/fixtures/fill-clipped.journal.jsonl | 13 + .../v14/fixtures/fill-clipped.scenario.json | 271 ++ .../v14/fixtures/quote-trade.journal.jsonl | 13 + .../v14/fixtures/quote-trade.scenario.json | 317 +++ .../v14/fixtures/quote-trade.scenario.jsonl | 4 + contracts/v14/journal.schema.json | 2420 +++++++++++++++++ contracts/v14/scenario-stream.schema.json | 78 + contracts/v14/scenario.schema.json | 770 ++++++ docs/api-reference.md | 2 +- docs/continuous-integration.md | 4 +- docs/execution-model.md | 14 +- docs/persistra.md | 8 +- docs/scenario.md | 25 +- lib/codec.ml | 71 +- lib/codec.mli | 1 + lib/contract.ml | 10 +- lib/engine.ml | 1 + lib/engine.mli | 11 + lib/execution.ml | 272 ++ lib/execution.mli | 7 + lib/execution_model.ml | 33 +- lib/external_replay.ml | 6 +- lib/market_event.ml | 92 + lib/market_event.mli | 52 + lib/market_slice.ml | 32 +- lib/market_slice.mli | 17 + lib/replay.ml | 6 +- lib/scenario.ml | 162 +- lib/scenario_shape.ml | 32 +- lib/scenario_validation.ml | 46 +- lib/strategy_protocol.ml | 63 +- mkdocs.yml | 4 +- scripts/check-deterministic-journals | 12 + scripts/check-documentation.py | 4 +- scripts/release_artifacts.py | 12 +- test/cli.t | 2 +- test/dune | 75 + test/test_diagnostic.ml | 3 +- test/test_domain.ml | 99 + test/test_execution.ml | 211 ++ test/test_scenario.ml | 101 +- test/test_strategy_protocol.ml | 4 +- 58 files changed, 7048 insertions(+), 184 deletions(-) create mode 100644 contracts/strategy/v12/README.md create mode 100644 contracts/strategy/v12/dune create mode 100644 contracts/strategy/v12/fixtures/external.scenario.json create mode 100644 contracts/strategy/v12/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v12/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v12/message.schema.json create mode 100644 contracts/strategy/v12/transcript.schema.json create mode 100644 contracts/v14/README.md create mode 100644 contracts/v14/dune create mode 100644 contracts/v14/fixtures/demo.journal.jsonl create mode 100644 contracts/v14/fixtures/demo.scenario.json create mode 100644 contracts/v14/fixtures/demo.scenario.jsonl create mode 100644 contracts/v14/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v14/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v14/fixtures/quote-trade.journal.jsonl create mode 100644 contracts/v14/fixtures/quote-trade.scenario.json create mode 100644 contracts/v14/fixtures/quote-trade.scenario.jsonl create mode 100644 contracts/v14/journal.schema.json create mode 100644 contracts/v14/scenario-stream.schema.json create mode 100644 contracts/v14/scenario.schema.json create mode 100644 lib/market_event.ml create mode 100644 lib/market_event.mli diff --git a/CHANGELOG.md b/CHANGELOG.md index 24a4c9e..b7aa900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Add causal quote/trade replay with displayed-liquidity capacity, aggressor-qualified passive + fills, maker/taker fee attribution, and economic event timestamps. +- Publish scenario/journal contract v14 and external strategy protocol v12 while preserving v13 + and protocol v11 as frozen compatibility contracts. - Add conservative next-open and adverse-touch completed-bar execution models with strict fixed spread and linear participation-impact configuration, explicit missing-volume policy, tick-aligned prices, and separate price-component audit attribution. diff --git a/README.md b/README.md index 8b92f9e..f307b7d 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,12 @@ scenario slices and scheduled or external intents fee-component attribution - Deterministic event IDs, ordered causal references, and order-creation attribution - Contract-selected compiled execution modules with versioned model-owned configuration and - capability descriptors; v13 adds next-open and adverse-touch models while freezing - `completed_bar_v1` + capability descriptors; v13 adds conservative bar models and v14 adds causal quote/trade replay + while freezing `completed_bar_v1` - Tick-aligned fixed-spread and participation-impact execution costs with separate reference, spread, impact, and final-price audit attribution +- Causally ordered quotes and aggressor-classified trades with displayed-liquidity limits, + maker/taker attribution, and event-time fills - Strict batch JSON and bounded-memory JSON Lines scenario parsing with JSON Schemas - Versioned synchronous JSON Lines strategy processes with per-request timeouts and strict lifecycle supervision @@ -94,7 +96,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v13/fixtures/demo.scenario.json \ + --input contracts/v14/fixtures/demo.scenario.json \ --validate-only ``` @@ -102,7 +104,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v13/fixtures/demo.scenario.json \ + --input contracts/v14/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -110,7 +112,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v13/fixtures/demo.scenario.jsonl \ + --input contracts/v14/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -119,7 +121,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v11/fixtures/external.scenario.json \ + --input contracts/strategy/v12/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -234,19 +236,19 @@ do not provide reducer snapshots or restart recovery. - [Diagnostic contract](docs/diagnostics.md) - [Scenario contract](docs/scenario.md) - [Contract conformance corpus](contracts/conformance/README.md) -- [Current contract v13 and conformance fixtures](contracts/v13/README.md) +- [Current contract v14 and conformance fixtures](contracts/v14/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v13/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v13/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v13/journal.schema.json) -- [External strategy protocol v11](contracts/strategy/v11/README.md) +- [Scenario JSON Schema](contracts/v14/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v14/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v14/journal.schema.json) +- [External strategy protocol v12](contracts/strategy/v12/README.md) - [Historical strategy protocol v3](contracts/strategy/v3/README.md) - [Historical strategy protocol v2](contracts/strategy/v2/README.md) - [Historical strategy protocol v1](contracts/strategy/v1/README.md) - [Persistra compatibility](docs/persistra.md) -- [Strategy message JSON Schema](contracts/strategy/v11/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v11/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v12/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v12/transcript.schema.json) - [Execution model](docs/execution-model.md) - [OCaml coverage](docs/coverage.md) - [Continuous integration and portability matrix](docs/continuous-integration.md) diff --git a/contracts/conformance/cases.json b/contracts/conformance/cases.json index a32b12a..3b62267 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -847,7 +847,9 @@ "kind": "strategy_response", "source": "strategy/v9/fixtures/external.strategy.jsonl", "record": 2, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "1", "protocol_version": "9", "mutations": [], @@ -861,7 +863,9 @@ "kind": "strategy_response", "source": "strategy/v9/fixtures/external.strategy.jsonl", "record": 4, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "2", "protocol_version": "9", "mutations": [], @@ -875,7 +879,9 @@ "kind": "strategy_response", "source": "strategy/v9/fixtures/external.strategy.jsonl", "record": 14, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "7", "protocol_version": "9", "mutations": [], @@ -889,12 +895,28 @@ "kind": "strategy_response", "source": "strategy/v9/fixtures/external.strategy.jsonl", "record": 14, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "7", "protocol_version": "9", "mutations": [ - { "op": "replace", "path": ["message_type"], "value": "error" }, - { "op": "replace", "path": ["payload"], "value": { "message": "fixture failure" } } + { + "op": "replace", + "path": [ + "message_type" + ], + "value": "error" + }, + { + "op": "replace", + "path": [ + "payload" + ], + "value": { + "message": "fixture failure" + } + } ], "schema_expectation": "accept", "runtime_expectation": "accept", @@ -1177,7 +1199,10 @@ "code": "strategy.protocol", "phase": "strategy", "message": "strategy initialization: invalid strategy response JSON", - "context": { "json_path": "$", "sequence": "1" }, + "context": { + "json_path": "$", + "sequence": "1" + }, "cause": null }, "evidence": { @@ -1216,7 +1241,10 @@ "strategy_protocol_version": "10", "strategy_sequence": "1", "message_type": "ready", - "payload": { "strategy_name": "conformance", "strategy_version": null } + "payload": { + "strategy_name": "conformance", + "strategy_version": null + } }, "mutations": [], "schema_expectation": "accept", @@ -1230,7 +1258,9 @@ "strategy_protocol_version": "10", "strategy_sequence": "2", "message_type": "intents", - "payload": { "intents": [] } + "payload": { + "intents": [] + } }, "mutations": [], "schema_expectation": "accept", @@ -1244,7 +1274,9 @@ "strategy_protocol_version": "10", "strategy_sequence": "7", "message_type": "error", - "payload": { "message": "fixture failure" } + "payload": { + "message": "fixture failure" + } }, "mutations": [], "schema_expectation": "accept" @@ -1262,7 +1294,10 @@ "code": "strategy.protocol", "phase": "strategy", "message": "strategy initialization: invalid strategy response JSON", - "context": { "json_path": "$", "sequence": "1" }, + "context": { + "json_path": "$", + "sequence": "1" + }, "cause": null }, "evidence": { @@ -1300,7 +1335,10 @@ "strategy_protocol_version": "11", "strategy_sequence": "1", "message_type": "ready", - "payload": { "strategy_name": "conformance", "strategy_version": null } + "payload": { + "strategy_name": "conformance", + "strategy_version": null + } }, "mutations": [], "schema_expectation": "accept", @@ -1314,7 +1352,9 @@ "strategy_protocol_version": "11", "strategy_sequence": "2", "message_type": "intents", - "payload": { "intents": [] } + "payload": { + "intents": [] + } }, "mutations": [], "schema_expectation": "accept", @@ -1328,7 +1368,9 @@ "strategy_protocol_version": "11", "strategy_sequence": "7", "message_type": "error", - "payload": { "message": "fixture failure" } + "payload": { + "message": "fixture failure" + } }, "mutations": [], "schema_expectation": "accept" @@ -1346,7 +1388,113 @@ "code": "strategy.protocol", "phase": "strategy", "message": "strategy initialization: invalid strategy response JSON", - "context": { "json_path": "$", "sequence": "1" }, + "context": { + "json_path": "$", + "sequence": "1" + }, + "cause": null + }, + "evidence": { + "encoding": "hex", + "prefix": "7b", + "observed_bytes": 1, + "truncated": false + } + }, + "mutations": [], + "schema_expectation": "accept" + }, + { + "name": "scenario-v14-valid", + "artifact": "scenario-v14", + "kind": "scenario", + "source": "v14/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept" + }, + { + "name": "scenario-v14-quote-trade-valid", + "artifact": "scenario-v14", + "kind": "scenario", + "source": "v14/fixtures/quote-trade.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept" + }, + { + "name": "scenario-stream-v14-valid", + "artifact": "scenario-stream-v14", + "kind": "scenario_stream", + "source": "v14/fixtures/quote-trade.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept" + }, + { + "name": "strategy-ready-valid-v12", + "artifact": "strategy-message-v12", + "instance": { + "strategy_protocol_version": "12", + "strategy_sequence": "1", + "message_type": "ready", + "payload": { + "strategy_name": "conformance", + "strategy_version": null + } + }, + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept", + "parser_expected": "ready" + }, + { + "name": "strategy-intents-valid-v12", + "artifact": "strategy-message-v12", + "instance": { + "strategy_protocol_version": "12", + "strategy_sequence": "2", + "message_type": "intents", + "payload": { + "intents": [] + } + }, + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept", + "parser_expected": "intents" + }, + { + "name": "strategy-error-valid-v12", + "artifact": "strategy-message-v12", + "instance": { + "strategy_protocol_version": "12", + "strategy_sequence": "7", + "message_type": "error", + "payload": { + "message": "fixture failure" + } + }, + "mutations": [], + "schema_expectation": "accept" + }, + { + "name": "strategy-v12-rejected-response-branch", + "artifact": "strategy-transcript-v12", + "instance": { + "strategy_diagnostic_version": "1", + "transcript_sequence": "2", + "record_type": "rejected_strategy_response", + "expected_strategy_sequence": "1", + "diagnostic": { + "diagnostic_version": "1", + "code": "strategy.protocol", + "phase": "strategy", + "message": "strategy initialization: invalid strategy response JSON", + "context": { + "json_path": "$", + "sequence": "1" + }, "cause": null }, "evidence": { diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index 97576ee..6d5e637 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -715,9 +715,18 @@ "version_field": "contract_version", "version": "11", "sources": [ - { "path": "v11/fixtures/demo.scenario.json", "format": "json" }, - { "path": "v11/fixtures/fill-clipped.scenario.json", "format": "json" }, - { "path": "strategy/v9/fixtures/external.scenario.json", "format": "json" } + { + "path": "v11/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v11/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "strategy/v9/fixtures/external.scenario.json", + "format": "json" + } ] }, { @@ -726,8 +735,14 @@ "version_field": "contract_version", "version": "11", "sources": [ - { "path": "v11/fixtures/demo.scenario.jsonl", "format": "jsonl" }, - { "path": "strategy/v9/fixtures/external.scenario.jsonl", "format": "jsonl" } + { + "path": "v11/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v9/fixtures/external.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -736,8 +751,14 @@ "version_field": "contract_version", "version": "11", "sources": [ - { "path": "v11/fixtures/demo.journal.jsonl", "format": "jsonl" }, - { "path": "v11/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" } + { + "path": "v11/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v11/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -746,7 +767,13 @@ "version_field": "strategy_protocol_version", "version": "9", "sources": [ - { "path": "strategy/v9/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"] } + { + "path": "strategy/v9/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } ] }, { @@ -755,7 +782,10 @@ "version_field": "strategy_protocol_version", "version": "9", "sources": [ - { "path": "strategy/v9/fixtures/external.strategy.jsonl", "format": "jsonl" } + { + "path": "strategy/v9/fixtures/external.strategy.jsonl", + "format": "jsonl" + } ] }, { @@ -764,9 +794,18 @@ "version_field": "contract_version", "version": "12", "sources": [ - { "path": "v12/fixtures/demo.scenario.json", "format": "json" }, - { "path": "v12/fixtures/fill-clipped.scenario.json", "format": "json" }, - { "path": "strategy/v10/fixtures/external.scenario.json", "format": "json" } + { + "path": "v12/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v12/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "strategy/v10/fixtures/external.scenario.json", + "format": "json" + } ] }, { @@ -775,8 +814,14 @@ "version_field": "contract_version", "version": "12", "sources": [ - { "path": "v12/fixtures/demo.scenario.jsonl", "format": "jsonl" }, - { "path": "strategy/v10/fixtures/external.scenario.jsonl", "format": "jsonl" } + { + "path": "v12/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v10/fixtures/external.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -785,8 +830,14 @@ "version_field": "contract_version", "version": "12", "sources": [ - { "path": "v12/fixtures/demo.journal.jsonl", "format": "jsonl" }, - { "path": "v12/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" } + { + "path": "v12/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v12/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -795,7 +846,13 @@ "version_field": "strategy_protocol_version", "version": "10", "sources": [ - { "path": "strategy/v10/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"] } + { + "path": "strategy/v10/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } ] }, { @@ -804,7 +861,10 @@ "version_field": "strategy_protocol_version", "version": "10", "sources": [ - { "path": "strategy/v10/fixtures/external.strategy.jsonl", "format": "jsonl" } + { + "path": "strategy/v10/fixtures/external.strategy.jsonl", + "format": "jsonl" + } ] }, { @@ -813,9 +873,18 @@ "version_field": "contract_version", "version": "13", "sources": [ - { "path": "v13/fixtures/demo.scenario.json", "format": "json" }, - { "path": "v13/fixtures/fill-clipped.scenario.json", "format": "json" }, - { "path": "strategy/v11/fixtures/external.scenario.json", "format": "json" } + { + "path": "v13/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v13/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "strategy/v11/fixtures/external.scenario.json", + "format": "json" + } ] }, { @@ -824,8 +893,14 @@ "version_field": "contract_version", "version": "13", "sources": [ - { "path": "v13/fixtures/demo.scenario.jsonl", "format": "jsonl" }, - { "path": "strategy/v11/fixtures/external.scenario.jsonl", "format": "jsonl" } + { + "path": "v13/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v11/fixtures/external.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -834,8 +909,14 @@ "version_field": "contract_version", "version": "13", "sources": [ - { "path": "v13/fixtures/demo.journal.jsonl", "format": "jsonl" }, - { "path": "v13/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" } + { + "path": "v13/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v13/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -844,7 +925,13 @@ "version_field": "strategy_protocol_version", "version": "11", "sources": [ - { "path": "strategy/v11/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"] } + { + "path": "strategy/v11/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } ] }, { @@ -853,7 +940,101 @@ "version_field": "strategy_protocol_version", "version": "11", "sources": [ - { "path": "strategy/v11/fixtures/external.strategy.jsonl", "format": "jsonl" } + { + "path": "strategy/v11/fixtures/external.strategy.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "scenario-v14", + "schema": "v14/scenario.schema.json", + "version_field": "contract_version", + "version": "14", + "sources": [ + { + "path": "v14/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v14/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "v14/fixtures/quote-trade.scenario.json", + "format": "json" + }, + { + "path": "strategy/v12/fixtures/external.scenario.json", + "format": "json" + } + ] + }, + { + "name": "scenario-stream-v14", + "schema": "v14/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "14", + "sources": [ + { + "path": "v14/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "v14/fixtures/quote-trade.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v12/fixtures/external.scenario.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "journal-v14", + "schema": "v14/journal.schema.json", + "version_field": "contract_version", + "version": "14", + "sources": [ + { + "path": "v14/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v14/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v14/fixtures/quote-trade.journal.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "strategy-message-v12", + "schema": "strategy/v12/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "12", + "sources": [ + { + "path": "strategy/v12/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } + ] + }, + { + "name": "strategy-transcript-v12", + "schema": "strategy/v12/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "12", + "sources": [ + { + "path": "strategy/v12/fixtures/external.strategy.jsonl", + "format": "jsonl" + } ] } ] diff --git a/contracts/strategy/v12/README.md b/contracts/strategy/v12/README.md new file mode 100644 index 0000000..5aa17ef --- /dev/null +++ b/contracts/strategy/v12/README.md @@ -0,0 +1,59 @@ +# External strategy protocol v12 + +Version 12 is a synchronous JSON Lines protocol over child-process standard input and output. +Trading Engine sends `initialize`, ordered `event` requests, and `shutdown`. The strategy answers +with `ready`, `intents`, and `stopped`. It may answer any request with `error`. +Protocol v11 remains available for scenario contract v13; earlier versions retain their frozen +shapes. + +Every message repeats `strategy_protocol_version: "12"` and a positive canonical +`strategy_sequence`. A response must repeat the sequence of its request. Only one request is +outstanding. Trading Engine rejects unknown or duplicate fields, invalid canonical values, +oversized lines, a wrong version or sequence, unexpected response types, EOF, timeout, and a +nonzero process exit. + +The event context contains the replay clock, a marked base-currency portfolio, deterministic group +exposure snapshots, all working orders, and the latest available bar for each instrument. Every +callback emitted for a market slice uses +that slice's `received_at` as `now` and uses its complete bars and FX vector. The portfolio reports +cash, equity, net, long, short, and gross market value plus every attributed cash ledger and +configured position. Position quantities and weights reflect applied fills. Weights are truncated +toward zero to six decimal places. `weights_available` is false and all weights are null when +equity is zero or negative. + +The `initialize` request identifies scenario contract v14 and includes the exact `initial_portfolio` +snapshot alongside the legacy cash projection. It also carries the complete versioned venue +calendars, nested execution configuration, financing policy, and settlement policy, so a strategy +can construct DAY orders and reject incompatible execution, financing, or settlement state before +replay. + +Matching pauses after each strategy callback. The engine applies the response against the exact +account and OMS state exposed by that callback before delivering another callback or considering +the next eligible order. Later same-slice contexts include the effects of earlier responses. The +eligible-order sequence is fixed at the start of matching, so newly submitted orders wait for a +later slice. Cancelling an order before its turn leaves its unused slice capacity available to the +next eligible order. + +Event payloads cover completed market slices with effective-time borrow and cash-rate observations +plus explicit settlement failures, fills, order updates, and rejected intents. Portfolio contexts +include cash-interest attribution and settled and unsettled cash and position quantities. Response +intents use the scenario v14 intent shapes. Market-slice events include lifecycle transitions and +the expanded corporate-action catalog, plus causally ordered quote/trade market events. + +External replay requires an empty batch schedule and empty streamed intent batches. The engine +records accepted messages in both directions in a deterministic transcript. A response rejected +for invalid JSON, fields, version, sequence, EOF, or size is never stored as an accepted exchange. +Instead, the partial transcript ends with a `rejected_strategy_response` diagnostic record. Version +1 rejection diagnostics use the shared +[`diagnostic/v1`](../../diagnostic/v1/README.md) contract. The transcript schema narrows that +contract to the `strategy.protocol` and `resource.limit` codes in the `strategy` phase. The record +includes the structured rejection diagnostic and at most the first 256 raw response bytes encoded +as lowercase hexadecimal. `observed_bytes` counts bytes available when the engine rejected the +response, and `truncated` reports whether the prefix omits observed bytes. The transcript and audit +journal retain partial files after failure and finalize only after their respective success checks. + +- `message.schema.json` validates individual requests and responses. +- `transcript.schema.json` validates accepted exchanges and rejected-response diagnostics. +- `fixtures/external.scenario.json` is the batch replay fixture. +- `fixtures/external.scenario.jsonl` is its bounded-memory stream form. +- `fixtures/external.strategy.jsonl` is the canonical protocol transcript. diff --git a/contracts/strategy/v12/dune b/contracts/strategy/v12/dune new file mode 100644 index 0000000..3351a8f --- /dev/null +++ b/contracts/strategy/v12/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v12/message.schema.json) + (transcript.schema.json as contracts/strategy/v12/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v12/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v12/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v12/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v12/fixtures/external.scenario.json b/contracts/strategy/v12/fixtures/external.scenario.json new file mode 100644 index 0000000..724125a --- /dev/null +++ b/contracts/strategy/v12/fixtures/external.scenario.json @@ -0,0 +1,306 @@ +{ + "contract_version": "14", + "metadata": { + "producer": "strategy-protocol-fixture" + }, + "run_id": "external-demo", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "10000" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "demo-equity-acme", + "symbol": "ACME", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "demo-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "demo-equity-acme" + ], + "sessions": [ + { + "session_date": "2026-01-01", + "policy": "holiday", + "phases": [] + }, + { + "session_date": "2026-01-02", + "policy": "regular", + "phases": [ + { + "phase": "premarket", + "opens_at": "2026-01-02T09:00:00Z", + "closes_at": "2026-01-02T14:25:00Z" + }, + { + "phase": "opening_auction", + "opens_at": "2026-01-02T14:25:00Z", + "closes_at": "2026-01-02T14:30:00Z" + }, + { + "phase": "regular", + "opens_at": "2026-01-02T14:30:00Z", + "closes_at": "2026-01-02T20:55:00Z" + }, + { + "phase": "closing_auction", + "opens_at": "2026-01-02T20:55:00Z", + "closes_at": "2026-01-02T21:00:00Z" + }, + { + "phase": "postmarket", + "opens_at": "2026-01-02T21:00:00Z", + "closes_at": "2026-01-03T01:00:00Z" + } + ] + }, + { + "session_date": "2026-01-05", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-05T14:30:00Z", + "closes_at": "2026-01-05T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-06", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-06T14:30:00Z", + "closes_at": "2026-01-06T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-07", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-07T14:30:00Z", + "closes_at": "2026-01-07T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-08", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-08T14:30:00Z", + "closes_at": "2026-01-08T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000", + "max_leverage": "2", + "short_borrow_bps": 0, + "instrument_policies": [ + { + "instrument_id": "demo-equity-acme", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "2", + "participation_bps": 5000, + "fee_schedules": [ + { + "schedule_id": "external-acme-fees-v1", + "instrument_id": "demo-equity-acme", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "0.25", + "rounding": "up", + "applies_to": "any" + }, + { + "name": "exchange", + "currency": "USD", + "kind": "notional_bps", + "value": 10, + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-01-02T14:30:00Z", + "end_at": "2026-01-02T21:00:00Z", + "available_at": "2026-01-02T21:00:01Z", + "received_at": "2026-01-02T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "100", + "high": "105", + "low": "99", + "close": "104", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-02T14:30:00Z", + "credit_rate_bps": 0, + "debit_rate_bps": 0 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-01-05T14:30:00Z", + "end_at": "2026-01-05T21:00:00Z", + "available_at": "2026-01-05T21:00:01Z", + "received_at": "2026-01-05T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "103", + "high": "108", + "low": "102", + "close": "107", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-05T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-05T14:30:00Z", + "credit_rate_bps": 0, + "debit_rate_bps": 0 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "demo-equity-acme", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/strategy/v12/fixtures/external.scenario.jsonl b/contracts/strategy/v12/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..793db28 --- /dev/null +++ b/contracts/strategy/v12/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"14","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"strategy-protocol-fixture"},"run_id":"external-demo","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00Z","closes_at":"2026-01-02T14:25:00Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00Z","closes_at":"2026-01-02T14:30:00Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00Z","closes_at":"2026-01-02T20:55:00Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00Z","closes_at":"2026-01-02T21:00:00Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00Z","closes_at":"2026-01-03T01:00:00Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00Z","closes_at":"2026-01-05T21:00:00Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00Z","closes_at":"2026-01-06T21:00:00Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00Z","closes_at":"2026-01-07T21:00:00Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00Z","closes_at":"2026-01-08T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":0,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_v1","configuration":{"version":"2","participation_bps":5000,"fee_schedules":[{"schedule_id":"external-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"14","payload":{"intents":[],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"14","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"14","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v12/fixtures/external.strategy.jsonl b/contracts/strategy/v12/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..acbe035 --- /dev/null +++ b/contracts/strategy/v12/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"12","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"14","scenario_sha256":"6809a3638fe668a2a11e56c42e9bac7e506c0064eb2cb0a3e71ba09d92839ee7","run_id":"external-demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00.000000Z","closes_at":"2026-01-02T14:25:00.000000Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00.000000Z","closes_at":"2026-01-02T14:30:00.000000Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00.000000Z","closes_at":"2026-01-02T20:55:00.000000Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00.000000Z","closes_at":"2026-01-02T21:00:00.000000Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00.000000Z","closes_at":"2026-01-03T01:00:00.000000Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00.000000Z","closes_at":"2026-01-05T21:00:00.000000Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00.000000Z","closes_at":"2026-01-06T21:00:00.000000Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00.000000Z","closes_at":"2026-01-07T21:00:00.000000Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00.000000Z","closes_at":"2026-01-08T18:00:00.000000Z"}]}]}],"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":0,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_v1","configuration":{"version":"2","participation_bps":5000,"fee_schedules":[{"schedule_id":"external-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"any"}]}]}},"metadata":{"producer":"strategy-protocol-fixture"}}}} +{"strategy_protocol_version":"12","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"12","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"2","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}}}}} +{"strategy_protocol_version":"12","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"2","message_type":"intents","payload":{"intents":[{"type":"target_quantities","targets":[{"instrument_id":"demo-equity-acme","quantity":"2"}]},{"type":"emit_metric","name":"fixture_signal","value":"2"}]}}} +{"strategy_protocol_version":"12","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"3","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"order_updated","order":{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} +{"strategy_protocol_version":"12","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"12","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"4","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"fill_received","fill":{"fill_id":"external-demo-fill-000000000001","order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"2","price":"103","notional":"206","fee":"0.456","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.206","quote_amount":"0.206"}]}}}}} +{"strategy_protocol_version":"12","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"12","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"5","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"order_updated","order":{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} +{"strategy_protocol_version":"12","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"12","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"6","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}}}}} +{"strategy_protocol_version":"12","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"12","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"12","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v12/message.schema.json b/contracts/strategy/v12/message.schema.json new file mode 100644 index 0000000..58c9e0c --- /dev/null +++ b/contracts/strategy/v12/message.schema.json @@ -0,0 +1,302 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v12/message.schema.json", + "title": "Trading Engine external strategy protocol v12 message", + "description": "One strict request or response in the synchronous JSON Lines strategy protocol. The engine additionally enforces direction, sequence pairing, canonical values, response limits, and lifecycle order.", + "oneOf": [ + { "$ref": "#/$defs/initialize" }, + { "$ref": "#/$defs/ready" }, + { "$ref": "#/$defs/event" }, + { "$ref": "#/$defs/intents" }, + { "$ref": "#/$defs/shutdown" }, + { "$ref": "#/$defs/stopped" }, + { "$ref": "#/$defs/error" } + ], + "$defs": { + "sequence": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "base": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_protocol_version", "strategy_sequence", "message_type", "payload"], + "properties": { + "strategy_protocol_version": { "const": "12" }, + "strategy_sequence": { "$ref": "#/$defs/sequence" }, + "message_type": { "type": "string" }, + "payload": { "type": "object" } + } + }, + "initialize": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "initialize" }, + "payload": { "$ref": "#/$defs/initializePayload" } + } + } + ] + }, + "initializePayload": { + "type": "object", + "additionalProperties": false, + "required": ["engine_version", "scenario_contract_version", "scenario_sha256", "run_id", "base_currency", "initial_cash", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "metadata"], + "properties": { + "engine_version": { "type": "string", "minLength": 1 }, + "scenario_contract_version": { "const": "14" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/venueCalendar" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/settlement" }, + "metadata": { "type": "object" } + } + }, + "ready": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "ready" }, + "payload": { "$ref": "#/$defs/readyPayload" } + } + } + ] + }, + "readyPayload": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_name", "strategy_version"], + "properties": { + "strategy_name": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, + "strategy_version": { + "oneOf": [ + { "type": "null" }, + { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } + ] + } + } + }, + "event": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "event" }, + "payload": { "$ref": "#/$defs/eventPayload" } + } + } + ] + }, + "eventPayload": { + "type": "object", + "additionalProperties": false, + "required": ["context", "event"], + "properties": { + "context": { "$ref": "#/$defs/context" }, + "event": { "$ref": "#/$defs/strategyEvent" } + } + }, + "context": { + "type": "object", + "additionalProperties": false, + "required": ["now", "portfolio", "working_orders", "latest_bars"], + "properties": { + "now": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/bar" } + } + } + }, + "portfolio": { + "type": "object", + "additionalProperties": false, + "required": ["base_currency", "cash", "net_market_value", "long_market_value", "short_market_value", "gross_exposure", "equity", "weights_available", "cash_weight", "cash_balances", "positions", "group_exposures"], + "properties": { + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "weights_available": { "type": "boolean" }, + "cash_weight": { "$ref": "#/$defs/optionalWeight" }, + "cash_balances": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json#/$defs/cashAttribution" } + }, + "positions": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/markedPosition" } + }, + "group_exposures": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" } + ] + }, + "markedPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "settled_quantity", "unsettled_quantity", "mark", "base_market_value", "weight"], + "properties": { + "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "weight": { "$ref": "#/$defs/optionalWeight" } + } + }, + "strategyEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "market_slice"], + "properties": { + "type": { "const": "market_slice_closed" }, + "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/marketSlice" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "fill"], + "properties": { + "type": { "const": "fill_received" }, + "fill": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json#/$defs/fill" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "order"], + "properties": { + "type": { "const": "order_updated" }, + "order": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json#/$defs/order" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "reason"], + "properties": { + "type": { "const": "intent_rejected" }, + "reason": { "type": "string", "minLength": 1 } + } + } + ] + }, + "intents": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "intents" }, + "payload": { "$ref": "#/$defs/intentsPayload" } + } + } + ] + }, + "intentsPayload": { + "type": "object", + "additionalProperties": false, + "required": ["intents"], + "properties": { + "intents": { + "type": "array", + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/intent" } + } + } + }, + "shutdown": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "shutdown" }, + "payload": { "$ref": "#/$defs/emptyPayload" } + } + } + ] + }, + "stopped": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "stopped" }, + "payload": { "$ref": "#/$defs/emptyPayload" } + } + } + ] + }, + "emptyPayload": { + "type": "object", + "additionalProperties": false, + "maxProperties": 0 + }, + "error": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "error" }, + "payload": { "$ref": "#/$defs/errorPayload" } + } + } + ] + }, + "errorPayload": { + "type": "object", + "additionalProperties": false, + "required": ["message"], + "properties": { + "message": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } + } + } + } +} diff --git a/contracts/strategy/v12/transcript.schema.json b/contracts/strategy/v12/transcript.schema.json new file mode 100644 index 0000000..f00912f --- /dev/null +++ b/contracts/strategy/v12/transcript.schema.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v12/transcript.schema.json", + "title": "Trading Engine external strategy protocol v12 transcript record", + "description": "One accepted exchange or rejected-response diagnostic retained from a supervised stdio strategy session.", + "oneOf": [ + { "$ref": "#/$defs/exchange" }, + { "$ref": "#/$defs/rejectedResponse" } + ], + "$defs": { + "canonicalSequence": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "exchange": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_protocol_version", "transcript_sequence", "direction", "message"], + "properties": { + "strategy_protocol_version": { "const": "12" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v12/message.schema.json" + } + } + }, + "rejectedResponse": { + "type": "object", + "additionalProperties": false, + "required": [ + "strategy_diagnostic_version", + "transcript_sequence", + "record_type", + "expected_strategy_sequence", + "diagnostic", + "evidence" + ], + "properties": { + "strategy_diagnostic_version": { "const": "1" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "record_type": { "const": "rejected_strategy_response" }, + "expected_strategy_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "diagnostic": { "$ref": "#/$defs/diagnostic" }, + "evidence": { "$ref": "#/$defs/evidence" } + } + }, + "diagnostic": { + "allOf": [ + { + "$ref": "https://github.com/fallblu/trading-engine/contracts/diagnostic/v1/diagnostic.schema.json" + }, + { + "properties": { + "code": { "enum": ["strategy.protocol", "resource.limit"] }, + "phase": { "const": "strategy" } + } + } + ] + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["encoding", "prefix", "observed_bytes", "truncated"], + "properties": { + "encoding": { "const": "hex" }, + "prefix": { + "type": "string", + "pattern": "^(?:[0-9a-f]{2}){0,256}$" + }, + "observed_bytes": { + "type": "integer", + "minimum": 0, + "maximum": 1048577 + }, + "truncated": { "type": "boolean" } + } + } + } +} diff --git a/contracts/v14/README.md b/contracts/v14/README.md new file mode 100644 index 0000000..cf247f4 --- /dev/null +++ b/contracts/v14/README.md @@ -0,0 +1,102 @@ +# Trading Engine contract v14 + +This directory is the authoritative v14 process and file contract shared by Trading Engine and its +clients. Versions 13 through 3 remain readable during their client transitions. + +- `scenario.schema.json` validates batch replay inputs. +- `scenario-stream.schema.json` validates each JSON Lines scenario-stream record. +- `journal.schema.json` validates each JSON Lines audit record. +- The files under `fixtures/` form the canonical valid conformance corpus. +- `fill-clipped.scenario.json` and its journal exercise a leverage-limited partial fill. + +Version 7 requires exactly one explicit risk policy per catalog instrument. Each policy defines +order, signed-position, notional, initial-margin, maintenance-margin, and shorting limits. Versioned +risk groups have explicit membership, may overlap, and can constrain gross, long, short, absolute +net, and gross-to-equity concentration exposure. + +Runtime validation requires exact currency, position-mark, and FX coverage; known instruments; +lot-aligned quantities; tick-aligned positive marks; basis with the same sign as quantity; +nonnegative fee histories; the base FX rate equal to one; instrument, group, aggregate exposure, +leverage, and initial-margin limits. Signed cash is valid. A successful v14 run emits `initial_state` +immediately after `run_started`, followed by a reconciled initial `valuation`, before market data. + +Admission and fill clipping include working-order reservations. When multiple groups limit the same +fill, lexical group identity is the deterministic tie breaker. Valuations and strategy contexts +carry group exposure snapshots, and clipping thresholds identify the exact instrument or group. + +Every v14 scenario, stream record, and journal record carries `"contract_version": "14"`. + +Version 8 adds explicit `market`, `limit`, `stop`, and `stop_limit` orders with `gtc`, `ioc`, +`fok`, `day`, and `gtd` time-in-force policies. `day` orders identify both their venue and the +exact versioned calendar; `gtd` orders carry an absolute expiry timestamp. Older contracts retain +their frozen mapping: market orders are IOC and limit orders are GTC. + +Stops evaluate only completed OHLCV bars. A gap through the trigger records the bar start as the +trigger time; an intrabar touch records the bar end. Trigger state and slice sequence are journaled, +and an activated order cannot execute before the following slice. A stop becomes a market order; +a stop-limit becomes its configured limit order. Splits adjust both trigger and limit prices. + +IOC orders cancel any remainder after their first eligible slice. FOK orders fill only when the +full remaining quantity fits both execution capacity and risk capacity, otherwise they cancel with +no fill. DAY orders cancel after matching the slice that reaches the selected session's final +phase close. GTD orders cancel before matching any completed bar whose end reaches or passes the +expiry, avoiding ambiguous partial-bar execution. + +The v10 `execution` object uses `completed_bar_v1` configuration version `"2"`: participation basis +points plus exactly one composable fee schedule per instrument. Named fixed, notional-basis-point, +and per-unit components declare currency, rounding, and maker/taker applicability. Optional +per-fill minimums and caps use the schedule settlement currency; negative components represent +rebates. Fills and valuations retain every native, quote, and base-currency attribution. Runtime +capabilities also advertise frozen configuration version `"1"` for older scenario contracts. + +Version 10 adds a required `financing` policy and effective-time observations on every market +slice. Borrow observations provide per-instrument locate availability, signed annual rates, and +recall state. Cash observations provide separate annual credit and debit rates per currency. +Policies select Actual/365 or Actual/360 day count, simple or daily compounding, missing-data +handling, locate rejection or fill clipping, and recall rejection or deterministic close-out. + +Borrow availability is enforced when a fill would create or increase a short. Recalls cancel +active sells and may submit priority IOC covers until the position is flat. Borrow charges and cash +interest use the exact slice interval, update native ledgers deterministically, and emit dedicated +journal records. Valuations report cash interest separately and include it in aggregate realized +P&L. Version 9 and earlier retain their frozen fixed-borrow behavior and wire shapes. + +Version 12 separates trade-date economic accounting from settlement-date availability. A required +settlement policy selects total or settled cash buying power and total or settled position +availability. Versioned calendars enumerate canonical business dates, and each instrument has an +explicit business-day lag. Every fill creates a deterministic settlement instruction containing +its cash and position movements, trade date, and due date. A due instruction either settles on the +first eligible slice or records a named failure supplied by that slice. + +Valuations and strategy contexts report settled and unsettled cash and quantities without changing +economic equity. Journals include instruction-created, completed, and failed events. Scenario v10 +and strategy protocol v8 retain their frozen immediate-settlement wire behavior. + +Version 12 adds exact stock-dividend, rights, and spin-off distributions. Each distribution names +its destination instrument, exact entitlement ratio, basis allocation in basis points, and either +rejects fractional entitlements or converts them to cash at an explicit price and currency. +Stock dividends adjust persistent targets and eligible working orders; every distribution journals +delivered quantity, fractional quantity, allocated basis, fractional basis, and cash in lieu. + +Lifecycle events keep stable instrument identity separate from mutable symbol and provider +mappings. Halt and resume transitions control tradability. Expiration and delisting are terminal, +cancel active orders, clear target exposure, and require an explicit hold or cash-out policy. +Cash-out specifies its terminal price and currency. Every transition journals the source event, +resulting listing state, provider provenance, liquidated quantity, and cash attribution. + +Version 13 adds `completed_bar_next_open_v1` and `completed_bar_adverse_touch_v1` without changing +the frozen `completed_bar_v1` semantics. Next-open limits require a marketable later open; +adverse-touch limits require a one-tick trade-through before a maker fill is eligible. Both models +declare fixed half-spread and linear participation-impact catalogs, including an explicit policy +for missing bar volume. Price costs round away from the reference price to instrument ticks and +cannot violate a limit. An `execution_price_selected` audit record attributes the reference price, +spread adjustment, impact adjustment, and final executable price before each fill. + +Version 14 adds `quote_trade_v1` and causally ordered `market_events`. Quotes expose bid/ask price +and displayed size. Trades expose price, size, and buy, sell, or unknown aggressor side. Each event +records economic, availability, and receipt timestamps plus a positive ingest sequence. Replay +orders events by availability, receipt, and ingest sequence. Marketable orders consume only +displayed quote liquidity; passive orders require appropriately aggressed trade evidence, and an +unknown aggressor never fills them. Event capacity is shared deterministically across order +priority and fills retain the event's economic timestamp. Completed bars remain the valuation +boundary. The `quote-trade` batch, stream, and journal fixtures demonstrate equivalent replay. diff --git a/contracts/v14/dune b/contracts/v14/dune new file mode 100644 index 0000000..17e522a --- /dev/null +++ b/contracts/v14/dune @@ -0,0 +1,27 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v14/journal.schema.json) + (scenario-stream.schema.json as contracts/v14/scenario-stream.schema.json) + (scenario.schema.json as contracts/v14/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v14/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v14/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl + as + contracts/v14/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v14/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v14/fixtures/fill-clipped.scenario.json) + (fixtures/quote-trade.journal.jsonl + as + contracts/v14/fixtures/quote-trade.journal.jsonl) + (fixtures/quote-trade.scenario.json + as + contracts/v14/fixtures/quote-trade.scenario.json) + (fixtures/quote-trade.scenario.jsonl + as + contracts/v14/fixtures/quote-trade.scenario.jsonl))) diff --git a/contracts/v14/fixtures/demo.journal.jsonl b/contracts/v14/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..3f41591 --- /dev/null +++ b/contracts/v14/fixtures/demo.journal.jsonl @@ -0,0 +1,29 @@ +{"contract_version":"14","engine_sequence":"1","event_id":"demo-event-000000000001","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"49cac47b2abf617f1610d65a29984ab6cd848cd42c28b5d190fb34369365b18f","execution_model":"completed_bar_adverse_touch_v1"}} +{"contract_version":"14","engine_sequence":"2","event_id":"demo-event-000000000002","causation_ids":["demo-event-000000000001"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"1","cost_basis":"90","realized_pnl":"5","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25"}],"marks":[{"instrument_id":"demo-equity-acme","price":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"10000","net_market_value":"100","long_market_value":"100","short_market_value":"0","gross_exposure":"100","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"10","equity":"10100","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"100","fx_rate":"1","market_value":"100","base_market_value":"100","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"10","base_unrealized_pnl":"10","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} +{"contract_version":"14","engine_sequence":"3","event_id":"demo-event-000000000003","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"10000","net_market_value":"100","long_market_value":"100","short_market_value":"0","gross_exposure":"100","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"10","equity":"10100","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"100","fx_rate":"1","market_value":"100","base_market_value":"100","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"10","base_unrealized_pnl":"10","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} +{"contract_version":"14","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} +{"contract_version":"14","engine_sequence":"5","event_id":"demo-event-000000000005","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"10000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-02T14:30:00.000000Z","period_end":"2026-01-02T21:00:00.000000Z","amount":"0.074201","closing_balance":"10000.074201"}} +{"contract_version":"14","engine_sequence":"6","event_id":"demo-event-000000000006","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} +{"contract_version":"14","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} +{"contract_version":"14","engine_sequence":"8","event_id":"demo-event-000000000008","causation_ids":["demo-event-000000000004","demo-event-000000000006"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"8.715","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000008","updated_event_id":"demo-event-000000000008","created_sequence":"8","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"14","engine_sequence":"9","event_id":"demo-event-000000000009","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"10000.074201","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5.074201","unrealized_pnl":"14","equity":"10104.074201","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000.074201","fx_rate":"1","base_value":"10000.074201","interest":"0.074201","base_interest":"0.074201","settled_amount":"10000.074201","unsettled_amount":"0","base_settled_value":"10000.074201","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"104","fx_rate":"1","market_value":"104","base_market_value":"104","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"14","base_unrealized_pnl":"14","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.074201","settled_cash":"10000.074201","unsettled_cash":"0","margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052.074201","maintenance_excess":"10078.074201","margin_call":false},"group_exposures":[]}} +{"contract_version":"14","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"13"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} +{"contract_version":"14","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"10000.074201","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-05T14:30:00.000000Z","period_end":"2026-01-05T21:00:00.000000Z","amount":"0.074201","closing_balance":"10000.148402"}} +{"contract_version":"14","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000008","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","reference_price":"103","spread_adjustment":"0.06","impact_adjustment":"0.13","final_price":"103.19"}} +{"contract_version":"14","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000012"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000001","order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"6.5","price":"103.19","notional":"670.735","fee":"0.920735","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_amount":"0.670735"}]}} +{"contract_version":"14","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000008","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"order_cancelled","payload":{"order":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"8.715","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000008","updated_event_id":"demo-event-000000000008","created_sequence":"8","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"6.5","filled_notional":"670.735","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"14","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000006","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","side":"buy","quantity":"2.215","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000015","updated_event_id":"demo-event-000000000015","created_sequence":"15","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"14","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9328.492667","net_market_value":"802.5","long_market_value":"802.5","short_market_value":"0","gross_exposure":"802.5","cost_basis":"761.655735","realized_pnl":"5.148402","unrealized_pnl":"40.844265","equity":"10130.992667","dividend_pnl":"1","execution_fees":"1.420735","borrow_fees":"0.25","total_fees":"1.670735","cash_balances":[{"currency":"USD","amount":"9328.492667","fx_rate":"1","base_value":"9328.492667","interest":"0.148402","base_interest":"0.148402","settled_amount":"9328.492667","unsettled_amount":"0","base_settled_value":"9328.492667","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7.5","mark":"107","fx_rate":"1","market_value":"802.5","base_market_value":"802.5","cost_basis":"761.655735","base_cost_basis":"761.655735","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.844265","base_unrealized_pnl":"40.844265","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.420735","base_execution_fees":"1.420735","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.670735","base_total_fees":"1.670735","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_currency":"USD","quote_amount":"0.25","base_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_currency":"USD","quote_amount":"0.670735","base_amount":"0.670735"}],"settled_quantity":"7.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_currency":"USD","quote_amount":"0.25","base_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_currency":"USD","quote_amount":"0.670735","base_amount":"0.670735"}],"cash_interest":"0.148402","settled_cash":"9328.492667","unsettled_cash":"0","margin":{"initial_requirement":"401.25","maintenance_requirement":"200.625","initial_excess":"9729.742667","maintenance_excess":"9930.367667","margin_call":false},"group_exposures":[]}} +{"contract_version":"14","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"3","start_at":"2026-01-06T14:30:00.000000Z","end_at":"2026-01-06T21:00:00.000000Z","available_at":"2026-01-06T21:00:01.000000Z","received_at":"2026-01-06T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"107","high":"109","low":"104","close":"105","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} +{"contract_version":"14","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"9328.492667","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-06T14:30:00.000000Z","period_end":"2026-01-06T21:00:00.000000Z","amount":"0.069218","closing_balance":"9328.561885"}} +{"contract_version":"14","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","side":"buy","reference_price":"107","spread_adjustment":"0.06","impact_adjustment":"0.01","final_price":"107.07"}} +{"contract_version":"14","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000002","order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"2.215","price":"107.07","notional":"237.16005","fee":"0.487161","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.237161","quote_amount":"0.237161"}]}} +{"contract_version":"14","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"target_portfolio_requested","payload":{"basis":"quantities","targets":[{"instrument_id":"demo-equity-acme","weight":null,"quantity":"2.5","reference_price":null}]}} +{"contract_version":"14","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000017","demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","side":"sell","quantity":"7.215","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000022","updated_event_id":"demo-event-000000000022","created_sequence":"22","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"14","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.914674","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.302946","realized_pnl":"5.21762","unrealized_pnl":"20.772054","equity":"10110.989674","dividend_pnl":"1","execution_fees":"1.907896","borrow_fees":"0.25","total_fees":"2.157896","cash_balances":[{"currency":"USD","amount":"9090.914674","fx_rate":"1","base_value":"9090.914674","interest":"0.21762","base_interest":"0.21762","settled_amount":"9090.914674","unsettled_amount":"0","base_settled_value":"9090.914674","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"9.715","mark":"105","fx_rate":"1","market_value":"1020.075","base_market_value":"1020.075","cost_basis":"999.302946","base_cost_basis":"999.302946","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.772054","base_unrealized_pnl":"20.772054","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.907896","base_execution_fees":"1.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.157896","base_total_fees":"2.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.5","quote_currency":"USD","quote_amount":"0.5","base_amount":"0.5"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.907896","quote_currency":"USD","quote_amount":"0.907896","base_amount":"0.907896"}],"settled_quantity":"9.715","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.5","quote_currency":"USD","quote_amount":"0.5","base_amount":"0.5"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.907896","quote_currency":"USD","quote_amount":"0.907896","base_amount":"0.907896"}],"cash_interest":"0.21762","settled_cash":"9090.914674","unsettled_cash":"0","margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.952174","maintenance_excess":"9855.970924","margin_call":false},"group_exposures":[]}} +{"contract_version":"14","engine_sequence":"24","event_id":"demo-event-000000000024","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"4","start_at":"2026-01-07T14:30:00.000000Z","end_at":"2026-01-07T21:00:00.000000Z","available_at":"2026-01-07T21:00:01.000000Z","received_at":"2026-01-07T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"105","high":"107","low":"103","close":"106","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} +{"contract_version":"14","engine_sequence":"25","event_id":"demo-event-000000000025","causation_ids":["demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"9090.914674","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-07T14:30:00.000000Z","period_end":"2026-01-07T21:00:00.000000Z","amount":"0.067455","closing_balance":"9090.982129"}} +{"contract_version":"14","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000022","demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","side":"sell","reference_price":"105","spread_adjustment":"0.06","impact_adjustment":"0.02","final_price":"104.92"}} +{"contract_version":"14","engine_sequence":"27","event_id":"demo-event-000000000027","causation_ids":["demo-event-000000000026"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000003","order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"sell","quantity":"7.215","price":"104.92","notional":"756.9978","fee":"1","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.756998","quote_amount":"0.756998"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_amount":"-0.006998"}]}} +{"contract_version":"14","engine_sequence":"28","event_id":"demo-event-000000000028","causation_ids":["demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.979929","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.154644","realized_pnl":"19.134573","unrealized_pnl":"7.845356","equity":"10111.979929","dividend_pnl":"1","execution_fees":"2.907896","borrow_fees":"0.25","total_fees":"3.157896","cash_balances":[{"currency":"USD","amount":"9846.979929","fx_rate":"1","base_value":"9846.979929","interest":"0.285075","base_interest":"0.285075","settled_amount":"9846.979929","unsettled_amount":"0","base_settled_value":"9846.979929","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","mark":"106","fx_rate":"1","market_value":"265","base_market_value":"265","cost_basis":"257.154644","base_cost_basis":"257.154644","realized_pnl":"18.849498","base_realized_pnl":"18.849498","unrealized_pnl":"7.845356","base_unrealized_pnl":"7.845356","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.907896","base_execution_fees":"2.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"settled_quantity":"2.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"cash_interest":"0.285075","settled_cash":"9846.979929","unsettled_cash":"0","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.479929","maintenance_excess":"10045.729929","margin_call":false},"group_exposures":[]}} +{"contract_version":"14","engine_sequence":"29","event_id":"demo-event-000000000029","causation_ids":["demo-event-000000000028"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"49cac47b2abf617f1610d65a29984ab6cd848cd42c28b5d190fb34369365b18f","execution_model":"completed_bar_adverse_touch_v1","valuation":{"base_currency":"USD","cash":"9846.979929","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.154644","realized_pnl":"19.134573","unrealized_pnl":"7.845356","equity":"10111.979929","dividend_pnl":"1","execution_fees":"2.907896","borrow_fees":"0.25","total_fees":"3.157896","cash_balances":[{"currency":"USD","amount":"9846.979929","fx_rate":"1","base_value":"9846.979929","interest":"0.285075","base_interest":"0.285075","settled_amount":"9846.979929","unsettled_amount":"0","base_settled_value":"9846.979929","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","mark":"106","fx_rate":"1","market_value":"265","base_market_value":"265","cost_basis":"257.154644","base_cost_basis":"257.154644","realized_pnl":"18.849498","base_realized_pnl":"18.849498","unrealized_pnl":"7.845356","base_unrealized_pnl":"7.845356","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.907896","base_execution_fees":"2.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"settled_quantity":"2.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"cash_interest":"0.285075","settled_cash":"9846.979929","unsettled_cash":"0","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.479929","maintenance_excess":"10045.729929","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v14/fixtures/demo.scenario.json b/contracts/v14/fixtures/demo.scenario.json new file mode 100644 index 0000000..55e7f7b --- /dev/null +++ b/contracts/v14/fixtures/demo.scenario.json @@ -0,0 +1,461 @@ +{ + "contract_version": "14", + "metadata": { + "producer": "trading-engine-demo", + "purpose": "deterministic conformance fixture" + }, + "run_id": "demo", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "10000" + } + ], + "positions": [ + { + "instrument_id": "demo-equity-acme", + "quantity": "1", + "cost_basis": "90", + "realized_pnl": "5", + "dividend_pnl": "1", + "execution_fees": "0.5", + "borrow_fees": "0.25" + } + ], + "marks": [ + { + "instrument_id": "demo-equity-acme", + "price": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "demo-equity-acme", + "symbol": "ACME", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "0.001" + } + ], + "venue_calendars": [ + { + "calendar_id": "demo-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "demo-equity-acme" + ], + "sessions": [ + { + "session_date": "2026-01-01", + "policy": "holiday", + "phases": [] + }, + { + "session_date": "2026-01-02", + "policy": "regular", + "phases": [ + { + "phase": "premarket", + "opens_at": "2026-01-02T09:00:00Z", + "closes_at": "2026-01-02T14:25:00Z" + }, + { + "phase": "opening_auction", + "opens_at": "2026-01-02T14:25:00Z", + "closes_at": "2026-01-02T14:30:00Z" + }, + { + "phase": "regular", + "opens_at": "2026-01-02T14:30:00Z", + "closes_at": "2026-01-02T20:55:00Z" + }, + { + "phase": "closing_auction", + "opens_at": "2026-01-02T20:55:00Z", + "closes_at": "2026-01-02T21:00:00Z" + }, + { + "phase": "postmarket", + "opens_at": "2026-01-02T21:00:00Z", + "closes_at": "2026-01-03T01:00:00Z" + } + ] + }, + { + "session_date": "2026-01-05", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-05T14:30:00Z", + "closes_at": "2026-01-05T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-06", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-06T14:30:00Z", + "closes_at": "2026-01-06T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-07", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-07T14:30:00Z", + "closes_at": "2026-01-07T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-08", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-08T14:30:00Z", + "closes_at": "2026-01-08T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000", + "max_leverage": "2", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "demo-equity-acme", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "completed_bar_adverse_touch_v1", + "configuration": { + "version": "1", + "participation_bps": 5000, + "fee_schedules": [ + { + "schedule_id": "demo-acme-fees-v1", + "instrument_id": "demo-equity-acme", + "settlement_currency": "USD", + "minimum": "0.3", + "maximum": "1", + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "0.25", + "rounding": "up", + "applies_to": "any" + }, + { + "name": "exchange", + "currency": "USD", + "kind": "notional_bps", + "value": 10, + "rounding": "up", + "applies_to": "taker" + }, + { + "name": "maker_rebate", + "currency": "USD", + "kind": "notional_bps", + "value": -2, + "rounding": "nearest", + "applies_to": "maker" + } + ] + } + ], + "spread_model": { + "model": "fixed_half_spread_v1", + "half_spread_bps": 5 + }, + "impact_model": { + "model": "linear_participation_v1", + "coefficient_bps": 25, + "missing_volume_policy": "reject" + } + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "target_weights", + "targets": [ + { + "instrument_id": "demo-equity-acme", + "weight": "0.1" + } + ] + }, + { + "type": "emit_metric", + "name": "desired_weight", + "value": "0.1" + } + ] + }, + { + "after_slice_sequence": "3", + "intents": [ + { + "type": "target_quantities", + "targets": [ + { + "instrument_id": "demo-equity-acme", + "quantity": "2.5" + } + ] + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-01-02T14:30:00Z", + "end_at": "2026-01-02T21:00:00Z", + "available_at": "2026-01-02T21:00:01Z", + "received_at": "2026-01-02T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "100", + "high": "105", + "low": "99", + "close": "104", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-01-05T14:30:00Z", + "end_at": "2026-01-05T21:00:00Z", + "available_at": "2026-01-05T21:00:01Z", + "received_at": "2026-01-05T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "103", + "high": "108", + "low": "102", + "close": "107", + "volume": "13" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-05T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-05T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [] + }, + { + "slice_sequence": "3", + "start_at": "2026-01-06T14:30:00Z", + "end_at": "2026-01-06T21:00:00Z", + "available_at": "2026-01-06T21:00:01Z", + "received_at": "2026-01-06T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "107", + "high": "109", + "low": "104", + "close": "105", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-06T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-06T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [] + }, + { + "slice_sequence": "4", + "start_at": "2026-01-07T14:30:00Z", + "end_at": "2026-01-07T21:00:00Z", + "available_at": "2026-01-07T21:00:01Z", + "received_at": "2026-01-07T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "105", + "high": "107", + "low": "103", + "close": "106", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-07T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-07T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "demo-equity-acme", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v14/fixtures/demo.scenario.jsonl b/contracts/v14/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..ae6e155 --- /dev/null +++ b/contracts/v14/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"14","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"run_id":"demo","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"1","cost_basis":"90","realized_pnl":"5","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25"}],"marks":[{"instrument_id":"demo-equity-acme","price":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"0.001"}],"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00Z","closes_at":"2026-01-02T14:25:00Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00Z","closes_at":"2026-01-02T14:30:00Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00Z","closes_at":"2026-01-02T20:55:00Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00Z","closes_at":"2026-01-02T21:00:00Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00Z","closes_at":"2026-01-03T01:00:00Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00Z","closes_at":"2026-01-05T21:00:00Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00Z","closes_at":"2026-01-06T21:00:00Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00Z","closes_at":"2026-01-07T21:00:00Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00Z","closes_at":"2026-01-08T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_adverse_touch_v1","configuration":{"version":"1","participation_bps":5000,"fee_schedules":[{"schedule_id":"demo-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":"0.3","maximum":"1","components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"taker"},{"name":"maker_rebate","currency":"USD","kind":"notional_bps","value":-2,"rounding":"nearest","applies_to":"maker"}]}],"spread_model":{"model":"fixed_half_spread_v1","half_spread_bps":5},"impact_model":{"model":"linear_participation_v1","coefficient_bps":25,"missing_volume_policy":"reject"}}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"14","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","weight":"0.1"}],"type":"target_weights"},{"name":"desired_weight","type":"emit_metric","value":"0.1"}],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"14","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"13"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"14","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","quantity":"2.5"}],"type":"target_quantities"}],"market_slice":{"available_at":"2026-01-06T21:00:01Z","bars":[{"close":"105","high":"109","instrument_id":"demo-equity-acme","low":"104","open":"107","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-06T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-06T21:00:02Z","slice_sequence":"3","start_at":"2026-01-06T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"14","payload":{"intents":[],"market_slice":{"available_at":"2026-01-07T21:00:01Z","bars":[{"close":"106","high":"107","instrument_id":"demo-equity-acme","low":"103","open":"105","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-07T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-07T21:00:02Z","slice_sequence":"4","start_at":"2026-01-07T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"14","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v14/fixtures/fill-clipped.journal.jsonl b/contracts/v14/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..1a1c149 --- /dev/null +++ b/contracts/v14/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"14","engine_sequence":"1","event_id":"fill-clipped-event-000000000001","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"77457107873a439fdb669f03d65eaf3a7843e1649a4a0036e68a958c7a48baad","execution_model":"completed_bar_v1"}} +{"contract_version":"14","engine_sequence":"2","event_id":"fill-clipped-event-000000000002","causation_ids":["fill-clipped-event-000000000001"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"550"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"550","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"550","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550","fx_rate":"1","base_value":"550","interest":"0","base_interest":"0","settled_amount":"550","unsettled_amount":"0","base_settled_value":"550","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"550","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} +{"contract_version":"14","engine_sequence":"3","event_id":"fill-clipped-event-000000000003","causation_ids":["fill-clipped-event-000000000002"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"550","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550","fx_rate":"1","base_value":"550","interest":"0","base_interest":"0","settled_amount":"550","unsettled_amount":"0","base_settled_value":"550","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"550","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"14","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} +{"contract_version":"14","engine_sequence":"5","event_id":"fill-clipped-event-000000000005","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"550","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.004081","closing_balance":"550.004081"}} +{"contract_version":"14","engine_sequence":"6","event_id":"fill-clipped-event-000000000006","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000006","updated_event_id":"fill-clipped-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"14","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550.004081","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.004081","unrealized_pnl":"0","equity":"550.004081","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.004081","fx_rate":"1","base_value":"550.004081","interest":"0.004081","base_interest":"0.004081","settled_amount":"550.004081","unsettled_amount":"0","base_settled_value":"550.004081","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.004081","settled_cash":"550.004081","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.004081","maintenance_excess":"550.004081","margin_call":false},"group_exposures":[]}} +{"contract_version":"14","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} +{"contract_version":"14","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"550.004081","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.004081","closing_balance":"550.008162"}} +{"contract_version":"14","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000006","fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_clipped","payload":{"reason":{"version":"1","policy":"max_leverage","threshold":{"unit":"ratio","value":"1"}},"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","proposed_quantity":"10","permitted_quantity":"0","price":"100"}} +{"contract_version":"14","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000006","fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"order_cancelled","payload":{"order":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000006","updated_event_id":"fill-clipped-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"14","engine_sequence":"12","event_id":"fill-clipped-event-000000000012","causation_ids":["fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550.008162","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.008162","unrealized_pnl":"0","equity":"550.008162","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.008162","fx_rate":"1","base_value":"550.008162","interest":"0.008162","base_interest":"0.008162","settled_amount":"550.008162","unsettled_amount":"0","base_settled_value":"550.008162","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"100","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]}} +{"contract_version":"14","engine_sequence":"13","event_id":"fill-clipped-event-000000000013","causation_ids":["fill-clipped-event-000000000012"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"77457107873a439fdb669f03d65eaf3a7843e1649a4a0036e68a958c7a48baad","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"550.008162","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.008162","unrealized_pnl":"0","equity":"550.008162","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.008162","fx_rate":"1","base_value":"550.008162","interest":"0.008162","base_interest":"0.008162","settled_amount":"550.008162","unsettled_amount":"0","base_settled_value":"550.008162","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"100","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v14/fixtures/fill-clipped.scenario.json b/contracts/v14/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..afee413 --- /dev/null +++ b/contracts/v14/fixtures/fill-clipped.scenario.json @@ -0,0 +1,271 @@ +{ + "contract_version": "14", + "metadata": { + "producer": "trading-engine", + "purpose": "fill clipping conformance fixture" + }, + "run_id": "fill-clipped", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "550" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "clip-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "clip-equity" + ], + "sessions": [ + { + "session_date": "2026-02-02", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-02T14:30:00Z", + "closes_at": "2026-02-02T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-03", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-03T14:30:00Z", + "closes_at": "2026-02-03T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-04", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-04T14:30:00Z", + "closes_at": "2026-02-04T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "clip-equity", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "2", + "participation_bps": 10000, + "fee_schedules": [ + { + "schedule_id": "clip-fees-v1", + "instrument_id": "clip-equity", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "10", + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "market", + "trigger_price": null, + "limit_price": null, + "time_in_force": "ioc", + "venue_id": null, + "calendar_id": null, + "expires_at": null + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-02-02T14:30:00Z", + "end_at": "2026-02-02T21:00:00Z", + "available_at": "2026-02-02T21:00:01Z", + "received_at": "2026-02-02T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "50", + "high": "50", + "low": "50", + "close": "50", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-02-03T14:30:00Z", + "end_at": "2026-02-03T21:00:00Z", + "available_at": "2026-02-03T21:00:01Z", + "received_at": "2026-02-03T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "100", + "high": "100", + "low": "100", + "close": "100", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-03T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-03T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "clip-equity", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v14/fixtures/quote-trade.journal.jsonl b/contracts/v14/fixtures/quote-trade.journal.jsonl new file mode 100644 index 0000000..0f729da --- /dev/null +++ b/contracts/v14/fixtures/quote-trade.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"14","engine_sequence":"1","event_id":"quote-trade-event-000000000001","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"fc4ba762e5464546c4ec601df566c99e6c5995928a18e5b72434775ed20b62ac","execution_model":"quote_trade_v1"}} +{"contract_version":"14","engine_sequence":"2","event_id":"quote-trade-event-000000000002","causation_ids":["quote-trade-event-000000000001"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} +{"contract_version":"14","engine_sequence":"3","event_id":"quote-trade-event-000000000003","causation_ids":["quote-trade-event-000000000002"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} +{"contract_version":"14","engine_sequence":"4","event_id":"quote-trade-event-000000000004","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} +{"contract_version":"14","engine_sequence":"5","event_id":"quote-trade-event-000000000005","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} +{"contract_version":"14","engine_sequence":"6","event_id":"quote-trade-event-000000000006","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"quote-trade-event-000000000006","updated_event_id":"quote-trade-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"14","engine_sequence":"7","event_id":"quote-trade-event-000000000007","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} +{"contract_version":"14","engine_sequence":"8","event_id":"quote-trade-event-000000000008","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}]}} +{"contract_version":"14","engine_sequence":"9","event_id":"quote-trade-event-000000000009","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} +{"contract_version":"14","engine_sequence":"10","event_id":"quote-trade-event-000000000010","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000001","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"99","notional":"396","fee":"10","executed_at":"2026-02-03T14:33:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"14","engine_sequence":"11","event_id":"quote-trade-event-000000000011","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000002","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:34:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"14","engine_sequence":"12","event_id":"quote-trade-event-000000000012","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]}} +{"contract_version":"14","engine_sequence":"13","event_id":"quote-trade-event-000000000013","causation_ids":["quote-trade-event-000000000012"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"fc4ba762e5464546c4ec601df566c99e6c5995928a18e5b72434775ed20b62ac","execution_model":"quote_trade_v1","valuation":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v14/fixtures/quote-trade.scenario.json b/contracts/v14/fixtures/quote-trade.scenario.json new file mode 100644 index 0000000..230f846 --- /dev/null +++ b/contracts/v14/fixtures/quote-trade.scenario.json @@ -0,0 +1,317 @@ +{ + "contract_version": "14", + "metadata": { + "producer": "trading-engine", + "purpose": "bounded quote and trade replay fixture" + }, + "run_id": "quote-trade", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "2000" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "clip-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "clip-equity" + ], + "sessions": [ + { + "session_date": "2026-02-02", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-02T14:30:00Z", + "closes_at": "2026-02-02T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-03", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-03T14:30:00Z", + "closes_at": "2026-02-03T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-04", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-04T14:30:00Z", + "closes_at": "2026-02-04T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "clip-equity", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "quote_trade_v1", + "configuration": { + "version": "1", + "participation_bps": 10000, + "fee_schedules": [ + { + "schedule_id": "clip-fees-v1", + "instrument_id": "clip-equity", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "10", + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "limit", + "trigger_price": null, + "limit_price": "100", + "time_in_force": "gtc", + "venue_id": null, + "calendar_id": null, + "expires_at": null + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-02-02T14:30:00Z", + "end_at": "2026-02-02T21:00:00Z", + "available_at": "2026-02-02T21:00:01Z", + "received_at": "2026-02-02T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "50", + "high": "50", + "low": "50", + "close": "50", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-02-03T14:30:00Z", + "end_at": "2026-02-03T21:00:00Z", + "available_at": "2026-02-03T21:00:01Z", + "received_at": "2026-02-03T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "100", + "high": "100", + "low": "100", + "close": "100", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-03T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-03T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [ + { + "type": "quote", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:31:00Z", + "available_at": "2026-02-03T14:31:01Z", + "received_at": "2026-02-03T14:31:02Z", + "ingest_sequence": "1", + "bid_price": "99", + "bid_quantity": "20", + "ask_price": "101", + "ask_quantity": "20" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:32:00Z", + "available_at": "2026-02-03T14:32:01Z", + "received_at": "2026-02-03T14:32:02Z", + "ingest_sequence": "2", + "price": "100", + "quantity": "5", + "aggressor_side": "unknown" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:33:00Z", + "available_at": "2026-02-03T14:33:01Z", + "received_at": "2026-02-03T14:33:02Z", + "ingest_sequence": "3", + "price": "99", + "quantity": "4", + "aggressor_side": "sell" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:34:00Z", + "available_at": "2026-02-03T14:34:01Z", + "received_at": "2026-02-03T14:34:02Z", + "ingest_sequence": "4", + "price": "100", + "quantity": "10", + "aggressor_side": "sell" + } + ] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "clip-equity", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v14/fixtures/quote-trade.scenario.jsonl b/contracts/v14/fixtures/quote-trade.scenario.jsonl new file mode 100644 index 0000000..b399827 --- /dev/null +++ b/contracts/v14/fixtures/quote-trade.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"14","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded quote and trade replay fixture"},"run_id":"quote-trade","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"quote_trade_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"14","scenario_sequence":"2","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]},"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}]}} +{"contract_version":"14","scenario_sequence":"3","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}]},"intents":[]}} +{"contract_version":"14","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v14/journal.schema.json b/contracts/v14/journal.schema.json new file mode 100644 index 0000000..399e678 --- /dev/null +++ b/contracts/v14/journal.schema.json @@ -0,0 +1,2420 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json", + "title": "Trading Engine v14 audit journal record", + "type": "object", + "additionalProperties": false, + "required": [ + "contract_version", + "engine_sequence", + "event_id", + "causation_ids", + "run_id", + "recorded_at", + "event_type", + "payload" + ], + "properties": { + "contract_version": { + "const": "14" + }, + "engine_sequence": { + "$ref": "#/$defs/sequence" + }, + "event_id": { + "$ref": "#/$defs/identifier" + }, + "causation_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/identifier" + } + }, + "run_id": { + "$ref": "#/$defs/identifier" + }, + "recorded_at": { + "$ref": "#/$defs/timestamp" + }, + "event_type": { + "enum": [ + "run_started", + "initial_state", + "market_slice_received", + "target_portfolio_requested", + "order_accepted", + "order_rejected", + "order_triggered", + "order_cancelled", + "split_applied", + "cash_dividend_applied", + "distribution_applied", + "lifecycle_applied", + "order_adjusted", + "execution_price_selected", + "fill_applied", + "settlement_instruction_created", + "settlement_completed", + "settlement_failed", + "fill_clipped", + "borrow_fee_applied", + "borrow_charge_applied", + "borrow_recall_received", + "cash_interest_applied", + "margin_call", + "margin_restored", + "intent_rejected", + "metric_emitted", + "valuation", + "run_completed" + ] + }, + "payload": { + "type": "object" + } + }, + "allOf": [ + { + "if": { "properties": { "event_type": { "const": "execution_price_selected" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/executionPriceSelected" } } } + }, + { + "if": { + "properties": { + "event_type": { + "const": "run_started" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/runStarted" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "initial_state" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/initialState" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "market_slice_received" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/marketSlice" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "target_portfolio_requested" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/targetPortfolio" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "order_accepted", + "order_rejected", + "order_triggered" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/order" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "order_cancelled" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/orderCancelled" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "split_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/splitApplied" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "cash_dividend_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/dividendApplied" + } + } + } + }, + { + "if": { "properties": { "event_type": { "const": "distribution_applied" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/distributionApplied" } } } + }, + { + "if": { "properties": { "event_type": { "const": "lifecycle_applied" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/lifecycleApplied" } } } + }, + { + "if": { + "properties": { + "event_type": { + "const": "order_adjusted" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/orderAdjusted" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "fill_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/fill" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "settlement_instruction_created", + "settlement_completed", + "settlement_failed" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/settlementInstruction" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "fill_clipped" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/fillClipped" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_fee_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowFee" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_charge_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowCharge" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_recall_received" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowRecall" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "cash_interest_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/cashInterest" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "margin_call", + "margin_restored", + "valuation" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/valuation" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "intent_rejected" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/intentRejected" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "metric_emitted" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/metric" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "run_completed" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/runCompleted" + } + } + } + } + ], + "$defs": { + "identifier": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" + }, + "signedDecimal": { + "type": "string", + "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" + }, + "unsignedDecimal": { + "type": "string", + "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "positiveDecimal": { + "type": "string", + "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "sequence": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "nonnegativeSequence": { + "type": "string", + "pattern": "^(?:0|[1-9][0-9]*)$" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" + }, + "runStarted": { + "type": "object", + "additionalProperties": false, + "required": [ + "scenario_sha256", + "execution_model" + ], + "properties": { + "scenario_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "execution_model": { + "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1"] + } + } + }, + "initialState": { + "type": "object", + "additionalProperties": false, + "required": [ + "portfolio", + "valuation" + ], + "properties": { + "portfolio": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/initialPortfolio" + }, + "valuation": { + "$ref": "#/$defs/valuation" + } + } + }, + "bar": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "open", + "high", + "low", + "close", + "volume" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "open": { + "$ref": "#/$defs/positiveDecimal" + }, + "high": { + "$ref": "#/$defs/positiveDecimal" + }, + "low": { + "$ref": "#/$defs/positiveDecimal" + }, + "close": { + "$ref": "#/$defs/positiveDecimal" + }, + "volume": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/unsignedDecimal" + } + ] + } + } + }, + "fxRate": { + "type": "object", + "additionalProperties": false, + "required": [ + "currency", + "rate" + ], + "properties": { + "currency": { + "$ref": "#/$defs/identifier" + }, + "rate": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "corporateAction": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "action_id", + "instrument_id", + "numerator", + "denominator" + ], + "properties": { + "type": { + "const": "split" + }, + "action_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "numerator": { + "$ref": "#/$defs/sequence" + }, + "denominator": { + "$ref": "#/$defs/sequence" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "action_id", + "instrument_id", + "amount_per_unit" + ], + "properties": { + "type": { + "const": "cash_dividend" + }, + "action_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "amount_per_unit": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], + "properties": { + "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "destination_instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" }, + "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fractional_policy": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/fractionalPolicy" } + } + } + ] + }, + "marketSlice": { + "type": "object", + "additionalProperties": false, + "required": [ + "slice_sequence", + "start_at", + "end_at", + "available_at", + "received_at", + "bars", + "fx_rates", + "corporate_actions", + "borrow_observations", + "cash_rate_observations", + "settlement_failures", + "lifecycle_events", + "market_events" + ], + "properties": { + "slice_sequence": { + "$ref": "#/$defs/sequence" + }, + "start_at": { + "$ref": "#/$defs/timestamp" + }, + "end_at": { + "$ref": "#/$defs/timestamp" + }, + "available_at": { + "$ref": "#/$defs/timestamp" + }, + "received_at": { + "$ref": "#/$defs/timestamp" + }, + "bars": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/bar" + } + }, + "fx_rates": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/fxRate" + } + }, + "corporate_actions": { + "type": "array", + "items": { + "$ref": "#/$defs/corporateAction" + } + }, + "borrow_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/borrowObservation" + } + }, + "cash_rate_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/cashRateObservation" + } + }, + "settlement_failures": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/settlementFailure" + } + }, + "lifecycle_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/lifecycleEvent" + } + }, + "market_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/marketEvent" + } + } + } + }, + "settlementInstruction": { + "type": "object", + "additionalProperties": false, + "required": ["instruction_id", "fill_id", "instrument_id", "currency", "cash_movement", "position_movement", "trade_date", "due_date", "status", "settled_at", "failed_at", "failure_reason"], + "properties": { + "instruction_id": { "$ref": "#/$defs/identifier" }, + "fill_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "currency": { "$ref": "#/$defs/identifier" }, + "cash_movement": { "$ref": "#/$defs/signedDecimal" }, + "position_movement": { "$ref": "#/$defs/signedDecimal" }, + "trade_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "due_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "status": { "enum": ["pending", "settled", "failed"] }, + "settled_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, + "failed_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, + "failure_reason": { "oneOf": [{ "type": "null" }, { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" }] } + } + }, + "targetPortfolio": { + "type": "object", + "additionalProperties": false, + "required": [ + "basis", + "targets" + ], + "properties": { + "basis": { + "enum": [ + "weights", + "quantities" + ] + }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "weight", + "quantity", + "reference_price" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "weight": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/signedDecimal" + } + ] + }, + "quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "reference_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + } + } + } + } + } + }, + "order": { + "type": "object", + "additionalProperties": false, + "required": [ + "order_id", + "instrument_id", + "side", + "quantity", + "order_kind", + "trigger_price", + "limit_price", + "time_in_force", + "venue_id", + "calendar_id", + "expires_at", + "origin", + "created_event_id", + "updated_event_id", + "created_sequence", + "created_at", + "eligible_after_slice_sequence", + "triggered_at", + "triggered_slice_sequence", + "filled_quantity", + "filled_notional", + "status", + "rejection_reason" + ], + "properties": { + "order_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "side": { + "enum": [ + "buy", + "sell" + ] + }, + "quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "order_kind": { + "enum": [ + "market", + "limit", + "stop", + "stop_limit" + ] + }, + "trigger_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + }, + "limit_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + }, + "time_in_force": { + "enum": [ + "gtc", + "ioc", + "fok", + "day", + "gtd" + ] + }, + "venue_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/identifier" + } + ] + }, + "calendar_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/identifier" + } + ] + }, + "expires_at": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/timestamp" + } + ] + }, + "origin": { + "enum": [ + "direct", + "target_rebalance", + "margin_liquidation", + "borrow_recall", + "instrument_halt", + "instrument_terminal" + ] + }, + "created_event_id": { + "$ref": "#/$defs/identifier" + }, + "updated_event_id": { + "$ref": "#/$defs/identifier" + }, + "created_sequence": { + "$ref": "#/$defs/sequence" + }, + "created_at": { + "$ref": "#/$defs/timestamp" + }, + "eligible_after_slice_sequence": { + "$ref": "#/$defs/nonnegativeSequence" + }, + "triggered_at": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/timestamp" + } + ] + }, + "triggered_slice_sequence": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/sequence" + } + ] + }, + "filled_quantity": { + "$ref": "#/$defs/unsignedDecimal" + }, + "filled_notional": { + "$ref": "#/$defs/unsignedDecimal" + }, + "status": { + "enum": [ + "working", + "partially_filled", + "filled", + "cancelled", + "rejected" + ] + }, + "rejection_reason": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "minLength": 1 + } + ] + } + } + }, + "orderCancelled": { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "reason" + ], + "properties": { + "order": { + "$ref": "#/$defs/order" + }, + "reason": { + "enum": [ + "strategy_requested", + "target_replaced", + "market_ioc", + "immediate_or_cancel", + "fill_or_kill", + "day_expired", + "gtd_expired", + "margin_call", + "borrow_recall" + ] + } + } + }, + "splitApplied": { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "previous_quantity", + "adjusted_quantity" + ], + "properties": { + "action": { + "$ref": "#/$defs/corporateAction" + }, + "previous_quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "adjusted_quantity": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "dividendApplied": { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "quantity", + "cash_amount" + ], + "properties": { + "action": { + "$ref": "#/$defs/corporateAction" + }, + "quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "cash_amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "distributionApplied": { + "type": "object", + "additionalProperties": false, + "required": ["action", "source_quantity", "destination_quantity", "fractional_quantity", "allocated_basis", "fractional_basis", "cash_in_lieu"], + "properties": { + "action": { "$ref": "#/$defs/corporateAction" }, + "source_quantity": { "$ref": "#/$defs/signedDecimal" }, + "destination_quantity": { "$ref": "#/$defs/signedDecimal" }, + "fractional_quantity": { "$ref": "#/$defs/signedDecimal" }, + "allocated_basis": { "$ref": "#/$defs/signedDecimal" }, + "fractional_basis": { "$ref": "#/$defs/signedDecimal" }, + "cash_in_lieu": { "$ref": "#/$defs/signedDecimal" } + } + }, + "lifecycleApplied": { + "type": "object", + "additionalProperties": false, + "required": ["lifecycle_event", "listing", "liquidated_quantity", "cash_amount"], + "properties": { + "lifecycle_event": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/lifecycleEvent" }, + "listing": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "symbol", "status", "provider_mappings"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "status": { "enum": ["tradable", "halted", "expired", "delisted"] }, + "provider_mappings": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["provider", "provider_instrument_id"], + "properties": { + "provider": { "$ref": "#/$defs/identifier" }, + "provider_instrument_id": { "$ref": "#/$defs/identifier" } + } + } + } + } + }, + "liquidated_quantity": { "$ref": "#/$defs/signedDecimal" }, + "cash_amount": { "$ref": "#/$defs/signedDecimal" } + } + }, + "orderAdjusted": { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "action_id" + ], + "properties": { + "order": { + "$ref": "#/$defs/order" + }, + "action_id": { + "$ref": "#/$defs/identifier" + } + } + }, + "executionPriceSelected": { + "type": "object", + "additionalProperties": false, + "required": ["order_id", "instrument_id", "side", "reference_price", "spread_adjustment", "impact_adjustment", "final_price"], + "properties": { + "order_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "reference_price": { "$ref": "#/$defs/positiveDecimal" }, + "spread_adjustment": { "$ref": "#/$defs/unsignedDecimal" }, + "impact_adjustment": { "$ref": "#/$defs/unsignedDecimal" }, + "final_price": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "fill": { + "type": "object", + "additionalProperties": false, + "required": [ + "fill_id", + "order_id", + "instrument_id", + "quote_currency", + "side", + "quantity", + "price", + "notional", + "fee", + "executed_at", + "slice_sequence", + "fee_components" + ], + "properties": { + "fill_id": { + "$ref": "#/$defs/identifier" + }, + "order_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "side": { + "enum": [ + "buy", + "sell" + ] + }, + "quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "price": { + "$ref": "#/$defs/positiveDecimal" + }, + "notional": { + "$ref": "#/$defs/positiveDecimal" + }, + "fee": { + "$ref": "#/$defs/signedDecimal" + }, + "fee_components": { + "type": "array", + "items": { + "$ref": "#/$defs/calculatedFeeComponent" + } + }, + "executed_at": { + "$ref": "#/$defs/timestamp" + }, + "slice_sequence": { + "$ref": "#/$defs/sequence" + } + } + }, + "calculatedFeeComponent": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "kind", + "currency", + "amount", + "quote_amount" + ], + "properties": { + "name": { + "$ref": "#/$defs/identifier" + }, + "kind": { + "enum": [ + "fixed", + "notional_bps", + "per_unit", + "minimum_adjustment", + "maximum_adjustment" + ] + }, + "currency": { + "$ref": "#/$defs/identifier" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "quote_amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "feeComponentAttribution": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "kind", + "currency", + "amount", + "quote_currency", + "quote_amount", + "base_amount" + ], + "properties": { + "name": { + "$ref": "#/$defs/identifier" + }, + "kind": { + "enum": [ + "fixed", + "notional_bps", + "per_unit", + "minimum_adjustment", + "maximum_adjustment" + ] + }, + "currency": { + "$ref": "#/$defs/identifier" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "quote_amount": { + "$ref": "#/$defs/signedDecimal" + }, + "base_amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "quantityThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "quantity" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "moneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "money" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "ratioThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "ratio" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "basisPointsThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "basis_points" + }, + "value": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + } + }, + "instrumentQuantityThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "unit", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "quantity" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "instrumentMoneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "unit", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "money" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "currencyMoneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "unit", "value"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "unit": { "const": "money" }, + "value": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "settlementPositionThreshold": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "unit", "value"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "unit": { "const": "quantity" }, + "value": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "instrumentBasisPointsThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "unit", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "basis_points" + }, + "value": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + } + }, + "instrumentShortingThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "value": { + "const": false + } + } + }, + "groupMoneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "group_id", + "unit", + "value" + ], + "properties": { + "group_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "money" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "groupRatioThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "group_id", + "unit", + "value" + ], + "properties": { + "group_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "ratio" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "fillClipReason": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_order_quantity" + }, + "threshold": { + "$ref": "#/$defs/quantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_long_position" + }, + "threshold": { + "$ref": "#/$defs/quantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_short_position" + }, + "threshold": { + "$ref": "#/$defs/quantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_gross_exposure" + }, + "threshold": { + "$ref": "#/$defs/moneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_leverage" + }, + "threshold": { + "$ref": "#/$defs/ratioThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "initial_margin" + }, + "threshold": { + "$ref": "#/$defs/basisPointsThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_max_long_position" + }, + "threshold": { + "$ref": "#/$defs/instrumentQuantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["version", "policy", "threshold"], + "properties": { + "version": { "const": "1" }, + "policy": { "const": "settlement_cash_buying_power" }, + "threshold": { "$ref": "#/$defs/currencyMoneyThreshold" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["version", "policy", "threshold"], + "properties": { + "version": { "const": "1" }, + "policy": { "const": "settlement_position_availability" }, + "threshold": { "$ref": "#/$defs/settlementPositionThreshold" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_max_short_position" + }, + "threshold": { + "$ref": "#/$defs/instrumentQuantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_max_notional_exposure" + }, + "threshold": { + "$ref": "#/$defs/instrumentMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_shorting_disabled" + }, + "threshold": { + "$ref": "#/$defs/instrumentShortingThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_borrow_availability" + }, + "threshold": { + "$ref": "#/$defs/instrumentQuantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_initial_margin" + }, + "threshold": { + "$ref": "#/$defs/instrumentBasisPointsThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_gross_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_long_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_short_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_absolute_net_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_concentration" + }, + "threshold": { + "$ref": "#/$defs/groupRatioThreshold" + } + } + } + ] + }, + "fillClipped": { + "type": "object", + "additionalProperties": false, + "required": [ + "reason", + "order_id", + "instrument_id", + "proposed_quantity", + "permitted_quantity", + "price" + ], + "properties": { + "reason": { + "$ref": "#/$defs/fillClipReason" + }, + "order_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "proposed_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "permitted_quantity": { + "$ref": "#/$defs/unsignedDecimal" + }, + "price": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "borrowFee": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "quote_currency", + "short_quantity", + "reference_price", + "borrow_bps", + "period_start", + "period_end", + "fee" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "short_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "reference_price": { + "$ref": "#/$defs/positiveDecimal" + }, + "borrow_bps": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + }, + "period_start": { + "$ref": "#/$defs/timestamp" + }, + "period_end": { + "$ref": "#/$defs/timestamp" + }, + "fee": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "borrowCharge": { + "type": "object", + "additionalProperties": false, + "required": [ + "observation", + "quote_currency", + "short_quantity", + "reference_price", + "day_count", + "compounding", + "period_start", + "period_end", + "amount" + ], + "properties": { + "observation": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/borrowObservation" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "short_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "reference_price": { + "$ref": "#/$defs/positiveDecimal" + }, + "day_count": { + "enum": [ + "actual_365", + "actual_360" + ] + }, + "compounding": { + "enum": [ + "simple", + "daily" + ] + }, + "period_start": { + "$ref": "#/$defs/timestamp" + }, + "period_end": { + "$ref": "#/$defs/timestamp" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "borrowRecall": { + "type": "object", + "additionalProperties": false, + "required": [ + "observation", + "short_quantity", + "close_out_quantity" + ], + "properties": { + "observation": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/borrowObservation" + }, + "short_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "close_out_quantity": { + "$ref": "#/$defs/unsignedDecimal" + } + } + }, + "cashInterest": { + "type": "object", + "additionalProperties": false, + "required": [ + "observation", + "opening_balance", + "applied_rate_bps", + "day_count", + "compounding", + "period_start", + "period_end", + "amount", + "closing_balance" + ], + "properties": { + "observation": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/cashRateObservation" + }, + "opening_balance": { + "$ref": "#/$defs/signedDecimal" + }, + "applied_rate_bps": { + "type": "integer", + "minimum": -1000000, + "maximum": 1000000 + }, + "day_count": { + "enum": [ + "actual_365", + "actual_360" + ] + }, + "compounding": { + "enum": [ + "simple", + "daily" + ] + }, + "period_start": { + "$ref": "#/$defs/timestamp" + }, + "period_end": { + "$ref": "#/$defs/timestamp" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "closing_balance": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "cashAttribution": { + "type": "object", + "additionalProperties": false, + "required": [ + "currency", + "amount", + "fx_rate", + "base_value", + "interest", + "base_interest", + "settled_amount", + "unsettled_amount", + "base_settled_value", + "base_unsettled_value" + ], + "properties": { + "currency": { + "$ref": "#/$defs/identifier" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "fx_rate": { + "$ref": "#/$defs/positiveDecimal" + }, + "base_value": { + "$ref": "#/$defs/signedDecimal" + }, + "interest": { + "$ref": "#/$defs/signedDecimal" + }, + "base_interest": { + "$ref": "#/$defs/signedDecimal" + }, + "settled_amount": { + "$ref": "#/$defs/signedDecimal" + }, + "unsettled_amount": { + "$ref": "#/$defs/signedDecimal" + }, + "base_settled_value": { + "$ref": "#/$defs/signedDecimal" + }, + "base_unsettled_value": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "positionAttribution": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "quote_currency", + "quantity", + "settled_quantity", + "unsettled_quantity", + "mark", + "fx_rate", + "market_value", + "base_market_value", + "cost_basis", + "base_cost_basis", + "realized_pnl", + "base_realized_pnl", + "unrealized_pnl", + "base_unrealized_pnl", + "dividend_pnl", + "base_dividend_pnl", + "execution_fees", + "base_execution_fees", + "borrow_fees", + "base_borrow_fees", + "total_fees", + "base_total_fees", + "execution_fee_components" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "settled_quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "unsettled_quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "mark": { + "$ref": "#/$defs/positiveDecimal" + }, + "fx_rate": { + "$ref": "#/$defs/positiveDecimal" + }, + "market_value": { + "$ref": "#/$defs/signedDecimal" + }, + "base_market_value": { + "$ref": "#/$defs/signedDecimal" + }, + "cost_basis": { + "$ref": "#/$defs/signedDecimal" + }, + "base_cost_basis": { + "$ref": "#/$defs/signedDecimal" + }, + "realized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "base_realized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "unrealized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "base_unrealized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "dividend_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "base_dividend_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "execution_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "base_execution_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "borrow_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "base_borrow_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "total_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "base_total_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "execution_fee_components": { + "type": "array", + "items": { + "$ref": "#/$defs/feeComponentAttribution" + } + } + } + }, + "margin": { + "type": "object", + "additionalProperties": false, + "required": [ + "initial_requirement", + "maintenance_requirement", + "initial_excess", + "maintenance_excess", + "margin_call" + ], + "properties": { + "initial_requirement": { + "$ref": "#/$defs/unsignedDecimal" + }, + "maintenance_requirement": { + "$ref": "#/$defs/unsignedDecimal" + }, + "initial_excess": { + "$ref": "#/$defs/signedDecimal" + }, + "maintenance_excess": { + "$ref": "#/$defs/signedDecimal" + }, + "margin_call": { + "type": "boolean" + } + } + }, + "groupExposure": { + "type": "object", + "additionalProperties": false, + "required": [ + "group_id", + "gross_exposure", + "net_exposure", + "long_exposure", + "short_exposure", + "concentration" + ], + "properties": { + "group_id": { + "$ref": "#/$defs/identifier" + }, + "gross_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "net_exposure": { + "$ref": "#/$defs/signedDecimal" + }, + "long_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "short_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "concentration": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/signedDecimal" + } + ] + } + } + }, + "valuation": { + "type": "object", + "additionalProperties": false, + "required": [ + "base_currency", + "cash", + "settled_cash", + "unsettled_cash", + "net_market_value", + "long_market_value", + "short_market_value", + "gross_exposure", + "cost_basis", + "realized_pnl", + "unrealized_pnl", + "equity", + "dividend_pnl", + "execution_fees", + "borrow_fees", + "cash_interest", + "total_fees", + "cash_balances", + "positions", + "margin", + "group_exposures", + "execution_fee_components" + ], + "properties": { + "base_currency": { + "$ref": "#/$defs/identifier" + }, + "cash": { + "$ref": "#/$defs/signedDecimal" + }, + "settled_cash": { + "$ref": "#/$defs/signedDecimal" + }, + "unsettled_cash": { + "$ref": "#/$defs/signedDecimal" + }, + "net_market_value": { + "$ref": "#/$defs/signedDecimal" + }, + "long_market_value": { + "$ref": "#/$defs/unsignedDecimal" + }, + "short_market_value": { + "$ref": "#/$defs/unsignedDecimal" + }, + "gross_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "cost_basis": { + "$ref": "#/$defs/signedDecimal" + }, + "realized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "unrealized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "equity": { + "$ref": "#/$defs/signedDecimal" + }, + "dividend_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "execution_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "borrow_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "total_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "cash_balances": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/cashAttribution" + } + }, + "positions": { + "type": "array", + "items": { + "$ref": "#/$defs/positionAttribution" + } + }, + "margin": { + "$ref": "#/$defs/margin" + }, + "group_exposures": { + "type": "array", + "items": { + "$ref": "#/$defs/groupExposure" + } + }, + "execution_fee_components": { + "type": "array", + "items": { + "$ref": "#/$defs/feeComponentAttribution" + } + }, + "cash_interest": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "intentRejected": { + "type": "object", + "additionalProperties": false, + "required": [ + "reason" + ], + "properties": { + "reason": { + "type": "string", + "minLength": 1 + } + } + }, + "metric": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "runCompleted": { + "type": "object", + "additionalProperties": false, + "required": [ + "scenario_sha256", + "execution_model", + "valuation", + "order_counts" + ], + "properties": { + "scenario_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "execution_model": { + "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1"] + }, + "valuation": { + "$ref": "#/$defs/valuation" + }, + "order_counts": { + "type": "object", + "additionalProperties": false, + "required": [ + "total", + "active", + "filled", + "rejected", + "cancelled" + ], + "properties": { + "total": { + "type": "integer", + "minimum": 0 + }, + "active": { + "type": "integer", + "minimum": 0 + }, + "filled": { + "type": "integer", + "minimum": 0 + }, + "rejected": { + "type": "integer", + "minimum": 0 + }, + "cancelled": { + "type": "integer", + "minimum": 0 + } + } + } + } + } + } +} diff --git a/contracts/v14/scenario-stream.schema.json b/contracts/v14/scenario-stream.schema.json new file mode 100644 index 0000000..6093644 --- /dev/null +++ b/contracts/v14/scenario-stream.schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v14/scenario-stream.schema.json", + "title": "Trading Engine v14 replay scenario stream record", + "description": "Market-slice records are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-record rule is enforced semantically.", + "oneOf": [ + { "$ref": "#/$defs/headerRecord" }, + { "$ref": "#/$defs/sliceRecord" }, + { "$ref": "#/$defs/endRecord" } + ], + "$defs": { + "headerRecord": { + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], + "properties": { + "contract_version": { "const": "14" }, + "scenario_sequence": { "const": "1" }, + "record_type": { "const": "scenario_header" }, + "payload": { "$ref": "#/$defs/headerPayload" } + } + }, + "sliceRecord": { + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], + "properties": { + "contract_version": { "const": "14" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/sequence" }, + "record_type": { "const": "market_slice" }, + "payload": { "$ref": "#/$defs/slicePayload" } + } + }, + "endRecord": { + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], + "properties": { + "contract_version": { "const": "14" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/sequence" }, + "record_type": { "const": "scenario_end" }, + "payload": { + "type": "object", + "additionalProperties": false, + "required": ["slice_count"], + "properties": { "slice_count": { "type": "string", "pattern": "^(?:0|[1-9][0-9]*)$" } } + } + } + }, + "headerPayload": { + "type": "object", + "additionalProperties": false, + "required": ["metadata", "run_id", "base_currency", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "max_internal_events"], + "properties": { + "metadata": { "type": "object" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/settlement" }, + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 } + } + }, + "slicePayload": { + "type": "object", + "additionalProperties": false, + "required": ["market_slice", "intents"], + "properties": { + "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v14/scenario.schema.json b/contracts/v14/scenario.schema.json new file mode 100644 index 0000000..f4117b5 --- /dev/null +++ b/contracts/v14/scenario.schema.json @@ -0,0 +1,770 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json", + "title": "Trading Engine v14 replay scenario", + "description": "Strict deterministic scenario contract with explicit venue-local session policies resolved to absolute instants outside the reducer.", + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "metadata", "run_id", "base_currency", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "max_internal_events", "schedule", "slices"], + "properties": { + "contract_version": { "const": "14" }, + "metadata": { "type": "object" }, + "run_id": { "$ref": "#/$defs/identifier" }, + "base_currency": { "$ref": "#/$defs/identifier" }, + "initial_portfolio": { "$ref": "#/$defs/initialPortfolio" }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/venueCalendar" } + }, + "risk": { "$ref": "#/$defs/risk" }, + "execution": { "$ref": "#/$defs/execution" }, + "financing": { "$ref": "#/$defs/financing" }, + "settlement": { "$ref": "#/$defs/settlement" }, + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 }, + "schedule": { "type": "array", "items": { "$ref": "#/$defs/scheduleItem" } }, + "slices": { + "description": "Slices are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-item rule is enforced semantically.", + "type": "array", + "items": { "$ref": "#/$defs/marketSlice" } + } + }, + "$defs": { + "settlement": { + "type": "object", + "additionalProperties": false, + "required": ["cash_buying_power", "position_availability", "calendars", "rules"], + "properties": { + "cash_buying_power": { "enum": ["total_cash", "settled_cash"] }, + "position_availability": { "enum": ["total_positions", "settled_positions"] }, + "calendars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementCalendar" } }, + "rules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementRule" } } + } + }, + "settlementCalendar": { + "type": "object", + "additionalProperties": false, + "required": ["calendar_id", "version", "business_dates"], + "properties": { + "calendar_id": { "$ref": "#/$defs/identifier" }, + "version": { "const": "1" }, + "business_dates": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" } } + } + }, + "settlementRule": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "calendar_id", "lag_business_days"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "calendar_id": { "$ref": "#/$defs/identifier" }, + "lag_business_days": { "type": "integer", "minimum": 0, "maximum": 30 } + } + }, + "settlementFailure": { + "type": "object", + "additionalProperties": false, + "required": ["instruction_id", "reason"], + "properties": { + "instruction_id": { "$ref": "#/$defs/identifier" }, + "reason": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } + } + }, + "financing": { + "type": "object", + "additionalProperties": false, + "required": ["day_count", "compounding", "borrow_missing_data", "cash_missing_data", "locate_policy", "recall_policy"], + "properties": { + "day_count": { "enum": ["actual_365", "actual_360"] }, + "compounding": { "enum": ["simple", "daily"] }, + "borrow_missing_data": { "enum": ["reject", "zero"] }, + "cash_missing_data": { "enum": ["reject", "zero"] }, + "locate_policy": { "enum": ["reject_order", "clip_fill"] }, + "recall_policy": { "enum": ["reject_new_shorts", "close_out"] } + } + }, + "borrowObservation": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "effective_at", "available_quantity", "annual_rate_bps", "recalled"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "effective_at": { "$ref": "#/$defs/timestamp" }, + "available_quantity": { "$ref": "#/$defs/unsignedDecimal" }, + "annual_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, + "recalled": { "type": "boolean" } + } + }, + "cashRateObservation": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "effective_at", "credit_rate_bps", "debit_rate_bps"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "effective_at": { "$ref": "#/$defs/timestamp" }, + "credit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, + "debit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 } + } + }, + "identifier": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" + }, + "signedDecimal": { + "type": "string", + "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" + }, + "unsignedDecimal": { + "type": "string", + "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "positiveDecimal": { + "type": "string", + "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "sequence": { "type": "string", "pattern": "^[1-9][0-9]*$" }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" + }, + "cashBalance": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "amount"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "amount": { "$ref": "#/$defs/signedDecimal" } + } + }, + "initialPortfolio": { + "type": "object", + "additionalProperties": false, + "required": ["cash", "positions", "marks", "fx_rates"], + "properties": { + "cash": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashBalance" } }, + "positions": { "type": "array", "items": { "$ref": "#/$defs/initialPosition" } }, + "marks": { "type": "array", "items": { "$ref": "#/$defs/initialMark" } }, + "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } } + } + }, + "initialPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "cost_basis", "realized_pnl", "dividend_pnl", "execution_fees", "borrow_fees"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "quantity": { "$ref": "#/$defs/signedDecimal" }, + "cost_basis": { "$ref": "#/$defs/signedDecimal" }, + "realized_pnl": { "$ref": "#/$defs/signedDecimal" }, + "dividend_pnl": { "$ref": "#/$defs/signedDecimal" }, + "execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, + "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "initialMark": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "price"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "price": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "instrument": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "symbol", "quote_currency", "tick_size", "lot_size"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "quote_currency": { "$ref": "#/$defs/identifier" }, + "tick_size": { "$ref": "#/$defs/positiveDecimal" }, + "lot_size": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "venueCalendar": { + "type": "object", + "additionalProperties": false, + "required": ["calendar_id", "calendar_version", "venue_id", "instrument_ids", "sessions"], + "properties": { + "calendar_id": { "$ref": "#/$defs/identifier" }, + "calendar_version": { "const": "1" }, + "venue_id": { "$ref": "#/$defs/identifier" }, + "instrument_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/identifier" } + }, + "sessions": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/venueSession" } + } + } + }, + "venueSession": { + "oneOf": [ + { "$ref": "#/$defs/openVenueSession" }, + { "$ref": "#/$defs/holidayVenueSession" } + ] + }, + "openVenueSession": { + "type": "object", + "additionalProperties": false, + "required": ["session_date", "policy", "phases"], + "properties": { + "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "policy": { "enum": ["regular", "early_close"] }, + "phases": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "items": { "$ref": "#/$defs/venuePhase" } + } + } + }, + "holidayVenueSession": { + "type": "object", + "additionalProperties": false, + "required": ["session_date", "policy", "phases"], + "properties": { + "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "policy": { "const": "holiday" }, + "phases": { "type": "array", "maxItems": 0 } + } + }, + "venuePhase": { + "type": "object", + "additionalProperties": false, + "required": ["phase", "opens_at", "closes_at"], + "properties": { + "phase": { "enum": ["premarket", "opening_auction", "regular", "closing_auction", "postmarket"] }, + "opens_at": { "$ref": "#/$defs/timestamp" }, + "closes_at": { "$ref": "#/$defs/timestamp" } + } + }, + "risk": { + "type": "object", + "additionalProperties": false, + "required": ["max_gross_exposure", "max_leverage", "short_borrow_bps", "instrument_policies", "groups"], + "properties": { + "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, + "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "instrument_policies": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/instrumentRiskPolicy" } + }, + "groups": { + "type": "array", + "items": { "$ref": "#/$defs/riskGroup" } + } + } + }, + "instrumentRiskPolicy": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "max_order_quantity", "max_long_position", "max_short_position", "max_notional_exposure", "initial_margin_bps", "maintenance_margin_bps", "shorting_allowed"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_notional_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "shorting_allowed": { "type": "boolean" } + } + }, + "nullablePositiveDecimal": { + "oneOf": [ + { "type": "null" }, + { "$ref": "#/$defs/positiveDecimal" } + ] + }, + "riskGroup": { + "type": "object", + "additionalProperties": false, + "required": ["group_id", "group_version", "group_type", "instrument_ids", "limits"], + "properties": { + "group_id": { "$ref": "#/$defs/identifier" }, + "group_version": { "const": "1" }, + "group_type": { "enum": ["issuer", "sector", "currency", "country", "asset_class", "custom"] }, + "instrument_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/identifier" } + }, + "limits": { "$ref": "#/$defs/riskGroupLimits" } + } + }, + "riskGroupLimits": { + "type": "object", + "additionalProperties": false, + "required": ["max_gross_exposure", "max_long_exposure", "max_short_exposure", "max_absolute_net_exposure", "max_concentration"], + "properties": { + "max_gross_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_long_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_short_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_absolute_net_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_concentration": { + "oneOf": [ + { "type": "null" }, + { "allOf": [{ "$ref": "#/$defs/positiveDecimal" }, { "pattern": "^(?:0[.][0-9]{0,5}[1-9]|1)$" }] } + ] + } + } + }, + "execution": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "enum": ["completed_bar_next_open_v1", "completed_bar_adverse_touch_v1"] }, + "configuration": { "$ref": "#/$defs/conservativeBarConfiguration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "quote_trade_v1" }, + "configuration": { "$ref": "#/$defs/quoteTradeConfiguration" } + } + } + ] + }, + "completedBarV1Configuration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules"], + "properties": { + "version": { "const": "2" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } + } + }, + "conservativeBarConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules", "spread_model", "impact_model"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, + "spread_model": { "$ref": "#/$defs/fixedSpreadModel" }, + "impact_model": { "$ref": "#/$defs/linearImpactModel" } + } + }, + "quoteTradeConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } + } + }, + "fixedSpreadModel": { + "type": "object", + "additionalProperties": false, + "required": ["model", "half_spread_bps"], + "properties": { + "model": { "const": "fixed_half_spread_v1" }, + "half_spread_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "linearImpactModel": { + "type": "object", + "additionalProperties": false, + "required": ["model", "coefficient_bps", "missing_volume_policy"], + "properties": { + "model": { "const": "linear_participation_v1" }, + "coefficient_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "missing_volume_policy": { "enum": ["reject", "zero_impact"] } + } + }, + "feeSchedule": { + "type": "object", + "additionalProperties": false, + "required": ["schedule_id", "instrument_id", "settlement_currency", "minimum", "maximum", "components"], + "properties": { + "schedule_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "settlement_currency": { "$ref": "#/$defs/identifier" }, + "minimum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, + "maximum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, + "components": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeComponent" } } + } + }, + "feeComponent": { + "type": "object", + "additionalProperties": false, + "required": ["name", "currency", "kind", "value", "rounding", "applies_to"], + "properties": { + "name": { "$ref": "#/$defs/identifier" }, + "currency": { "$ref": "#/$defs/identifier" }, + "kind": { "enum": ["fixed", "notional_bps", "per_unit"] }, + "value": { "oneOf": [{ "$ref": "#/$defs/signedDecimal" }, { "type": "integer", "minimum": -10000, "maximum": 10000 }] }, + "rounding": { "enum": ["up", "down", "nearest"] }, + "applies_to": { "enum": ["any", "maker", "taker"] } + }, + "allOf": [ + { "if": { "properties": { "kind": { "const": "notional_bps" } } }, "then": { "properties": { "value": { "type": "integer" } } } }, + { "if": { "properties": { "kind": { "enum": ["fixed", "per_unit"] } } }, "then": { "properties": { "value": { "$ref": "#/$defs/signedDecimal" } } } } + ] + }, + "scheduleItem": { + "type": "object", + "additionalProperties": false, + "required": ["after_slice_sequence", "intents"], + "properties": { + "after_slice_sequence": { "$ref": "#/$defs/sequence" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "#/$defs/intent" } } + } + }, + "intent": { + "oneOf": [ + { "$ref": "#/$defs/targetWeights" }, + { "$ref": "#/$defs/targetQuantities" }, + { "$ref": "#/$defs/submitOrder" }, + { "$ref": "#/$defs/cancelOrder" }, + { "$ref": "#/$defs/metric" } + ] + }, + "targetWeights": { + "type": "object", + "additionalProperties": false, + "required": ["type", "targets"], + "properties": { + "type": { "const": "target_weights" }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "weight"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "weight": { "$ref": "#/$defs/signedDecimal" } + } + } + } + } + }, + "targetQuantities": { + "type": "object", + "additionalProperties": false, + "required": ["type", "targets"], + "properties": { + "type": { "const": "target_quantities" }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "quantity": { "$ref": "#/$defs/signedDecimal" } + } + } + } + } + }, + "submitOrder": { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "side", "quantity", "order_kind", "trigger_price", "limit_price", "time_in_force", "venue_id", "calendar_id", "expires_at"], + "properties": { + "type": { "const": "submit_order" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "order_kind": { "enum": ["market", "limit", "stop", "stop_limit"] }, + "trigger_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, + "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, + "time_in_force": { "enum": ["gtc", "ioc", "fok", "day", "gtd"] }, + "venue_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, + "calendar_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, + "expires_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] } + }, + "allOf": [ + { "if": { "properties": { "order_kind": { "const": "market" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "type": "null" } } } }, + { "if": { "properties": { "order_kind": { "const": "limit" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, + { "if": { "properties": { "order_kind": { "const": "stop" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "type": "null" } } } }, + { "if": { "properties": { "order_kind": { "const": "stop_limit" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, + { "if": { "properties": { "time_in_force": { "const": "day" } } }, "then": { "properties": { "venue_id": { "$ref": "#/$defs/identifier" }, "calendar_id": { "$ref": "#/$defs/identifier" }, "expires_at": { "type": "null" } } } }, + { "if": { "properties": { "time_in_force": { "const": "gtd" } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "$ref": "#/$defs/timestamp" } } } }, + { "if": { "properties": { "time_in_force": { "enum": ["gtc", "ioc", "fok"] } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "type": "null" } } } } + ] + }, + "cancelOrder": { + "type": "object", + "additionalProperties": false, + "required": ["type", "order_id"], + "properties": { + "type": { "const": "cancel_order" }, + "order_id": { "$ref": "#/$defs/identifier" } + } + }, + "metric": { + "type": "object", + "additionalProperties": false, + "required": ["type", "name", "value"], + "properties": { + "type": { "const": "emit_metric" }, + "name": { "type": "string" }, + "value": { "type": "string" } + } + }, + "marketSlice": { + "type": "object", + "additionalProperties": false, + "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "market_events", "fx_rates", "corporate_actions", "borrow_observations", "cash_rate_observations", "settlement_failures", "lifecycle_events"], + "properties": { + "slice_sequence": { "$ref": "#/$defs/sequence" }, + "start_at": { "$ref": "#/$defs/timestamp" }, + "end_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "bars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/bar" } }, + "market_events": { "type": "array", "items": { "$ref": "#/$defs/marketEvent" } }, + "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } }, + "corporate_actions": { "type": "array", "items": { "$ref": "#/$defs/corporateAction" } }, + "borrow_observations": { "type": "array", "items": { "$ref": "#/$defs/borrowObservation" } }, + "cash_rate_observations": { "type": "array", "items": { "$ref": "#/$defs/cashRateObservation" } }, + "settlement_failures": { "type": "array", "items": { "$ref": "#/$defs/settlementFailure" } }, + "lifecycle_events": { "type": "array", "items": { "$ref": "#/$defs/lifecycleEvent" } } + } + }, + "bar": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "open", "high", "low", "close", "volume"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "open": { "$ref": "#/$defs/positiveDecimal" }, + "high": { "$ref": "#/$defs/positiveDecimal" }, + "low": { "$ref": "#/$defs/positiveDecimal" }, + "close": { "$ref": "#/$defs/positiveDecimal" }, + "volume": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] } + } + }, + "marketEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "bid_price", "bid_quantity", "ask_price", "ask_quantity"], + "properties": { + "type": { "const": "quote" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "bid_price": { "$ref": "#/$defs/positiveDecimal" }, + "bid_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "ask_price": { "$ref": "#/$defs/positiveDecimal" }, + "ask_quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "price", "quantity", "aggressor_side"], + "properties": { + "type": { "const": "trade" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "aggressor_side": { "enum": ["buy", "sell", "unknown"] } + } + } + ] + }, + "fxRate": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "rate"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "rate": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "corporateAction": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "numerator", "denominator"], + "properties": { + "type": { "const": "split" }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "amount_per_unit"], + "properties": { + "type": { "const": "cash_dividend" }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "amount_per_unit": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], + "properties": { + "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "destination_instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" }, + "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fractional_policy": { "$ref": "#/$defs/fractionalPolicy" } + } + } + ] + }, + "fractionalPolicy": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["policy"], + "properties": { "policy": { "const": "reject" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["policy", "price", "currency"], + "properties": { + "policy": { "const": "cash_in_lieu" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "currency": { "$ref": "#/$defs/identifier" } + } + } + ] + }, + "terminalPolicy": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["policy"], + "properties": { "policy": { "const": "hold" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["policy", "price", "currency"], + "properties": { + "policy": { "const": "cash_out" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "currency": { "$ref": "#/$defs/identifier" } + } + } + ] + }, + "lifecycleEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "reason"], + "properties": { + "type": { "const": "halt" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "reason": { "type": "string", "minLength": 1 } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id"], + "properties": { + "type": { "const": "resume" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "symbol", "provider", "provider_instrument_id"], + "properties": { + "type": { "const": "identifier_change" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "provider": { "$ref": "#/$defs/identifier" }, + "provider_instrument_id": { "$ref": "#/$defs/identifier" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "terminal_policy"], + "properties": { + "type": { "const": "expiration" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "terminal_policy": { "$ref": "#/$defs/terminalPolicy" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "terminal_policy", "reason"], + "properties": { + "type": { "const": "delisting" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "terminal_policy": { "$ref": "#/$defs/terminalPolicy" }, + "reason": { "type": "string", "minLength": 1 } + } + } + ] + } + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index fdfddd4..52529d2 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -7,4 +7,4 @@ that every public interface has a corresponding page. The generated reference describes library types and functions. The versioned JSON and JSON Lines -files under [Contracts](../contracts/v13/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v14/README.md) remain authoritative for process boundaries. diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 4c10789..ef3971a 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -21,8 +21,8 @@ Every runtime cell replays the frozen v3 demo, v5 demo, and v5 risk-limited fill canonical fixtures. Standard output and standard error are captured separately because human diagnostics may contain platform-specific paths or process details and are not part of the journal contract. -The full test suite additionally validates and replays the current v13 batch, stream, journal, and -strategy-v8 fixtures, including financing attribution and the reconciled first valuation. +The full test suite additionally validates and replays the current v14 batch, stream, journal, and +strategy-v12 fixtures, including quote/trade causality and the reconciled first valuation. Coverage runs once in the exact locked Ubuntu environment. The required Persistra job also runs once against its full pinned commit; it is not repeated across dependency or operating-system diff --git a/docs/execution-model.md b/docs/execution-model.md index 6a9e18f..597573e 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,12 +1,12 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v13 advertises `completed_bar_v1`, `completed_bar_next_open_v1`, and -`completed_bar_adverse_touch_v1`; embedders can inject another module through +Contract v14 advertises `completed_bar_v1`, `completed_bar_next_open_v1`, +`completed_bar_adverse_touch_v1`, and `quote_trade_v1`; embedders can inject another module through the typed engine configuration without introducing runtime shared-library loading. The selected name is repeated in both terminal audit records. -Each compiled model owns a strict configuration contract. The v13 envelope separates selection from +Each compiled model owns a strict configuration contract. The v14 envelope separates selection from model-specific parameters: ```json @@ -51,6 +51,14 @@ The conservative models use strict configuration version `"1"`. Both require `sp `model: "linear_participation_v1"`, `coefficient_bps`, and `missing_volume_policy`. The latter is either `reject` or `zero_impact`; no ambient spread or volume data is inferred. +The quote/trade model also uses configuration version `"1"`, with `participation_bps` and the same +fee-schedule catalog. It consumes each slice's events in `(available_at, received_at, +ingest_sequence)` order. Market orders and marketable limits consume only the displayed quote size +on their side. Passive buys consume only sell-aggressor trades at or below their limit; passive +sells consume only buy-aggressor trades at or above it. An `unknown` aggressor never supplies a +passive fill. Each event has independent, lot-rounded capacity, and its `event_at` is the fill's +economic timestamp. Completed bars remain required solely for synchronized valuation. + ## Eligibility An order records the slice after which it is eligible. The matcher requires: diff --git a/docs/persistra.md b/docs/persistra.md index 7a23c24..68864ad 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -54,12 +54,12 @@ lifecycle belong to Persistra. Persistra currently uses the transitional v3 [scenario](../contracts/v3/scenario.schema.json) and [journal](../contracts/v3/journal.schema.json) schemas and their adjacent conformance fixtures for -structural checks. The engine advertises current contract v13 while retaining v12 through v3 and +structural checks. The engine advertises current contract v14 while retaining v13 through v3 and exact v3 journal output for v3 inputs. The engine parser is authoritative for ordering, catalog coverage, causality, tick, lot, risk, and accounting invariants that JSON Schema cannot express. External strategies use the separate -[strategy protocol v11](../contracts/strategy/v11/README.md). Persistra's host turns protocol +[strategy protocol v12](../contracts/strategy/v12/README.md). Persistra's host turns protocol initialization, marked portfolio contexts, market-slice, fill, order, and rejection events into typed callbacks. Realized weights are available only for positive equity. The retained run manifest binds the strategy identity, executable hash, declared input hashes, transcript hash, @@ -79,14 +79,14 @@ compatibility claim. - **Engine:** `--capabilities` is the authoritative machine-readable surface. The engine must reject unsupported versions and malformed or semantically invalid input before reporting a successful run. -- **Scenario:** Frozen scenario and stream artifacts do not change. The current v13 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v14 contract may receive additive changes only when old valid inputs retain their meaning; breaking changes need a new version. Transitional v3 support remains explicit in `--capabilities`. - **Journal:** A run emits the journal version paired with its accepted scenario. Record ordering, causal references, scenario hashing, terminal completion, and exact accounting remain runtime invariants even when JSON Schema cannot express them. - **Strategy:** Protocol and transcript versions are independent of scenario versions. The current - external boundary is strategy v11; a host must complete its exact initialization, event, + external boundary is strategy v12; a host must complete its exact initialization, event, shutdown, timeout, and rejection lifecycle. - **Persistra:** The required integration gate uses a full Persistra commit and its v3 scenario, journal, and strategy integration tests. Passing that gate claims compatibility only for the diff --git a/docs/scenario.md b/docs/scenario.md index 8111c8f..fe9e0a5 100644 --- a/docs/scenario.md +++ b/docs/scenario.md @@ -4,8 +4,8 @@ A replay scenario uses either one strict JSON object or a strict JSON Lines stre weights, quantities, money, and sequences are canonical JSON strings. Counts and basis points are JSON integers. Unknown, missing, duplicate, noncanonical, and non-finite values fail parsing. -Use [the v13 demo](../contracts/v13/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v13/scenario.schema.json) provides structural validation. +Use [the v14 demo](../contracts/v14/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v14/scenario.schema.json) provides structural validation. The engine parser also enforces cross-field and cross-record invariants. Diagnostics identify the failed field or array item. Stream diagnostics additionally retain the record line and sequence. @@ -32,8 +32,8 @@ The batch object and stream header share one domain-construction path and the sa checks. Stream items reuse the batch slice and intent validators directly; no synthetic batch scenario is constructed. -The [stream record JSON Schema](../contracts/v13/scenario-stream.schema.json) validates each line, -and [the v13 stream fixture](../contracts/v13/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v14/scenario-stream.schema.json) validates each line, +and [the v14 stream fixture](../contracts/v14/fixtures/demo.scenario.jsonl) is the canonical example. The engine validates the entire stream before creating a journal. It then replays one record at a time without retaining prior slices, scheduled batches, or audit events. Reducer state still retains current account, order, target, and latest-bar state required by execution semantics. @@ -42,7 +42,7 @@ retains current account, order, target, and latest-bar state required by executi | Field | Meaning | |---|---| -| `contract_version` | Required string identifying this file contract; v13 is `"13"` | +| `contract_version` | Required string identifying this file contract; v14 is `"14"` | | `metadata` | Required arbitrary JSON object preserved for provenance and ignored by execution | | `run_id` | Stable identity used in generated IDs | | `base_currency` | Reporting currency used for aggregate risk and valuation | @@ -130,7 +130,7 @@ order types, data requirements, and limits through `--capabilities.execution_mod v8 and earlier contracts retain completed-bar configuration version `"1"`; v3 and v4 preserve their flat execution object unchanged. -Contract v13 also accepts `completed_bar_next_open_v1` and +Contract v13 introduced `completed_bar_next_open_v1` and `completed_bar_adverse_touch_v1`, each with strict configuration version `"1"`. They retain `participation_bps` and `fee_schedules`, and additionally require: @@ -234,7 +234,7 @@ has zero available quantity. The latest observation remains active until replace missing-data handling, `reject_order` or `clip_fill` locate behavior, and `reject_new_shorts` or `close_out` recall behavior. -The v13 `settlement` object selects `total_cash` or `settled_cash` buying power and +The v12 `settlement` object selects `total_cash` or `settled_cash` buying power and `total_positions` or `settled_positions` availability. Its immutable calendars contain ordered canonical business dates, and each instrument has exactly one calendar and a lag from zero through 30 business days. A fill updates economic accounting immediately and creates a deterministic @@ -262,12 +262,21 @@ terminal and require either `hold` or an explicit quote-currency `cash_out` pric terminal events cancel active orders. Terminal events set persistent target exposure to zero and cash-out clears the position with exact realized-P&L attribution. +Version 14 slices add `market_events`. A quote records bid/ask prices and displayed quantities; a +trade records price, quantity, and `buy`, `sell`, or `unknown` aggressor side. Every event also +records `event_at`, `available_at`, `received_at`, and a positive `ingest_sequence`. Events are +strictly ordered by availability, receipt, and ingest sequence; economic time cannot follow +availability, and no event may escape its containing slice's time or observability boundary. +Prices and quantities align to the instrument tick and lot. The +[`quote-trade` fixture](../contracts/v14/fixtures/quote-trade.scenario.json) demonstrates passive +fills and has an equivalent bounded JSON Lines replay. + For causal next-open execution, an order-changing schedule entry's anchor `received_at` is no later than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v13/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v14/journal.schema.json) validates each JSON Lines record. Every record contains `contract_version`, `engine_sequence`, deterministic `event_id`, ordered `causation_ids`, `run_id`, `recorded_at`, `event_type`, and an event-specific `payload`. Causal references are unique prior event IDs from the same run. The version is repeated on every record diff --git a/lib/codec.ml b/lib/codec.ml index 98d2c67..2ec80da 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -359,6 +359,36 @@ let settlement_failure_to_yojson failure = ("reason", string failure.reason); ] +let market_event_to_yojson event = + let common = + [ + ("instrument_id", instrument_id event.Market_event.instrument_id); + ("event_at", timestamp event.event_at); + ("available_at", timestamp event.available_at); + ("received_at", timestamp event.received_at); + ("ingest_sequence", int64 event.ingest_sequence); + ] + in + match event.kind with + | Market_event.Quote { bid_price; bid_quantity; ask_price; ask_quantity } -> + `Assoc + ((("type", string "quote") :: common) + @ [ + ("bid_price", price bid_price); + ("bid_quantity", quantity bid_quantity); + ("ask_price", price ask_price); + ("ask_quantity", quantity ask_quantity); + ]) + | Market_event.Trade { price = value; quantity = size; aggressor_side } -> + `Assoc + ((("type", string "trade") :: common) + @ [ + ("price", price value); + ("quantity", quantity size); + ( "aggressor_side", + string (Market_event.aggressor_side_to_string aggressor_side) ); + ]) + let versioned_market_slice_to_yojson ~contract_version market_slice = `Assoc [ @@ -375,9 +405,10 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ); ] |> function - | `Assoc fields when List.mem contract_version [ "13"; "12"; "11"; "10" ] -> + | `Assoc fields + when List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] -> let settlement = - if List.mem contract_version [ "13"; "12"; "11" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then [ ( "settlement_failures", `List @@ -387,7 +418,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = else [] in let lifecycle = - if List.mem contract_version [ "13"; "12" ] then + if List.mem contract_version [ "14"; "13"; "12" ] then [ ( "lifecycle_events", `List @@ -396,6 +427,16 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ] else [] in + let market_events = + if String.equal contract_version "14" then + [ + ( "market_events", + `List + (List.map market_event_to_yojson + market_slice.Market_slice.market_events) ); + ] + else [] + in `Assoc (fields @ [ @@ -408,7 +449,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = (List.map cash_rate_observation_to_yojson market_slice.Market_slice.cash_rate_observations) ); ] - @ settlement @ lifecycle) + @ settlement @ lifecycle @ market_events) | json -> json let market_slice_to_yojson market_slice = @@ -426,6 +467,9 @@ let market_slice_to_yojson_v12 market_slice = let market_slice_to_yojson_v13 market_slice = versioned_market_slice_to_yojson ~contract_version:"13" market_slice +let market_slice_to_yojson_v14 market_slice = + versioned_market_slice_to_yojson ~contract_version:"14" market_slice + let request_fields request = let kind, limit_price = match request.Order.kind with @@ -529,7 +573,7 @@ let order_to_yojson_v8 order = ]) let versioned_order_to_yojson ~contract_version order = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] then order_to_yojson_v8 order else order_to_yojson order @@ -727,7 +771,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "cash_balances", `List (List.map - (if List.mem contract_version [ "13"; "12"; "11" ] then + (if List.mem contract_version [ "14"; "13"; "12"; "11" ] then cash_attribution_to_yojson_v11 else if String.equal contract_version "10" then cash_attribution_to_yojson_v10 @@ -736,7 +780,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "positions", `List (List.map - (if List.mem contract_version [ "13"; "12"; "11" ] then + (if List.mem contract_version [ "14"; "13"; "12"; "11" ] then position_attribution_to_yojson_v11 else if String.equal contract_version "9" @@ -746,15 +790,15 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = valuation.positions) ); ] |> function - | `Assoc fields when List.mem contract_version [ "13"; "12"; "11"; "10"; "9" ] - -> + | `Assoc fields + when List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9" ] -> let financing = - if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then [ ("cash_interest", money valuation.Account.cash_interest) ] else [] in let settlement = - if List.mem contract_version [ "13"; "12"; "11" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then [ ("settled_cash", money valuation.Account.settled_cash); ("unsettled_cash", money valuation.unsettled_cash); @@ -801,7 +845,8 @@ let valuation_to_yojson ~contract_version valuation = | `Assoc fields -> let fields = fields @ [ ("margin", margin_to_yojson valuation.margin) ] in let fields = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + then fields @ [ ( "group_exposures", @@ -942,7 +987,7 @@ let payload_to_yojson ~contract_version = function ("final_price", price attribution.final_price); ] | Audit.Fill_applied fill -> - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9" ] then fill_to_yojson_v9 fill else fill_to_yojson fill | Audit.Settlement_instruction_created instruction diff --git a/lib/codec.mli b/lib/codec.mli index 0b10ce3..a097970 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -8,6 +8,7 @@ val market_slice_to_yojson_v10 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v11 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v12 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v13 : Market_slice.t -> Yojson.Safe.t +val market_slice_to_yojson_v14 : Market_slice.t -> Yojson.Safe.t val order_to_yojson : Order.t -> Yojson.Safe.t val order_to_yojson_v8 : Order.t -> Yojson.Safe.t val fill_to_yojson : Fill.t -> Yojson.Safe.t diff --git a/lib/contract.ml b/lib/contract.ml index 6f14005..da5fc9b 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,11 +1,12 @@ -let version = "13" -let previous_version = "12" +let version = "14" +let previous_version = "13" let legacy_journal_version = "3" let supported_versions = [ version; previous_version; + "12"; "11"; "10"; "9"; @@ -18,8 +19,8 @@ let supported_versions = ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "11" -let previous_strategy_protocol_version = "10" +let strategy_protocol_version = "12" +let previous_strategy_protocol_version = "11" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -38,6 +39,7 @@ let capabilities_to_yojson () = [ strategy_protocol_version; previous_strategy_protocol_version; + "10"; "9"; "8"; "7"; diff --git a/lib/engine.ml b/lib/engine.ml index dfe9c08..c5443b0 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -69,6 +69,7 @@ let config_v11 ~contract_version ~risk ~venue_calendars ~execution_model let config_v12 = config_v11 let config_v13 = config_v12 +let config_v14 = config_v13 let valid_sha256 value = String.length value = 64 diff --git a/lib/engine.mli b/lib/engine.mli index 2cd959b..4849421 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -62,6 +62,17 @@ val config_v13 : max_internal_events:int -> (config, string) result +val config_v14 : + contract_version:string -> + risk:Risk.t -> + venue_calendars:Venue_calendar.t list -> + execution_model:Execution_model.t -> + execution:Execution.t -> + financing:Financing.policy -> + settlement:Settlement.policy -> + max_internal_events:int -> + (config, string) result + module Interactive : sig type t type progress diff --git a/lib/execution.ml b/lib/execution.ml index 7a4d04c..1a4fca0 100644 --- a/lib/execution.ml +++ b/lib/execution.ml @@ -538,6 +538,278 @@ let start_slice_next_open state = start_slice_with_policy Next_open_only state let start_slice_adverse_touch state = start_slice_with_policy Adverse_touch state +type observable_liquidity = + | Quote_liquidity of { bid : Scalar.Quantity.t; ask : Scalar.Quantity.t } + | Trade_liquidity of Scalar.Quantity.t + +let event_capacity state instrument quantity = + let* capacity = + Scalar.Quantity.bps_floor quantity ~bps:state.participation_bps + in + Scalar.Quantity.round_toward_zero_to_multiple capacity + ~multiple:instrument.Instrument.lot_size + +let validate_market_event instrument (event : Market_event.t) = + let tick = instrument.Instrument.tick_size in + let aligned = function + | Market_event.Quote { bid_price; ask_price; _ } -> + Scalar.Price.is_multiple bid_price ~tick + && Scalar.Price.is_multiple ask_price ~tick + | Trade { price; _ } -> Scalar.Price.is_multiple price ~tick + in + if not (Id.Instrument.equal instrument.id event.instrument_id) then + Error "execution instrument differs from the market event instrument" + else if not (aligned event.kind) then + Error "market event price is not aligned to the instrument tick size" + else Ok () + +let event_trigger order (event : Market_event.t) = + let observed_price = + match (event.kind, order.Order.request.side) with + | Market_event.Quote { ask_price; _ }, Order.Buy -> ask_price + | Quote { bid_price; _ }, Sell -> bid_price + | Trade { price; _ }, _ -> price + in + match (order.request.kind, order.request.side) with + | Order.Stop trigger, Buy | Stop_limit { trigger_price = trigger; _ }, Buy -> + Scalar.Price.compare observed_price trigger >= 0 + | Order.Stop trigger, Sell | Stop_limit { trigger_price = trigger; _ }, Sell + -> + Scalar.Price.compare observed_price trigger <= 0 + | (Market | Limit _), _ -> false + +let event_opportunity order (event : Market_event.t) liquidity = + match + (event.kind, liquidity, Order.effective_kind order, order.request.side) + with + | Quote { ask_price; _ }, Quote_liquidity { ask; _ }, Some Market, Buy -> + Some (ask_price, ask, Fee_schedule.Taker) + | Quote { bid_price; _ }, Quote_liquidity { bid; _ }, Some Market, Sell -> + Some (bid_price, bid, Fee_schedule.Taker) + | Quote { ask_price; _ }, Quote_liquidity { ask; _ }, Some (Limit limit), Buy + when Scalar.Price.compare ask_price limit <= 0 -> + Some (ask_price, ask, Fee_schedule.Taker) + | Quote { bid_price; _ }, Quote_liquidity { bid; _ }, Some (Limit limit), Sell + when Scalar.Price.compare bid_price limit >= 0 -> + Some (bid_price, bid, Fee_schedule.Taker) + | ( Trade { price; aggressor_side = Market_event.Sell; _ }, + Trade_liquidity quantity, + Some (Limit limit), + Buy ) + when Scalar.Price.compare price limit <= 0 -> + Some (price, quantity, Fee_schedule.Maker) + | ( Trade { price; aggressor_side = Market_event.Buy; _ }, + Trade_liquidity quantity, + Some (Limit limit), + Sell ) + when Scalar.Price.compare price limit >= 0 -> + Some (price, quantity, Fee_schedule.Maker) + | _ -> None + +let consume_observable side liquidity quantity = + match liquidity with + | Quote_liquidity { bid; ask } -> + if side = Order.Buy then + Result.map + (fun ask -> Quote_liquidity { bid; ask }) + (Scalar.Quantity.subtract ask quantity) + else + Result.map + (fun bid -> Quote_liquidity { bid; ask }) + (Scalar.Quantity.subtract bid quantity) + | Trade_liquidity available -> + Result.map + (fun value -> Trade_liquidity value) + (Scalar.Quantity.subtract available quantity) + +let start_slice_quote_trade state ~instruments ~oms + (market_slice : Market_slice.t) = + let instrument_map = + List.fold_left + (fun map instrument -> + Id.Instrument.Map.add instrument.Instrument.id instrument map) + Id.Instrument.Map.empty instruments + in + let prepare_event event = + match + Id.Instrument.Map.find_opt event.Market_event.instrument_id instrument_map + with + | None -> Error "market event refers to an unknown instrument" + | Some instrument -> + let* () = validate_market_event instrument event in + if + Ptime.compare event.event_at market_slice.start_at < 0 + || Ptime.compare event.event_at market_slice.end_at > 0 + || Ptime.compare event.received_at market_slice.received_at > 0 + then Error "market event falls outside its observable slice boundary" + else + let* liquidity = + match event.kind with + | Market_event.Quote { bid_quantity; ask_quantity; _ } -> + let* bid = event_capacity state instrument bid_quantity in + let* ask = event_capacity state instrument ask_quantity in + Ok (Quote_liquidity { bid; ask }) + | Trade { quantity; _ } -> + Result.map + (fun value -> Trade_liquidity value) + (event_capacity state instrument quantity) + in + Ok (event, instrument, liquidity) + in + let* events = + List.fold_right + (fun event result -> + let* prepared = prepare_event event in + let* remaining = result in + Ok (prepared :: remaining)) + market_slice.market_events (Ok []) + in + let eligible = + Oms.active_orders oms + |> List.filter (fun order -> + Int64.compare order.Order.eligible_after_slice_sequence + market_slice.slice_sequence + < 0 + && Ptime.compare order.created_at market_slice.start_at <= 0 + && + match order.trigger_state with + | Some (Order.Triggered { triggered_slice_sequence; _ }) -> + Int64.compare triggered_slice_sequence market_slice.slice_sequence + < 0 + | Some Order.Dormant | None -> true) + |> List.sort compare_execution_order + in + let order_ids = List.map (fun order -> order.Order.id) eligible in + let market_ioc_orders = + eligible + |> List.filter_map (fun order -> + if Order.is_ioc order && not (Order.is_dormant_stop order) then + Some order.Order.id + else None) + in + let rec make_events = function + | [] -> cursor (fun ~oms:_ -> Ok (Finished market_ioc_orders)) + | (event, instrument, liquidity) :: remaining_events -> + make_orders event instrument liquidity order_ids remaining_events + and make_orders event instrument liquidity remaining remaining_events = + Cursor + (fun current_oms -> + match remaining with + | [] -> + let (Cursor next) = make_events remaining_events in + next current_oms + | order_id :: remaining_orders -> ( + match Oms.find current_oms order_id with + | None -> Error "eligible order disappeared during market replay" + | Some order when not (Order.is_active order) -> + let (Cursor next) = + make_orders event instrument liquidity remaining_orders + remaining_events + in + next current_oms + | Some order + when not + (Id.Instrument.equal order.request.instrument_id + event.Market_event.instrument_id) -> + let (Cursor next) = + make_orders event instrument liquidity remaining_orders + remaining_events + in + next current_oms + | Some order when Order.is_dormant_stop order -> + let continuation = + make_orders event instrument liquidity remaining_orders + remaining_events + in + if event_trigger order event then + Ok + (Triggered + ( order.id, + event.event_at, + market_slice.slice_sequence, + continuation )) + else + let (Cursor next) = continuation in + next current_oms + | Some order -> ( + match event_opportunity order event liquidity with + | None -> + let (Cursor next) = + make_orders event instrument liquidity remaining_orders + remaining_events + in + next current_oms + | Some (price, available, fee_liquidity) -> + let quantity = + Scalar.Quantity.minimum available + (Order.remaining_quantity order) + in + if + Scalar.Quantity.is_zero quantity + || Order.is_fok order + && Scalar.Quantity.compare quantity + (Order.remaining_quantity order) + < 0 + then + let (Cursor next) = + make_orders event instrument liquidity remaining_orders + remaining_events + in + next current_oms + else + let* notional = Scalar.Money.notional price quantity in + let* fee_components, fee = + calculate_fee state ~instrument ~notional ~quantity + ~liquidity:fee_liquidity + ~fx_rates: + (List.map + (fun mark -> + (mark.Market_slice.currency, mark.rate)) + market_slice.fx_rates) + in + let proposed = + { + order_id = order.id; + quantity; + price; + fee; + fee_components; + liquidity = fee_liquidity; + executed_at = event.event_at; + price_attribution = None; + } + in + let continue applied_quantity = + if Scalar.Quantity.compare applied_quantity quantity > 0 + then + Error + "applied fill quantity exceeds observable liquidity" + else if + Scalar.Quantity.compare applied_quantity + Scalar.Quantity.zero + < 0 + then Error "applied fill quantity must be nonnegative" + else if + not + (Scalar.Quantity.is_multiple applied_quantity + ~lot:instrument.Instrument.lot_size) + then + Error + "applied fill quantity is not aligned to the \ + instrument lot size" + else + let* liquidity = + consume_observable order.request.side liquidity + applied_quantity + in + Ok + (make_orders event instrument liquidity + remaining_orders remaining_events) + in + Ok (Proposed (proposed, continue))))) + in + Ok (make_events events) + let finished market_ioc_orders = cursor (fun ~oms:_ -> Ok (Finished market_ioc_orders)) diff --git a/lib/execution.mli b/lib/execution.mli index c625232..8ba8765 100644 --- a/lib/execution.mli +++ b/lib/execution.mli @@ -100,6 +100,13 @@ val start_slice_adverse_touch : Market_slice.t -> (cursor, string) result +val start_slice_quote_trade : + t -> + instruments:Instrument.t list -> + oms:Oms.t -> + Market_slice.t -> + (cursor, string) result + val finished : Id.Order.t list -> cursor (** Build a cursor that immediately finishes. This supports execution models that intentionally produce no proposals. *) diff --git a/lib/execution_model.ml b/lib/execution_model.ml index 11b13d5..ddd6154 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -37,6 +37,11 @@ module Completed_bar_adverse_touch_v1 = struct let start_slice = Execution.start_slice_adverse_touch end +module Quote_trade_v1 = struct + let name = "quote_trade_v1" + let start_slice = Execution.start_slice_quote_trade +end + let of_module model = model let name (module Model : S) = Model.name @@ -45,6 +50,7 @@ let builtins : t list = (module Completed_bar_v1); (module Completed_bar_next_open_v1); (module Completed_bar_adverse_touch_v1); + (module Quote_trade_v1); ] let supported = List.map name builtins @@ -54,7 +60,7 @@ let completed_bar_v1_contract = version = "2"; previous_versions = [ "1" ]; scenario_contract_versions = - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; legacy_required_fields = [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ]; @@ -75,7 +81,7 @@ let conservative_contract = { version = "1"; previous_versions = []; - scenario_contract_versions = [ "13" ]; + scenario_contract_versions = [ "14"; "13" ]; required_fields = [ "version"; @@ -100,11 +106,34 @@ let conservative_contract = ]; } +let quote_trade_contract = + { + version = "1"; + previous_versions = []; + scenario_contract_versions = [ "14" ]; + required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; + legacy_required_fields = []; + supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; + data_requirements = + [ + "causally_ordered_bid_ask_quotes"; + "aggressor_classified_trades_for_passive_fills"; + "completed_bars_for_valuation"; + ]; + limits = + `Assoc + [ + ( "participation_bps", + `Assoc [ ("minimum", `Int 0); ("maximum", `Int 10_000) ] ); + ]; + } + let configuration_contract model = match name model with | "completed_bar_v1" -> completed_bar_v1_contract | "completed_bar_next_open_v1" | "completed_bar_adverse_touch_v1" -> conservative_contract + | "quote_trade_v1" -> quote_trade_contract | unsupported -> invalid_arg (Printf.sprintf "execution model %S has no configuration contract" diff --git a/lib/external_replay.ml b/lib/external_replay.ml index ce795ce..23c422c 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -105,7 +105,11 @@ let create_runner ~contract_version ~run_id ~scenario_sha256 ~risk Engine.config_v10 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~max_internal_events | Some financing, Some settlement -> - if String.equal contract_version "13" then + if String.equal contract_version "14" then + Engine.config_v14 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else if String.equal contract_version "13" then Engine.config_v13 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events diff --git a/lib/market_event.ml b/lib/market_event.ml new file mode 100644 index 0000000..294a4b7 --- /dev/null +++ b/lib/market_event.ml @@ -0,0 +1,92 @@ +type aggressor_side = Buy | Sell | Unknown + +type kind = + | Quote of { + bid_price : Scalar.Price.t; + bid_quantity : Scalar.Quantity.t; + ask_price : Scalar.Price.t; + ask_quantity : Scalar.Quantity.t; + } + | Trade of { + price : Scalar.Price.t; + quantity : Scalar.Quantity.t; + aggressor_side : aggressor_side; + } + +type t = { + instrument_id : Id.Instrument.t; + event_at : Ptime.t; + available_at : Ptime.t; + received_at : Ptime.t; + ingest_sequence : int64; + kind : kind; +} + +let validate_common ~event_at ~available_at ~received_at ~ingest_sequence = + if Int64.compare ingest_sequence 0L <= 0 then + Error "market event ingest sequence must be positive" + else if Ptime.compare available_at event_at < 0 then + Error "market event availability must not precede event time" + else if Ptime.compare received_at available_at < 0 then + Error "market event receipt must not precede availability" + else Ok () + +let quote ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~bid_price ~bid_quantity ~ask_price ~ask_quantity = + let ( let* ) result function_ = Result.bind result function_ in + let* () = + validate_common ~event_at ~available_at ~received_at ~ingest_sequence + in + if Scalar.Price.compare bid_price ask_price >= 0 then + Error "quote bid price must be below ask price" + else if + Scalar.Quantity.is_zero bid_quantity || Scalar.Quantity.is_zero ask_quantity + then Error "quote quantities must be positive" + else + Ok + { + instrument_id; + event_at; + available_at; + received_at; + ingest_sequence; + kind = Quote { bid_price; bid_quantity; ask_price; ask_quantity }; + } + +let trade ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~price ~quantity ~aggressor_side = + let ( let* ) result function_ = Result.bind result function_ in + let* () = + validate_common ~event_at ~available_at ~received_at ~ingest_sequence + in + if Scalar.Quantity.is_zero quantity then + Error "trade quantity must be positive" + else + Ok + { + instrument_id; + event_at; + available_at; + received_at; + ingest_sequence; + kind = Trade { price; quantity; aggressor_side }; + } + +let compare_replay_order left right = + let availability = Ptime.compare left.available_at right.available_at in + if availability <> 0 then availability + else + let receipt = Ptime.compare left.received_at right.received_at in + if receipt <> 0 then receipt + else Int64.compare left.ingest_sequence right.ingest_sequence + +let aggressor_side_to_string = function + | Buy -> "buy" + | Sell -> "sell" + | Unknown -> "unknown" + +let aggressor_side_of_string = function + | "buy" -> Ok Buy + | "sell" -> Ok Sell + | "unknown" -> Ok Unknown + | _ -> Error "trade aggressor_side must be buy, sell, or unknown" diff --git a/lib/market_event.mli b/lib/market_event.mli new file mode 100644 index 0000000..d8a3287 --- /dev/null +++ b/lib/market_event.mli @@ -0,0 +1,52 @@ +(** Causally observable quote and trade events. *) + +type aggressor_side = Buy | Sell | Unknown + +type kind = + | Quote of { + bid_price : Scalar.Price.t; + bid_quantity : Scalar.Quantity.t; + ask_price : Scalar.Price.t; + ask_quantity : Scalar.Quantity.t; + } + | Trade of { + price : Scalar.Price.t; + quantity : Scalar.Quantity.t; + aggressor_side : aggressor_side; + } + +type t = private { + instrument_id : Id.Instrument.t; + event_at : Ptime.t; + available_at : Ptime.t; + received_at : Ptime.t; + ingest_sequence : int64; + kind : kind; +} + +val quote : + instrument_id:Id.Instrument.t -> + event_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + ingest_sequence:int64 -> + bid_price:Scalar.Price.t -> + bid_quantity:Scalar.Quantity.t -> + ask_price:Scalar.Price.t -> + ask_quantity:Scalar.Quantity.t -> + (t, string) result + +val trade : + instrument_id:Id.Instrument.t -> + event_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + ingest_sequence:int64 -> + price:Scalar.Price.t -> + quantity:Scalar.Quantity.t -> + aggressor_side:aggressor_side -> + (t, string) result + +val compare_replay_order : t -> t -> int +val aggressor_side_to_string : aggressor_side -> string +val aggressor_side_of_string : string -> (aggressor_side, string) result diff --git a/lib/market_slice.ml b/lib/market_slice.ml index d50c61c..e9a1613 100644 --- a/lib/market_slice.ml +++ b/lib/market_slice.ml @@ -7,6 +7,7 @@ type t = { available_at : Ptime.t; received_at : Ptime.t; bars : Bar.t list; + market_events : Market_event.t list; fx_rates : fx_mark list; corporate_actions : Corporate_action.t list; lifecycle_events : Instrument_lifecycle.event list; @@ -31,9 +32,10 @@ let fx_mark ~currency ~rate = let compare_bar left right = Id.Instrument.compare left.Bar.instrument_id right.Bar.instrument_id -let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at +let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations ~settlement_failures ~lifecycle_events = + ~cash_rate_observations ~settlement_failures ~lifecycle_events + ~market_events = if Int64.compare slice_sequence 0L <= 0 then Error "market slice sequence must be positive" else if Ptime.compare start_at end_at >= 0 then @@ -123,6 +125,15 @@ let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at (String.equal left.Settlement.instruction_id right.instruction_id)) && unique_failure remaining in + let rec ordered_events = function + | [] | [ _ ] -> true + | left :: (right :: _ as remaining) -> + Market_event.compare_replay_order left right < 0 + && Int64.compare left.Market_event.ingest_sequence + right.Market_event.ingest_sequence + < 0 + && ordered_events remaining + in if not (unique bars) then Error "market slice must contain one bar per instrument" else if fx_rates = [] then Error "market slice must contain FX rates" @@ -138,6 +149,10 @@ let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at Error "market slice cash rate currencies must be unique" else if not (unique_failure settlement_failures) then Error "market slice settlement failure instruction IDs must be unique" + else if not (ordered_events market_events) then + Error + "market events must be strictly ordered by availability, receipt, and \ + ingest sequence" else Ok { @@ -147,6 +162,7 @@ let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at available_at; received_at; bars; + market_events; fx_rates; corporate_actions; lifecycle_events; @@ -155,6 +171,13 @@ let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at settlement_failures; } +let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at + ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations ~settlement_failures ~lifecycle_events = + create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars + ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations + ~settlement_failures ~lifecycle_events ~market_events:[] + let create_v13 = create_v12 let create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at @@ -193,9 +216,10 @@ let compare_replay_order left right = let pp formatter state = Format.fprintf formatter - "slice[%Ld] bars=%d fx=%d actions=%d lifecycle=%d borrow=%d cash_rates=%d \ - failures=%d" + "slice[%Ld] bars=%d events=%d fx=%d actions=%d lifecycle=%d borrow=%d \ + cash_rates=%d failures=%d" state.slice_sequence (List.length state.bars) + (List.length state.market_events) (List.length state.fx_rates) (List.length state.corporate_actions) (List.length state.lifecycle_events) diff --git a/lib/market_slice.mli b/lib/market_slice.mli index 6f08692..5785f03 100644 --- a/lib/market_slice.mli +++ b/lib/market_slice.mli @@ -12,6 +12,7 @@ type t = private { available_at : Ptime.t; received_at : Ptime.t; bars : Bar.t list; + market_events : Market_event.t list; fx_rates : fx_mark list; corporate_actions : Corporate_action.t list; lifecycle_events : Instrument_lifecycle.event list; @@ -88,6 +89,22 @@ val create_v13 : lifecycle_events:Instrument_lifecycle.event list -> (t, string) result +val create_v14 : + slice_sequence:int64 -> + start_at:Ptime.t -> + end_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + bars:Bar.t list -> + fx_rates:fx_mark list -> + corporate_actions:Corporate_action.t list -> + borrow_observations:Financing.borrow_observation list -> + cash_rate_observations:Financing.cash_rate_observation list -> + settlement_failures:Settlement.failure list -> + lifecycle_events:Instrument_lifecycle.event list -> + market_events:Market_event.t list -> + (t, string) result + val bar : t -> Id.Instrument.t -> Bar.t option val fx_rate : t -> string -> Scalar.Price.t option val compare_replay_order : t -> t -> int diff --git a/lib/replay.ml b/lib/replay.ml index 459c992..6807cda 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -78,7 +78,11 @@ let engine_config ~contract_version ~risk ~venue_calendars ~execution_model Engine.config_v10 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~max_internal_events | Some financing, Some settlement -> - if String.equal contract_version "13" then + if String.equal contract_version "14" then + Engine.config_v14 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else if String.equal contract_version "13" then Engine.config_v13 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events diff --git a/lib/scenario.ml b/lib/scenario.ml index 734e1bd..dcf86bc 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -554,8 +554,8 @@ let parse_v7_risk base_currency instruments json = ~max_gross_exposure ~max_leverage ~short_borrow_bps let parse_risk ~contract_version base_currency instruments json = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7" ] then - parse_v7_risk base_currency instruments json + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] + then parse_v7_risk base_currency instruments json else parse_legacy_risk base_currency instruments json let parse_execution_values fields = @@ -803,8 +803,9 @@ let parse_versioned_execution ~contract_version ~instruments json = List.mem model_name [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] then parse_conservative_execution instruments configuration - else if String.equal version "2" then - parse_execution_v2 instruments configuration + else if + String.equal model_name "quote_trade_v1" || String.equal version "2" + then parse_execution_v2 instruments configuration else parse_execution_values configuration in Ok (execution_model, execution) @@ -812,7 +813,7 @@ let parse_versioned_execution ~contract_version ~instruments json = let parse_execution ~contract_version ~instruments json = if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then parse_versioned_execution ~contract_version ~instruments json else parse_legacy_execution ~contract_version json @@ -862,7 +863,7 @@ let parse_portfolio_intent ~name ~parse_target make json = let parse_submit_intent ~contract_version json = let versioned = - List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8" ] + List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] in let* fields = object_fields ~name:"submit_order intent" @@ -1598,21 +1599,124 @@ let parse_cash_rate_observation json = Financing.cash_rate_observation ~currency ~effective_at ~credit_rate_bps ~debit_rate_bps +let parse_market_event json = + let* loose_fields = + match json with + | `Assoc fields -> Ok fields + | _ -> Error "market event must be a JSON object" + in + let* type_name = + Result.bind (field loose_fields "type") (string ~name:"market event type") + in + let common_fields = + [ + "type"; + "instrument_id"; + "event_at"; + "available_at"; + "received_at"; + "ingest_sequence"; + ] + in + let specific_fields = + match type_name with + | "quote" -> [ "bid_price"; "bid_quantity"; "ask_price"; "ask_quantity" ] + | "trade" -> [ "price"; "quantity"; "aggressor_side" ] + | _ -> [] + in + let* () = + if specific_fields = [] then + Error "market event type must be quote or trade" + else Ok () + in + let* fields = + object_fields + ~name:(type_name ^ " market event") + ~expected:(common_fields @ specific_fields) + json + in + let* instrument_id = + Result.bind + (field fields "instrument_id") + (parse_id Id.Instrument.of_string ~name:"market event instrument_id") + in + let* event_at = + Result.bind (field fields "event_at") + (parse_timestamp ~name:"market event_at") + in + let* available_at = + Result.bind + (field fields "available_at") + (parse_timestamp ~name:"market available_at") + in + let* received_at = + Result.bind + (field fields "received_at") + (parse_timestamp ~name:"market received_at") + in + let* ingest_sequence = + Result.bind + (field fields "ingest_sequence") + (parse_int64 ~name:"market ingest_sequence") + in + match type_name with + | "quote" -> + let* bid_price = + Result.bind (field fields "bid_price") (parse_price ~name:"bid_price") + in + let* bid_quantity = + Result.bind + (field fields "bid_quantity") + (parse_quantity ~name:"bid_quantity") + in + let* ask_price = + Result.bind (field fields "ask_price") (parse_price ~name:"ask_price") + in + let* ask_quantity = + Result.bind + (field fields "ask_quantity") + (parse_quantity ~name:"ask_quantity") + in + Market_event.quote ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~bid_price ~bid_quantity ~ask_price ~ask_quantity + | "trade" -> + let* price = + Result.bind (field fields "price") (parse_price ~name:"trade price") + in + let* quantity = + Result.bind (field fields "quantity") + (parse_quantity ~name:"trade quantity") + in + let* aggressor_side = + Result.bind + (Result.bind + (field fields "aggressor_side") + (string ~name:"aggressor_side")) + Market_event.aggressor_side_of_string + in + Market_event.trade ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~price ~quantity ~aggressor_side + | _ -> assert false + let parse_slice ~contract_version json = let financing_fields = - if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then [ "borrow_observations"; "cash_rate_observations" ] else [] in let settlement_fields = - if List.mem contract_version [ "13"; "12"; "11" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then [ "settlement_failures" ] else [] in let lifecycle_fields = - if List.mem contract_version [ "13"; "12" ] then [ "lifecycle_events" ] + if List.mem contract_version [ "14"; "13"; "12" ] then + [ "lifecycle_events" ] else [] in + let market_event_fields = + if String.equal contract_version "14" then [ "market_events" ] else [] + in let* fields = object_fields ~name:"market slice" ~expected: @@ -1626,7 +1730,8 @@ let parse_slice ~contract_version json = "fx_rates"; "corporate_actions"; ] - @ financing_fields @ settlement_fields @ lifecycle_fields) + @ financing_fields @ settlement_fields @ lifecycle_fields + @ market_event_fields) json in let* sequence_json = field fields "slice_sequence" in @@ -1648,7 +1753,7 @@ let parse_slice ~contract_version json = let* actions_json = field fields "corporate_actions" in let* actions_json = list ~name:"corporate_actions" actions_json in let* corporate_actions = map_list parse_corporate_action actions_json in - if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then let* borrow_json = Result.bind (field fields "borrow_observations") @@ -1663,7 +1768,7 @@ let parse_slice ~contract_version json = let* cash_rate_observations = map_list parse_cash_rate_observation cash_json in - if List.mem contract_version [ "13"; "12"; "11" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then let* failures_json = Result.bind (field fields "settlement_failures") @@ -1672,20 +1777,32 @@ let parse_slice ~contract_version json = let* settlement_failures = map_list parse_settlement_failure failures_json in - if List.mem contract_version [ "13"; "12" ] then + if List.mem contract_version [ "14"; "13"; "12" ] then let* lifecycle_json = Result.bind (field fields "lifecycle_events") (list ~name:"lifecycle_events") in let* lifecycle_events = map_list parse_lifecycle_event lifecycle_json in - let create = - if String.equal contract_version "13" then Market_slice.create_v13 - else Market_slice.create_v12 - in - create ~slice_sequence ~start_at ~end_at ~available_at ~received_at - ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations ~settlement_failures ~lifecycle_events + if String.equal contract_version "14" then + let* events_json = + Result.bind + (field fields "market_events") + (list ~name:"market_events") + in + let* market_events = map_list parse_market_event events_json in + Market_slice.create_v14 ~slice_sequence ~start_at ~end_at + ~available_at ~received_at ~bars ~fx_rates ~corporate_actions + ~borrow_observations ~cash_rate_observations ~settlement_failures + ~lifecycle_events ~market_events + else + let create = + if String.equal contract_version "13" then Market_slice.create_v13 + else Market_slice.create_v12 + in + create ~slice_sequence ~start_at ~end_at ~available_at ~received_at + ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations ~settlement_failures ~lifecycle_events else Market_slice.create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations @@ -1730,7 +1847,8 @@ let construct_header ~root ~contract_path ~contract_version in let* initial_cash, initial_portfolio = if - List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + List.mem contract_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state @@ -1800,7 +1918,7 @@ let construct_header ~root ~contract_path ~contract_version | _, _ -> Ok Financing.legacy_policy in let financing = - if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then Some financing else None in diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index 6888490..e85661d 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -70,7 +70,9 @@ let common ~root ~contract_version fields = let* run_id = field ~root fields "run_id" in let* base_currency = field ~root fields "base_currency" in let initial_field = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + if + List.mem contract_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -79,19 +81,19 @@ let common ~root ~contract_version fields = let venue_calendars = if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then List.assoc_opt "venue_calendars" fields else None in let* risk = field ~root fields "risk" in let* execution = field ~root fields "execution" in let financing = - if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then List.assoc_opt "financing" fields else None in let settlement = - if List.mem contract_version [ "13"; "12"; "11" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then List.assoc_opt "settlement" fields else None in @@ -124,12 +126,14 @@ let batch json = let calendar_fields = if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + if + List.mem contract_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -150,11 +154,12 @@ let batch json = "slices"; ] @ calendar_fields - @ (if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + @ (if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then [ "financing" ] else []) @ - if List.mem contract_version [ "13"; "12"; "11" ] then [ "settlement" ] + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + [ "settlement" ] else []) json in @@ -169,12 +174,14 @@ let stream_header ~contract_version json = let calendar_fields = if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + if + List.mem contract_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -192,11 +199,12 @@ let stream_header ~contract_version json = "max_internal_events"; ] @ calendar_fields - @ (if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + @ (if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then [ "financing" ] else []) @ - if List.mem contract_version [ "13"; "12"; "11" ] then [ "settlement" ] + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + [ "settlement" ] else []) json in diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 527b0b8..45032db 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -48,7 +48,9 @@ let validate_venue_calendars ~root catalog venue_calendars = let header ~root ~contract_version ~base_currency ~initial_cash ~instruments ~venue_calendars ~max_internal_events = let* () = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + if + List.mem contract_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Ok () else Account.create ~base_currency ~initial_cash @@ -69,7 +71,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments let* () = if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -89,7 +91,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments (child root (if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio.cash" else "initial_cash")) "initial cash must contain every scenario currency exactly once" @@ -384,6 +386,39 @@ let validate_slices_at ~paths ~base_currency ~currencies ~instruments slices = bar.volume) market_slice.bars in + let market_events_valid = + List.for_all + (fun (event : Market_event.t) -> + match + Id.Instrument.Map.find_opt event.instrument_id instrument_map + with + | None -> false + | Some instrument -> + let prices, quantities = + match event.kind with + | Market_event.Quote + { bid_price; bid_quantity; ask_price; ask_quantity } -> + ( [ bid_price; ask_price ], + [ bid_quantity; ask_quantity ] ) + | Trade { price; quantity; _ } -> ([ price ], [ quantity ]) + in + List.for_all + (fun price -> + Scalar.Price.is_multiple price ~tick:instrument.tick_size) + prices + && List.for_all + (fun quantity -> + Scalar.Quantity.is_multiple quantity + ~lot:instrument.lot_size) + quantities + && Ptime.compare event.event_at market_slice.start_at >= 0 + && Ptime.compare event.event_at market_slice.end_at <= 0 + && Ptime.compare event.available_at market_slice.available_at + <= 0 + && Ptime.compare event.received_at market_slice.received_at + <= 0) + market_slice.market_events + in if not (Id.Instrument.Set.equal catalog ids) then fail ~json_path:(child root "bars") "each market slice must contain every configured instrument" @@ -413,6 +448,11 @@ let validate_slices_at ~paths ~base_currency ~currencies ~instruments slices = else if not bars_aligned then fail ~json_path:(child root "bars") "market prices and volumes must align with instrument increments" + else if not market_events_valid then + fail + ~json_path:(child root "market_events") + "market events must be known, aligned, and observable within the \ + slice" else if Option.exists (fun sequence -> diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index ece0f9a..120f8cb 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -103,7 +103,7 @@ let group_kind_to_string = function let nullable render = Option.fold ~none:`Null ~some:render let modern_protocol protocol_version = - List.mem protocol_version [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] let financing_to_yojson policy = `Assoc @@ -253,8 +253,10 @@ let execution_to_yojson ~protocol_version model execution = ] in if - String.equal protocol_version "11" - && not (String.equal (Execution_model.name model) "completed_bar_v1") + List.mem protocol_version [ "12"; "11" ] + && List.mem + (Execution_model.name model) + [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] then let costs = Execution.cost_model execution |> Option.get in `Assoc @@ -288,7 +290,25 @@ let execution_to_yojson ~protocol_version model execution = ] ); ] ); ] - else if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7" ] then + else if + String.equal protocol_version "12" + && String.equal (Execution_model.name model) "quote_trade_v1" + then + `Assoc + [ + ("model", string (Execution_model.name model)); + ( "configuration", + `Assoc + [ + ("version", string "1"); + ("participation_bps", `Int (Execution.participation_bps execution)); + ( "fee_schedules", + `List + (List.map fee_schedule_to_yojson + (Execution.fee_schedules execution)) ); + ] ); + ] + else if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7" ] then `Assoc [ ("model", string (Execution_model.name model)); @@ -327,6 +347,7 @@ let execution_to_yojson ~protocol_version model execution = let protocol_version initialization = match initialization.scenario_contract_version with + | "14" -> "12" | "13" -> "11" | "12" -> "10" | "11" -> "9" @@ -374,7 +395,7 @@ let initialize_message ~sequence:message_sequence initialization = ] in let fields = - if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then + if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then let initial_portfolio = Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson initialization.initial_portfolio @@ -387,14 +408,14 @@ let initialize_message ~sequence:message_sequence initialization = ( "venue_calendars", `List (List.map venue_calendar_to_yojson venue_calendars) ); ]; - (if List.mem protocol_version [ "11"; "10"; "9"; "8" ] then + (if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8" ] then [ ( "financing", Option.fold ~none:`Null ~some:financing_to_yojson initialization.financing ); ] else []); - (if List.mem protocol_version [ "11"; "10"; "9" ] then + (if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then [ ( "settlement", Option.fold ~none:`Null ~some:settlement_to_yojson @@ -428,14 +449,14 @@ let cash_attribution_to_yojson ~protocol_version ("fx_rate", price balance.fx_rate); ("base_value", money balance.base_value); ] - @ (if List.mem protocol_version [ "11"; "10"; "9"; "8" ] then + @ (if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8" ] then [ ("interest", money balance.interest); ("base_interest", money balance.base_interest); ] else []) @ - if List.mem protocol_version [ "11"; "10"; "9" ] then + if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then [ ("settled_amount", money balance.settled_amount); ("unsettled_amount", money balance.unsettled_amount); @@ -455,7 +476,7 @@ let marked_position_to_yojson ~protocol_version ("weight", Option.fold ~none:`Null ~some:weight position.weight); ] @ - if List.mem protocol_version [ "11"; "10"; "9" ] then + if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then [ ("settled_quantity", quantity position.settled_quantity); ("unsettled_quantity", quantity position.unsettled_quantity); @@ -538,7 +559,9 @@ let context_to_yojson ~protocol_version context = ( "working_orders", `List (List.map - (if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7"; "6" ] + (if + List.mem protocol_version + [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 else Codec.order_to_yojson) working_orders) ); @@ -551,7 +574,11 @@ let event_to_yojson ~protocol_version = function [ ("type", string "market_slice_closed"); ( "market_slice", - if List.mem protocol_version [ "11"; "10" ] then + if String.equal protocol_version "12" then + Codec.market_slice_to_yojson_v14 market_slice + else if String.equal protocol_version "11" then + Codec.market_slice_to_yojson_v13 market_slice + else if String.equal protocol_version "10" then Codec.market_slice_to_yojson_v12 market_slice else if String.equal protocol_version "9" then Codec.market_slice_to_yojson_v11 market_slice @@ -564,8 +591,8 @@ let event_to_yojson ~protocol_version = function [ ("type", string "fill_received"); ( "fill", - if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7" ] then - Codec.fill_to_yojson_v9 fill + if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7" ] + then Codec.fill_to_yojson_v9 fill else Codec.fill_to_yojson fill ); ] | Strategy.Order_updated order -> @@ -573,8 +600,9 @@ let event_to_yojson ~protocol_version = function [ ("type", string "order_updated"); ( "order", - if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then - Codec.order_to_yojson_v8 order + if + List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + then Codec.order_to_yojson_v8 order else Codec.order_to_yojson order ); ] | Strategy.Intent_rejected reason -> @@ -661,7 +689,8 @@ let parse_intents_payload ~protocol_version json = let* intent = Scenario.intent_of_yojson ~contract_version: - (if String.equal protocol_version "11" then "13" + (if String.equal protocol_version "12" then "14" + else if String.equal protocol_version "11" then "13" else if String.equal protocol_version "10" then "12" else if String.equal protocol_version "9" then "11" else if String.equal protocol_version "8" then "10" diff --git a/mkdocs.yml b/mkdocs.yml index 7c61eb4..81af8aa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,14 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v13: contracts/v13/README.md + - Current v14: contracts/v14/README.md - Transitional v5: contracts/v5/README.md - Transitional v4: contracts/v4/README.md - Transitional v3: contracts/v3/README.md - Frozen v2: contracts/v2/README.md - Historical v1: contracts/v1/README.md - External strategy: - - Current v11: contracts/strategy/v11/README.md + - Current v12: contracts/strategy/v12/README.md - Historical v3: contracts/strategy/v3/README.md - Historical v2: contracts/strategy/v2/README.md - Historical v1: contracts/strategy/v1/README.md diff --git a/scripts/check-deterministic-journals b/scripts/check-deterministic-journals index 671c25d..5966154 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -98,3 +98,15 @@ compare_journal \ v13-fill-clipped \ contracts/v13/fixtures/fill-clipped.scenario.json \ contracts/v13/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v14-demo \ + contracts/v14/fixtures/demo.scenario.json \ + contracts/v14/fixtures/demo.journal.jsonl +compare_journal \ + v14-fill-clipped \ + contracts/v14/fixtures/fill-clipped.scenario.json \ + contracts/v14/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v14-quote-trade \ + contracts/v14/fixtures/quote-trade.scenario.json \ + contracts/v14/fixtures/quote-trade.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index f6acfa2..1e73038 100644 --- a/scripts/check-documentation.py +++ b/scripts/check-documentation.py @@ -26,13 +26,13 @@ "docs/persistra.md", "SECURITY.md", "contracts/conformance/README.md", - "contracts/v13/README.md", + "contracts/v14/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v11/README.md", + "contracts/strategy/v12/README.md", "contracts/strategy/v3/README.md", "contracts/strategy/v2/README.md", "contracts/strategy/v1/README.md", diff --git a/scripts/release_artifacts.py b/scripts/release_artifacts.py index 6607931..62fd115 100644 --- a/scripts/release_artifacts.py +++ b/scripts/release_artifacts.py @@ -376,8 +376,8 @@ def verify_release( ( "bin/trading-engine", "lib/trading_engine/opam", - "share/trading_engine/contracts/v13/scenario.schema.json", - "share/trading_engine/contracts/v13/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v14/scenario.schema.json", + "share/trading_engine/contracts/v14/fixtures/demo.scenario.json", "doc/trading_engine/README.md", ), epoch, @@ -388,7 +388,7 @@ def verify_release( ( "trading_engine.opam", "contracts/v1/scenario.schema.json", - "contracts/v13/fixtures/demo.scenario.json", + "contracts/v14/fixtures/demo.scenario.json", "docs/architecture.md", ".github/workflows/release-candidate.yml", ), @@ -400,8 +400,8 @@ def verify_release( ( "contracts/conformance/manifest.json", "contracts/v1/scenario.schema.json", - "contracts/v13/fixtures/demo.scenario.json", - "contracts/strategy/v11/message.schema.json", + "contracts/v14/fixtures/demo.scenario.json", + "contracts/strategy/v12/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v13/scenario.schema.json", + "contracts/v14/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index 62a7360..9291ade 100644 --- a/test/cli.t +++ b/test/cli.t @@ -2,7 +2,7 @@ 1.0.0 $ ../bin/main.exe --capabilities - {"engine_version":"1.0.0","scenario_contract_versions":["13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["13","12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["13","12","11","10","9","8","7","6","5","4","3"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"2":["version","participation_bps","fee_schedules"],"1":["version","participation_bps","fixed_fee","fee_bps"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"fee_bps":{"minimum":0,"maximum":10000},"fixed_fee":{"minimum":"0","unit":"money"}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_adverse_touch_v1","configuration_versions":["1"],"scenario_contract_versions":["13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}}],"strategy_protocol_versions":["11","10","9","8","7","6","5","4","3"],"resource_limits":{"version":"1","scenario_record_bytes":1048576,"strategy_message_bytes":1048576,"internal_events":100000,"catalog_instruments":4096,"intents_per_batch":4096,"artifact_record_bytes":2097152}} + {"engine_version":"1.0.0","scenario_contract_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"2":["version","participation_bps","fee_schedules"],"1":["version","participation_bps","fixed_fee","fee_bps"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"fee_bps":{"minimum":0,"maximum":10000},"fixed_fee":{"minimum":"0","unit":"money"}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_adverse_touch_v1","configuration_versions":["1"],"scenario_contract_versions":["14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["14"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}}],"strategy_protocol_versions":["12","11","10","9","8","7","6","5","4","3"],"resource_limits":{"version":"1","scenario_record_bytes":1048576,"strategy_message_bytes":1048576,"internal_events":100000,"catalog_instruments":4096,"intents_per_batch":4096,"artifact_record_bytes":2097152}} $ ../bin/main.exe --validate-only --input ../contracts/v8/fixtures/demo.scenario.json valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=85f7c99e0666159579c79256b3d0dc5f9c328e1275b79465fe1d4c93883e68f1 diff --git a/test/dune b/test/dune index fa0aebb..2c57ecb 100644 --- a/test/dune +++ b/test/dune @@ -78,6 +78,17 @@ ../contracts/v13/journal.schema.json ../contracts/v13/scenario-stream.schema.json ../contracts/v13/scenario.schema.json + ../contracts/v14/fixtures/demo.journal.jsonl + ../contracts/v14/fixtures/demo.scenario.json + ../contracts/v14/fixtures/demo.scenario.jsonl + ../contracts/v14/fixtures/fill-clipped.journal.jsonl + ../contracts/v14/fixtures/fill-clipped.scenario.json + ../contracts/v14/fixtures/quote-trade.journal.jsonl + ../contracts/v14/fixtures/quote-trade.scenario.json + ../contracts/v14/fixtures/quote-trade.scenario.jsonl + ../contracts/v14/journal.schema.json + ../contracts/v14/scenario-stream.schema.json + ../contracts/v14/scenario.schema.json ../contracts/v6/fixtures/demo.scenario.json ../contracts/v6/fixtures/demo.scenario.jsonl ../contracts/v5/fixtures/demo.scenario.json @@ -94,6 +105,7 @@ ../contracts/strategy/v9/fixtures/external.strategy.jsonl ../contracts/strategy/v10/fixtures/external.strategy.jsonl ../contracts/strategy/v11/fixtures/external.strategy.jsonl + ../contracts/strategy/v12/fixtures/external.strategy.jsonl ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries @@ -112,6 +124,69 @@ (modules fuzz_protocol) (libraries trading_engine yojson unix)) +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v14/fixtures/demo.journal.jsonl + ../contracts/v14/fixtures/demo.scenario.json + ../contracts/v14/fixtures/demo.scenario.jsonl + ../contracts/v14/journal.schema.json + ../contracts/v14/scenario-stream.schema.json + ../contracts/v14/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v14/scenario.schema.json} + %{dep:../contracts/v14/scenario-stream.schema.json} + %{dep:../contracts/v14/journal.schema.json} + %{dep:../contracts/v14/fixtures/demo.scenario.json} + %{dep:../contracts/v14/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v14/fixtures/demo.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v14/fixtures/quote-trade.journal.jsonl + ../contracts/v14/fixtures/quote-trade.scenario.json + ../contracts/v14/fixtures/quote-trade.scenario.jsonl + ../contracts/v14/journal.schema.json + ../contracts/v14/scenario-stream.schema.json + ../contracts/v14/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v14/scenario.schema.json} + %{dep:../contracts/v14/scenario-stream.schema.json} + %{dep:../contracts/v14/journal.schema.json} + %{dep:../contracts/v14/fixtures/quote-trade.scenario.json} + %{dep:../contracts/v14/fixtures/quote-trade.scenario.jsonl} + %{dep:../contracts/v14/fixtures/quote-trade.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_strategy_schema.py + ../contracts/v14/scenario.schema.json + ../contracts/v14/journal.schema.json + ../contracts/diagnostic/v1/diagnostic.schema.json + ../contracts/strategy/v12/message.schema.json + ../contracts/strategy/v12/transcript.schema.json + ../contracts/strategy/v12/fixtures/external.strategy.jsonl) + (action + (run + python3 + %{dep:validate_strategy_schema.py} + %{dep:../contracts/v14/scenario.schema.json} + %{dep:../contracts/v14/journal.schema.json} + %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} + %{dep:../contracts/strategy/v12/message.schema.json} + %{dep:../contracts/strategy/v12/transcript.schema.json} + %{dep:../contracts/strategy/v12/fixtures/external.strategy.jsonl}))) + (rule (alias runtest) (deps diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index fcac251..6c487f9 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -112,6 +112,7 @@ let capabilities_describe_execution_contracts () = "completed_bar_v1"; "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1"; + "quote_trade_v1"; ] names; let model = List.hd models in @@ -135,7 +136,7 @@ let capabilities_describe_execution_contracts () = (strings "configuration_versions"); Alcotest.(check (list string)) "scenario contracts" - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] (strings "scenario_contract_versions"); Alcotest.(check (list string)) "required fields" diff --git a/test/test_domain.ml b/test/test_domain.ml index 83a09f0..e3a50fd 100644 --- a/test/test_domain.ml +++ b/test/test_domain.ml @@ -100,6 +100,104 @@ let market_slice_validation () = Alcotest.(check bool) "premature availability rejected" true (Result.is_error result) +let market_event_validation () = + let instrument_id = instrument_id "event-validation" in + let event_at = timestamp "2026-01-03T14:30:00Z" in + let available_at = timestamp "2026-01-03T14:30:01Z" in + let received_at = timestamp "2026-01-03T14:30:02Z" in + let quote ?(ingest_sequence = 1L) ?(bid_price = "99") + ?(bid_quantity = quantity "1") ?(ask_price = "101") () = + T.Market_event.quote ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~bid_price:(price bid_price) ~bid_quantity + ~ask_price:(price ask_price) ~ask_quantity:(quantity "1") + in + Alcotest.(check bool) + "nonpositive ingest rejected" true + (Result.is_error (quote ~ingest_sequence:0L ())); + Alcotest.(check bool) + "crossed quote rejected" true + (Result.is_error (quote ~bid_price:"101" ~ask_price:"100" ())); + Alcotest.(check bool) + "zero displayed quantity rejected" true + (Result.is_error (quote ~bid_quantity:T.Scalar.Quantity.zero ())); + Alcotest.(check bool) + "zero ask quantity rejected" true + (Result.is_error + (T.Market_event.quote ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence:1L ~bid_price:(price "99") + ~bid_quantity:(quantity "1") ~ask_price:(price "101") + ~ask_quantity:T.Scalar.Quantity.zero)); + Alcotest.(check bool) + "receipt before availability rejected" true + (Result.is_error + (T.Market_event.trade ~instrument_id ~event_at ~available_at + ~received_at:event_at ~ingest_sequence:1L ~price:(price "100") + ~quantity:(quantity "1") ~aggressor_side:T.Market_event.Sell)); + Alcotest.(check bool) + "zero trade quantity rejected" true + (Result.is_error + (T.Market_event.trade ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence:1L ~price:(price "100") + ~quantity:T.Scalar.Quantity.zero + ~aggressor_side:T.Market_event.Unknown)); + Alcotest.(check bool) + "availability before event rejected" true + (Result.is_error + (T.Market_event.trade ~instrument_id ~event_at + ~available_at:(timestamp "2026-01-03T14:29:59Z") + ~received_at ~ingest_sequence:1L ~price:(price "100") + ~quantity:(quantity "1") ~aggressor_side:T.Market_event.Buy)); + let first = quote ~ingest_sequence:2L () |> ok in + let second = quote ~ingest_sequence:1L () |> ok in + let later_receipt = + T.Market_event.quote ~instrument_id ~event_at ~available_at + ~received_at:(timestamp "2026-01-03T14:30:03Z") + ~ingest_sequence:3L ~bid_price:(price "99") ~bid_quantity:(quantity "1") + ~ask_price:(price "101") ~ask_quantity:(quantity "1") + |> ok + in + Alcotest.(check bool) + "receipt breaks replay-order tie" true + (T.Market_event.compare_replay_order first later_receipt < 0); + List.iter + (fun (wire, side) -> + Alcotest.(check string) + (wire ^ " side round trip") + wire + (T.Market_event.aggressor_side_of_string wire + |> ok |> T.Market_event.aggressor_side_to_string); + Alcotest.(check string) + (wire ^ " constructor rendering") + wire + (T.Market_event.aggressor_side_to_string side)) + [ + ("buy", T.Market_event.Buy); + ("sell", T.Market_event.Sell); + ("unknown", T.Market_event.Unknown); + ]; + Alcotest.(check bool) + "unknown aggressor spelling rejected" true + (Result.is_error (T.Market_event.aggressor_side_of_string "ambiguous")); + let base = market_slice 2L in + Alcotest.(check string) + "slice rendering includes market-event count" + "slice[2] bars=1 events=0 fx=1 actions=0 lifecycle=0 borrow=0 cash_rates=0 \ + failures=0" + (Format.asprintf "%a" T.Market_slice.pp base); + Alcotest.(check bool) + "nonmonotonic ingest rejected" true + (Result.is_error + (T.Market_slice.create_v14 ~slice_sequence:base.slice_sequence + ~start_at:base.start_at ~end_at:base.end_at + ~available_at:base.available_at ~received_at:base.received_at + ~bars:base.bars ~fx_rates:base.fx_rates + ~corporate_actions:base.corporate_actions + ~borrow_observations:base.borrow_observations + ~cash_rate_observations:base.cash_rate_observations + ~settlement_failures:base.settlement_failures + ~lifecycle_events:base.lifecycle_events + ~market_events:[ first; second ])) + let bar_validation_boundaries () = let instrument_id = instrument_id "bar-validation" in let create ?(open_price = "100") ?(high_price = "110") ?(low_price = "90") @@ -298,6 +396,7 @@ let tests = Alcotest.test_case "portfolio weight rounds toward zero" `Quick portfolio_weight_rounds_toward_zero; Alcotest.test_case "market slice validation" `Quick market_slice_validation; + Alcotest.test_case "market event validation" `Quick market_event_validation; Alcotest.test_case "bar validation boundaries" `Quick bar_validation_boundaries; Alcotest.test_case "corporate action validation boundaries" `Quick diff --git a/test/test_execution.ml b/test/test_execution.ml index bcae7e6..377b8ea 100644 --- a/test/test_execution.ml +++ b/test/test_execution.ml @@ -37,6 +37,207 @@ let conservative_step start ?(kind = T.Order.Market) ?(side = T.Order.Buy) let cursor = start engine ~instruments:[ instrument () ] ~oms slice |> ok in T.Execution.next cursor ~oms |> ok +let market_event_time second = + timestamp (Printf.sprintf "2026-01-03T14:30:%02dZ" second) + +let quote_event ?(sequence = 1L) ?(second = 1) ?(bid = "99") + ?(bid_quantity = "5") ?(ask = "101") ?(ask_quantity = "5") () = + let event_at = market_event_time second in + T.Market_event.quote + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~bid_price:(price bid) + ~bid_quantity:(quantity bid_quantity) ~ask_price:(price ask) + ~ask_quantity:(quantity ask_quantity) + |> ok + +let trade_event ?(sequence = 2L) ?(second = 2) ?(price_value = "100") + ?(quantity_value = "5") ?(aggressor_side = T.Market_event.Unknown) () = + let event_at = market_event_time second in + T.Market_event.trade + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~price:(price price_value) + ~quantity:(quantity quantity_value) ~aggressor_side + |> ok + +let quote_trade_slice events = + let base = market_slice 2L in + T.Market_slice.create_v14 ~slice_sequence:base.slice_sequence + ~start_at:base.start_at ~end_at:base.end_at ~available_at:base.available_at + ~received_at:base.received_at ~bars:base.bars ~fx_rates:base.fx_rates + ~corporate_actions:base.corporate_actions + ~borrow_observations:base.borrow_observations + ~cash_rate_observations:base.cash_rate_observations + ~settlement_failures:base.settlement_failures + ~lifecycle_events:base.lifecycle_events ~market_events:events + |> ok + +let quote_trade_execution ?(participation_bps = 10_000) () = + let fees = conservative_execution () |> T.Execution.fee_schedules in + T.Execution.create_v2 ~participation_bps ~fee_schedules:fees |> ok + +let liquidity_name = function + | T.Fee_schedule.Maker -> "maker" + | Taker -> "taker" + +let quote_trade_step ?(kind = T.Order.Market) ?(side = T.Order.Buy) events = + let oms, _ = oms_with_order (request ~kind ~side ()) in + let cursor = + T.Execution.start_slice_quote_trade (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms (quote_trade_slice events) + |> ok + in + T.Execution.next cursor ~oms |> ok + +let quote_trade_consumes_displayed_liquidity () = + match quote_trade_step [ quote_event ~ask_quantity:"3" () ] with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check price_testable "buy executes at displayed ask" + (price "101") proposal.price; + Alcotest.check quantity_testable "displayed size caps fill" (quantity "3") + proposal.quantity; + Alcotest.(check string) + "quote fill is taker" "taker" + (liquidity_name proposal.liquidity); + Alcotest.(check string) + "economic event time" "2026-01-03T14:30:01.000000Z" + (T.Codec.ptime_to_string proposal.executed_at) + | _ -> Alcotest.fail "marketable quote did not produce a fill" + +let quote_trade_passive_fills_require_aggressor_evidence () = + let limit = T.Order.Limit (price "100") in + let events = + [ + quote_event (); + trade_event ~price_value:"99" (); + trade_event ~sequence:3L ~second:3 ~price_value:"99" + ~aggressor_side:T.Market_event.Sell (); + ] + in + match quote_trade_step ~kind:limit events with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check price_testable "passive fill uses observed trade" + (price "99") proposal.price; + Alcotest.(check string) + "trade fill is maker" "maker" + (liquidity_name proposal.liquidity); + Alcotest.(check string) + "unknown aggressor was skipped" "2026-01-03T14:30:03.000000Z" + (T.Codec.ptime_to_string proposal.executed_at) + | _ -> Alcotest.fail "qualified passive trade did not produce a fill" + +let quote_trade_sell_paths_use_bid_and_buy_aggressors () = + (match quote_trade_step ~side:T.Order.Sell [ quote_event ~bid:"99" () ] with + | T.Execution.Proposed (proposal, continue) -> ( + Alcotest.check price_testable "sell executes at displayed bid" + (price "99") proposal.price; + let cursor = continue proposal.quantity |> ok in + match T.Execution.next cursor ~oms:T.Oms.empty |> ok with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "consumed quote should finish") + | _ -> Alcotest.fail "sell quote did not produce a fill"); + let passive = T.Order.Limit (price "100") in + match + quote_trade_step ~kind:passive ~side:T.Order.Sell + [ trade_event ~price_value:"101" ~aggressor_side:T.Market_event.Buy () ] + with + | T.Execution.Proposed (proposal, continue) -> + Alcotest.check price_testable "passive sell uses trade price" + (price "101") proposal.price; + Alcotest.(check string) + "passive sell is maker" "maker" + (liquidity_name proposal.liquidity); + ignore (continue proposal.quantity |> ok) + | _ -> Alcotest.fail "buy-aggressor trade did not fill passive sell" + +let quote_trade_limits_fok_and_continuations () = + let marketable = T.Order.Limit (price "102") in + (match + quote_trade_step ~kind:marketable [ quote_event ~ask_quantity:"3" () ] + with + | T.Execution.Proposed (proposal, continue) -> + Alcotest.check quantity_testable "marketable limit uses displayed size" + (quantity "3") proposal.quantity; + Alcotest.(check bool) + "over-consumption rejected" true + (Result.is_error (continue (quantity "4"))); + Alcotest.(check bool) + "negative application rejected" true + (Result.is_error (continue (quantity "-1"))) + | _ -> Alcotest.fail "marketable limit did not execute"); + let oms, _ = + oms_with_order + (request_v8 ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) + in + let cursor = + T.Execution.start_slice_quote_trade (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms + (quote_trade_slice [ quote_event ~ask_quantity:"3" () ]) + |> ok + in + match T.Execution.next cursor ~oms |> ok with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "FOK order filled partial displayed liquidity" + +let quote_trade_stop_and_event_boundaries () = + let oms, order = + oms_with_order + (request_v8 + ~kind:(T.Order.Stop (price "100")) + ~time_in_force:T.Order.Gtc ()) + in + let cursor = + T.Execution.start_slice_quote_trade (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms + (quote_trade_slice [ quote_event ~ask:"101" () ]) + |> ok + in + (match T.Execution.next cursor ~oms |> ok with + | T.Execution.Triggered (order_id, triggered_at, 2L, _) -> + Alcotest.(check string) + "triggered order" + (T.Id.Order.to_string order.id) + (T.Id.Order.to_string order_id); + Alcotest.(check string) + "quote trigger uses event time" "2026-01-03T14:30:01.000000Z" + (T.Codec.ptime_to_string triggered_at) + | _ -> Alcotest.fail "stop was not triggered by observable quote"); + let other_event = + let event_at = market_event_time 1 in + T.Market_event.quote ~instrument_id:(instrument_id "other") ~event_at + ~available_at:event_at ~received_at:event_at ~ingest_sequence:1L + ~bid_price:(price "99") ~bid_quantity:(quantity "1") + ~ask_price:(price "101") ~ask_quantity:(quantity "1") + |> ok + in + Alcotest.(check bool) + "unknown event instrument rejected" true + (Result.is_error + (T.Execution.start_slice_quote_trade (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms + (quote_trade_slice [ other_event ]))); + let old_at = timestamp "2026-01-02T14:30:00Z" in + let old_event = + T.Market_event.trade + ~instrument_id:(instrument_id "test-equity") + ~event_at:old_at ~available_at:old_at ~received_at:old_at + ~ingest_sequence:1L ~price:(price "100") ~quantity:(quantity "1") + ~aggressor_side:T.Market_event.Unknown + |> ok + in + Alcotest.(check bool) + "event outside slice rejected" true + (Result.is_error + (T.Execution.start_slice_quote_trade (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms + (quote_trade_slice [ old_event ]))) + let conservative_limit_models_diverge () = let engine = conservative_execution () in let limit = T.Order.Limit (price "100") in @@ -658,6 +859,16 @@ let incomplete_market_slice_returns_error () = let tests = [ + Alcotest.test_case "quote replay consumes displayed liquidity" `Quick + quote_trade_consumes_displayed_liquidity; + Alcotest.test_case "passive trade requires aggressor evidence" `Quick + quote_trade_passive_fills_require_aggressor_evidence; + Alcotest.test_case "quote replay sell paths" `Quick + quote_trade_sell_paths_use_bid_and_buy_aggressors; + Alcotest.test_case "quote replay limits, FOK, and continuations" `Quick + quote_trade_limits_fok_and_continuations; + Alcotest.test_case "quote replay stops and boundaries" `Quick + quote_trade_stop_and_event_boundaries; Alcotest.test_case "conservative limit models diverge" `Quick conservative_limit_models_diverge; Alcotest.test_case "conservative costs are attributed" `Quick diff --git a/test/test_scenario.ml b/test/test_scenario.ml index 76306b3..3c57b8f 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -2,12 +2,16 @@ open Test_support module T = Trading_engine let demo_document () = - In_channel.with_open_bin "../contracts/v13/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v14/fixtures/demo.scenario.json" In_channel.input_all let demo () = T.Scenario.of_string (demo_document ()) |> ok let demo_hash () = T.Sha256.digest_string (demo_document ()) -let stream_path = "../contracts/v13/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v14/fixtures/demo.scenario.jsonl" +let quote_trade_path = "../contracts/v14/fixtures/quote-trade.scenario.json" + +let quote_trade_stream_path = + "../contracts/v14/fixtures/quote-trade.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -75,7 +79,7 @@ let write_large_stream path slice_count = ~effective_at:start_at ~credit_rate_bps:0 ~debit_rate_bps:0 |> ok in - T.Market_slice.create_v13 ~slice_sequence:(Int64.of_int index) + T.Market_slice.create_v14 ~slice_sequence:(Int64.of_int index) ~start_at ~end_at:(add_seconds base (offset + 1)) ~available_at:(add_seconds base (offset + 2)) @@ -89,13 +93,13 @@ let write_large_stream path slice_count = ~fx_rates:[ fx_mark () ] ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] ~cash_rate_observations:[ cash_rate ] ~settlement_failures:[] - ~lifecycle_events:[] + ~lifecycle_events:[] ~market_events:[] |> ok in let payload = `Assoc [ - ("market_slice", T.Codec.market_slice_to_yojson_v13 market_slice); + ("market_slice", T.Codec.market_slice_to_yojson_v14 market_slice); ("intents", `List []); ] in @@ -140,9 +144,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v13/scenario.schema.json"; - check_schema "../contracts/v13/scenario-stream.schema.json"; - check_schema "../contracts/v13/journal.schema.json" + check_schema "../contracts/v14/scenario.schema.json"; + check_schema "../contracts/v14/scenario-stream.schema.json"; + check_schema "../contracts/v14/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -203,7 +207,7 @@ let v12_distributions_and_lifecycle_parse () = |> ok in let market_slice = - T.Market_slice.create_v13 ~slice_sequence:1L + T.Market_slice.create_v14 ~slice_sequence:1L ~start_at:(timestamp "2026-01-02T14:30:00Z") ~end_at:(timestamp "2026-01-02T20:55:00Z") ~available_at:(timestamp "2026-01-02T21:00:00Z") @@ -245,6 +249,7 @@ let v12_distributions_and_lifecycle_parse () = reason = "acquisition"; }); ] + ~market_events:[] |> ok in let document = @@ -341,7 +346,7 @@ let v12_distributions_and_lifecycle_parse () = | _ -> Alcotest.fail "demo slice must be an object" in `List - (T.Codec.market_slice_to_yojson_v13 market_slice + (T.Codec.market_slice_to_yojson_v14 market_slice :: List.map add_child_bar rest) | _ -> Alcotest.fail "demo slices must be nonempty" in @@ -431,8 +436,8 @@ let contract_version_is_required_and_supported () = let unsupported_diagnostic = T.Scenario.of_yojson unsupported |> error in Alcotest.(check string) "unsupported version diagnosed" - "unsupported scenario contract_version \"2\" (expected one of 13, 12, 11, \ - 10, 9, 8, 7, 6, 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 14, 13, 12, \ + 11, 10, 9, 8, 7, 6, 5, 4, 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -582,7 +587,7 @@ let dense_schedule_document slice_count = ~effective_at:start_at ~credit_rate_bps:100 ~debit_rate_bps:200 |> ok in - T.Market_slice.create_v13 ~slice_sequence:(Int64.of_int index) ~start_at + T.Market_slice.create_v14 ~slice_sequence:(Int64.of_int index) ~start_at ~end_at:(add_seconds base (time_offset + 1)) ~available_at:(add_seconds base (time_offset + 2)) ~received_at:(add_seconds base (time_offset + 3)) @@ -595,8 +600,8 @@ let dense_schedule_document slice_count = ~fx_rates:[ fx_mark () ] ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] ~cash_rate_observations:[ cash_rate_observation ] - ~settlement_failures:[] ~lifecycle_events:[] - |> ok |> T.Codec.market_slice_to_yojson_v13) + ~settlement_failures:[] ~lifecycle_events:[] ~market_events:[] + |> ok |> T.Codec.market_slice_to_yojson_v14) in let schedule = List.init slice_count (fun offset -> @@ -1180,7 +1185,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v13/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v14/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -1208,7 +1213,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v13/fixtures/fill-clipped.scenario.json" + "../contracts/v14/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in @@ -1222,11 +1227,69 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v13/fixtures/fill-clipped.journal.jsonl" + "../contracts/v14/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual +let quote_trade_replay_is_causal_and_stream_equivalent () = + let document = + In_channel.with_open_bin quote_trade_path In_channel.input_all + in + let scenario = T.Scenario.of_string document |> ok in + let batch = + T.Replay.run ~scenario_sha256:(T.Sha256.digest_string document) scenario + |> ok + in + let batch_journal = + batch.audits |> List.map T.Codec.audit_to_string |> String.concat "\n" + |> fun value -> value ^ "\n" + in + let golden = + In_channel.with_open_bin + "../contracts/v14/fixtures/quote-trade.journal.jsonl" In_channel.input_all + in + Alcotest.(check string) "quote/trade golden journal" golden batch_journal; + let fills = + List.filter_map + (fun (audit : T.Audit.t) -> + match audit.event with + | T.Audit.Fill_applied fill -> Some fill + | _ -> None) + batch.audits + in + Alcotest.(check (list string)) + "only aggressor-qualified trade liquidity fills" + [ "4@99@2026-02-03T14:33:00.000000Z"; "6@100@2026-02-03T14:34:00.000000Z" ] + (List.map + (fun (fill : T.Fill.t) -> + Printf.sprintf "%s@%s@%s" + (T.Scalar.Quantity.to_decimal_string fill.quantity) + (T.Scalar.Price.to_decimal_string fill.price) + (T.Codec.ptime_to_string fill.executed_at)) + fills); + let stream_hash = T.Sha256.digest_file quote_trade_stream_path |> ok in + let expected = + T.Replay.run ~scenario_sha256:stream_hash scenario |> ok |> fun result -> + result.audits |> List.map T.Codec.audit_to_string |> String.concat "\n" + |> fun value -> value ^ "\n" + in + let journal = Filename.temp_file "trading-engine-quote-trade" ".jsonl" in + Sys.remove journal; + Fun.protect + ~finally:(fun () -> + if Sys.file_exists journal then Sys.remove journal; + if Sys.file_exists (journal ^ ".partial") then + Sys.remove (journal ^ ".partial")) + (fun () -> + let streamed = + T.Replay.run_stream ~journal_path:journal quote_trade_stream_path |> ok + in + Alcotest.(check int64) "two streamed slices" 2L streamed.slice_count; + Alcotest.(check string) + "quote/trade stream and batch journals agree" expected + (In_channel.with_open_bin journal In_channel.input_all)) + let journal_is_created_exclusively () = let scenario = demo () in let existing = Filename.temp_file "trading-engine" ".jsonl" in @@ -1569,6 +1632,8 @@ let tests = v3_replay_matches_frozen_golden_file; Alcotest.test_case "fill clipping fixture reconciles" `Quick fill_clipping_fixture_reconciles; + Alcotest.test_case "quote/trade replay is causal and stream equivalent" + `Quick quote_trade_replay_is_causal_and_stream_equivalent; Alcotest.test_case "exclusive journal creation" `Quick journal_is_created_exclusively; Alcotest.test_case "exclusive journal finalization" `Quick diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index a0fe99c..9ad4f27 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -45,7 +45,7 @@ let initialize_message_is_complete () = T.Strategy_protocol.initialize_message ~sequence:1L (initialization ()) in Alcotest.(check string) - "protocol version" "11" + "protocol version" "12" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -265,7 +265,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "11"); + ("strategy_protocol_version", `String "12"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); From af4882d2e2b5d1385c5eb2d337b1d2d3ce750fcc Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 17:28:26 -0400 Subject: [PATCH 49/57] feat: add bounded order book replay --- CHANGELOG.md | 4 + README.md | 28 +- contracts/conformance/cases.json | 119 + contracts/conformance/manifest.json | 103 + contracts/strategy/v13/README.md | 61 + contracts/strategy/v13/dune | 15 + .../v13/fixtures/external.scenario.json | 308 +++ .../v13/fixtures/external.scenario.jsonl | 4 + .../v13/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v13/message.schema.json | 302 ++ contracts/strategy/v13/transcript.schema.json | 82 + contracts/v15/README.md | 117 + contracts/v15/dune | 36 + contracts/v15/fixtures/demo.journal.jsonl | 29 + contracts/v15/fixtures/demo.scenario.json | 465 ++++ contracts/v15/fixtures/demo.scenario.jsonl | 6 + .../v15/fixtures/fill-clipped.journal.jsonl | 13 + .../v15/fixtures/fill-clipped.scenario.json | 273 ++ .../v15/fixtures/order-book.journal.jsonl | 13 + .../v15/fixtures/order-book.scenario.json | 378 +++ .../v15/fixtures/order-book.scenario.jsonl | 4 + .../v15/fixtures/quote-trade.journal.jsonl | 13 + .../v15/fixtures/quote-trade.scenario.json | 319 +++ .../v15/fixtures/quote-trade.scenario.jsonl | 4 + contracts/v15/journal.schema.json | 2427 +++++++++++++++++ contracts/v15/scenario-stream.schema.json | 78 + contracts/v15/scenario.schema.json | 870 ++++++ docs/api-reference.md | 2 +- docs/continuous-integration.md | 2 +- docs/execution-model.md | 18 +- docs/persistra.md | 8 +- docs/scenario.md | 24 +- lib/codec.ml | 97 +- lib/codec.mli | 1 + lib/contract.ml | 10 +- lib/engine.ml | 5 + lib/engine.mli | 11 + lib/execution.ml | 674 +++++ lib/execution.mli | 14 + lib/execution_model.ml | 39 +- lib/external_replay.ml | 6 +- lib/market_slice.ml | 35 +- lib/market_slice.mli | 18 + lib/order_book_event.ml | 132 + lib/order_book_event.mli | 77 + lib/replay.ml | 6 +- lib/scenario.ml | 203 +- lib/scenario_shape.ml | 28 +- lib/scenario_validation.ml | 6 +- lib/strategy_protocol.ml | 60 +- mkdocs.yml | 4 +- scripts/check-deterministic-journals | 16 + scripts/check-documentation.py | 4 +- scripts/release_artifacts.py | 12 +- test/cli.t | 2 +- test/dune | 116 +- test/test_diagnostic.ml | 3 +- test/test_domain.ml | 115 +- test/test_execution.ml | 396 +++ test/test_scenario.ml | 108 +- test/test_strategy_protocol.ml | 4 +- 61 files changed, 8150 insertions(+), 191 deletions(-) create mode 100644 contracts/strategy/v13/README.md create mode 100644 contracts/strategy/v13/dune create mode 100644 contracts/strategy/v13/fixtures/external.scenario.json create mode 100644 contracts/strategy/v13/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v13/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v13/message.schema.json create mode 100644 contracts/strategy/v13/transcript.schema.json create mode 100644 contracts/v15/README.md create mode 100644 contracts/v15/dune create mode 100644 contracts/v15/fixtures/demo.journal.jsonl create mode 100644 contracts/v15/fixtures/demo.scenario.json create mode 100644 contracts/v15/fixtures/demo.scenario.jsonl create mode 100644 contracts/v15/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v15/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v15/fixtures/order-book.journal.jsonl create mode 100644 contracts/v15/fixtures/order-book.scenario.json create mode 100644 contracts/v15/fixtures/order-book.scenario.jsonl create mode 100644 contracts/v15/fixtures/quote-trade.journal.jsonl create mode 100644 contracts/v15/fixtures/quote-trade.scenario.json create mode 100644 contracts/v15/fixtures/quote-trade.scenario.jsonl create mode 100644 contracts/v15/journal.schema.json create mode 100644 contracts/v15/scenario-stream.schema.json create mode 100644 contracts/v15/scenario.schema.json create mode 100644 lib/order_book_event.ml create mode 100644 lib/order_book_event.mli diff --git a/CHANGELOG.md b/CHANGELOG.md index b7aa900..d9352a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Add bounded level-two order-book replay with fresh snapshots, contiguous absolute updates, + multi-level marketable depth, deterministic passive queue position, and locked-book support. +- Publish scenario/journal contract v15 and external strategy protocol v13 while preserving v14 + and protocol v12 as frozen compatibility contracts. - Add causal quote/trade replay with displayed-liquidity capacity, aggressor-qualified passive fills, maker/taker fee attribution, and economic event timestamps. - Publish scenario/journal contract v14 and external strategy protocol v12 while preserving v13 diff --git a/README.md b/README.md index f307b7d..00905de 100644 --- a/README.md +++ b/README.md @@ -61,12 +61,14 @@ scenario slices and scheduled or external intents fee-component attribution - Deterministic event IDs, ordered causal references, and order-creation attribution - Contract-selected compiled execution modules with versioned model-owned configuration and - capability descriptors; v13 adds conservative bar models and v14 adds causal quote/trade replay - while freezing `completed_bar_v1` + capability descriptors; v13 adds conservative bar models, v14 adds causal quote/trade replay, + and v15 adds bounded level-two order-book replay while freezing `completed_bar_v1` - Tick-aligned fixed-spread and participation-impact execution costs with separate reference, spread, impact, and final-price audit attribution - Causally ordered quotes and aggressor-classified trades with displayed-liquidity limits, maker/taker attribution, and event-time fills +- Bounded order-book snapshots and contiguous updates with price-time queue simulation, + multi-level depth consumption, partial fills, and locked-book support - Strict batch JSON and bounded-memory JSON Lines scenario parsing with JSON Schemas - Versioned synchronous JSON Lines strategy processes with per-request timeouts and strict lifecycle supervision @@ -96,7 +98,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v14/fixtures/demo.scenario.json \ + --input contracts/v15/fixtures/demo.scenario.json \ --validate-only ``` @@ -104,7 +106,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v14/fixtures/demo.scenario.json \ + --input contracts/v15/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -112,7 +114,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v14/fixtures/demo.scenario.jsonl \ + --input contracts/v15/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -121,7 +123,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v12/fixtures/external.scenario.json \ + --input contracts/strategy/v13/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -236,19 +238,19 @@ do not provide reducer snapshots or restart recovery. - [Diagnostic contract](docs/diagnostics.md) - [Scenario contract](docs/scenario.md) - [Contract conformance corpus](contracts/conformance/README.md) -- [Current contract v14 and conformance fixtures](contracts/v14/README.md) +- [Current contract v15 and conformance fixtures](contracts/v15/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v14/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v14/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v14/journal.schema.json) -- [External strategy protocol v12](contracts/strategy/v12/README.md) +- [Scenario JSON Schema](contracts/v15/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v15/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v15/journal.schema.json) +- [External strategy protocol v13](contracts/strategy/v13/README.md) - [Historical strategy protocol v3](contracts/strategy/v3/README.md) - [Historical strategy protocol v2](contracts/strategy/v2/README.md) - [Historical strategy protocol v1](contracts/strategy/v1/README.md) - [Persistra compatibility](docs/persistra.md) -- [Strategy message JSON Schema](contracts/strategy/v12/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v12/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v13/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v13/transcript.schema.json) - [Execution model](docs/execution-model.md) - [OCaml coverage](docs/coverage.md) - [Continuous integration and portability matrix](docs/continuous-integration.md) diff --git a/contracts/conformance/cases.json b/contracts/conformance/cases.json index 3b62267..0430ba0 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -921,6 +921,68 @@ "schema_expectation": "accept", "runtime_expectation": "accept", "rule": "structural" + }, + { + "name": "scenario-v15-valid", + "artifact": "scenario-v15", + "kind": "scenario", + "source": "v15/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-v15-order-book-valid", + "artifact": "scenario-v15", + "kind": "scenario", + "source": "v15/fixtures/order-book.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-stream-v15-valid", + "artifact": "scenario-stream-v15", + "kind": "scenario_stream", + "source": "v15/fixtures/order-book.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-ready-valid-v13", + "artifact": "strategy-message-v13", + "kind": "strategy_response", + "source": "strategy/v13/fixtures/external.strategy.jsonl", + "record": 2, + "extract": [ + "message" + ], + "expected_sequence": "1", + "protocol_version": "13", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-intents-valid-v13", + "artifact": "strategy-message-v13", + "kind": "strategy_response", + "source": "strategy/v13/fixtures/external.strategy.jsonl", + "record": 4, + "extract": [ + "message" + ], + "expected_sequence": "2", + "protocol_version": "13", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" } ], "schema_only_cases": [ @@ -1506,6 +1568,63 @@ }, "mutations": [], "schema_expectation": "accept" + }, + { + "name": "strategy-stopped-valid-v13", + "artifact": "strategy-message-v13", + "instance": { + "strategy_protocol_version": "13", + "strategy_sequence": "7", + "message_type": "stopped", + "payload": {} + }, + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept", + "parser_expected": "stopped" + }, + { + "name": "strategy-error-valid-v13", + "artifact": "strategy-message-v13", + "instance": { + "strategy_protocol_version": "13", + "strategy_sequence": "7", + "message_type": "error", + "payload": { + "message": "fixture failure" + } + }, + "mutations": [], + "schema_expectation": "accept" + }, + { + "name": "strategy-v13-rejected-response-branch", + "artifact": "strategy-transcript-v13", + "instance": { + "strategy_diagnostic_version": "1", + "transcript_sequence": "2", + "record_type": "rejected_strategy_response", + "expected_strategy_sequence": "1", + "diagnostic": { + "diagnostic_version": "1", + "code": "strategy.protocol", + "phase": "strategy", + "message": "strategy initialization: invalid strategy response JSON", + "context": { + "json_path": "$", + "sequence": "1" + }, + "cause": null + }, + "evidence": { + "encoding": "hex", + "prefix": "7b", + "observed_bytes": 1, + "truncated": false + } + }, + "mutations": [], + "schema_expectation": "accept" } ] } diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index 6d5e637..c68ce46 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -1036,6 +1036,109 @@ "format": "jsonl" } ] + }, + { + "name": "scenario-v15", + "schema": "v15/scenario.schema.json", + "version_field": "contract_version", + "version": "15", + "sources": [ + { + "path": "v15/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v15/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "v15/fixtures/quote-trade.scenario.json", + "format": "json" + }, + { + "path": "v15/fixtures/order-book.scenario.json", + "format": "json" + }, + { + "path": "strategy/v13/fixtures/external.scenario.json", + "format": "json" + } + ] + }, + { + "name": "scenario-stream-v15", + "schema": "v15/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "15", + "sources": [ + { + "path": "v15/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "v15/fixtures/quote-trade.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "v15/fixtures/order-book.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v13/fixtures/external.scenario.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "journal-v15", + "schema": "v15/journal.schema.json", + "version_field": "contract_version", + "version": "15", + "sources": [ + { + "path": "v15/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v15/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v15/fixtures/quote-trade.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v15/fixtures/order-book.journal.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "strategy-message-v13", + "schema": "strategy/v13/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "13", + "sources": [ + { + "path": "strategy/v13/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } + ] + }, + { + "name": "strategy-transcript-v13", + "schema": "strategy/v13/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "13", + "sources": [ + { + "path": "strategy/v13/fixtures/external.strategy.jsonl", + "format": "jsonl" + } + ] } ] } diff --git a/contracts/strategy/v13/README.md b/contracts/strategy/v13/README.md new file mode 100644 index 0000000..bc7a8d5 --- /dev/null +++ b/contracts/strategy/v13/README.md @@ -0,0 +1,61 @@ +# External strategy protocol v13 + +Version 13 is a synchronous JSON Lines protocol over child-process standard input and output. +Trading Engine sends `initialize`, ordered `event` requests, and `shutdown`. The strategy answers +with `ready`, `intents`, and `stopped`. It may answer any request with `error`. +Protocol v11 remains available for scenario contract v13; earlier versions retain their frozen +shapes. + +Every message repeats `strategy_protocol_version: "13"` and a positive canonical +`strategy_sequence`. A response must repeat the sequence of its request. Only one request is +outstanding. Trading Engine rejects unknown or duplicate fields, invalid canonical values, +oversized lines, a wrong version or sequence, unexpected response types, EOF, timeout, and a +nonzero process exit. + +The event context contains the replay clock, a marked base-currency portfolio, deterministic group +exposure snapshots, all working orders, and the latest available bar for each instrument. Every +callback emitted for a market slice uses +that slice's `received_at` as `now` and uses its complete bars and FX vector. The portfolio reports +cash, equity, net, long, short, and gross market value plus every attributed cash ledger and +configured position. Position quantities and weights reflect applied fills. Weights are truncated +toward zero to six decimal places. `weights_available` is false and all weights are null when +equity is zero or negative. + +The `initialize` request identifies scenario contract v15 and includes the exact `initial_portfolio` +snapshot alongside the legacy cash projection. It also carries the complete versioned venue +calendars, nested execution configuration, financing policy, and settlement policy, so a strategy +can construct DAY orders and reject incompatible execution, financing, or settlement state before +replay. + +Matching pauses after each strategy callback. The engine applies the response against the exact +account and OMS state exposed by that callback before delivering another callback or considering +the next eligible order. Later same-slice contexts include the effects of earlier responses. The +eligible-order sequence is fixed at the start of matching, so newly submitted orders wait for a +later slice. Cancelling an order before its turn leaves its unused slice capacity available to the +next eligible order. + +Event payloads cover completed market slices with effective-time borrow and cash-rate observations +plus explicit settlement failures, fills, order updates, and rejected intents. Portfolio contexts +include cash-interest attribution and settled and unsettled cash and position quantities. Response +intents use the scenario v15 intent shapes. Market-slice events include lifecycle transitions and +the expanded corporate-action catalog, plus causally ordered quote/trade market events. +Protocol v13 also carries bounded order-book snapshots and incrementals and advertises the +`order_book_v1` configuration, including its maximum depth. + +External replay requires an empty batch schedule and empty streamed intent batches. The engine +records accepted messages in both directions in a deterministic transcript. A response rejected +for invalid JSON, fields, version, sequence, EOF, or size is never stored as an accepted exchange. +Instead, the partial transcript ends with a `rejected_strategy_response` diagnostic record. Version +1 rejection diagnostics use the shared +[`diagnostic/v1`](../../diagnostic/v1/README.md) contract. The transcript schema narrows that +contract to the `strategy.protocol` and `resource.limit` codes in the `strategy` phase. The record +includes the structured rejection diagnostic and at most the first 256 raw response bytes encoded +as lowercase hexadecimal. `observed_bytes` counts bytes available when the engine rejected the +response, and `truncated` reports whether the prefix omits observed bytes. The transcript and audit +journal retain partial files after failure and finalize only after their respective success checks. + +- `message.schema.json` validates individual requests and responses. +- `transcript.schema.json` validates accepted exchanges and rejected-response diagnostics. +- `fixtures/external.scenario.json` is the batch replay fixture. +- `fixtures/external.scenario.jsonl` is its bounded-memory stream form. +- `fixtures/external.strategy.jsonl` is the canonical protocol transcript. diff --git a/contracts/strategy/v13/dune b/contracts/strategy/v13/dune new file mode 100644 index 0000000..70ad1c7 --- /dev/null +++ b/contracts/strategy/v13/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v13/message.schema.json) + (transcript.schema.json as contracts/strategy/v13/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v13/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v13/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v13/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v13/fixtures/external.scenario.json b/contracts/strategy/v13/fixtures/external.scenario.json new file mode 100644 index 0000000..5d45ea2 --- /dev/null +++ b/contracts/strategy/v13/fixtures/external.scenario.json @@ -0,0 +1,308 @@ +{ + "contract_version": "15", + "metadata": { + "producer": "strategy-protocol-fixture" + }, + "run_id": "external-demo", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "10000" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "demo-equity-acme", + "symbol": "ACME", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "demo-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "demo-equity-acme" + ], + "sessions": [ + { + "session_date": "2026-01-01", + "policy": "holiday", + "phases": [] + }, + { + "session_date": "2026-01-02", + "policy": "regular", + "phases": [ + { + "phase": "premarket", + "opens_at": "2026-01-02T09:00:00Z", + "closes_at": "2026-01-02T14:25:00Z" + }, + { + "phase": "opening_auction", + "opens_at": "2026-01-02T14:25:00Z", + "closes_at": "2026-01-02T14:30:00Z" + }, + { + "phase": "regular", + "opens_at": "2026-01-02T14:30:00Z", + "closes_at": "2026-01-02T20:55:00Z" + }, + { + "phase": "closing_auction", + "opens_at": "2026-01-02T20:55:00Z", + "closes_at": "2026-01-02T21:00:00Z" + }, + { + "phase": "postmarket", + "opens_at": "2026-01-02T21:00:00Z", + "closes_at": "2026-01-03T01:00:00Z" + } + ] + }, + { + "session_date": "2026-01-05", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-05T14:30:00Z", + "closes_at": "2026-01-05T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-06", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-06T14:30:00Z", + "closes_at": "2026-01-06T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-07", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-07T14:30:00Z", + "closes_at": "2026-01-07T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-08", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-08T14:30:00Z", + "closes_at": "2026-01-08T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000", + "max_leverage": "2", + "short_borrow_bps": 0, + "instrument_policies": [ + { + "instrument_id": "demo-equity-acme", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "2", + "participation_bps": 5000, + "fee_schedules": [ + { + "schedule_id": "external-acme-fees-v1", + "instrument_id": "demo-equity-acme", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "0.25", + "rounding": "up", + "applies_to": "any" + }, + { + "name": "exchange", + "currency": "USD", + "kind": "notional_bps", + "value": 10, + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-01-02T14:30:00Z", + "end_at": "2026-01-02T21:00:00Z", + "available_at": "2026-01-02T21:00:01Z", + "received_at": "2026-01-02T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "100", + "high": "105", + "low": "99", + "close": "104", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-02T14:30:00Z", + "credit_rate_bps": 0, + "debit_rate_bps": 0 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-01-05T14:30:00Z", + "end_at": "2026-01-05T21:00:00Z", + "available_at": "2026-01-05T21:00:01Z", + "received_at": "2026-01-05T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "103", + "high": "108", + "low": "102", + "close": "107", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-05T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-05T14:30:00Z", + "credit_rate_bps": 0, + "debit_rate_bps": 0 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "demo-equity-acme", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/strategy/v13/fixtures/external.scenario.jsonl b/contracts/strategy/v13/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..15ab15e --- /dev/null +++ b/contracts/strategy/v13/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"strategy-protocol-fixture"},"run_id":"external-demo","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00Z","closes_at":"2026-01-02T14:25:00Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00Z","closes_at":"2026-01-02T14:30:00Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00Z","closes_at":"2026-01-02T20:55:00Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00Z","closes_at":"2026-01-02T21:00:00Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00Z","closes_at":"2026-01-03T01:00:00Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00Z","closes_at":"2026-01-05T21:00:00Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00Z","closes_at":"2026-01-06T21:00:00Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00Z","closes_at":"2026-01-07T21:00:00Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00Z","closes_at":"2026-01-08T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":0,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_v1","configuration":{"version":"2","participation_bps":5000,"fee_schedules":[{"schedule_id":"external-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"15","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v13/fixtures/external.strategy.jsonl b/contracts/strategy/v13/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..13c41e1 --- /dev/null +++ b/contracts/strategy/v13/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"13","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"15","scenario_sha256":"6809a3638fe668a2a11e56c42e9bac7e506c0064eb2cb0a3e71ba09d92839ee7","run_id":"external-demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00.000000Z","closes_at":"2026-01-02T14:25:00.000000Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00.000000Z","closes_at":"2026-01-02T14:30:00.000000Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00.000000Z","closes_at":"2026-01-02T20:55:00.000000Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00.000000Z","closes_at":"2026-01-02T21:00:00.000000Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00.000000Z","closes_at":"2026-01-03T01:00:00.000000Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00.000000Z","closes_at":"2026-01-05T21:00:00.000000Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00.000000Z","closes_at":"2026-01-06T21:00:00.000000Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00.000000Z","closes_at":"2026-01-07T21:00:00.000000Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00.000000Z","closes_at":"2026-01-08T18:00:00.000000Z"}]}]}],"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":0,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_v1","configuration":{"version":"2","participation_bps":5000,"fee_schedules":[{"schedule_id":"external-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"any"}]}]}},"metadata":{"producer":"strategy-protocol-fixture"}}}} +{"strategy_protocol_version":"13","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"13","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"2","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} +{"strategy_protocol_version":"13","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"2","message_type":"intents","payload":{"intents":[{"type":"target_quantities","targets":[{"instrument_id":"demo-equity-acme","quantity":"2"}]},{"type":"emit_metric","name":"fixture_signal","value":"2"}]}}} +{"strategy_protocol_version":"13","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"3","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"order_updated","order":{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} +{"strategy_protocol_version":"13","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"13","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"4","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"fill_received","fill":{"fill_id":"external-demo-fill-000000000001","order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"2","price":"103","notional":"206","fee":"0.456","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.206","quote_amount":"0.206"}]}}}}} +{"strategy_protocol_version":"13","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"13","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"5","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"order_updated","order":{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} +{"strategy_protocol_version":"13","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"13","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"6","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} +{"strategy_protocol_version":"13","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"13","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"13","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v13/message.schema.json b/contracts/strategy/v13/message.schema.json new file mode 100644 index 0000000..ac9b452 --- /dev/null +++ b/contracts/strategy/v13/message.schema.json @@ -0,0 +1,302 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v13/message.schema.json", + "title": "Trading Engine external strategy protocol v13 message", + "description": "One strict request or response in the synchronous JSON Lines strategy protocol. The engine additionally enforces direction, sequence pairing, canonical values, response limits, and lifecycle order.", + "oneOf": [ + { "$ref": "#/$defs/initialize" }, + { "$ref": "#/$defs/ready" }, + { "$ref": "#/$defs/event" }, + { "$ref": "#/$defs/intents" }, + { "$ref": "#/$defs/shutdown" }, + { "$ref": "#/$defs/stopped" }, + { "$ref": "#/$defs/error" } + ], + "$defs": { + "sequence": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "base": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_protocol_version", "strategy_sequence", "message_type", "payload"], + "properties": { + "strategy_protocol_version": { "const": "13" }, + "strategy_sequence": { "$ref": "#/$defs/sequence" }, + "message_type": { "type": "string" }, + "payload": { "type": "object" } + } + }, + "initialize": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "initialize" }, + "payload": { "$ref": "#/$defs/initializePayload" } + } + } + ] + }, + "initializePayload": { + "type": "object", + "additionalProperties": false, + "required": ["engine_version", "scenario_contract_version", "scenario_sha256", "run_id", "base_currency", "initial_cash", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "metadata"], + "properties": { + "engine_version": { "type": "string", "minLength": 1 }, + "scenario_contract_version": { "const": "15" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/venueCalendar" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/settlement" }, + "metadata": { "type": "object" } + } + }, + "ready": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "ready" }, + "payload": { "$ref": "#/$defs/readyPayload" } + } + } + ] + }, + "readyPayload": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_name", "strategy_version"], + "properties": { + "strategy_name": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "strategy_version": { + "oneOf": [ + { "type": "null" }, + { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } + ] + } + } + }, + "event": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "event" }, + "payload": { "$ref": "#/$defs/eventPayload" } + } + } + ] + }, + "eventPayload": { + "type": "object", + "additionalProperties": false, + "required": ["context", "event"], + "properties": { + "context": { "$ref": "#/$defs/context" }, + "event": { "$ref": "#/$defs/strategyEvent" } + } + }, + "context": { + "type": "object", + "additionalProperties": false, + "required": ["now", "portfolio", "working_orders", "latest_bars"], + "properties": { + "now": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/bar" } + } + } + }, + "portfolio": { + "type": "object", + "additionalProperties": false, + "required": ["base_currency", "cash", "net_market_value", "long_market_value", "short_market_value", "gross_exposure", "equity", "weights_available", "cash_weight", "cash_balances", "positions", "group_exposures"], + "properties": { + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "weights_available": { "type": "boolean" }, + "cash_weight": { "$ref": "#/$defs/optionalWeight" }, + "cash_balances": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/cashAttribution" } + }, + "positions": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/markedPosition" } + }, + "group_exposures": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" } + ] + }, + "markedPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "settled_quantity", "unsettled_quantity", "mark", "base_market_value", "weight"], + "properties": { + "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "weight": { "$ref": "#/$defs/optionalWeight" } + } + }, + "strategyEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "market_slice"], + "properties": { + "type": { "const": "market_slice_closed" }, + "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/marketSlice" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "fill"], + "properties": { + "type": { "const": "fill_received" }, + "fill": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/fill" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "order"], + "properties": { + "type": { "const": "order_updated" }, + "order": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/order" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "reason"], + "properties": { + "type": { "const": "intent_rejected" }, + "reason": { "type": "string", "minLength": 1 } + } + } + ] + }, + "intents": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "intents" }, + "payload": { "$ref": "#/$defs/intentsPayload" } + } + } + ] + }, + "intentsPayload": { + "type": "object", + "additionalProperties": false, + "required": ["intents"], + "properties": { + "intents": { + "type": "array", + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/intent" } + } + } + }, + "shutdown": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "shutdown" }, + "payload": { "$ref": "#/$defs/emptyPayload" } + } + } + ] + }, + "stopped": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "stopped" }, + "payload": { "$ref": "#/$defs/emptyPayload" } + } + } + ] + }, + "emptyPayload": { + "type": "object", + "additionalProperties": false, + "maxProperties": 0 + }, + "error": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "error" }, + "payload": { "$ref": "#/$defs/errorPayload" } + } + } + ] + }, + "errorPayload": { + "type": "object", + "additionalProperties": false, + "required": ["message"], + "properties": { + "message": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } + } + } + } +} diff --git a/contracts/strategy/v13/transcript.schema.json b/contracts/strategy/v13/transcript.schema.json new file mode 100644 index 0000000..031b51b --- /dev/null +++ b/contracts/strategy/v13/transcript.schema.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v13/transcript.schema.json", + "title": "Trading Engine external strategy protocol v13 transcript record", + "description": "One accepted exchange or rejected-response diagnostic retained from a supervised stdio strategy session.", + "oneOf": [ + { "$ref": "#/$defs/exchange" }, + { "$ref": "#/$defs/rejectedResponse" } + ], + "$defs": { + "canonicalSequence": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "exchange": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_protocol_version", "transcript_sequence", "direction", "message"], + "properties": { + "strategy_protocol_version": { "const": "13" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v13/message.schema.json" + } + } + }, + "rejectedResponse": { + "type": "object", + "additionalProperties": false, + "required": [ + "strategy_diagnostic_version", + "transcript_sequence", + "record_type", + "expected_strategy_sequence", + "diagnostic", + "evidence" + ], + "properties": { + "strategy_diagnostic_version": { "const": "1" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "record_type": { "const": "rejected_strategy_response" }, + "expected_strategy_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "diagnostic": { "$ref": "#/$defs/diagnostic" }, + "evidence": { "$ref": "#/$defs/evidence" } + } + }, + "diagnostic": { + "allOf": [ + { + "$ref": "https://github.com/fallblu/trading-engine/contracts/diagnostic/v1/diagnostic.schema.json" + }, + { + "properties": { + "code": { "enum": ["strategy.protocol", "resource.limit"] }, + "phase": { "const": "strategy" } + } + } + ] + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["encoding", "prefix", "observed_bytes", "truncated"], + "properties": { + "encoding": { "const": "hex" }, + "prefix": { + "type": "string", + "pattern": "^(?:[0-9a-f]{2}){0,256}$" + }, + "observed_bytes": { + "type": "integer", + "minimum": 0, + "maximum": 1048577 + }, + "truncated": { "type": "boolean" } + } + } + } +} diff --git a/contracts/v15/README.md b/contracts/v15/README.md new file mode 100644 index 0000000..a1917b8 --- /dev/null +++ b/contracts/v15/README.md @@ -0,0 +1,117 @@ +# Trading Engine contract v15 + +This directory is the authoritative v15 process and file contract shared by Trading Engine and its +clients. Versions 14 through 3 remain readable during their client transitions. + +- `scenario.schema.json` validates batch replay inputs. +- `scenario-stream.schema.json` validates each JSON Lines scenario-stream record. +- `journal.schema.json` validates each JSON Lines audit record. +- The files under `fixtures/` form the canonical valid conformance corpus. +- `fill-clipped.scenario.json` and its journal exercise a leverage-limited partial fill. + +Version 7 requires exactly one explicit risk policy per catalog instrument. Each policy defines +order, signed-position, notional, initial-margin, maintenance-margin, and shorting limits. Versioned +risk groups have explicit membership, may overlap, and can constrain gross, long, short, absolute +net, and gross-to-equity concentration exposure. + +Runtime validation requires exact currency, position-mark, and FX coverage; known instruments; +lot-aligned quantities; tick-aligned positive marks; basis with the same sign as quantity; +nonnegative fee histories; the base FX rate equal to one; instrument, group, aggregate exposure, +leverage, and initial-margin limits. Signed cash is valid. A successful v15 run emits `initial_state` +immediately after `run_started`, followed by a reconciled initial `valuation`, before market data. + +Admission and fill clipping include working-order reservations. When multiple groups limit the same +fill, lexical group identity is the deterministic tie breaker. Valuations and strategy contexts +carry group exposure snapshots, and clipping thresholds identify the exact instrument or group. + +Every v15 scenario, stream record, and journal record carries `"contract_version": "15"`. + +Version 8 adds explicit `market`, `limit`, `stop`, and `stop_limit` orders with `gtc`, `ioc`, +`fok`, `day`, and `gtd` time-in-force policies. `day` orders identify both their venue and the +exact versioned calendar; `gtd` orders carry an absolute expiry timestamp. Older contracts retain +their frozen mapping: market orders are IOC and limit orders are GTC. + +Stops evaluate only completed OHLCV bars. A gap through the trigger records the bar start as the +trigger time; an intrabar touch records the bar end. Trigger state and slice sequence are journaled, +and an activated order cannot execute before the following slice. A stop becomes a market order; +a stop-limit becomes its configured limit order. Splits adjust both trigger and limit prices. + +IOC orders cancel any remainder after their first eligible slice. FOK orders fill only when the +full remaining quantity fits both execution capacity and risk capacity, otherwise they cancel with +no fill. DAY orders cancel after matching the slice that reaches the selected session's final +phase close. GTD orders cancel before matching any completed bar whose end reaches or passes the +expiry, avoiding ambiguous partial-bar execution. + +The v10 `execution` object uses `completed_bar_v1` configuration version `"2"`: participation basis +points plus exactly one composable fee schedule per instrument. Named fixed, notional-basis-point, +and per-unit components declare currency, rounding, and maker/taker applicability. Optional +per-fill minimums and caps use the schedule settlement currency; negative components represent +rebates. Fills and valuations retain every native, quote, and base-currency attribution. Runtime +capabilities also advertise frozen configuration version `"1"` for older scenario contracts. + +Version 10 adds a required `financing` policy and effective-time observations on every market +slice. Borrow observations provide per-instrument locate availability, signed annual rates, and +recall state. Cash observations provide separate annual credit and debit rates per currency. +Policies select Actual/365 or Actual/360 day count, simple or daily compounding, missing-data +handling, locate rejection or fill clipping, and recall rejection or deterministic close-out. + +Borrow availability is enforced when a fill would create or increase a short. Recalls cancel +active sells and may submit priority IOC covers until the position is flat. Borrow charges and cash +interest use the exact slice interval, update native ledgers deterministically, and emit dedicated +journal records. Valuations report cash interest separately and include it in aggregate realized +P&L. Version 9 and earlier retain their frozen fixed-borrow behavior and wire shapes. + +Version 12 separates trade-date economic accounting from settlement-date availability. A required +settlement policy selects total or settled cash buying power and total or settled position +availability. Versioned calendars enumerate canonical business dates, and each instrument has an +explicit business-day lag. Every fill creates a deterministic settlement instruction containing +its cash and position movements, trade date, and due date. A due instruction either settles on the +first eligible slice or records a named failure supplied by that slice. + +Valuations and strategy contexts report settled and unsettled cash and quantities without changing +economic equity. Journals include instruction-created, completed, and failed events. Scenario v10 +and strategy protocol v8 retain their frozen immediate-settlement wire behavior. + +Version 12 adds exact stock-dividend, rights, and spin-off distributions. Each distribution names +its destination instrument, exact entitlement ratio, basis allocation in basis points, and either +rejects fractional entitlements or converts them to cash at an explicit price and currency. +Stock dividends adjust persistent targets and eligible working orders; every distribution journals +delivered quantity, fractional quantity, allocated basis, fractional basis, and cash in lieu. + +Lifecycle events keep stable instrument identity separate from mutable symbol and provider +mappings. Halt and resume transitions control tradability. Expiration and delisting are terminal, +cancel active orders, clear target exposure, and require an explicit hold or cash-out policy. +Cash-out specifies its terminal price and currency. Every transition journals the source event, +resulting listing state, provider provenance, liquidated quantity, and cash attribution. + +Version 13 adds `completed_bar_next_open_v1` and `completed_bar_adverse_touch_v1` without changing +the frozen `completed_bar_v1` semantics. Next-open limits require a marketable later open; +adverse-touch limits require a one-tick trade-through before a maker fill is eligible. Both models +declare fixed half-spread and linear participation-impact catalogs, including an explicit policy +for missing bar volume. Price costs round away from the reference price to instrument ticks and +cannot violate a limit. An `execution_price_selected` audit record attributes the reference price, +spread adjustment, impact adjustment, and final executable price before each fill. + +Version 14 adds `quote_trade_v1` and causally ordered `market_events`. Quotes expose bid/ask price +and displayed size. Trades expose price, size, and buy, sell, or unknown aggressor side. Each event +records economic, availability, and receipt timestamps plus a positive ingest sequence. Replay +orders events by availability, receipt, and ingest sequence. Marketable orders consume only +displayed quote liquidity; passive orders require appropriately aggressed trade evidence, and an +unknown aggressor never fills them. Event capacity is shared deterministically across order +priority and fills retain the event's economic timestamp. Completed bars remain the valuation +boundary. The `quote-trade` batch, stream, and journal fixtures demonstrate equivalent replay. + +Version 15 adds `order_book_v1` and bounded level-two `order_book_events`. Every per-instrument +slice bundle starts with a complete snapshot and continues with contiguous absolute set, delete, +and aggressor-classified trade updates. Snapshots and updates reject crossed books, missing +deletes, sequence gaps, tick or lot misalignment, and depth beyond the configured limit; locked +books are valid. State is rebuilt from each slice snapshot, so replay never depends on hidden data +from a prior slice. + +Marketable orders walk observable opposite-side depth in price priority. Passive limit orders join +behind displayed same-price quantity and earlier engine orders. Reductions decrease quantity ahead, +adds join behind, and only appropriately aggressed trades consume the queue and fill the order. +Partial fills and cancellations therefore remain deterministic. Book liquidity is independent of +bar and quote/trade execution semantics, while completed bars remain the valuation boundary. The +`order-book` batch, stream, and journal fixtures demonstrate cancellation, queue depletion, maker +fills, bounded state, and batch/stream equivalence. diff --git a/contracts/v15/dune b/contracts/v15/dune new file mode 100644 index 0000000..d1742ac --- /dev/null +++ b/contracts/v15/dune @@ -0,0 +1,36 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v15/journal.schema.json) + (scenario-stream.schema.json as contracts/v15/scenario-stream.schema.json) + (scenario.schema.json as contracts/v15/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v15/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v15/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl + as + contracts/v15/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v15/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v15/fixtures/fill-clipped.scenario.json) + (fixtures/quote-trade.journal.jsonl + as + contracts/v15/fixtures/quote-trade.journal.jsonl) + (fixtures/quote-trade.scenario.json + as + contracts/v15/fixtures/quote-trade.scenario.json) + (fixtures/quote-trade.scenario.jsonl + as + contracts/v15/fixtures/quote-trade.scenario.jsonl) + (fixtures/order-book.journal.jsonl + as + contracts/v15/fixtures/order-book.journal.jsonl) + (fixtures/order-book.scenario.json + as + contracts/v15/fixtures/order-book.scenario.json) + (fixtures/order-book.scenario.jsonl + as + contracts/v15/fixtures/order-book.scenario.jsonl))) diff --git a/contracts/v15/fixtures/demo.journal.jsonl b/contracts/v15/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..c14e704 --- /dev/null +++ b/contracts/v15/fixtures/demo.journal.jsonl @@ -0,0 +1,29 @@ +{"contract_version":"15","engine_sequence":"1","event_id":"demo-event-000000000001","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"068f28c64905f5847ed3ecfac808940c3f1ba43e0d198d96f11929ba234703bc","execution_model":"completed_bar_adverse_touch_v1"}} +{"contract_version":"15","engine_sequence":"2","event_id":"demo-event-000000000002","causation_ids":["demo-event-000000000001"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"1","cost_basis":"90","realized_pnl":"5","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25"}],"marks":[{"instrument_id":"demo-equity-acme","price":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"10000","net_market_value":"100","long_market_value":"100","short_market_value":"0","gross_exposure":"100","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"10","equity":"10100","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"100","fx_rate":"1","market_value":"100","base_market_value":"100","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"10","base_unrealized_pnl":"10","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} +{"contract_version":"15","engine_sequence":"3","event_id":"demo-event-000000000003","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"10000","net_market_value":"100","long_market_value":"100","short_market_value":"0","gross_exposure":"100","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"10","equity":"10100","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"100","fx_rate":"1","market_value":"100","base_market_value":"100","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"10","base_unrealized_pnl":"10","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"5","event_id":"demo-event-000000000005","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"10000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-02T14:30:00.000000Z","period_end":"2026-01-02T21:00:00.000000Z","amount":"0.074201","closing_balance":"10000.074201"}} +{"contract_version":"15","engine_sequence":"6","event_id":"demo-event-000000000006","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} +{"contract_version":"15","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} +{"contract_version":"15","engine_sequence":"8","event_id":"demo-event-000000000008","causation_ids":["demo-event-000000000004","demo-event-000000000006"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"8.715","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000008","updated_event_id":"demo-event-000000000008","created_sequence":"8","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"9","event_id":"demo-event-000000000009","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"10000.074201","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5.074201","unrealized_pnl":"14","equity":"10104.074201","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000.074201","fx_rate":"1","base_value":"10000.074201","interest":"0.074201","base_interest":"0.074201","settled_amount":"10000.074201","unsettled_amount":"0","base_settled_value":"10000.074201","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"104","fx_rate":"1","market_value":"104","base_market_value":"104","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"14","base_unrealized_pnl":"14","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.074201","settled_cash":"10000.074201","unsettled_cash":"0","margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052.074201","maintenance_excess":"10078.074201","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"13"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"10000.074201","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-05T14:30:00.000000Z","period_end":"2026-01-05T21:00:00.000000Z","amount":"0.074201","closing_balance":"10000.148402"}} +{"contract_version":"15","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000008","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","reference_price":"103","spread_adjustment":"0.06","impact_adjustment":"0.13","final_price":"103.19"}} +{"contract_version":"15","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000012"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000001","order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"6.5","price":"103.19","notional":"670.735","fee":"0.920735","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_amount":"0.670735"}]}} +{"contract_version":"15","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000008","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"order_cancelled","payload":{"order":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"8.715","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000008","updated_event_id":"demo-event-000000000008","created_sequence":"8","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"6.5","filled_notional":"670.735","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"15","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000006","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","side":"buy","quantity":"2.215","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000015","updated_event_id":"demo-event-000000000015","created_sequence":"15","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9328.492667","net_market_value":"802.5","long_market_value":"802.5","short_market_value":"0","gross_exposure":"802.5","cost_basis":"761.655735","realized_pnl":"5.148402","unrealized_pnl":"40.844265","equity":"10130.992667","dividend_pnl":"1","execution_fees":"1.420735","borrow_fees":"0.25","total_fees":"1.670735","cash_balances":[{"currency":"USD","amount":"9328.492667","fx_rate":"1","base_value":"9328.492667","interest":"0.148402","base_interest":"0.148402","settled_amount":"9328.492667","unsettled_amount":"0","base_settled_value":"9328.492667","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7.5","mark":"107","fx_rate":"1","market_value":"802.5","base_market_value":"802.5","cost_basis":"761.655735","base_cost_basis":"761.655735","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.844265","base_unrealized_pnl":"40.844265","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.420735","base_execution_fees":"1.420735","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.670735","base_total_fees":"1.670735","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_currency":"USD","quote_amount":"0.25","base_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_currency":"USD","quote_amount":"0.670735","base_amount":"0.670735"}],"settled_quantity":"7.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_currency":"USD","quote_amount":"0.25","base_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_currency":"USD","quote_amount":"0.670735","base_amount":"0.670735"}],"cash_interest":"0.148402","settled_cash":"9328.492667","unsettled_cash":"0","margin":{"initial_requirement":"401.25","maintenance_requirement":"200.625","initial_excess":"9729.742667","maintenance_excess":"9930.367667","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"3","start_at":"2026-01-06T14:30:00.000000Z","end_at":"2026-01-06T21:00:00.000000Z","available_at":"2026-01-06T21:00:01.000000Z","received_at":"2026-01-06T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"107","high":"109","low":"104","close":"105","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"9328.492667","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-06T14:30:00.000000Z","period_end":"2026-01-06T21:00:00.000000Z","amount":"0.069218","closing_balance":"9328.561885"}} +{"contract_version":"15","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","side":"buy","reference_price":"107","spread_adjustment":"0.06","impact_adjustment":"0.01","final_price":"107.07"}} +{"contract_version":"15","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000002","order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"2.215","price":"107.07","notional":"237.16005","fee":"0.487161","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.237161","quote_amount":"0.237161"}]}} +{"contract_version":"15","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"target_portfolio_requested","payload":{"basis":"quantities","targets":[{"instrument_id":"demo-equity-acme","weight":null,"quantity":"2.5","reference_price":null}]}} +{"contract_version":"15","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000017","demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","side":"sell","quantity":"7.215","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000022","updated_event_id":"demo-event-000000000022","created_sequence":"22","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.914674","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.302946","realized_pnl":"5.21762","unrealized_pnl":"20.772054","equity":"10110.989674","dividend_pnl":"1","execution_fees":"1.907896","borrow_fees":"0.25","total_fees":"2.157896","cash_balances":[{"currency":"USD","amount":"9090.914674","fx_rate":"1","base_value":"9090.914674","interest":"0.21762","base_interest":"0.21762","settled_amount":"9090.914674","unsettled_amount":"0","base_settled_value":"9090.914674","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"9.715","mark":"105","fx_rate":"1","market_value":"1020.075","base_market_value":"1020.075","cost_basis":"999.302946","base_cost_basis":"999.302946","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.772054","base_unrealized_pnl":"20.772054","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.907896","base_execution_fees":"1.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.157896","base_total_fees":"2.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.5","quote_currency":"USD","quote_amount":"0.5","base_amount":"0.5"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.907896","quote_currency":"USD","quote_amount":"0.907896","base_amount":"0.907896"}],"settled_quantity":"9.715","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.5","quote_currency":"USD","quote_amount":"0.5","base_amount":"0.5"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.907896","quote_currency":"USD","quote_amount":"0.907896","base_amount":"0.907896"}],"cash_interest":"0.21762","settled_cash":"9090.914674","unsettled_cash":"0","margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.952174","maintenance_excess":"9855.970924","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"24","event_id":"demo-event-000000000024","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"4","start_at":"2026-01-07T14:30:00.000000Z","end_at":"2026-01-07T21:00:00.000000Z","available_at":"2026-01-07T21:00:01.000000Z","received_at":"2026-01-07T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"105","high":"107","low":"103","close":"106","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"25","event_id":"demo-event-000000000025","causation_ids":["demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"9090.914674","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-07T14:30:00.000000Z","period_end":"2026-01-07T21:00:00.000000Z","amount":"0.067455","closing_balance":"9090.982129"}} +{"contract_version":"15","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000022","demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","side":"sell","reference_price":"105","spread_adjustment":"0.06","impact_adjustment":"0.02","final_price":"104.92"}} +{"contract_version":"15","engine_sequence":"27","event_id":"demo-event-000000000027","causation_ids":["demo-event-000000000026"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000003","order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"sell","quantity":"7.215","price":"104.92","notional":"756.9978","fee":"1","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.756998","quote_amount":"0.756998"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_amount":"-0.006998"}]}} +{"contract_version":"15","engine_sequence":"28","event_id":"demo-event-000000000028","causation_ids":["demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.979929","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.154644","realized_pnl":"19.134573","unrealized_pnl":"7.845356","equity":"10111.979929","dividend_pnl":"1","execution_fees":"2.907896","borrow_fees":"0.25","total_fees":"3.157896","cash_balances":[{"currency":"USD","amount":"9846.979929","fx_rate":"1","base_value":"9846.979929","interest":"0.285075","base_interest":"0.285075","settled_amount":"9846.979929","unsettled_amount":"0","base_settled_value":"9846.979929","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","mark":"106","fx_rate":"1","market_value":"265","base_market_value":"265","cost_basis":"257.154644","base_cost_basis":"257.154644","realized_pnl":"18.849498","base_realized_pnl":"18.849498","unrealized_pnl":"7.845356","base_unrealized_pnl":"7.845356","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.907896","base_execution_fees":"2.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"settled_quantity":"2.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"cash_interest":"0.285075","settled_cash":"9846.979929","unsettled_cash":"0","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.479929","maintenance_excess":"10045.729929","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"29","event_id":"demo-event-000000000029","causation_ids":["demo-event-000000000028"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"068f28c64905f5847ed3ecfac808940c3f1ba43e0d198d96f11929ba234703bc","execution_model":"completed_bar_adverse_touch_v1","valuation":{"base_currency":"USD","cash":"9846.979929","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.154644","realized_pnl":"19.134573","unrealized_pnl":"7.845356","equity":"10111.979929","dividend_pnl":"1","execution_fees":"2.907896","borrow_fees":"0.25","total_fees":"3.157896","cash_balances":[{"currency":"USD","amount":"9846.979929","fx_rate":"1","base_value":"9846.979929","interest":"0.285075","base_interest":"0.285075","settled_amount":"9846.979929","unsettled_amount":"0","base_settled_value":"9846.979929","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","mark":"106","fx_rate":"1","market_value":"265","base_market_value":"265","cost_basis":"257.154644","base_cost_basis":"257.154644","realized_pnl":"18.849498","base_realized_pnl":"18.849498","unrealized_pnl":"7.845356","base_unrealized_pnl":"7.845356","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.907896","base_execution_fees":"2.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"settled_quantity":"2.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"cash_interest":"0.285075","settled_cash":"9846.979929","unsettled_cash":"0","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.479929","maintenance_excess":"10045.729929","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v15/fixtures/demo.scenario.json b/contracts/v15/fixtures/demo.scenario.json new file mode 100644 index 0000000..b2f17e1 --- /dev/null +++ b/contracts/v15/fixtures/demo.scenario.json @@ -0,0 +1,465 @@ +{ + "contract_version": "15", + "metadata": { + "producer": "trading-engine-demo", + "purpose": "deterministic conformance fixture" + }, + "run_id": "demo", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "10000" + } + ], + "positions": [ + { + "instrument_id": "demo-equity-acme", + "quantity": "1", + "cost_basis": "90", + "realized_pnl": "5", + "dividend_pnl": "1", + "execution_fees": "0.5", + "borrow_fees": "0.25" + } + ], + "marks": [ + { + "instrument_id": "demo-equity-acme", + "price": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "demo-equity-acme", + "symbol": "ACME", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "0.001" + } + ], + "venue_calendars": [ + { + "calendar_id": "demo-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "demo-equity-acme" + ], + "sessions": [ + { + "session_date": "2026-01-01", + "policy": "holiday", + "phases": [] + }, + { + "session_date": "2026-01-02", + "policy": "regular", + "phases": [ + { + "phase": "premarket", + "opens_at": "2026-01-02T09:00:00Z", + "closes_at": "2026-01-02T14:25:00Z" + }, + { + "phase": "opening_auction", + "opens_at": "2026-01-02T14:25:00Z", + "closes_at": "2026-01-02T14:30:00Z" + }, + { + "phase": "regular", + "opens_at": "2026-01-02T14:30:00Z", + "closes_at": "2026-01-02T20:55:00Z" + }, + { + "phase": "closing_auction", + "opens_at": "2026-01-02T20:55:00Z", + "closes_at": "2026-01-02T21:00:00Z" + }, + { + "phase": "postmarket", + "opens_at": "2026-01-02T21:00:00Z", + "closes_at": "2026-01-03T01:00:00Z" + } + ] + }, + { + "session_date": "2026-01-05", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-05T14:30:00Z", + "closes_at": "2026-01-05T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-06", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-06T14:30:00Z", + "closes_at": "2026-01-06T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-07", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-07T14:30:00Z", + "closes_at": "2026-01-07T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-08", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-08T14:30:00Z", + "closes_at": "2026-01-08T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000", + "max_leverage": "2", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "demo-equity-acme", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "completed_bar_adverse_touch_v1", + "configuration": { + "version": "1", + "participation_bps": 5000, + "fee_schedules": [ + { + "schedule_id": "demo-acme-fees-v1", + "instrument_id": "demo-equity-acme", + "settlement_currency": "USD", + "minimum": "0.3", + "maximum": "1", + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "0.25", + "rounding": "up", + "applies_to": "any" + }, + { + "name": "exchange", + "currency": "USD", + "kind": "notional_bps", + "value": 10, + "rounding": "up", + "applies_to": "taker" + }, + { + "name": "maker_rebate", + "currency": "USD", + "kind": "notional_bps", + "value": -2, + "rounding": "nearest", + "applies_to": "maker" + } + ] + } + ], + "spread_model": { + "model": "fixed_half_spread_v1", + "half_spread_bps": 5 + }, + "impact_model": { + "model": "linear_participation_v1", + "coefficient_bps": 25, + "missing_volume_policy": "reject" + } + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "target_weights", + "targets": [ + { + "instrument_id": "demo-equity-acme", + "weight": "0.1" + } + ] + }, + { + "type": "emit_metric", + "name": "desired_weight", + "value": "0.1" + } + ] + }, + { + "after_slice_sequence": "3", + "intents": [ + { + "type": "target_quantities", + "targets": [ + { + "instrument_id": "demo-equity-acme", + "quantity": "2.5" + } + ] + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-01-02T14:30:00Z", + "end_at": "2026-01-02T21:00:00Z", + "available_at": "2026-01-02T21:00:01Z", + "received_at": "2026-01-02T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "100", + "high": "105", + "low": "99", + "close": "104", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-01-05T14:30:00Z", + "end_at": "2026-01-05T21:00:00Z", + "available_at": "2026-01-05T21:00:01Z", + "received_at": "2026-01-05T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "103", + "high": "108", + "low": "102", + "close": "107", + "volume": "13" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-05T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-05T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "3", + "start_at": "2026-01-06T14:30:00Z", + "end_at": "2026-01-06T21:00:00Z", + "available_at": "2026-01-06T21:00:01Z", + "received_at": "2026-01-06T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "107", + "high": "109", + "low": "104", + "close": "105", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-06T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-06T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "4", + "start_at": "2026-01-07T14:30:00Z", + "end_at": "2026-01-07T21:00:00Z", + "available_at": "2026-01-07T21:00:01Z", + "received_at": "2026-01-07T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "105", + "high": "107", + "low": "103", + "close": "106", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-07T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-07T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "demo-equity-acme", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v15/fixtures/demo.scenario.jsonl b/contracts/v15/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..251ded8 --- /dev/null +++ b/contracts/v15/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"run_id":"demo","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"1","cost_basis":"90","realized_pnl":"5","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25"}],"marks":[{"instrument_id":"demo-equity-acme","price":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"0.001"}],"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00Z","closes_at":"2026-01-02T14:25:00Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00Z","closes_at":"2026-01-02T14:30:00Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00Z","closes_at":"2026-01-02T20:55:00Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00Z","closes_at":"2026-01-02T21:00:00Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00Z","closes_at":"2026-01-03T01:00:00Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00Z","closes_at":"2026-01-05T21:00:00Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00Z","closes_at":"2026-01-06T21:00:00Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00Z","closes_at":"2026-01-07T21:00:00Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00Z","closes_at":"2026-01-08T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_adverse_touch_v1","configuration":{"version":"1","participation_bps":5000,"fee_schedules":[{"schedule_id":"demo-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":"0.3","maximum":"1","components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"taker"},{"name":"maker_rebate","currency":"USD","kind":"notional_bps","value":-2,"rounding":"nearest","applies_to":"maker"}]}],"spread_model":{"model":"fixed_half_spread_v1","half_spread_bps":5},"impact_model":{"model":"linear_participation_v1","coefficient_bps":25,"missing_volume_policy":"reject"}}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"15","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","weight":"0.1"}],"type":"target_weights"},{"name":"desired_weight","type":"emit_metric","value":"0.1"}],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"13"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"15","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","quantity":"2.5"}],"type":"target_quantities"}],"market_slice":{"available_at":"2026-01-06T21:00:01Z","bars":[{"close":"105","high":"109","instrument_id":"demo-equity-acme","low":"104","open":"107","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-06T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-06T21:00:02Z","slice_sequence":"3","start_at":"2026-01-06T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-07T21:00:01Z","bars":[{"close":"106","high":"107","instrument_id":"demo-equity-acme","low":"103","open":"105","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-07T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-07T21:00:02Z","slice_sequence":"4","start_at":"2026-01-07T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"15","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v15/fixtures/fill-clipped.journal.jsonl b/contracts/v15/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..133c789 --- /dev/null +++ b/contracts/v15/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"15","engine_sequence":"1","event_id":"fill-clipped-event-000000000001","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"02138360b4f18f05c851efd2c16fecc1adb49ae668f6f7e72c3d903bea561002","execution_model":"completed_bar_v1"}} +{"contract_version":"15","engine_sequence":"2","event_id":"fill-clipped-event-000000000002","causation_ids":["fill-clipped-event-000000000001"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"550"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"550","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"550","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550","fx_rate":"1","base_value":"550","interest":"0","base_interest":"0","settled_amount":"550","unsettled_amount":"0","base_settled_value":"550","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"550","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} +{"contract_version":"15","engine_sequence":"3","event_id":"fill-clipped-event-000000000003","causation_ids":["fill-clipped-event-000000000002"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"550","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550","fx_rate":"1","base_value":"550","interest":"0","base_interest":"0","settled_amount":"550","unsettled_amount":"0","base_settled_value":"550","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"550","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"5","event_id":"fill-clipped-event-000000000005","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"550","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.004081","closing_balance":"550.004081"}} +{"contract_version":"15","engine_sequence":"6","event_id":"fill-clipped-event-000000000006","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000006","updated_event_id":"fill-clipped-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550.004081","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.004081","unrealized_pnl":"0","equity":"550.004081","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.004081","fx_rate":"1","base_value":"550.004081","interest":"0.004081","base_interest":"0.004081","settled_amount":"550.004081","unsettled_amount":"0","base_settled_value":"550.004081","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.004081","settled_cash":"550.004081","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.004081","maintenance_excess":"550.004081","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"550.004081","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.004081","closing_balance":"550.008162"}} +{"contract_version":"15","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000006","fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_clipped","payload":{"reason":{"version":"1","policy":"max_leverage","threshold":{"unit":"ratio","value":"1"}},"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","proposed_quantity":"10","permitted_quantity":"0","price":"100"}} +{"contract_version":"15","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000006","fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"order_cancelled","payload":{"order":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000006","updated_event_id":"fill-clipped-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"15","engine_sequence":"12","event_id":"fill-clipped-event-000000000012","causation_ids":["fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550.008162","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.008162","unrealized_pnl":"0","equity":"550.008162","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.008162","fx_rate":"1","base_value":"550.008162","interest":"0.008162","base_interest":"0.008162","settled_amount":"550.008162","unsettled_amount":"0","base_settled_value":"550.008162","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"100","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"13","event_id":"fill-clipped-event-000000000013","causation_ids":["fill-clipped-event-000000000012"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"02138360b4f18f05c851efd2c16fecc1adb49ae668f6f7e72c3d903bea561002","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"550.008162","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.008162","unrealized_pnl":"0","equity":"550.008162","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.008162","fx_rate":"1","base_value":"550.008162","interest":"0.008162","base_interest":"0.008162","settled_amount":"550.008162","unsettled_amount":"0","base_settled_value":"550.008162","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"100","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v15/fixtures/fill-clipped.scenario.json b/contracts/v15/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..319e246 --- /dev/null +++ b/contracts/v15/fixtures/fill-clipped.scenario.json @@ -0,0 +1,273 @@ +{ + "contract_version": "15", + "metadata": { + "producer": "trading-engine", + "purpose": "fill clipping conformance fixture" + }, + "run_id": "fill-clipped", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "550" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "clip-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "clip-equity" + ], + "sessions": [ + { + "session_date": "2026-02-02", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-02T14:30:00Z", + "closes_at": "2026-02-02T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-03", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-03T14:30:00Z", + "closes_at": "2026-02-03T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-04", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-04T14:30:00Z", + "closes_at": "2026-02-04T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "clip-equity", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "2", + "participation_bps": 10000, + "fee_schedules": [ + { + "schedule_id": "clip-fees-v1", + "instrument_id": "clip-equity", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "10", + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "market", + "trigger_price": null, + "limit_price": null, + "time_in_force": "ioc", + "venue_id": null, + "calendar_id": null, + "expires_at": null + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-02-02T14:30:00Z", + "end_at": "2026-02-02T21:00:00Z", + "available_at": "2026-02-02T21:00:01Z", + "received_at": "2026-02-02T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "50", + "high": "50", + "low": "50", + "close": "50", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-02-03T14:30:00Z", + "end_at": "2026-02-03T21:00:00Z", + "available_at": "2026-02-03T21:00:01Z", + "received_at": "2026-02-03T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "100", + "high": "100", + "low": "100", + "close": "100", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-03T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-03T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "clip-equity", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v15/fixtures/order-book.journal.jsonl b/contracts/v15/fixtures/order-book.journal.jsonl new file mode 100644 index 0000000..7c9fded --- /dev/null +++ b/contracts/v15/fixtures/order-book.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"15","engine_sequence":"1","event_id":"order-book-event-000000000001","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"f330f2a12a85e8b24bd3bc1bbb7fdf9a08654ed0d7dd8f0a3c712e031ef80128","execution_model":"order_book_v1"}} +{"contract_version":"15","engine_sequence":"2","event_id":"order-book-event-000000000002","causation_ids":["order-book-event-000000000001"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} +{"contract_version":"15","engine_sequence":"3","event_id":"order-book-event-000000000003","causation_ids":["order-book-event-000000000002"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"4","event_id":"order-book-event-000000000004","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00.000000Z","available_at":"2026-02-02T14:31:01.000000Z","received_at":"2026-02-02T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}} +{"contract_version":"15","engine_sequence":"5","event_id":"order-book-event-000000000005","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} +{"contract_version":"15","engine_sequence":"6","event_id":"order-book-event-000000000006","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"order-book-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"order-book-event-000000000006","updated_event_id":"order-book-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"7","event_id":"order-book-event-000000000007","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"8","event_id":"order-book-event-000000000008","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00.000000Z","available_at":"2026-02-03T14:35:01.000000Z","received_at":"2026-02-03T14:35:02.000000Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00.000000Z","available_at":"2026-02-03T14:36:01.000000Z","received_at":"2026-02-03T14:36:02.000000Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}} +{"contract_version":"15","engine_sequence":"9","event_id":"order-book-event-000000000009","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} +{"contract_version":"15","engine_sequence":"10","event_id":"order-book-event-000000000010","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000001","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"100","notional":"400","fee":"10","executed_at":"2026-02-03T14:35:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"15","engine_sequence":"11","event_id":"order-book-event-000000000011","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000002","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:36:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"15","engine_sequence":"12","event_id":"order-book-event-000000000012","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"980.02968","fx_rate":"1","base_value":"980.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"980.02968","unsettled_amount":"0","base_settled_value":"980.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"980.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"13","event_id":"order-book-event-000000000013","causation_ids":["order-book-event-000000000012"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"f330f2a12a85e8b24bd3bc1bbb7fdf9a08654ed0d7dd8f0a3c712e031ef80128","execution_model":"order_book_v1","valuation":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"980.02968","fx_rate":"1","base_value":"980.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"980.02968","unsettled_amount":"0","base_settled_value":"980.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"980.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v15/fixtures/order-book.scenario.json b/contracts/v15/fixtures/order-book.scenario.json new file mode 100644 index 0000000..7e27ab1 --- /dev/null +++ b/contracts/v15/fixtures/order-book.scenario.json @@ -0,0 +1,378 @@ +{ + "contract_version": "15", + "metadata": { + "producer": "trading-engine", + "purpose": "bounded order-book replay conformance fixture" + }, + "run_id": "order-book", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "2000" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "clip-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "clip-equity" + ], + "sessions": [ + { + "session_date": "2026-02-02", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-02T14:30:00Z", + "closes_at": "2026-02-02T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-03", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-03T14:30:00Z", + "closes_at": "2026-02-03T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-04", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-04T14:30:00Z", + "closes_at": "2026-02-04T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "clip-equity", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "order_book_v1", + "configuration": { + "version": "1", + "participation_bps": 10000, + "fee_schedules": [ + { + "schedule_id": "clip-fees-v1", + "instrument_id": "clip-equity", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "10", + "rounding": "up", + "applies_to": "any" + } + ] + } + ], + "max_depth_levels": 10 + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "limit", + "trigger_price": null, + "limit_price": "100", + "time_in_force": "gtc", + "venue_id": null, + "calendar_id": null, + "expires_at": null + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-02-02T14:30:00Z", + "end_at": "2026-02-02T21:00:00Z", + "available_at": "2026-02-02T21:00:01Z", + "received_at": "2026-02-02T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "50", + "high": "50", + "low": "50", + "close": "50", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [ + { + "type": "snapshot", + "instrument_id": "clip-equity", + "event_at": "2026-02-02T14:31:00Z", + "available_at": "2026-02-02T14:31:01Z", + "received_at": "2026-02-02T14:31:02Z", + "ingest_sequence": "1", + "book_sequence": "1", + "bids": [ + { + "price": "49", + "quantity": "20" + } + ], + "asks": [ + { + "price": "51", + "quantity": "20" + } + ] + } + ] + }, + { + "slice_sequence": "2", + "start_at": "2026-02-03T14:30:00Z", + "end_at": "2026-02-03T21:00:00Z", + "available_at": "2026-02-03T21:00:01Z", + "received_at": "2026-02-03T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "100", + "high": "100", + "low": "100", + "close": "100", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-03T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-03T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [ + { + "type": "snapshot", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:31:00Z", + "available_at": "2026-02-03T14:31:01Z", + "received_at": "2026-02-03T14:31:02Z", + "ingest_sequence": "1", + "book_sequence": "1", + "bids": [ + { + "price": "100", + "quantity": "5" + } + ], + "asks": [ + { + "price": "101", + "quantity": "20" + } + ] + }, + { + "type": "set", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:32:00Z", + "available_at": "2026-02-03T14:32:01Z", + "received_at": "2026-02-03T14:32:02Z", + "ingest_sequence": "2", + "book_sequence": "2", + "side": "bid", + "price": "100", + "quantity": "3" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:33:00Z", + "available_at": "2026-02-03T14:33:01Z", + "received_at": "2026-02-03T14:33:02Z", + "ingest_sequence": "3", + "book_sequence": "3", + "price": "100", + "quantity": "3", + "aggressor_side": "sell" + }, + { + "type": "set", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:34:00Z", + "available_at": "2026-02-03T14:34:01Z", + "received_at": "2026-02-03T14:34:02Z", + "ingest_sequence": "4", + "book_sequence": "4", + "side": "bid", + "price": "100", + "quantity": "10" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:35:00Z", + "available_at": "2026-02-03T14:35:01Z", + "received_at": "2026-02-03T14:35:02Z", + "ingest_sequence": "5", + "book_sequence": "5", + "price": "100", + "quantity": "4", + "aggressor_side": "sell" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:36:00Z", + "available_at": "2026-02-03T14:36:01Z", + "received_at": "2026-02-03T14:36:02Z", + "ingest_sequence": "6", + "book_sequence": "6", + "price": "100", + "quantity": "6", + "aggressor_side": "sell" + } + ] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "clip-equity", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v15/fixtures/order-book.scenario.jsonl b/contracts/v15/fixtures/order-book.scenario.jsonl new file mode 100644 index 0000000..ef9c90e --- /dev/null +++ b/contracts/v15/fixtures/order-book.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded order-book replay conformance fixture"},"run_id":"order-book","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"order_book_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}],"max_depth_levels":10}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"15","scenario_sequence":"2","record_type":"market_slice","payload":{"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}],"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00Z","available_at":"2026-02-02T14:31:01Z","received_at":"2026-02-02T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}}} +{"contract_version":"15","scenario_sequence":"3","record_type":"market_slice","payload":{"intents":[],"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00Z","available_at":"2026-02-03T14:35:01Z","received_at":"2026-02-03T14:35:02Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00Z","available_at":"2026-02-03T14:36:01Z","received_at":"2026-02-03T14:36:02Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}}} +{"contract_version":"15","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v15/fixtures/quote-trade.journal.jsonl b/contracts/v15/fixtures/quote-trade.journal.jsonl new file mode 100644 index 0000000..20bc174 --- /dev/null +++ b/contracts/v15/fixtures/quote-trade.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"15","engine_sequence":"1","event_id":"quote-trade-event-000000000001","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"b062a14acf0722c6d77e0e81985e12bf3a2a150dd7e493e1a811a393032c8d7f","execution_model":"quote_trade_v1"}} +{"contract_version":"15","engine_sequence":"2","event_id":"quote-trade-event-000000000002","causation_ids":["quote-trade-event-000000000001"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} +{"contract_version":"15","engine_sequence":"3","event_id":"quote-trade-event-000000000003","causation_ids":["quote-trade-event-000000000002"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"4","event_id":"quote-trade-event-000000000004","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"5","event_id":"quote-trade-event-000000000005","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} +{"contract_version":"15","engine_sequence":"6","event_id":"quote-trade-event-000000000006","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"quote-trade-event-000000000006","updated_event_id":"quote-trade-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"7","event_id":"quote-trade-event-000000000007","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"8","event_id":"quote-trade-event-000000000008","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"9","event_id":"quote-trade-event-000000000009","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} +{"contract_version":"15","engine_sequence":"10","event_id":"quote-trade-event-000000000010","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000001","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"99","notional":"396","fee":"10","executed_at":"2026-02-03T14:33:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"15","engine_sequence":"11","event_id":"quote-trade-event-000000000011","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000002","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:34:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"15","engine_sequence":"12","event_id":"quote-trade-event-000000000012","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"13","event_id":"quote-trade-event-000000000013","causation_ids":["quote-trade-event-000000000012"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"b062a14acf0722c6d77e0e81985e12bf3a2a150dd7e493e1a811a393032c8d7f","execution_model":"quote_trade_v1","valuation":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v15/fixtures/quote-trade.scenario.json b/contracts/v15/fixtures/quote-trade.scenario.json new file mode 100644 index 0000000..6eb58b8 --- /dev/null +++ b/contracts/v15/fixtures/quote-trade.scenario.json @@ -0,0 +1,319 @@ +{ + "contract_version": "15", + "metadata": { + "producer": "trading-engine", + "purpose": "bounded quote and trade replay fixture" + }, + "run_id": "quote-trade", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "2000" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "clip-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "clip-equity" + ], + "sessions": [ + { + "session_date": "2026-02-02", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-02T14:30:00Z", + "closes_at": "2026-02-02T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-03", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-03T14:30:00Z", + "closes_at": "2026-02-03T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-04", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-04T14:30:00Z", + "closes_at": "2026-02-04T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "clip-equity", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "quote_trade_v1", + "configuration": { + "version": "1", + "participation_bps": 10000, + "fee_schedules": [ + { + "schedule_id": "clip-fees-v1", + "instrument_id": "clip-equity", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "10", + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "limit", + "trigger_price": null, + "limit_price": "100", + "time_in_force": "gtc", + "venue_id": null, + "calendar_id": null, + "expires_at": null + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-02-02T14:30:00Z", + "end_at": "2026-02-02T21:00:00Z", + "available_at": "2026-02-02T21:00:01Z", + "received_at": "2026-02-02T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "50", + "high": "50", + "low": "50", + "close": "50", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-02-03T14:30:00Z", + "end_at": "2026-02-03T21:00:00Z", + "available_at": "2026-02-03T21:00:01Z", + "received_at": "2026-02-03T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "100", + "high": "100", + "low": "100", + "close": "100", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-03T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-03T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [ + { + "type": "quote", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:31:00Z", + "available_at": "2026-02-03T14:31:01Z", + "received_at": "2026-02-03T14:31:02Z", + "ingest_sequence": "1", + "bid_price": "99", + "bid_quantity": "20", + "ask_price": "101", + "ask_quantity": "20" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:32:00Z", + "available_at": "2026-02-03T14:32:01Z", + "received_at": "2026-02-03T14:32:02Z", + "ingest_sequence": "2", + "price": "100", + "quantity": "5", + "aggressor_side": "unknown" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:33:00Z", + "available_at": "2026-02-03T14:33:01Z", + "received_at": "2026-02-03T14:33:02Z", + "ingest_sequence": "3", + "price": "99", + "quantity": "4", + "aggressor_side": "sell" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:34:00Z", + "available_at": "2026-02-03T14:34:01Z", + "received_at": "2026-02-03T14:34:02Z", + "ingest_sequence": "4", + "price": "100", + "quantity": "10", + "aggressor_side": "sell" + } + ], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "clip-equity", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v15/fixtures/quote-trade.scenario.jsonl b/contracts/v15/fixtures/quote-trade.scenario.jsonl new file mode 100644 index 0000000..6c2b50d --- /dev/null +++ b/contracts/v15/fixtures/quote-trade.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded quote and trade replay fixture"},"run_id":"quote-trade","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"quote_trade_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"15","scenario_sequence":"2","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]},"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}]}} +{"contract_version":"15","scenario_sequence":"3","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]},"intents":[]}} +{"contract_version":"15","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v15/journal.schema.json b/contracts/v15/journal.schema.json new file mode 100644 index 0000000..d9360c0 --- /dev/null +++ b/contracts/v15/journal.schema.json @@ -0,0 +1,2427 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json", + "title": "Trading Engine v15 audit journal record", + "type": "object", + "additionalProperties": false, + "required": [ + "contract_version", + "engine_sequence", + "event_id", + "causation_ids", + "run_id", + "recorded_at", + "event_type", + "payload" + ], + "properties": { + "contract_version": { + "const": "15" + }, + "engine_sequence": { + "$ref": "#/$defs/sequence" + }, + "event_id": { + "$ref": "#/$defs/identifier" + }, + "causation_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/identifier" + } + }, + "run_id": { + "$ref": "#/$defs/identifier" + }, + "recorded_at": { + "$ref": "#/$defs/timestamp" + }, + "event_type": { + "enum": [ + "run_started", + "initial_state", + "market_slice_received", + "target_portfolio_requested", + "order_accepted", + "order_rejected", + "order_triggered", + "order_cancelled", + "split_applied", + "cash_dividend_applied", + "distribution_applied", + "lifecycle_applied", + "order_adjusted", + "execution_price_selected", + "fill_applied", + "settlement_instruction_created", + "settlement_completed", + "settlement_failed", + "fill_clipped", + "borrow_fee_applied", + "borrow_charge_applied", + "borrow_recall_received", + "cash_interest_applied", + "margin_call", + "margin_restored", + "intent_rejected", + "metric_emitted", + "valuation", + "run_completed" + ] + }, + "payload": { + "type": "object" + } + }, + "allOf": [ + { + "if": { "properties": { "event_type": { "const": "execution_price_selected" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/executionPriceSelected" } } } + }, + { + "if": { + "properties": { + "event_type": { + "const": "run_started" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/runStarted" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "initial_state" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/initialState" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "market_slice_received" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/marketSlice" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "target_portfolio_requested" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/targetPortfolio" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "order_accepted", + "order_rejected", + "order_triggered" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/order" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "order_cancelled" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/orderCancelled" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "split_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/splitApplied" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "cash_dividend_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/dividendApplied" + } + } + } + }, + { + "if": { "properties": { "event_type": { "const": "distribution_applied" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/distributionApplied" } } } + }, + { + "if": { "properties": { "event_type": { "const": "lifecycle_applied" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/lifecycleApplied" } } } + }, + { + "if": { + "properties": { + "event_type": { + "const": "order_adjusted" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/orderAdjusted" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "fill_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/fill" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "settlement_instruction_created", + "settlement_completed", + "settlement_failed" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/settlementInstruction" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "fill_clipped" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/fillClipped" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_fee_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowFee" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_charge_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowCharge" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_recall_received" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowRecall" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "cash_interest_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/cashInterest" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "margin_call", + "margin_restored", + "valuation" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/valuation" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "intent_rejected" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/intentRejected" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "metric_emitted" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/metric" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "run_completed" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/runCompleted" + } + } + } + } + ], + "$defs": { + "identifier": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" + }, + "signedDecimal": { + "type": "string", + "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" + }, + "unsignedDecimal": { + "type": "string", + "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "positiveDecimal": { + "type": "string", + "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "sequence": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "nonnegativeSequence": { + "type": "string", + "pattern": "^(?:0|[1-9][0-9]*)$" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" + }, + "runStarted": { + "type": "object", + "additionalProperties": false, + "required": [ + "scenario_sha256", + "execution_model" + ], + "properties": { + "scenario_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "execution_model": { + "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] + } + } + }, + "initialState": { + "type": "object", + "additionalProperties": false, + "required": [ + "portfolio", + "valuation" + ], + "properties": { + "portfolio": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/initialPortfolio" + }, + "valuation": { + "$ref": "#/$defs/valuation" + } + } + }, + "bar": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "open", + "high", + "low", + "close", + "volume" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "open": { + "$ref": "#/$defs/positiveDecimal" + }, + "high": { + "$ref": "#/$defs/positiveDecimal" + }, + "low": { + "$ref": "#/$defs/positiveDecimal" + }, + "close": { + "$ref": "#/$defs/positiveDecimal" + }, + "volume": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/unsignedDecimal" + } + ] + } + } + }, + "fxRate": { + "type": "object", + "additionalProperties": false, + "required": [ + "currency", + "rate" + ], + "properties": { + "currency": { + "$ref": "#/$defs/identifier" + }, + "rate": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "corporateAction": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "action_id", + "instrument_id", + "numerator", + "denominator" + ], + "properties": { + "type": { + "const": "split" + }, + "action_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "numerator": { + "$ref": "#/$defs/sequence" + }, + "denominator": { + "$ref": "#/$defs/sequence" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "action_id", + "instrument_id", + "amount_per_unit" + ], + "properties": { + "type": { + "const": "cash_dividend" + }, + "action_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "amount_per_unit": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], + "properties": { + "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "destination_instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" }, + "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fractional_policy": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/fractionalPolicy" } + } + } + ] + }, + "marketSlice": { + "type": "object", + "additionalProperties": false, + "required": [ + "slice_sequence", + "start_at", + "end_at", + "available_at", + "received_at", + "bars", + "fx_rates", + "corporate_actions", + "borrow_observations", + "cash_rate_observations", + "settlement_failures", + "lifecycle_events", + "market_events", + "order_book_events" + ], + "properties": { + "slice_sequence": { + "$ref": "#/$defs/sequence" + }, + "start_at": { + "$ref": "#/$defs/timestamp" + }, + "end_at": { + "$ref": "#/$defs/timestamp" + }, + "available_at": { + "$ref": "#/$defs/timestamp" + }, + "received_at": { + "$ref": "#/$defs/timestamp" + }, + "bars": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/bar" + } + }, + "fx_rates": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/fxRate" + } + }, + "corporate_actions": { + "type": "array", + "items": { + "$ref": "#/$defs/corporateAction" + } + }, + "borrow_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/borrowObservation" + } + }, + "cash_rate_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/cashRateObservation" + } + }, + "settlement_failures": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/settlementFailure" + } + }, + "lifecycle_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/lifecycleEvent" + } + }, + "market_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/marketEvent" + } + }, + "order_book_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/orderBookEvent" + } + } + } + }, + "settlementInstruction": { + "type": "object", + "additionalProperties": false, + "required": ["instruction_id", "fill_id", "instrument_id", "currency", "cash_movement", "position_movement", "trade_date", "due_date", "status", "settled_at", "failed_at", "failure_reason"], + "properties": { + "instruction_id": { "$ref": "#/$defs/identifier" }, + "fill_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "currency": { "$ref": "#/$defs/identifier" }, + "cash_movement": { "$ref": "#/$defs/signedDecimal" }, + "position_movement": { "$ref": "#/$defs/signedDecimal" }, + "trade_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "due_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "status": { "enum": ["pending", "settled", "failed"] }, + "settled_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, + "failed_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, + "failure_reason": { "oneOf": [{ "type": "null" }, { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" }] } + } + }, + "targetPortfolio": { + "type": "object", + "additionalProperties": false, + "required": [ + "basis", + "targets" + ], + "properties": { + "basis": { + "enum": [ + "weights", + "quantities" + ] + }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "weight", + "quantity", + "reference_price" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "weight": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/signedDecimal" + } + ] + }, + "quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "reference_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + } + } + } + } + } + }, + "order": { + "type": "object", + "additionalProperties": false, + "required": [ + "order_id", + "instrument_id", + "side", + "quantity", + "order_kind", + "trigger_price", + "limit_price", + "time_in_force", + "venue_id", + "calendar_id", + "expires_at", + "origin", + "created_event_id", + "updated_event_id", + "created_sequence", + "created_at", + "eligible_after_slice_sequence", + "triggered_at", + "triggered_slice_sequence", + "filled_quantity", + "filled_notional", + "status", + "rejection_reason" + ], + "properties": { + "order_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "side": { + "enum": [ + "buy", + "sell" + ] + }, + "quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "order_kind": { + "enum": [ + "market", + "limit", + "stop", + "stop_limit" + ] + }, + "trigger_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + }, + "limit_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + }, + "time_in_force": { + "enum": [ + "gtc", + "ioc", + "fok", + "day", + "gtd" + ] + }, + "venue_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/identifier" + } + ] + }, + "calendar_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/identifier" + } + ] + }, + "expires_at": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/timestamp" + } + ] + }, + "origin": { + "enum": [ + "direct", + "target_rebalance", + "margin_liquidation", + "borrow_recall", + "instrument_halt", + "instrument_terminal" + ] + }, + "created_event_id": { + "$ref": "#/$defs/identifier" + }, + "updated_event_id": { + "$ref": "#/$defs/identifier" + }, + "created_sequence": { + "$ref": "#/$defs/sequence" + }, + "created_at": { + "$ref": "#/$defs/timestamp" + }, + "eligible_after_slice_sequence": { + "$ref": "#/$defs/nonnegativeSequence" + }, + "triggered_at": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/timestamp" + } + ] + }, + "triggered_slice_sequence": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/sequence" + } + ] + }, + "filled_quantity": { + "$ref": "#/$defs/unsignedDecimal" + }, + "filled_notional": { + "$ref": "#/$defs/unsignedDecimal" + }, + "status": { + "enum": [ + "working", + "partially_filled", + "filled", + "cancelled", + "rejected" + ] + }, + "rejection_reason": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "minLength": 1 + } + ] + } + } + }, + "orderCancelled": { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "reason" + ], + "properties": { + "order": { + "$ref": "#/$defs/order" + }, + "reason": { + "enum": [ + "strategy_requested", + "target_replaced", + "market_ioc", + "immediate_or_cancel", + "fill_or_kill", + "day_expired", + "gtd_expired", + "margin_call", + "borrow_recall" + ] + } + } + }, + "splitApplied": { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "previous_quantity", + "adjusted_quantity" + ], + "properties": { + "action": { + "$ref": "#/$defs/corporateAction" + }, + "previous_quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "adjusted_quantity": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "dividendApplied": { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "quantity", + "cash_amount" + ], + "properties": { + "action": { + "$ref": "#/$defs/corporateAction" + }, + "quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "cash_amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "distributionApplied": { + "type": "object", + "additionalProperties": false, + "required": ["action", "source_quantity", "destination_quantity", "fractional_quantity", "allocated_basis", "fractional_basis", "cash_in_lieu"], + "properties": { + "action": { "$ref": "#/$defs/corporateAction" }, + "source_quantity": { "$ref": "#/$defs/signedDecimal" }, + "destination_quantity": { "$ref": "#/$defs/signedDecimal" }, + "fractional_quantity": { "$ref": "#/$defs/signedDecimal" }, + "allocated_basis": { "$ref": "#/$defs/signedDecimal" }, + "fractional_basis": { "$ref": "#/$defs/signedDecimal" }, + "cash_in_lieu": { "$ref": "#/$defs/signedDecimal" } + } + }, + "lifecycleApplied": { + "type": "object", + "additionalProperties": false, + "required": ["lifecycle_event", "listing", "liquidated_quantity", "cash_amount"], + "properties": { + "lifecycle_event": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/lifecycleEvent" }, + "listing": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "symbol", "status", "provider_mappings"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "status": { "enum": ["tradable", "halted", "expired", "delisted"] }, + "provider_mappings": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["provider", "provider_instrument_id"], + "properties": { + "provider": { "$ref": "#/$defs/identifier" }, + "provider_instrument_id": { "$ref": "#/$defs/identifier" } + } + } + } + } + }, + "liquidated_quantity": { "$ref": "#/$defs/signedDecimal" }, + "cash_amount": { "$ref": "#/$defs/signedDecimal" } + } + }, + "orderAdjusted": { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "action_id" + ], + "properties": { + "order": { + "$ref": "#/$defs/order" + }, + "action_id": { + "$ref": "#/$defs/identifier" + } + } + }, + "executionPriceSelected": { + "type": "object", + "additionalProperties": false, + "required": ["order_id", "instrument_id", "side", "reference_price", "spread_adjustment", "impact_adjustment", "final_price"], + "properties": { + "order_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "reference_price": { "$ref": "#/$defs/positiveDecimal" }, + "spread_adjustment": { "$ref": "#/$defs/unsignedDecimal" }, + "impact_adjustment": { "$ref": "#/$defs/unsignedDecimal" }, + "final_price": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "fill": { + "type": "object", + "additionalProperties": false, + "required": [ + "fill_id", + "order_id", + "instrument_id", + "quote_currency", + "side", + "quantity", + "price", + "notional", + "fee", + "executed_at", + "slice_sequence", + "fee_components" + ], + "properties": { + "fill_id": { + "$ref": "#/$defs/identifier" + }, + "order_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "side": { + "enum": [ + "buy", + "sell" + ] + }, + "quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "price": { + "$ref": "#/$defs/positiveDecimal" + }, + "notional": { + "$ref": "#/$defs/positiveDecimal" + }, + "fee": { + "$ref": "#/$defs/signedDecimal" + }, + "fee_components": { + "type": "array", + "items": { + "$ref": "#/$defs/calculatedFeeComponent" + } + }, + "executed_at": { + "$ref": "#/$defs/timestamp" + }, + "slice_sequence": { + "$ref": "#/$defs/sequence" + } + } + }, + "calculatedFeeComponent": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "kind", + "currency", + "amount", + "quote_amount" + ], + "properties": { + "name": { + "$ref": "#/$defs/identifier" + }, + "kind": { + "enum": [ + "fixed", + "notional_bps", + "per_unit", + "minimum_adjustment", + "maximum_adjustment" + ] + }, + "currency": { + "$ref": "#/$defs/identifier" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "quote_amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "feeComponentAttribution": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "kind", + "currency", + "amount", + "quote_currency", + "quote_amount", + "base_amount" + ], + "properties": { + "name": { + "$ref": "#/$defs/identifier" + }, + "kind": { + "enum": [ + "fixed", + "notional_bps", + "per_unit", + "minimum_adjustment", + "maximum_adjustment" + ] + }, + "currency": { + "$ref": "#/$defs/identifier" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "quote_amount": { + "$ref": "#/$defs/signedDecimal" + }, + "base_amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "quantityThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "quantity" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "moneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "money" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "ratioThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "ratio" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "basisPointsThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "basis_points" + }, + "value": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + } + }, + "instrumentQuantityThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "unit", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "quantity" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "instrumentMoneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "unit", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "money" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "currencyMoneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "unit", "value"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "unit": { "const": "money" }, + "value": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "settlementPositionThreshold": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "unit", "value"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "unit": { "const": "quantity" }, + "value": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "instrumentBasisPointsThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "unit", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "basis_points" + }, + "value": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + } + }, + "instrumentShortingThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "value": { + "const": false + } + } + }, + "groupMoneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "group_id", + "unit", + "value" + ], + "properties": { + "group_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "money" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "groupRatioThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "group_id", + "unit", + "value" + ], + "properties": { + "group_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "ratio" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "fillClipReason": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_order_quantity" + }, + "threshold": { + "$ref": "#/$defs/quantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_long_position" + }, + "threshold": { + "$ref": "#/$defs/quantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_short_position" + }, + "threshold": { + "$ref": "#/$defs/quantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_gross_exposure" + }, + "threshold": { + "$ref": "#/$defs/moneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_leverage" + }, + "threshold": { + "$ref": "#/$defs/ratioThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "initial_margin" + }, + "threshold": { + "$ref": "#/$defs/basisPointsThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_max_long_position" + }, + "threshold": { + "$ref": "#/$defs/instrumentQuantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["version", "policy", "threshold"], + "properties": { + "version": { "const": "1" }, + "policy": { "const": "settlement_cash_buying_power" }, + "threshold": { "$ref": "#/$defs/currencyMoneyThreshold" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["version", "policy", "threshold"], + "properties": { + "version": { "const": "1" }, + "policy": { "const": "settlement_position_availability" }, + "threshold": { "$ref": "#/$defs/settlementPositionThreshold" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_max_short_position" + }, + "threshold": { + "$ref": "#/$defs/instrumentQuantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_max_notional_exposure" + }, + "threshold": { + "$ref": "#/$defs/instrumentMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_shorting_disabled" + }, + "threshold": { + "$ref": "#/$defs/instrumentShortingThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_borrow_availability" + }, + "threshold": { + "$ref": "#/$defs/instrumentQuantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_initial_margin" + }, + "threshold": { + "$ref": "#/$defs/instrumentBasisPointsThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_gross_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_long_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_short_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_absolute_net_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_concentration" + }, + "threshold": { + "$ref": "#/$defs/groupRatioThreshold" + } + } + } + ] + }, + "fillClipped": { + "type": "object", + "additionalProperties": false, + "required": [ + "reason", + "order_id", + "instrument_id", + "proposed_quantity", + "permitted_quantity", + "price" + ], + "properties": { + "reason": { + "$ref": "#/$defs/fillClipReason" + }, + "order_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "proposed_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "permitted_quantity": { + "$ref": "#/$defs/unsignedDecimal" + }, + "price": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "borrowFee": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "quote_currency", + "short_quantity", + "reference_price", + "borrow_bps", + "period_start", + "period_end", + "fee" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "short_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "reference_price": { + "$ref": "#/$defs/positiveDecimal" + }, + "borrow_bps": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + }, + "period_start": { + "$ref": "#/$defs/timestamp" + }, + "period_end": { + "$ref": "#/$defs/timestamp" + }, + "fee": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "borrowCharge": { + "type": "object", + "additionalProperties": false, + "required": [ + "observation", + "quote_currency", + "short_quantity", + "reference_price", + "day_count", + "compounding", + "period_start", + "period_end", + "amount" + ], + "properties": { + "observation": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/borrowObservation" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "short_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "reference_price": { + "$ref": "#/$defs/positiveDecimal" + }, + "day_count": { + "enum": [ + "actual_365", + "actual_360" + ] + }, + "compounding": { + "enum": [ + "simple", + "daily" + ] + }, + "period_start": { + "$ref": "#/$defs/timestamp" + }, + "period_end": { + "$ref": "#/$defs/timestamp" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "borrowRecall": { + "type": "object", + "additionalProperties": false, + "required": [ + "observation", + "short_quantity", + "close_out_quantity" + ], + "properties": { + "observation": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/borrowObservation" + }, + "short_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "close_out_quantity": { + "$ref": "#/$defs/unsignedDecimal" + } + } + }, + "cashInterest": { + "type": "object", + "additionalProperties": false, + "required": [ + "observation", + "opening_balance", + "applied_rate_bps", + "day_count", + "compounding", + "period_start", + "period_end", + "amount", + "closing_balance" + ], + "properties": { + "observation": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/cashRateObservation" + }, + "opening_balance": { + "$ref": "#/$defs/signedDecimal" + }, + "applied_rate_bps": { + "type": "integer", + "minimum": -1000000, + "maximum": 1000000 + }, + "day_count": { + "enum": [ + "actual_365", + "actual_360" + ] + }, + "compounding": { + "enum": [ + "simple", + "daily" + ] + }, + "period_start": { + "$ref": "#/$defs/timestamp" + }, + "period_end": { + "$ref": "#/$defs/timestamp" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "closing_balance": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "cashAttribution": { + "type": "object", + "additionalProperties": false, + "required": [ + "currency", + "amount", + "fx_rate", + "base_value", + "interest", + "base_interest", + "settled_amount", + "unsettled_amount", + "base_settled_value", + "base_unsettled_value" + ], + "properties": { + "currency": { + "$ref": "#/$defs/identifier" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "fx_rate": { + "$ref": "#/$defs/positiveDecimal" + }, + "base_value": { + "$ref": "#/$defs/signedDecimal" + }, + "interest": { + "$ref": "#/$defs/signedDecimal" + }, + "base_interest": { + "$ref": "#/$defs/signedDecimal" + }, + "settled_amount": { + "$ref": "#/$defs/signedDecimal" + }, + "unsettled_amount": { + "$ref": "#/$defs/signedDecimal" + }, + "base_settled_value": { + "$ref": "#/$defs/signedDecimal" + }, + "base_unsettled_value": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "positionAttribution": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "quote_currency", + "quantity", + "settled_quantity", + "unsettled_quantity", + "mark", + "fx_rate", + "market_value", + "base_market_value", + "cost_basis", + "base_cost_basis", + "realized_pnl", + "base_realized_pnl", + "unrealized_pnl", + "base_unrealized_pnl", + "dividend_pnl", + "base_dividend_pnl", + "execution_fees", + "base_execution_fees", + "borrow_fees", + "base_borrow_fees", + "total_fees", + "base_total_fees", + "execution_fee_components" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "settled_quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "unsettled_quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "mark": { + "$ref": "#/$defs/positiveDecimal" + }, + "fx_rate": { + "$ref": "#/$defs/positiveDecimal" + }, + "market_value": { + "$ref": "#/$defs/signedDecimal" + }, + "base_market_value": { + "$ref": "#/$defs/signedDecimal" + }, + "cost_basis": { + "$ref": "#/$defs/signedDecimal" + }, + "base_cost_basis": { + "$ref": "#/$defs/signedDecimal" + }, + "realized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "base_realized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "unrealized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "base_unrealized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "dividend_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "base_dividend_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "execution_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "base_execution_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "borrow_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "base_borrow_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "total_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "base_total_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "execution_fee_components": { + "type": "array", + "items": { + "$ref": "#/$defs/feeComponentAttribution" + } + } + } + }, + "margin": { + "type": "object", + "additionalProperties": false, + "required": [ + "initial_requirement", + "maintenance_requirement", + "initial_excess", + "maintenance_excess", + "margin_call" + ], + "properties": { + "initial_requirement": { + "$ref": "#/$defs/unsignedDecimal" + }, + "maintenance_requirement": { + "$ref": "#/$defs/unsignedDecimal" + }, + "initial_excess": { + "$ref": "#/$defs/signedDecimal" + }, + "maintenance_excess": { + "$ref": "#/$defs/signedDecimal" + }, + "margin_call": { + "type": "boolean" + } + } + }, + "groupExposure": { + "type": "object", + "additionalProperties": false, + "required": [ + "group_id", + "gross_exposure", + "net_exposure", + "long_exposure", + "short_exposure", + "concentration" + ], + "properties": { + "group_id": { + "$ref": "#/$defs/identifier" + }, + "gross_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "net_exposure": { + "$ref": "#/$defs/signedDecimal" + }, + "long_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "short_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "concentration": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/signedDecimal" + } + ] + } + } + }, + "valuation": { + "type": "object", + "additionalProperties": false, + "required": [ + "base_currency", + "cash", + "settled_cash", + "unsettled_cash", + "net_market_value", + "long_market_value", + "short_market_value", + "gross_exposure", + "cost_basis", + "realized_pnl", + "unrealized_pnl", + "equity", + "dividend_pnl", + "execution_fees", + "borrow_fees", + "cash_interest", + "total_fees", + "cash_balances", + "positions", + "margin", + "group_exposures", + "execution_fee_components" + ], + "properties": { + "base_currency": { + "$ref": "#/$defs/identifier" + }, + "cash": { + "$ref": "#/$defs/signedDecimal" + }, + "settled_cash": { + "$ref": "#/$defs/signedDecimal" + }, + "unsettled_cash": { + "$ref": "#/$defs/signedDecimal" + }, + "net_market_value": { + "$ref": "#/$defs/signedDecimal" + }, + "long_market_value": { + "$ref": "#/$defs/unsignedDecimal" + }, + "short_market_value": { + "$ref": "#/$defs/unsignedDecimal" + }, + "gross_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "cost_basis": { + "$ref": "#/$defs/signedDecimal" + }, + "realized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "unrealized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "equity": { + "$ref": "#/$defs/signedDecimal" + }, + "dividend_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "execution_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "borrow_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "total_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "cash_balances": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/cashAttribution" + } + }, + "positions": { + "type": "array", + "items": { + "$ref": "#/$defs/positionAttribution" + } + }, + "margin": { + "$ref": "#/$defs/margin" + }, + "group_exposures": { + "type": "array", + "items": { + "$ref": "#/$defs/groupExposure" + } + }, + "execution_fee_components": { + "type": "array", + "items": { + "$ref": "#/$defs/feeComponentAttribution" + } + }, + "cash_interest": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "intentRejected": { + "type": "object", + "additionalProperties": false, + "required": [ + "reason" + ], + "properties": { + "reason": { + "type": "string", + "minLength": 1 + } + } + }, + "metric": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "runCompleted": { + "type": "object", + "additionalProperties": false, + "required": [ + "scenario_sha256", + "execution_model", + "valuation", + "order_counts" + ], + "properties": { + "scenario_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "execution_model": { + "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] + }, + "valuation": { + "$ref": "#/$defs/valuation" + }, + "order_counts": { + "type": "object", + "additionalProperties": false, + "required": [ + "total", + "active", + "filled", + "rejected", + "cancelled" + ], + "properties": { + "total": { + "type": "integer", + "minimum": 0 + }, + "active": { + "type": "integer", + "minimum": 0 + }, + "filled": { + "type": "integer", + "minimum": 0 + }, + "rejected": { + "type": "integer", + "minimum": 0 + }, + "cancelled": { + "type": "integer", + "minimum": 0 + } + } + } + } + } + } +} diff --git a/contracts/v15/scenario-stream.schema.json b/contracts/v15/scenario-stream.schema.json new file mode 100644 index 0000000..5af7c61 --- /dev/null +++ b/contracts/v15/scenario-stream.schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v15/scenario-stream.schema.json", + "title": "Trading Engine v15 replay scenario stream record", + "description": "Market-slice records are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-record rule is enforced semantically.", + "oneOf": [ + { "$ref": "#/$defs/headerRecord" }, + { "$ref": "#/$defs/sliceRecord" }, + { "$ref": "#/$defs/endRecord" } + ], + "$defs": { + "headerRecord": { + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], + "properties": { + "contract_version": { "const": "15" }, + "scenario_sequence": { "const": "1" }, + "record_type": { "const": "scenario_header" }, + "payload": { "$ref": "#/$defs/headerPayload" } + } + }, + "sliceRecord": { + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], + "properties": { + "contract_version": { "const": "15" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/sequence" }, + "record_type": { "const": "market_slice" }, + "payload": { "$ref": "#/$defs/slicePayload" } + } + }, + "endRecord": { + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], + "properties": { + "contract_version": { "const": "15" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/sequence" }, + "record_type": { "const": "scenario_end" }, + "payload": { + "type": "object", + "additionalProperties": false, + "required": ["slice_count"], + "properties": { "slice_count": { "type": "string", "pattern": "^(?:0|[1-9][0-9]*)$" } } + } + } + }, + "headerPayload": { + "type": "object", + "additionalProperties": false, + "required": ["metadata", "run_id", "base_currency", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "max_internal_events"], + "properties": { + "metadata": { "type": "object" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/settlement" }, + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 } + } + }, + "slicePayload": { + "type": "object", + "additionalProperties": false, + "required": ["market_slice", "intents"], + "properties": { + "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v15/scenario.schema.json b/contracts/v15/scenario.schema.json new file mode 100644 index 0000000..ffab82b --- /dev/null +++ b/contracts/v15/scenario.schema.json @@ -0,0 +1,870 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json", + "title": "Trading Engine v15 replay scenario", + "description": "Strict deterministic scenario contract with explicit venue-local session policies resolved to absolute instants outside the reducer.", + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "metadata", "run_id", "base_currency", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "max_internal_events", "schedule", "slices"], + "properties": { + "contract_version": { "const": "15" }, + "metadata": { "type": "object" }, + "run_id": { "$ref": "#/$defs/identifier" }, + "base_currency": { "$ref": "#/$defs/identifier" }, + "initial_portfolio": { "$ref": "#/$defs/initialPortfolio" }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/venueCalendar" } + }, + "risk": { "$ref": "#/$defs/risk" }, + "execution": { "$ref": "#/$defs/execution" }, + "financing": { "$ref": "#/$defs/financing" }, + "settlement": { "$ref": "#/$defs/settlement" }, + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 }, + "schedule": { "type": "array", "items": { "$ref": "#/$defs/scheduleItem" } }, + "slices": { + "description": "Slices are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-item rule is enforced semantically.", + "type": "array", + "items": { "$ref": "#/$defs/marketSlice" } + } + }, + "$defs": { + "settlement": { + "type": "object", + "additionalProperties": false, + "required": ["cash_buying_power", "position_availability", "calendars", "rules"], + "properties": { + "cash_buying_power": { "enum": ["total_cash", "settled_cash"] }, + "position_availability": { "enum": ["total_positions", "settled_positions"] }, + "calendars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementCalendar" } }, + "rules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementRule" } } + } + }, + "settlementCalendar": { + "type": "object", + "additionalProperties": false, + "required": ["calendar_id", "version", "business_dates"], + "properties": { + "calendar_id": { "$ref": "#/$defs/identifier" }, + "version": { "const": "1" }, + "business_dates": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" } } + } + }, + "settlementRule": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "calendar_id", "lag_business_days"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "calendar_id": { "$ref": "#/$defs/identifier" }, + "lag_business_days": { "type": "integer", "minimum": 0, "maximum": 30 } + } + }, + "settlementFailure": { + "type": "object", + "additionalProperties": false, + "required": ["instruction_id", "reason"], + "properties": { + "instruction_id": { "$ref": "#/$defs/identifier" }, + "reason": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } + } + }, + "financing": { + "type": "object", + "additionalProperties": false, + "required": ["day_count", "compounding", "borrow_missing_data", "cash_missing_data", "locate_policy", "recall_policy"], + "properties": { + "day_count": { "enum": ["actual_365", "actual_360"] }, + "compounding": { "enum": ["simple", "daily"] }, + "borrow_missing_data": { "enum": ["reject", "zero"] }, + "cash_missing_data": { "enum": ["reject", "zero"] }, + "locate_policy": { "enum": ["reject_order", "clip_fill"] }, + "recall_policy": { "enum": ["reject_new_shorts", "close_out"] } + } + }, + "borrowObservation": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "effective_at", "available_quantity", "annual_rate_bps", "recalled"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "effective_at": { "$ref": "#/$defs/timestamp" }, + "available_quantity": { "$ref": "#/$defs/unsignedDecimal" }, + "annual_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, + "recalled": { "type": "boolean" } + } + }, + "cashRateObservation": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "effective_at", "credit_rate_bps", "debit_rate_bps"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "effective_at": { "$ref": "#/$defs/timestamp" }, + "credit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, + "debit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 } + } + }, + "identifier": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" + }, + "signedDecimal": { + "type": "string", + "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" + }, + "unsignedDecimal": { + "type": "string", + "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "positiveDecimal": { + "type": "string", + "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "sequence": { "type": "string", "pattern": "^[1-9][0-9]*$" }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" + }, + "cashBalance": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "amount"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "amount": { "$ref": "#/$defs/signedDecimal" } + } + }, + "initialPortfolio": { + "type": "object", + "additionalProperties": false, + "required": ["cash", "positions", "marks", "fx_rates"], + "properties": { + "cash": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashBalance" } }, + "positions": { "type": "array", "items": { "$ref": "#/$defs/initialPosition" } }, + "marks": { "type": "array", "items": { "$ref": "#/$defs/initialMark" } }, + "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } } + } + }, + "initialPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "cost_basis", "realized_pnl", "dividend_pnl", "execution_fees", "borrow_fees"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "quantity": { "$ref": "#/$defs/signedDecimal" }, + "cost_basis": { "$ref": "#/$defs/signedDecimal" }, + "realized_pnl": { "$ref": "#/$defs/signedDecimal" }, + "dividend_pnl": { "$ref": "#/$defs/signedDecimal" }, + "execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, + "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "initialMark": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "price"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "price": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "instrument": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "symbol", "quote_currency", "tick_size", "lot_size"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "quote_currency": { "$ref": "#/$defs/identifier" }, + "tick_size": { "$ref": "#/$defs/positiveDecimal" }, + "lot_size": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "venueCalendar": { + "type": "object", + "additionalProperties": false, + "required": ["calendar_id", "calendar_version", "venue_id", "instrument_ids", "sessions"], + "properties": { + "calendar_id": { "$ref": "#/$defs/identifier" }, + "calendar_version": { "const": "1" }, + "venue_id": { "$ref": "#/$defs/identifier" }, + "instrument_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/identifier" } + }, + "sessions": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/venueSession" } + } + } + }, + "venueSession": { + "oneOf": [ + { "$ref": "#/$defs/openVenueSession" }, + { "$ref": "#/$defs/holidayVenueSession" } + ] + }, + "openVenueSession": { + "type": "object", + "additionalProperties": false, + "required": ["session_date", "policy", "phases"], + "properties": { + "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "policy": { "enum": ["regular", "early_close"] }, + "phases": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "items": { "$ref": "#/$defs/venuePhase" } + } + } + }, + "holidayVenueSession": { + "type": "object", + "additionalProperties": false, + "required": ["session_date", "policy", "phases"], + "properties": { + "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "policy": { "const": "holiday" }, + "phases": { "type": "array", "maxItems": 0 } + } + }, + "venuePhase": { + "type": "object", + "additionalProperties": false, + "required": ["phase", "opens_at", "closes_at"], + "properties": { + "phase": { "enum": ["premarket", "opening_auction", "regular", "closing_auction", "postmarket"] }, + "opens_at": { "$ref": "#/$defs/timestamp" }, + "closes_at": { "$ref": "#/$defs/timestamp" } + } + }, + "risk": { + "type": "object", + "additionalProperties": false, + "required": ["max_gross_exposure", "max_leverage", "short_borrow_bps", "instrument_policies", "groups"], + "properties": { + "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, + "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "instrument_policies": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/instrumentRiskPolicy" } + }, + "groups": { + "type": "array", + "items": { "$ref": "#/$defs/riskGroup" } + } + } + }, + "instrumentRiskPolicy": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "max_order_quantity", "max_long_position", "max_short_position", "max_notional_exposure", "initial_margin_bps", "maintenance_margin_bps", "shorting_allowed"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_notional_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "shorting_allowed": { "type": "boolean" } + } + }, + "nullablePositiveDecimal": { + "oneOf": [ + { "type": "null" }, + { "$ref": "#/$defs/positiveDecimal" } + ] + }, + "riskGroup": { + "type": "object", + "additionalProperties": false, + "required": ["group_id", "group_version", "group_type", "instrument_ids", "limits"], + "properties": { + "group_id": { "$ref": "#/$defs/identifier" }, + "group_version": { "const": "1" }, + "group_type": { "enum": ["issuer", "sector", "currency", "country", "asset_class", "custom"] }, + "instrument_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/identifier" } + }, + "limits": { "$ref": "#/$defs/riskGroupLimits" } + } + }, + "riskGroupLimits": { + "type": "object", + "additionalProperties": false, + "required": ["max_gross_exposure", "max_long_exposure", "max_short_exposure", "max_absolute_net_exposure", "max_concentration"], + "properties": { + "max_gross_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_long_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_short_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_absolute_net_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_concentration": { + "oneOf": [ + { "type": "null" }, + { "allOf": [{ "$ref": "#/$defs/positiveDecimal" }, { "pattern": "^(?:0[.][0-9]{0,5}[1-9]|1)$" }] } + ] + } + } + }, + "execution": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "enum": ["completed_bar_next_open_v1", "completed_bar_adverse_touch_v1"] }, + "configuration": { "$ref": "#/$defs/conservativeBarConfiguration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "quote_trade_v1" }, + "configuration": { "$ref": "#/$defs/quoteTradeConfiguration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "order_book_v1" }, + "configuration": { "$ref": "#/$defs/orderBookConfiguration" } + } + } + ] + }, + "completedBarV1Configuration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules"], + "properties": { + "version": { "const": "2" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } + } + }, + "conservativeBarConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules", "spread_model", "impact_model"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, + "spread_model": { "$ref": "#/$defs/fixedSpreadModel" }, + "impact_model": { "$ref": "#/$defs/linearImpactModel" } + } + }, + "quoteTradeConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } + } + }, + "orderBookConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules", "max_depth_levels"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, + "max_depth_levels": { "type": "integer", "minimum": 1, "maximum": 1024 } + } + }, + "fixedSpreadModel": { + "type": "object", + "additionalProperties": false, + "required": ["model", "half_spread_bps"], + "properties": { + "model": { "const": "fixed_half_spread_v1" }, + "half_spread_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "linearImpactModel": { + "type": "object", + "additionalProperties": false, + "required": ["model", "coefficient_bps", "missing_volume_policy"], + "properties": { + "model": { "const": "linear_participation_v1" }, + "coefficient_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "missing_volume_policy": { "enum": ["reject", "zero_impact"] } + } + }, + "feeSchedule": { + "type": "object", + "additionalProperties": false, + "required": ["schedule_id", "instrument_id", "settlement_currency", "minimum", "maximum", "components"], + "properties": { + "schedule_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "settlement_currency": { "$ref": "#/$defs/identifier" }, + "minimum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, + "maximum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, + "components": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeComponent" } } + } + }, + "feeComponent": { + "type": "object", + "additionalProperties": false, + "required": ["name", "currency", "kind", "value", "rounding", "applies_to"], + "properties": { + "name": { "$ref": "#/$defs/identifier" }, + "currency": { "$ref": "#/$defs/identifier" }, + "kind": { "enum": ["fixed", "notional_bps", "per_unit"] }, + "value": { "oneOf": [{ "$ref": "#/$defs/signedDecimal" }, { "type": "integer", "minimum": -10000, "maximum": 10000 }] }, + "rounding": { "enum": ["up", "down", "nearest"] }, + "applies_to": { "enum": ["any", "maker", "taker"] } + }, + "allOf": [ + { "if": { "properties": { "kind": { "const": "notional_bps" } } }, "then": { "properties": { "value": { "type": "integer" } } } }, + { "if": { "properties": { "kind": { "enum": ["fixed", "per_unit"] } } }, "then": { "properties": { "value": { "$ref": "#/$defs/signedDecimal" } } } } + ] + }, + "scheduleItem": { + "type": "object", + "additionalProperties": false, + "required": ["after_slice_sequence", "intents"], + "properties": { + "after_slice_sequence": { "$ref": "#/$defs/sequence" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "#/$defs/intent" } } + } + }, + "intent": { + "oneOf": [ + { "$ref": "#/$defs/targetWeights" }, + { "$ref": "#/$defs/targetQuantities" }, + { "$ref": "#/$defs/submitOrder" }, + { "$ref": "#/$defs/cancelOrder" }, + { "$ref": "#/$defs/metric" } + ] + }, + "targetWeights": { + "type": "object", + "additionalProperties": false, + "required": ["type", "targets"], + "properties": { + "type": { "const": "target_weights" }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "weight"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "weight": { "$ref": "#/$defs/signedDecimal" } + } + } + } + } + }, + "targetQuantities": { + "type": "object", + "additionalProperties": false, + "required": ["type", "targets"], + "properties": { + "type": { "const": "target_quantities" }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "quantity": { "$ref": "#/$defs/signedDecimal" } + } + } + } + } + }, + "submitOrder": { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "side", "quantity", "order_kind", "trigger_price", "limit_price", "time_in_force", "venue_id", "calendar_id", "expires_at"], + "properties": { + "type": { "const": "submit_order" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "order_kind": { "enum": ["market", "limit", "stop", "stop_limit"] }, + "trigger_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, + "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, + "time_in_force": { "enum": ["gtc", "ioc", "fok", "day", "gtd"] }, + "venue_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, + "calendar_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, + "expires_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] } + }, + "allOf": [ + { "if": { "properties": { "order_kind": { "const": "market" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "type": "null" } } } }, + { "if": { "properties": { "order_kind": { "const": "limit" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, + { "if": { "properties": { "order_kind": { "const": "stop" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "type": "null" } } } }, + { "if": { "properties": { "order_kind": { "const": "stop_limit" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, + { "if": { "properties": { "time_in_force": { "const": "day" } } }, "then": { "properties": { "venue_id": { "$ref": "#/$defs/identifier" }, "calendar_id": { "$ref": "#/$defs/identifier" }, "expires_at": { "type": "null" } } } }, + { "if": { "properties": { "time_in_force": { "const": "gtd" } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "$ref": "#/$defs/timestamp" } } } }, + { "if": { "properties": { "time_in_force": { "enum": ["gtc", "ioc", "fok"] } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "type": "null" } } } } + ] + }, + "cancelOrder": { + "type": "object", + "additionalProperties": false, + "required": ["type", "order_id"], + "properties": { + "type": { "const": "cancel_order" }, + "order_id": { "$ref": "#/$defs/identifier" } + } + }, + "metric": { + "type": "object", + "additionalProperties": false, + "required": ["type", "name", "value"], + "properties": { + "type": { "const": "emit_metric" }, + "name": { "type": "string" }, + "value": { "type": "string" } + } + }, + "marketSlice": { + "type": "object", + "additionalProperties": false, + "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "market_events", "order_book_events", "fx_rates", "corporate_actions", "borrow_observations", "cash_rate_observations", "settlement_failures", "lifecycle_events"], + "properties": { + "slice_sequence": { "$ref": "#/$defs/sequence" }, + "start_at": { "$ref": "#/$defs/timestamp" }, + "end_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "bars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/bar" } }, + "market_events": { "type": "array", "items": { "$ref": "#/$defs/marketEvent" } }, + "order_book_events": { "type": "array", "items": { "$ref": "#/$defs/orderBookEvent" } }, + "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } }, + "corporate_actions": { "type": "array", "items": { "$ref": "#/$defs/corporateAction" } }, + "borrow_observations": { "type": "array", "items": { "$ref": "#/$defs/borrowObservation" } }, + "cash_rate_observations": { "type": "array", "items": { "$ref": "#/$defs/cashRateObservation" } }, + "settlement_failures": { "type": "array", "items": { "$ref": "#/$defs/settlementFailure" } }, + "lifecycle_events": { "type": "array", "items": { "$ref": "#/$defs/lifecycleEvent" } } + } + }, + "bar": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "open", "high", "low", "close", "volume"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "open": { "$ref": "#/$defs/positiveDecimal" }, + "high": { "$ref": "#/$defs/positiveDecimal" }, + "low": { "$ref": "#/$defs/positiveDecimal" }, + "close": { "$ref": "#/$defs/positiveDecimal" }, + "volume": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] } + } + }, + "marketEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "bid_price", "bid_quantity", "ask_price", "ask_quantity"], + "properties": { + "type": { "const": "quote" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "bid_price": { "$ref": "#/$defs/positiveDecimal" }, + "bid_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "ask_price": { "$ref": "#/$defs/positiveDecimal" }, + "ask_quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "price", "quantity", "aggressor_side"], + "properties": { + "type": { "const": "trade" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "aggressor_side": { "enum": ["buy", "sell", "unknown"] } + } + } + ] + }, + "orderBookLevel": { + "type": "object", + "additionalProperties": false, + "required": ["price", "quantity"], + "properties": { + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "orderBookEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "bids", "asks"], + "properties": { + "type": { "const": "snapshot" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "bids": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } }, + "asks": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price", "quantity"], + "properties": { + "type": { "const": "set" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "side": { "enum": ["bid", "ask"] }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price"], + "properties": { + "type": { "const": "delete" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "side": { "enum": ["bid", "ask"] }, + "price": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "price", "quantity", "aggressor_side"], + "properties": { + "type": { "const": "trade" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "aggressor_side": { "enum": ["buy", "sell", "unknown"] } + } + } + ] + }, + "fxRate": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "rate"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "rate": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "corporateAction": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "numerator", "denominator"], + "properties": { + "type": { "const": "split" }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "amount_per_unit"], + "properties": { + "type": { "const": "cash_dividend" }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "amount_per_unit": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], + "properties": { + "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "destination_instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" }, + "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fractional_policy": { "$ref": "#/$defs/fractionalPolicy" } + } + } + ] + }, + "fractionalPolicy": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["policy"], + "properties": { "policy": { "const": "reject" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["policy", "price", "currency"], + "properties": { + "policy": { "const": "cash_in_lieu" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "currency": { "$ref": "#/$defs/identifier" } + } + } + ] + }, + "terminalPolicy": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["policy"], + "properties": { "policy": { "const": "hold" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["policy", "price", "currency"], + "properties": { + "policy": { "const": "cash_out" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "currency": { "$ref": "#/$defs/identifier" } + } + } + ] + }, + "lifecycleEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "reason"], + "properties": { + "type": { "const": "halt" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "reason": { "type": "string", "minLength": 1 } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id"], + "properties": { + "type": { "const": "resume" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "symbol", "provider", "provider_instrument_id"], + "properties": { + "type": { "const": "identifier_change" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "provider": { "$ref": "#/$defs/identifier" }, + "provider_instrument_id": { "$ref": "#/$defs/identifier" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "terminal_policy"], + "properties": { + "type": { "const": "expiration" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "terminal_policy": { "$ref": "#/$defs/terminalPolicy" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "terminal_policy", "reason"], + "properties": { + "type": { "const": "delisting" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "terminal_policy": { "$ref": "#/$defs/terminalPolicy" }, + "reason": { "type": "string", "minLength": 1 } + } + } + ] + } + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index 52529d2..a5d0aed 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -7,4 +7,4 @@ that every public interface has a corresponding page. The generated reference describes library types and functions. The versioned JSON and JSON Lines -files under [Contracts](../contracts/v14/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v15/README.md) remain authoritative for process boundaries. diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index ef3971a..aff839c 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -21,7 +21,7 @@ Every runtime cell replays the frozen v3 demo, v5 demo, and v5 risk-limited fill canonical fixtures. Standard output and standard error are captured separately because human diagnostics may contain platform-specific paths or process details and are not part of the journal contract. -The full test suite additionally validates and replays the current v14 batch, stream, journal, and +The full test suite additionally validates and replays the current v15 batch, stream, journal, and strategy-v12 fixtures, including quote/trade causality and the reconciled first valuation. Coverage runs once in the exact locked Ubuntu environment. The required Persistra job also runs diff --git a/docs/execution-model.md b/docs/execution-model.md index 597573e..53d70d6 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,12 +1,12 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v14 advertises `completed_bar_v1`, `completed_bar_next_open_v1`, -`completed_bar_adverse_touch_v1`, and `quote_trade_v1`; embedders can inject another module through +Contract v15 advertises `completed_bar_v1`, `completed_bar_next_open_v1`, +`completed_bar_adverse_touch_v1`, `quote_trade_v1`, and `order_book_v1`; embedders can inject another module through the typed engine configuration without introducing runtime shared-library loading. The selected name is repeated in both terminal audit records. -Each compiled model owns a strict configuration contract. The v14 envelope separates selection from +Each compiled model owns a strict configuration contract. The v15 envelope separates selection from model-specific parameters: ```json @@ -59,6 +59,18 @@ sells consume only buy-aggressor trades at or above it. An `unknown` aggressor n passive fill. Each event has independent, lot-rounded capacity, and its `event_at` is the fill's economic timestamp. Completed bars remain required solely for synchronized valuation. +The order-book model uses configuration version `"1"`, adding `max_depth_levels` from 1 through +1,024. Each instrument's slice-local bundle begins with a full bid/ask snapshot and uses contiguous +absolute set, delete, and trade updates. Crossed states, gaps, missing deletes, and states beyond +the depth limit fail replay; a locked best bid and ask is accepted. Each later slice starts from a +fresh snapshot, so no unbounded or hidden book state survives a slice boundary. + +Marketable orders walk observable opposite-side levels in price order. Passive limits start behind +the displayed quantity at their price and behind earlier engine orders. Reductions decrease queue +ahead, additions join behind, and an aggressor-qualified trade consumes queue ahead before filling +the order. This model has its own liquidity state and does not reuse completed-bar or quote/trade +fill semantics. Bars remain mandatory only for valuation. + ## Eligibility An order records the slice after which it is eligible. The matcher requires: diff --git a/docs/persistra.md b/docs/persistra.md index 68864ad..f66b6be 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -54,12 +54,12 @@ lifecycle belong to Persistra. Persistra currently uses the transitional v3 [scenario](../contracts/v3/scenario.schema.json) and [journal](../contracts/v3/journal.schema.json) schemas and their adjacent conformance fixtures for -structural checks. The engine advertises current contract v14 while retaining v13 through v3 and +structural checks. The engine advertises current contract v15 while retaining v14 through v3 and exact v3 journal output for v3 inputs. The engine parser is authoritative for ordering, catalog coverage, causality, tick, lot, risk, and accounting invariants that JSON Schema cannot express. External strategies use the separate -[strategy protocol v12](../contracts/strategy/v12/README.md). Persistra's host turns protocol +[strategy protocol v13](../contracts/strategy/v13/README.md). Persistra's host turns protocol initialization, marked portfolio contexts, market-slice, fill, order, and rejection events into typed callbacks. Realized weights are available only for positive equity. The retained run manifest binds the strategy identity, executable hash, declared input hashes, transcript hash, @@ -79,14 +79,14 @@ compatibility claim. - **Engine:** `--capabilities` is the authoritative machine-readable surface. The engine must reject unsupported versions and malformed or semantically invalid input before reporting a successful run. -- **Scenario:** Frozen scenario and stream artifacts do not change. The current v14 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v15 contract may receive additive changes only when old valid inputs retain their meaning; breaking changes need a new version. Transitional v3 support remains explicit in `--capabilities`. - **Journal:** A run emits the journal version paired with its accepted scenario. Record ordering, causal references, scenario hashing, terminal completion, and exact accounting remain runtime invariants even when JSON Schema cannot express them. - **Strategy:** Protocol and transcript versions are independent of scenario versions. The current - external boundary is strategy v12; a host must complete its exact initialization, event, + external boundary is strategy v13; a host must complete its exact initialization, event, shutdown, timeout, and rejection lifecycle. - **Persistra:** The required integration gate uses a full Persistra commit and its v3 scenario, journal, and strategy integration tests. Passing that gate claims compatibility only for the diff --git a/docs/scenario.md b/docs/scenario.md index fe9e0a5..64b50cb 100644 --- a/docs/scenario.md +++ b/docs/scenario.md @@ -4,8 +4,8 @@ A replay scenario uses either one strict JSON object or a strict JSON Lines stre weights, quantities, money, and sequences are canonical JSON strings. Counts and basis points are JSON integers. Unknown, missing, duplicate, noncanonical, and non-finite values fail parsing. -Use [the v14 demo](../contracts/v14/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v14/scenario.schema.json) provides structural validation. +Use [the v15 demo](../contracts/v15/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v15/scenario.schema.json) provides structural validation. The engine parser also enforces cross-field and cross-record invariants. Diagnostics identify the failed field or array item. Stream diagnostics additionally retain the record line and sequence. @@ -32,8 +32,8 @@ The batch object and stream header share one domain-construction path and the sa checks. Stream items reuse the batch slice and intent validators directly; no synthetic batch scenario is constructed. -The [stream record JSON Schema](../contracts/v14/scenario-stream.schema.json) validates each line, -and [the v14 stream fixture](../contracts/v14/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v15/scenario-stream.schema.json) validates each line, +and [the v15 stream fixture](../contracts/v15/fixtures/demo.scenario.jsonl) is the canonical example. The engine validates the entire stream before creating a journal. It then replays one record at a time without retaining prior slices, scheduled batches, or audit events. Reducer state still retains current account, order, target, and latest-bar state required by execution semantics. @@ -42,7 +42,7 @@ retains current account, order, target, and latest-bar state required by executi | Field | Meaning | |---|---| -| `contract_version` | Required string identifying this file contract; v14 is `"14"` | +| `contract_version` | Required string identifying this file contract; v15 is `"15"` | | `metadata` | Required arbitrary JSON object preserved for provenance and ignored by execution | | `run_id` | Stable identity used in generated IDs | | `base_currency` | Reporting currency used for aggregate risk and valuation | @@ -268,15 +268,25 @@ records `event_at`, `available_at`, `received_at`, and a positive `ingest_sequen strictly ordered by availability, receipt, and ingest sequence; economic time cannot follow availability, and no event may escape its containing slice's time or observability boundary. Prices and quantities align to the instrument tick and lot. The -[`quote-trade` fixture](../contracts/v14/fixtures/quote-trade.scenario.json) demonstrates passive +[`quote-trade` fixture](../contracts/v15/fixtures/quote-trade.scenario.json) demonstrates passive fills and has an equivalent bounded JSON Lines replay. +Version 15 slices add `order_book_events`. Every configured instrument supplies a fresh full +snapshot followed by contiguous absolute `set`, `delete`, and aggressor-classified `trade` updates. +Snapshots contain price-ordered unique bid and ask levels. Crossed states are invalid, while locked +books are accepted. Runtime validation enforces the configured `max_depth_levels`, known levels on +delete, sequence continuity, slice observability, and tick/lot alignment. Marketable orders walk +the visible book; passive limits queue behind displayed same-price depth, with reductions moving +them forward and additions joining behind. The +[`order-book` fixture](../contracts/v15/fixtures/order-book.scenario.json) demonstrates bounded +queue replay and has equivalent JSON Lines and journal artifacts. + For causal next-open execution, an order-changing schedule entry's anchor `received_at` is no later than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v14/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v15/journal.schema.json) validates each JSON Lines record. Every record contains `contract_version`, `engine_sequence`, deterministic `event_id`, ordered `causation_ids`, `run_id`, `recorded_at`, `event_type`, and an event-specific `payload`. Causal references are unique prior event IDs from the same run. The version is repeated on every record diff --git a/lib/codec.ml b/lib/codec.ml index 2ec80da..238a9d5 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -389,6 +389,57 @@ let market_event_to_yojson event = string (Market_event.aggressor_side_to_string aggressor_side) ); ]) +let order_book_level_to_yojson level = + `Assoc + [ + ("price", price level.Order_book_event.price); + ("quantity", quantity level.quantity); + ] + +let order_book_event_to_yojson event = + let common = + [ + ("instrument_id", instrument_id event.Order_book_event.instrument_id); + ("event_at", timestamp event.event_at); + ("available_at", timestamp event.available_at); + ("received_at", timestamp event.received_at); + ("ingest_sequence", int64 event.ingest_sequence); + ("book_sequence", int64 event.book_sequence); + ] + in + match event.kind with + | Order_book_event.Snapshot { bids; asks } -> + `Assoc + ((("type", string "snapshot") :: common) + @ [ + ("bids", `List (List.map order_book_level_to_yojson bids)); + ("asks", `List (List.map order_book_level_to_yojson asks)); + ]) + | Set { side; price = value; quantity = size } -> + `Assoc + ((("type", string "set") :: common) + @ [ + ("side", string (Order_book_event.side_to_string side)); + ("price", price value); + ("quantity", quantity size); + ]) + | Delete { side; price = value } -> + `Assoc + ((("type", string "delete") :: common) + @ [ + ("side", string (Order_book_event.side_to_string side)); + ("price", price value); + ]) + | Trade { price = value; quantity = size; aggressor_side } -> + `Assoc + ((("type", string "trade") :: common) + @ [ + ("price", price value); + ("quantity", quantity size); + ( "aggressor_side", + string (Market_event.aggressor_side_to_string aggressor_side) ); + ]) + let versioned_market_slice_to_yojson ~contract_version market_slice = `Assoc [ @@ -406,9 +457,9 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ] |> function | `Assoc fields - when List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] -> + when List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] -> let settlement = - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then [ ( "settlement_failures", `List @@ -418,7 +469,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = else [] in let lifecycle = - if List.mem contract_version [ "14"; "13"; "12" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12" ] then [ ( "lifecycle_events", `List @@ -428,7 +479,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = else [] in let market_events = - if String.equal contract_version "14" then + if List.mem contract_version [ "15"; "14" ] then [ ( "market_events", `List @@ -437,6 +488,16 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ] else [] in + let order_book_events = + if String.equal contract_version "15" then + [ + ( "order_book_events", + `List + (List.map order_book_event_to_yojson + market_slice.Market_slice.order_book_events) ); + ] + else [] + in `Assoc (fields @ [ @@ -449,7 +510,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = (List.map cash_rate_observation_to_yojson market_slice.Market_slice.cash_rate_observations) ); ] - @ settlement @ lifecycle @ market_events) + @ settlement @ lifecycle @ market_events @ order_book_events) | json -> json let market_slice_to_yojson market_slice = @@ -470,6 +531,9 @@ let market_slice_to_yojson_v13 market_slice = let market_slice_to_yojson_v14 market_slice = versioned_market_slice_to_yojson ~contract_version:"14" market_slice +let market_slice_to_yojson_v15 market_slice = + versioned_market_slice_to_yojson ~contract_version:"15" market_slice + let request_fields request = let kind, limit_price = match request.Order.kind with @@ -573,8 +637,8 @@ let order_to_yojson_v8 order = ]) let versioned_order_to_yojson ~contract_version order = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] then - order_to_yojson_v8 order + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + then order_to_yojson_v8 order else order_to_yojson order let fill_to_yojson fill = @@ -771,7 +835,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "cash_balances", `List (List.map - (if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then cash_attribution_to_yojson_v11 else if String.equal contract_version "10" then cash_attribution_to_yojson_v10 @@ -780,7 +844,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "positions", `List (List.map - (if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then position_attribution_to_yojson_v11 else if String.equal contract_version "9" @@ -791,14 +855,15 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ] |> function | `Assoc fields - when List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9" ] -> + when List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9" ] + -> let financing = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then [ ("cash_interest", money valuation.Account.cash_interest) ] else [] in let settlement = - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then [ ("settled_cash", money valuation.Account.settled_cash); ("unsettled_cash", money valuation.unsettled_cash); @@ -845,7 +910,9 @@ let valuation_to_yojson ~contract_version valuation = | `Assoc fields -> let fields = fields @ [ ("margin", margin_to_yojson valuation.margin) ] in let fields = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + if + List.mem contract_version + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] then fields @ [ @@ -987,8 +1054,8 @@ let payload_to_yojson ~contract_version = function ("final_price", price attribution.final_price); ] | Audit.Fill_applied fill -> - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9" ] then - fill_to_yojson_v9 fill + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9" ] + then fill_to_yojson_v9 fill else fill_to_yojson fill | Audit.Settlement_instruction_created instruction | Audit.Settlement_completed instruction diff --git a/lib/codec.mli b/lib/codec.mli index a097970..12ad70c 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -9,6 +9,7 @@ val market_slice_to_yojson_v11 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v12 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v13 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v14 : Market_slice.t -> Yojson.Safe.t +val market_slice_to_yojson_v15 : Market_slice.t -> Yojson.Safe.t val order_to_yojson : Order.t -> Yojson.Safe.t val order_to_yojson_v8 : Order.t -> Yojson.Safe.t val fill_to_yojson : Fill.t -> Yojson.Safe.t diff --git a/lib/contract.ml b/lib/contract.ml index da5fc9b..f901c21 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,11 +1,12 @@ -let version = "14" -let previous_version = "13" +let version = "15" +let previous_version = "14" let legacy_journal_version = "3" let supported_versions = [ version; previous_version; + "13"; "12"; "11"; "10"; @@ -19,8 +20,8 @@ let supported_versions = ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "12" -let previous_strategy_protocol_version = "11" +let strategy_protocol_version = "13" +let previous_strategy_protocol_version = "12" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -39,6 +40,7 @@ let capabilities_to_yojson () = [ strategy_protocol_version; previous_strategy_protocol_version; + "11"; "10"; "9"; "8"; diff --git a/lib/engine.ml b/lib/engine.ml index c5443b0..49af7ba 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -27,6 +27,10 @@ let make_config ~venue_calendars ~contract_version ~risk ~execution_model [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] && Option.is_none (Execution.cost_model execution) then Error "execution model and pricing configuration are incompatible" + else if + String.equal (Execution_model.name execution_model) "order_book_v1" + <> Option.is_some (Execution.book_depth_limit execution) + then Error "execution model and order-book configuration are incompatible" else if max_internal_events <= 0 then Error "maximum internal events must be positive" else if max_internal_events > Resource_limits.internal_events then @@ -70,6 +74,7 @@ let config_v11 ~contract_version ~risk ~venue_calendars ~execution_model let config_v12 = config_v11 let config_v13 = config_v12 let config_v14 = config_v13 +let config_v15 = config_v14 let valid_sha256 value = String.length value = 64 diff --git a/lib/engine.mli b/lib/engine.mli index 4849421..ca85780 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -73,6 +73,17 @@ val config_v14 : max_internal_events:int -> (config, string) result +val config_v15 : + contract_version:string -> + risk:Risk.t -> + venue_calendars:Venue_calendar.t list -> + execution_model:Execution_model.t -> + execution:Execution.t -> + financing:Financing.policy -> + settlement:Settlement.policy -> + max_internal_events:int -> + (config, string) result + module Interactive : sig type t type progress diff --git a/lib/execution.ml b/lib/execution.ml index 1a4fca0..4e60910 100644 --- a/lib/execution.ml +++ b/lib/execution.ml @@ -14,6 +14,7 @@ type t = { participation_bps : int; fee_configuration : fee_configuration; cost_model : cost_model option; + book_depth_limit : int option; } type price_attribution = { @@ -67,6 +68,7 @@ let create ~participation_bps ~fixed_fee ~fee_bps = participation_bps; fee_configuration = Legacy { fixed_fee; fee_bps }; cost_model = None; + book_depth_limit = None; } let create_v2 ~participation_bps ~fee_schedules = @@ -91,6 +93,7 @@ let create_v2 ~participation_bps ~fee_schedules = participation_bps; fee_configuration = Schedules schedules; cost_model = None; + book_depth_limit = None; }) (List.fold_left add (Ok Id.Instrument.Map.empty) fee_schedules) @@ -111,7 +114,16 @@ let create_conservative ~participation_bps ~fee_schedules ~half_spread_bps }) (create_v2 ~participation_bps ~fee_schedules) +let create_order_book ~participation_bps ~fee_schedules ~max_depth_levels = + if max_depth_levels <= 0 || max_depth_levels > 1024 then + Error "order-book depth limit must be between 1 and 1024" + else + Result.map + (fun state -> { state with book_depth_limit = Some max_depth_levels }) + (create_v2 ~participation_bps ~fee_schedules) + let participation_bps state = state.participation_bps +let book_depth_limit state = state.book_depth_limit let fixed_fee state = match state.fee_configuration with @@ -810,6 +822,668 @@ let start_slice_quote_trade state ~instruments ~oms in Ok (make_events events) +type book_state = { + book_sequence : int64; + bids : Order_book_event.level list; + asks : Order_book_event.level list; +} + +type book_view = + | Book_snapshot of book_state + | Book_added of Order_book_event.side * Scalar.Price.t * Scalar.Quantity.t + | Book_reduced of Order_book_event.side * Scalar.Price.t * Scalar.Quantity.t + | Book_trade of + Scalar.Price.t * Scalar.Quantity.t * Market_event.aggressor_side + +let book_level_quantity price levels = + List.find_opt + (fun (level : Order_book_event.level) -> + Scalar.Price.compare level.price price = 0) + levels + |> Option.map (fun level -> level.Order_book_event.quantity) + |> Option.value ~default:Scalar.Quantity.zero + +let sort_book_levels side levels = + List.sort + (fun (left : Order_book_event.level) right -> + let comparison = Scalar.Price.compare left.price right.price in + match side with + | Order_book_event.Bid -> -comparison + | Order_book_event.Ask -> comparison) + levels + +let replace_book_level side price quantity levels = + let level = Order_book_event.level ~price ~quantity |> Result.get_ok in + level + :: List.filter + (fun (existing : Order_book_event.level) -> + Scalar.Price.compare existing.price price <> 0) + levels + |> sort_book_levels side + +let remove_book_level price levels = + List.filter + (fun (level : Order_book_event.level) -> + Scalar.Price.compare level.price price <> 0) + levels + +let consume_book_levels price quantity levels = + let rec consume reversed = function + | [] -> Error "order-book execution level disappeared" + | (level : Order_book_event.level) :: remaining -> + if Scalar.Price.compare level.price price <> 0 then + consume (level :: reversed) remaining + else if Scalar.Quantity.compare quantity level.quantity > 0 then + Error "applied fill quantity exceeds order-book liquidity" + else if Scalar.Quantity.compare quantity level.quantity = 0 then + Ok (List.rev_append reversed remaining) + else + let* remaining_quantity = + Scalar.Quantity.subtract level.quantity quantity + in + let* level = + Order_book_event.level ~price:level.price + ~quantity:remaining_quantity + in + Ok (List.rev_append reversed (level :: remaining)) + in + consume [] levels + +let consume_book_view order price quantity = function + | Book_snapshot book -> ( + match order.Order.request.side with + | Buy -> + Result.map + (fun asks -> Book_snapshot { book with asks }) + (consume_book_levels price quantity book.asks) + | Sell -> + Result.map + (fun bids -> Book_snapshot { book with bids }) + (consume_book_levels price quantity book.bids)) + | Book_added (side, added_price, available) + when Scalar.Price.compare price added_price = 0 -> + if Scalar.Quantity.compare quantity available > 0 then + Error "applied fill quantity exceeds order-book liquidity" + else if Scalar.Quantity.compare quantity available = 0 then + Ok (Book_added (side, added_price, Scalar.Quantity.zero)) + else + let* remaining = Scalar.Quantity.subtract available quantity in + Ok (Book_added (side, added_price, remaining)) + | view -> Ok view + +let valid_book depth_limit book = + List.length book.bids <= depth_limit + && List.length book.asks <= depth_limit + && + match (book.bids, book.asks) with + | bid :: _, ask :: _ -> Scalar.Price.compare bid.price ask.price <= 0 + | _ -> true + +let consume_feed_trade aggressor price quantity book = + let eligible level = + match aggressor with + | Market_event.Buy -> + Scalar.Price.compare level.Order_book_event.price price <= 0 + | Sell -> Scalar.Price.compare level.price price >= 0 + | Unknown -> false + in + let rec consume remaining consumed = function + | levels when Scalar.Quantity.is_zero remaining -> + Ok (List.rev_append consumed levels) + | level :: levels when eligible level -> + if Scalar.Quantity.compare level.quantity remaining <= 0 then + let* remaining = Scalar.Quantity.subtract remaining level.quantity in + consume remaining consumed levels + else + let* quantity = Scalar.Quantity.subtract level.quantity remaining in + let* level = Order_book_event.level ~price:level.price ~quantity in + Ok (List.rev_append consumed (level :: levels)) + | _ -> Error "order-book trade exceeds observable depth" + in + match aggressor with + | Market_event.Buy -> + Result.map + (fun asks -> { book with asks }) + (consume quantity [] book.asks) + | Sell -> + Result.map + (fun bids -> { book with bids }) + (consume quantity [] book.bids) + | Unknown -> Ok book + +let start_slice_order_book state ~instruments ~oms + (market_slice : Market_slice.t) = + let depth_limit = Option.value state.book_depth_limit ~default:0 in + if depth_limit = 0 then Error "order-book execution configuration is required" + else + let instrument_map = + List.fold_left + (fun map instrument -> + Id.Instrument.Map.add instrument.Instrument.id instrument map) + Id.Instrument.Map.empty instruments + in + let validate_event instrument (event : Order_book_event.t) = + let prices, quantities = + match event.kind with + | Snapshot { bids; asks } -> + ( List.map (fun level -> level.Order_book_event.price) (bids @ asks), + List.map + (fun level -> level.Order_book_event.quantity) + (bids @ asks) ) + | Set { price; quantity; _ } -> ([ price ], [ quantity ]) + | Delete { price; _ } -> ([ price ], []) + | Trade { price; quantity; _ } -> ([ price ], [ quantity ]) + in + if + not + (List.for_all + (fun price -> + Scalar.Price.is_multiple price + ~tick:instrument.Instrument.tick_size) + prices) + then Error "order-book price is not aligned to the instrument tick size" + else if + not + (List.for_all + (fun quantity -> + Scalar.Quantity.is_multiple quantity ~lot:instrument.lot_size) + quantities) + then Error "order-book quantity is not aligned to the instrument lot size" + else if + Ptime.compare event.event_at market_slice.start_at < 0 + || Ptime.compare event.event_at market_slice.end_at > 0 + || Ptime.compare event.available_at market_slice.available_at > 0 + || Ptime.compare event.received_at market_slice.received_at > 0 + then Error "order-book event falls outside its observable slice boundary" + else Ok () + in + let prepare (books, prepared) (event : Order_book_event.t) = + let* instrument = + match Id.Instrument.Map.find_opt event.instrument_id instrument_map with + | Some instrument -> Ok instrument + | None -> Error "order-book event refers to an unknown instrument" + in + let* () = validate_event instrument event in + let prior = Id.Instrument.Map.find_opt event.instrument_id books in + let* book, view = + match (prior, event.kind) with + | None, Snapshot { bids; asks } -> + let book = { book_sequence = event.book_sequence; bids; asks } in + if valid_book depth_limit book then Ok (book, Book_snapshot book) + else Error "order-book snapshot exceeds depth or crosses" + | Some _, Snapshot _ -> + Error "order-book bundle contains more than one snapshot" + | None, _ -> Error "order-book bundle must begin with a snapshot" + | Some prior, kind -> ( + if Int64.succ prior.book_sequence <> event.book_sequence then + Error "order-book sequences must be contiguous" + else + let next_sequence book = + { book with book_sequence = event.book_sequence } + in + match kind with + | Set { side; price; quantity } -> + let levels = + match side with + | Bid -> prior.bids + | Order_book_event.Ask -> prior.asks + in + let old_quantity = book_level_quantity price levels in + let levels = replace_book_level side price quantity levels in + let book = + match side with + | Order_book_event.Bid -> + next_sequence { prior with bids = levels } + | Order_book_event.Ask -> + next_sequence { prior with asks = levels } + in + if not (valid_book depth_limit book) then + Error "order-book update exceeds depth or crosses" + else if Scalar.Quantity.compare quantity old_quantity > 0 then + let* added = + Scalar.Quantity.subtract quantity old_quantity + in + Ok (book, Book_added (side, price, added)) + else + let* removed = + Scalar.Quantity.subtract old_quantity quantity + in + Ok (book, Book_reduced (side, price, removed)) + | Delete { side; price } -> + let levels = + match side with + | Bid -> prior.bids + | Order_book_event.Ask -> prior.asks + in + let old_quantity = book_level_quantity price levels in + if Scalar.Quantity.is_zero old_quantity then + Error "order-book delete refers to a missing level" + else + let levels = remove_book_level price levels in + let book = + match side with + | Order_book_event.Bid -> + next_sequence { prior with bids = levels } + | Order_book_event.Ask -> + next_sequence { prior with asks = levels } + in + Ok (book, Book_reduced (side, price, old_quantity)) + | Trade { price; quantity; aggressor_side } -> + let* book = + consume_feed_trade aggressor_side price quantity prior + in + Ok + ( next_sequence book, + Book_trade (price, quantity, aggressor_side) ) + | Snapshot _ -> assert false) + in + Ok + ( Id.Instrument.Map.add event.instrument_id book books, + (event, instrument, view) :: prepared ) + in + let* books, reversed = + List.fold_left + (fun result event -> + Result.bind result (fun state -> prepare state event)) + (Ok (Id.Instrument.Map.empty, [])) + market_slice.order_book_events + in + let expected = + List.map (fun instrument -> instrument.Instrument.id) instruments + |> Id.Instrument.Set.of_list + in + let observed = + Id.Instrument.Map.fold + (fun instrument_id _ ids -> Id.Instrument.Set.add instrument_id ids) + books Id.Instrument.Set.empty + in + if not (Id.Instrument.Set.equal expected observed) then + Error "order-book snapshots must cover every configured instrument" + else + let events = List.rev reversed in + let eligible = + Oms.active_orders oms + |> List.filter (fun order -> + Int64.compare order.Order.eligible_after_slice_sequence + market_slice.slice_sequence + < 0 + && Ptime.compare order.created_at market_slice.start_at <= 0) + |> List.sort compare_execution_order + in + let order_ids = List.map (fun order -> order.Order.id) eligible in + let market_ioc_orders = + List.filter_map + (fun order -> + if Order.is_ioc order && not (Order.is_dormant_stop order) then + Some order.Order.id + else None) + eligible + in + let queue_for_snapshot queues instrument book = + let rec build prior queues = function + | [] -> Ok queues + | order :: remaining -> ( + match Order.effective_kind order with + | Some (Order.Limit limit) + when Id.Instrument.equal order.request.instrument_id + instrument.Instrument.id -> + let opposite = + match order.request.side with + | Buy -> book.asks + | Sell -> book.bids + in + let marketable = + match opposite with + | [] -> false + | best :: _ -> ( + match order.request.side with + | Buy -> Scalar.Price.compare best.price limit <= 0 + | Sell -> Scalar.Price.compare best.price limit >= 0) + in + if marketable then build (order :: prior) queues remaining + else + let same_side = + match order.request.side with + | Buy -> book.bids + | Sell -> book.asks + in + let external_quantity = + book_level_quantity limit same_side + in + let* ahead = + List.fold_left + (fun result earlier -> + let* ahead = result in + match Order.effective_kind earlier with + | Some (Order.Limit earlier_limit) + when earlier.request.side = order.request.side + && Id.Instrument.equal + earlier.request.instrument_id + order.request.instrument_id + && Scalar.Price.compare earlier_limit limit = 0 + -> + Scalar.Quantity.add ahead + (Order.remaining_quantity earlier) + | _ -> Ok ahead) + (Ok external_quantity) prior + in + build (order :: prior) + (Id.Order.Map.add order.id ahead queues) + remaining + | _ -> build (order :: prior) queues remaining) + in + build [] queues eligible + in + let order_matches_level order side price = + match Order.effective_kind order with + | Some Order.Market -> + (order.request.side = Buy && side = Order_book_event.Ask) + || (order.request.side = Sell && side = Order_book_event.Bid) + | Some (Limit limit) -> + order.request.side = Buy + && side = Order_book_event.Ask + && Scalar.Price.compare price limit <= 0 + || order.request.side = Sell + && side = Order_book_event.Bid + && Scalar.Price.compare price limit >= 0 + | _ -> false + in + let levels_for_order order view = + match view with + | Book_snapshot book -> + let levels = + match order.Order.request.side with + | Buy -> book.asks + | Sell -> book.bids + in + List.filter + (fun level -> + order_matches_level order + (match order.request.side with + | Buy -> Order_book_event.Ask + | Sell -> Order_book_event.Bid) + level.Order_book_event.price) + levels + | Book_added (side, price, quantity) + when order_matches_level order side price + && not (Scalar.Quantity.is_zero quantity) -> + [ Order_book_event.level ~price ~quantity |> Result.get_ok ] + | _ -> [] + in + let reduce_queue queues instrument_id side price removed = + Id.Order.Map.mapi + (fun order_id ahead -> + match Oms.find oms order_id with + | Some order + when Id.Instrument.equal order.request.instrument_id instrument_id + && (match order.request.side with + | Buy -> side = Order_book_event.Bid + | Sell -> side = Order_book_event.Ask) + && + match Order.effective_kind order with + | Some (Limit limit) -> Scalar.Price.compare limit price = 0 + | _ -> false -> + if Scalar.Quantity.compare removed ahead >= 0 then + Scalar.Quantity.zero + else Scalar.Quantity.subtract ahead removed |> Result.get_ok + | _ -> ahead) + queues + in + let trade_allowances queues instrument_id price quantity aggressor = + Id.Order.Map.fold + (fun order_id ahead (queues, allowances) -> + match Oms.find oms order_id with + | Some order + when Id.Instrument.equal order.request.instrument_id instrument_id + && (match (order.request.side, aggressor) with + | Buy, Market_event.Sell | Sell, Buy -> true + | _ -> false) + && + match Order.effective_kind order with + | Some (Limit limit) -> ( + match order.request.side with + | Buy -> Scalar.Price.compare price limit <= 0 + | Sell -> Scalar.Price.compare price limit >= 0) + | _ -> false -> + let next_ahead = + if Scalar.Quantity.compare quantity ahead >= 0 then + Scalar.Quantity.zero + else Scalar.Quantity.subtract ahead quantity |> Result.get_ok + in + let through = + if Scalar.Quantity.compare quantity ahead <= 0 then + Scalar.Quantity.zero + else Scalar.Quantity.subtract quantity ahead |> Result.get_ok + in + ( Id.Order.Map.add order_id next_ahead queues, + Id.Order.Map.add order_id through allowances ) + | _ -> (queues, allowances)) + queues + (queues, Id.Order.Map.empty) + in + let rec make_events queues = function + | [] -> Ok (cursor (fun ~oms:_ -> Ok (Finished market_ioc_orders))) + | ((event : Order_book_event.t), instrument, view) :: remaining_events + -> + let* queues, allowances = + match view with + | Book_snapshot book -> + Result.map + (fun queues -> (queues, Id.Order.Map.empty)) + (queue_for_snapshot queues instrument book) + | Book_reduced (side, price, removed) -> + Ok + ( reduce_queue queues event.instrument_id side price removed, + Id.Order.Map.empty ) + | Book_trade (price, quantity, aggressor) -> + Ok + (trade_allowances queues event.instrument_id price quantity + aggressor) + | Book_added _ -> Ok (queues, Id.Order.Map.empty) + in + Ok + (make_orders queues allowances event instrument view order_ids + remaining_events) + and make_orders queues allowances event instrument view remaining + remaining_events = + Cursor + (fun current_oms -> + match remaining with + | [] -> + let* cursor = make_events queues remaining_events in + let (Cursor next) = cursor in + next current_oms + | order_id :: remaining_orders -> ( + match Oms.find current_oms order_id with + | None -> + Error "eligible order disappeared during order-book replay" + | Some order when not (Order.is_active order) -> + let (Cursor next) = + make_orders queues allowances event instrument view + remaining_orders remaining_events + in + next current_oms + | Some order + when not + (Id.Instrument.equal order.request.instrument_id + event.Order_book_event.instrument_id) -> + let (Cursor next) = + make_orders queues allowances event instrument view + remaining_orders remaining_events + in + next current_oms + | Some order when Order.is_dormant_stop order -> + let observed = + match view with + | Book_snapshot book -> ( + match order.request.side with + | Buy -> ( + match book.asks with + | level :: _ -> Some level.Order_book_event.price + | [] -> None) + | Sell -> ( + match book.bids with + | level :: _ -> Some level.Order_book_event.price + | [] -> None)) + | Book_added (_, price, _) + | Book_reduced (_, price, _) + | Book_trade (price, _, _) -> + Some price + in + let triggered = + match + (order.request.kind, order.request.side, observed) + with + | ( ( Stop trigger + | Stop_limit { trigger_price = trigger; _ } ), + Buy, + Some price ) -> + Scalar.Price.compare price trigger >= 0 + | ( ( Stop trigger + | Stop_limit { trigger_price = trigger; _ } ), + Sell, + Some price ) -> + Scalar.Price.compare price trigger <= 0 + | _ -> false + in + let continuation = + make_orders queues allowances event instrument view + remaining_orders remaining_events + in + if triggered then + Ok + (Triggered + ( order.id, + event.event_at, + market_slice.slice_sequence, + continuation )) + else + let (Cursor next) = continuation in + next current_oms + | Some order -> ( + let levels = levels_for_order order view in + let passive = Id.Order.Map.find_opt order.id allowances in + let* fok_capacity = + List.fold_left + (fun result (level : Order_book_event.level) -> + let* total = result in + let* capacity = + event_capacity state instrument level.quantity + in + Scalar.Quantity.add total capacity) + (Ok Scalar.Quantity.zero) levels + in + let opportunity = + match (levels, passive, view) with + | level :: _, _, _ -> + Some + ( level.price, + level.quantity, + Fee_schedule.Taker, + true ) + | [], Some quantity, Book_trade (price, _, _) -> + Some (price, quantity, Fee_schedule.Maker, false) + | _ -> None + in + match opportunity with + | None -> + let (Cursor next) = + make_orders queues allowances event instrument view + remaining_orders remaining_events + in + next current_oms + | Some (price, available, fee_liquidity, repeat_order) -> + let* capacity = + event_capacity state instrument available + in + let quantity = + Scalar.Quantity.minimum capacity + (Order.remaining_quantity order) + in + if + Scalar.Quantity.is_zero quantity + || Order.is_fok order + && Scalar.Quantity.compare + (if levels = [] then capacity + else fok_capacity) + (Order.remaining_quantity order) + < 0 + then + let (Cursor next) = + make_orders queues allowances event instrument view + remaining_orders remaining_events + in + next current_oms + else + let* notional = + Scalar.Money.notional price quantity + in + let* fee_components, fee = + calculate_fee state ~instrument ~notional ~quantity + ~liquidity:fee_liquidity + ~fx_rates: + (List.map + (fun mark -> + (mark.Market_slice.currency, mark.rate)) + market_slice.fx_rates) + in + let proposed = + { + order_id = order.id; + quantity; + price; + fee; + fee_components; + liquidity = fee_liquidity; + executed_at = event.event_at; + price_attribution = None; + } + in + let continue applied_quantity = + if + Scalar.Quantity.compare applied_quantity quantity + > 0 + then + Error + "applied fill quantity exceeds order-book \ + liquidity" + else if + Scalar.Quantity.compare applied_quantity + Scalar.Quantity.zero + < 0 + then + Error "applied fill quantity must be nonnegative" + else if + not + (Scalar.Quantity.is_multiple applied_quantity + ~lot:instrument.Instrument.lot_size) + then + Error + "applied fill quantity is not aligned to the \ + instrument lot size" + else + let* next_view = + if repeat_order then + consume_book_view order price applied_quantity + view + else Ok view + in + let next_orders = + if + repeat_order + && not + (Scalar.Quantity.is_zero applied_quantity) + then order_id :: remaining_orders + else remaining_orders + in + Ok + (make_orders queues allowances event instrument + next_view next_orders remaining_events) + in + Ok (Proposed (proposed, continue))))) + in + make_events Id.Order.Map.empty events + let finished market_ioc_orders = cursor (fun ~oms:_ -> Ok (Finished market_ioc_orders)) diff --git a/lib/execution.mli b/lib/execution.mli index 8ba8765..da67d1c 100644 --- a/lib/execution.mli +++ b/lib/execution.mli @@ -62,7 +62,14 @@ val create_conservative : missing_volume_policy:missing_volume_policy -> (t, string) result +val create_order_book : + participation_bps:int -> + fee_schedules:Fee_schedule.t list -> + max_depth_levels:int -> + (t, string) result + val participation_bps : t -> int +val book_depth_limit : t -> int option val fixed_fee : t -> Scalar.Money.t val fee_bps : t -> int val fee_schedules : t -> Fee_schedule.t list @@ -107,6 +114,13 @@ val start_slice_quote_trade : Market_slice.t -> (cursor, string) result +val start_slice_order_book : + t -> + instruments:Instrument.t list -> + oms:Oms.t -> + Market_slice.t -> + (cursor, string) result + val finished : Id.Order.t list -> cursor (** Build a cursor that immediately finishes. This supports execution models that intentionally produce no proposals. *) diff --git a/lib/execution_model.ml b/lib/execution_model.ml index ddd6154..eef0033 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -42,6 +42,11 @@ module Quote_trade_v1 = struct let start_slice = Execution.start_slice_quote_trade end +module Order_book_v1 = struct + let name = "order_book_v1" + let start_slice = Execution.start_slice_order_book +end + let of_module model = model let name (module Model : S) = Model.name @@ -51,6 +56,7 @@ let builtins : t list = (module Completed_bar_next_open_v1); (module Completed_bar_adverse_touch_v1); (module Quote_trade_v1); + (module Order_book_v1); ] let supported = List.map name builtins @@ -60,7 +66,7 @@ let completed_bar_v1_contract = version = "2"; previous_versions = [ "1" ]; scenario_contract_versions = - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; legacy_required_fields = [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ]; @@ -81,7 +87,7 @@ let conservative_contract = { version = "1"; previous_versions = []; - scenario_contract_versions = [ "14"; "13" ]; + scenario_contract_versions = [ "15"; "14"; "13" ]; required_fields = [ "version"; @@ -110,7 +116,7 @@ let quote_trade_contract = { version = "1"; previous_versions = []; - scenario_contract_versions = [ "14" ]; + scenario_contract_versions = [ "15"; "14" ]; required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; legacy_required_fields = []; supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; @@ -128,12 +134,39 @@ let quote_trade_contract = ]; } +let order_book_contract = + { + version = "1"; + previous_versions = []; + scenario_contract_versions = [ "15" ]; + required_fields = + [ "version"; "participation_bps"; "fee_schedules"; "max_depth_levels" ]; + legacy_required_fields = []; + supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; + data_requirements = + [ + "slice_open_level_two_snapshot"; + "contiguous_absolute_level_updates"; + "aggressor_classified_depth_consuming_trades"; + "completed_bars_for_valuation"; + ]; + limits = + `Assoc + [ + ( "participation_bps", + `Assoc [ ("minimum", `Int 0); ("maximum", `Int 10_000) ] ); + ( "max_depth_levels", + `Assoc [ ("minimum", `Int 1); ("maximum", `Int 1024) ] ); + ]; + } + let configuration_contract model = match name model with | "completed_bar_v1" -> completed_bar_v1_contract | "completed_bar_next_open_v1" | "completed_bar_adverse_touch_v1" -> conservative_contract | "quote_trade_v1" -> quote_trade_contract + | "order_book_v1" -> order_book_contract | unsupported -> invalid_arg (Printf.sprintf "execution model %S has no configuration contract" diff --git a/lib/external_replay.ml b/lib/external_replay.ml index 23c422c..1c95491 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -105,7 +105,11 @@ let create_runner ~contract_version ~run_id ~scenario_sha256 ~risk Engine.config_v10 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~max_internal_events | Some financing, Some settlement -> - if String.equal contract_version "14" then + if String.equal contract_version "15" then + Engine.config_v15 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else if String.equal contract_version "14" then Engine.config_v14 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events diff --git a/lib/market_slice.ml b/lib/market_slice.ml index e9a1613..130efe0 100644 --- a/lib/market_slice.ml +++ b/lib/market_slice.ml @@ -8,6 +8,7 @@ type t = { received_at : Ptime.t; bars : Bar.t list; market_events : Market_event.t list; + order_book_events : Order_book_event.t list; fx_rates : fx_mark list; corporate_actions : Corporate_action.t list; lifecycle_events : Instrument_lifecycle.event list; @@ -32,10 +33,10 @@ let fx_mark ~currency ~rate = let compare_bar left right = Id.Instrument.compare left.Bar.instrument_id right.Bar.instrument_id -let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at +let create_v15 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations ~settlement_failures ~lifecycle_events - ~market_events = + ~market_events ~order_book_events = if Int64.compare slice_sequence 0L <= 0 then Error "market slice sequence must be positive" else if Ptime.compare start_at end_at >= 0 then @@ -134,6 +135,15 @@ let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at < 0 && ordered_events remaining in + let rec ordered_book_events = function + | [] | [ _ ] -> true + | left :: (right :: _ as remaining) -> + Order_book_event.compare_replay_order left right < 0 + && Int64.compare left.Order_book_event.ingest_sequence + right.Order_book_event.ingest_sequence + < 0 + && ordered_book_events remaining + in if not (unique bars) then Error "market slice must contain one bar per instrument" else if fx_rates = [] then Error "market slice must contain FX rates" @@ -153,6 +163,10 @@ let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at Error "market events must be strictly ordered by availability, receipt, and \ ingest sequence" + else if not (ordered_book_events order_book_events) then + Error + "order-book events must be strictly ordered by availability, receipt, \ + and ingest sequence" else Ok { @@ -163,6 +177,7 @@ let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at received_at; bars; market_events; + order_book_events; fx_rates; corporate_actions; lifecycle_events; @@ -171,12 +186,21 @@ let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at settlement_failures; } +let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at + ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations ~settlement_failures ~lifecycle_events + ~market_events = + create_v15 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars + ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations + ~settlement_failures ~lifecycle_events ~market_events ~order_book_events:[] + let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations ~settlement_failures ~lifecycle_events = - create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars + create_v15 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations ~settlement_failures ~lifecycle_events ~market_events:[] + ~order_book_events:[] let create_v13 = create_v12 @@ -216,10 +240,11 @@ let compare_replay_order left right = let pp formatter state = Format.fprintf formatter - "slice[%Ld] bars=%d events=%d fx=%d actions=%d lifecycle=%d borrow=%d \ - cash_rates=%d failures=%d" + "slice[%Ld] bars=%d events=%d book_events=%d fx=%d actions=%d lifecycle=%d \ + borrow=%d cash_rates=%d failures=%d" state.slice_sequence (List.length state.bars) (List.length state.market_events) + (List.length state.order_book_events) (List.length state.fx_rates) (List.length state.corporate_actions) (List.length state.lifecycle_events) diff --git a/lib/market_slice.mli b/lib/market_slice.mli index 5785f03..a1e0ded 100644 --- a/lib/market_slice.mli +++ b/lib/market_slice.mli @@ -13,6 +13,7 @@ type t = private { received_at : Ptime.t; bars : Bar.t list; market_events : Market_event.t list; + order_book_events : Order_book_event.t list; fx_rates : fx_mark list; corporate_actions : Corporate_action.t list; lifecycle_events : Instrument_lifecycle.event list; @@ -105,6 +106,23 @@ val create_v14 : market_events:Market_event.t list -> (t, string) result +val create_v15 : + slice_sequence:int64 -> + start_at:Ptime.t -> + end_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + bars:Bar.t list -> + fx_rates:fx_mark list -> + corporate_actions:Corporate_action.t list -> + borrow_observations:Financing.borrow_observation list -> + cash_rate_observations:Financing.cash_rate_observation list -> + settlement_failures:Settlement.failure list -> + lifecycle_events:Instrument_lifecycle.event list -> + market_events:Market_event.t list -> + order_book_events:Order_book_event.t list -> + (t, string) result + val bar : t -> Id.Instrument.t -> Bar.t option val fx_rate : t -> string -> Scalar.Price.t option val compare_replay_order : t -> t -> int diff --git a/lib/order_book_event.ml b/lib/order_book_event.ml new file mode 100644 index 0000000..701c837 --- /dev/null +++ b/lib/order_book_event.ml @@ -0,0 +1,132 @@ +type side = Bid | Ask +type level = { price : Scalar.Price.t; quantity : Scalar.Quantity.t } + +type kind = + | Snapshot of { bids : level list; asks : level list } + | Set of { side : side; price : Scalar.Price.t; quantity : Scalar.Quantity.t } + | Delete of { side : side; price : Scalar.Price.t } + | Trade of { + price : Scalar.Price.t; + quantity : Scalar.Quantity.t; + aggressor_side : Market_event.aggressor_side; + } + +type t = { + instrument_id : Id.Instrument.t; + event_at : Ptime.t; + available_at : Ptime.t; + received_at : Ptime.t; + ingest_sequence : int64; + book_sequence : int64; + kind : kind; +} + +let level ~price ~quantity = + if Scalar.Quantity.is_zero quantity then + Error "order-book level quantity must be positive" + else Ok { price; quantity } + +let validate_common ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence = + if Int64.compare ingest_sequence 0L <= 0 then + Error "order-book ingest sequence must be positive" + else if Int64.compare book_sequence 0L <= 0 then + Error "order-book sequence must be positive" + else if Ptime.compare available_at event_at < 0 then + Error "order-book availability must not precede event time" + else if Ptime.compare received_at available_at < 0 then + Error "order-book receipt must not precede availability" + else Ok () + +let ordered_levels side levels = + let rec ordered = function + | [] | [ _ ] -> true + | left :: (right :: _ as remaining) -> + let comparison = Scalar.Price.compare left.price right.price in + (match side with Bid -> comparison > 0 | Ask -> comparison < 0) + && ordered remaining + in + ordered levels + +let snapshot ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~book_sequence ~bids ~asks = + let ( let* ) = Result.bind in + let* () = + validate_common ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence + in + if bids = [] || asks = [] then + Error "order-book snapshot must contain bid and ask depth" + else if not (ordered_levels Bid bids && ordered_levels Ask asks) then + Error "order-book snapshot levels must be unique and price ordered" + else if Scalar.Price.compare (List.hd bids).price (List.hd asks).price > 0 + then Error "crossed order-book snapshot is invalid" + else + Ok + { + instrument_id; + event_at; + available_at; + received_at; + ingest_sequence; + book_sequence; + kind = Snapshot { bids; asks }; + } + +let create_change kind ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~book_sequence = + Result.map + (fun () -> + { + instrument_id; + event_at; + available_at; + received_at; + ingest_sequence; + book_sequence; + kind; + }) + (validate_common ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence) + +let set ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence ~side ~price ~quantity = + if Scalar.Quantity.is_zero quantity then + Error "order-book set quantity must be positive" + else + create_change + (Set { side; price; quantity }) + ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence + +let delete ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence ~side ~price = + create_change + (Delete { side; price }) + ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence + +let trade ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence ~price ~quantity ~aggressor_side = + if Scalar.Quantity.is_zero quantity then + Error "order-book trade quantity must be positive" + else + create_change + (Trade { price; quantity; aggressor_side }) + ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence + +let compare_replay_order left right = + let availability = Ptime.compare left.available_at right.available_at in + if availability <> 0 then availability + else + let receipt = Ptime.compare left.received_at right.received_at in + if receipt <> 0 then receipt + else Int64.compare left.ingest_sequence right.ingest_sequence + +let side_to_string = function Bid -> "bid" | Ask -> "ask" + +let side_of_string = function + | "bid" -> Ok Bid + | "ask" -> Ok Ask + | _ -> Error "order-book side must be bid or ask" diff --git a/lib/order_book_event.mli b/lib/order_book_event.mli new file mode 100644 index 0000000..4fc6eb3 --- /dev/null +++ b/lib/order_book_event.mli @@ -0,0 +1,77 @@ +(** Causally ordered level-two order-book observations. *) + +type side = Bid | Ask +type level = private { price : Scalar.Price.t; quantity : Scalar.Quantity.t } + +type kind = + | Snapshot of { bids : level list; asks : level list } + | Set of { side : side; price : Scalar.Price.t; quantity : Scalar.Quantity.t } + | Delete of { side : side; price : Scalar.Price.t } + | Trade of { + price : Scalar.Price.t; + quantity : Scalar.Quantity.t; + aggressor_side : Market_event.aggressor_side; + } + +type t = private { + instrument_id : Id.Instrument.t; + event_at : Ptime.t; + available_at : Ptime.t; + received_at : Ptime.t; + ingest_sequence : int64; + book_sequence : int64; + kind : kind; +} + +val level : + price:Scalar.Price.t -> quantity:Scalar.Quantity.t -> (level, string) result + +val snapshot : + instrument_id:Id.Instrument.t -> + event_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + ingest_sequence:int64 -> + book_sequence:int64 -> + bids:level list -> + asks:level list -> + (t, string) result + +val set : + instrument_id:Id.Instrument.t -> + event_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + ingest_sequence:int64 -> + book_sequence:int64 -> + side:side -> + price:Scalar.Price.t -> + quantity:Scalar.Quantity.t -> + (t, string) result + +val delete : + instrument_id:Id.Instrument.t -> + event_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + ingest_sequence:int64 -> + book_sequence:int64 -> + side:side -> + price:Scalar.Price.t -> + (t, string) result + +val trade : + instrument_id:Id.Instrument.t -> + event_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + ingest_sequence:int64 -> + book_sequence:int64 -> + price:Scalar.Price.t -> + quantity:Scalar.Quantity.t -> + aggressor_side:Market_event.aggressor_side -> + (t, string) result + +val compare_replay_order : t -> t -> int +val side_to_string : side -> string +val side_of_string : string -> (side, string) result diff --git a/lib/replay.ml b/lib/replay.ml index 6807cda..3378072 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -78,7 +78,11 @@ let engine_config ~contract_version ~risk ~venue_calendars ~execution_model Engine.config_v10 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~max_internal_events | Some financing, Some settlement -> - if String.equal contract_version "14" then + if String.equal contract_version "15" then + Engine.config_v15 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else if String.equal contract_version "14" then Engine.config_v14 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events diff --git a/lib/scenario.ml b/lib/scenario.ml index dcf86bc..81877d0 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -554,7 +554,9 @@ let parse_v7_risk base_currency instruments json = ~max_gross_exposure ~max_leverage ~short_borrow_bps let parse_risk ~contract_version base_currency instruments json = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] + if + List.mem contract_version + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] then parse_v7_risk base_currency instruments json else parse_legacy_risk base_currency instruments json @@ -687,6 +689,18 @@ let parse_execution_v2 instruments fields = in Execution.create_v2 ~participation_bps ~fee_schedules:schedules +let parse_order_book_execution instruments fields = + let* participation_bps, fee_schedules = + parse_execution_common instruments fields + in + let* max_depth_levels = + Result.bind + (field fields "max_depth_levels") + (integer ~name:"max_depth_levels") + in + Execution.create_order_book ~participation_bps ~fee_schedules + ~max_depth_levels + let parse_conservative_execution instruments fields = let* participation_bps, fee_schedules = parse_execution_common instruments fields @@ -803,6 +817,8 @@ let parse_versioned_execution ~contract_version ~instruments json = List.mem model_name [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] then parse_conservative_execution instruments configuration + else if String.equal model_name "order_book_v1" then + parse_order_book_execution instruments configuration else if String.equal model_name "quote_trade_v1" || String.equal version "2" then parse_execution_v2 instruments configuration @@ -813,7 +829,7 @@ let parse_versioned_execution ~contract_version ~instruments json = let parse_execution ~contract_version ~instruments json = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then parse_versioned_execution ~contract_version ~instruments json else parse_legacy_execution ~contract_version json @@ -863,7 +879,7 @@ let parse_portfolio_intent ~name ~parse_target make json = let parse_submit_intent ~contract_version json = let versioned = - List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] in let* fields = object_fields ~name:"submit_order intent" @@ -1698,24 +1714,161 @@ let parse_market_event json = ~ingest_sequence ~price ~quantity ~aggressor_side | _ -> assert false +let parse_order_book_level json = + let* fields = + object_fields ~name:"order-book level" ~expected:[ "price"; "quantity" ] + json + in + let* price = + Result.bind (field fields "price") (parse_price ~name:"book level price") + in + let* quantity = + Result.bind (field fields "quantity") + (parse_quantity ~name:"book level quantity") + in + Order_book_event.level ~price ~quantity + +let parse_order_book_event json = + let* loose_fields = + match json with + | `Assoc fields -> Ok fields + | _ -> Error "order-book event must be a JSON object" + in + let* type_name = + Result.bind + (field loose_fields "type") + (string ~name:"order-book event type") + in + let common = + [ + "type"; + "instrument_id"; + "event_at"; + "available_at"; + "received_at"; + "ingest_sequence"; + "book_sequence"; + ] + in + let specific = + match type_name with + | "snapshot" -> [ "bids"; "asks" ] + | "set" -> [ "side"; "price"; "quantity" ] + | "delete" -> [ "side"; "price" ] + | "trade" -> [ "price"; "quantity"; "aggressor_side" ] + | _ -> [] + in + let* () = + if specific = [] then + Error "order-book event type must be snapshot, set, delete, or trade" + else Ok () + in + let* fields = + object_fields + ~name:(type_name ^ " order-book event") + ~expected:(common @ specific) json + in + let* instrument_id = + Result.bind + (field fields "instrument_id") + (parse_id Id.Instrument.of_string ~name:"order-book instrument_id") + in + let* event_at = + Result.bind (field fields "event_at") + (parse_timestamp ~name:"order-book event_at") + in + let* available_at = + Result.bind + (field fields "available_at") + (parse_timestamp ~name:"order-book available_at") + in + let* received_at = + Result.bind + (field fields "received_at") + (parse_timestamp ~name:"order-book received_at") + in + let* ingest_sequence = + Result.bind + (field fields "ingest_sequence") + (parse_int64 ~name:"order-book ingest_sequence") + in + let* book_sequence = + Result.bind + (field fields "book_sequence") + (parse_int64 ~name:"order-book book_sequence") + in + let side () = + let* value = + Result.bind (field fields "side") (string ~name:"order-book side") + in + Order_book_event.side_of_string value + in + let price () = + Result.bind (field fields "price") (parse_price ~name:"order-book price") + in + let quantity () = + Result.bind (field fields "quantity") + (parse_quantity ~name:"order-book quantity") + in + match type_name with + | "snapshot" -> + let* bids_json = + Result.bind (field fields "bids") (list ~name:"order-book bids") + in + let* asks_json = + Result.bind (field fields "asks") (list ~name:"order-book asks") + in + let* bids = map_list parse_order_book_level bids_json in + let* asks = map_list parse_order_book_level asks_json in + Order_book_event.snapshot ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence ~book_sequence ~bids ~asks + | "set" -> + let* side = side () in + let* price = price () in + let* quantity = quantity () in + Order_book_event.set ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~book_sequence ~side ~price ~quantity + | "delete" -> + let* side = side () in + let* price = price () in + Order_book_event.delete ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence ~book_sequence ~side ~price + | "trade" -> + let* price = price () in + let* quantity = quantity () in + let* aggressor_name = + Result.bind + (field fields "aggressor_side") + (string ~name:"order-book aggressor_side") + in + let* aggressor_side = + Market_event.aggressor_side_of_string aggressor_name + in + Order_book_event.trade ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~book_sequence ~price ~quantity ~aggressor_side + | _ -> assert false + let parse_slice ~contract_version json = let financing_fields = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then [ "borrow_observations"; "cash_rate_observations" ] else [] in let settlement_fields = - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then [ "settlement_failures" ] else [] in let lifecycle_fields = - if List.mem contract_version [ "14"; "13"; "12" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12" ] then [ "lifecycle_events" ] else [] in let market_event_fields = - if String.equal contract_version "14" then [ "market_events" ] else [] + if List.mem contract_version [ "15"; "14" ] then [ "market_events" ] else [] + in + let order_book_event_fields = + if String.equal contract_version "15" then [ "order_book_events" ] else [] in let* fields = object_fields ~name:"market slice" @@ -1731,7 +1884,7 @@ let parse_slice ~contract_version json = "corporate_actions"; ] @ financing_fields @ settlement_fields @ lifecycle_fields - @ market_event_fields) + @ market_event_fields @ order_book_event_fields) json in let* sequence_json = field fields "slice_sequence" in @@ -1753,7 +1906,7 @@ let parse_slice ~contract_version json = let* actions_json = field fields "corporate_actions" in let* actions_json = list ~name:"corporate_actions" actions_json in let* corporate_actions = map_list parse_corporate_action actions_json in - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then let* borrow_json = Result.bind (field fields "borrow_observations") @@ -1768,7 +1921,7 @@ let parse_slice ~contract_version json = let* cash_rate_observations = map_list parse_cash_rate_observation cash_json in - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then let* failures_json = Result.bind (field fields "settlement_failures") @@ -1777,24 +1930,38 @@ let parse_slice ~contract_version json = let* settlement_failures = map_list parse_settlement_failure failures_json in - if List.mem contract_version [ "14"; "13"; "12" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12" ] then let* lifecycle_json = Result.bind (field fields "lifecycle_events") (list ~name:"lifecycle_events") in let* lifecycle_events = map_list parse_lifecycle_event lifecycle_json in - if String.equal contract_version "14" then + if List.mem contract_version [ "15"; "14" ] then let* events_json = Result.bind (field fields "market_events") (list ~name:"market_events") in let* market_events = map_list parse_market_event events_json in - Market_slice.create_v14 ~slice_sequence ~start_at ~end_at - ~available_at ~received_at ~bars ~fx_rates ~corporate_actions - ~borrow_observations ~cash_rate_observations ~settlement_failures - ~lifecycle_events ~market_events + if String.equal contract_version "15" then + let* book_events_json = + Result.bind + (field fields "order_book_events") + (list ~name:"order_book_events") + in + let* order_book_events = + map_list parse_order_book_event book_events_json + in + Market_slice.create_v15 ~slice_sequence ~start_at ~end_at + ~available_at ~received_at ~bars ~fx_rates ~corporate_actions + ~borrow_observations ~cash_rate_observations ~settlement_failures + ~lifecycle_events ~market_events ~order_book_events + else + Market_slice.create_v14 ~slice_sequence ~start_at ~end_at + ~available_at ~received_at ~bars ~fx_rates ~corporate_actions + ~borrow_observations ~cash_rate_observations ~settlement_failures + ~lifecycle_events ~market_events else let create = if String.equal contract_version "13" then Market_slice.create_v13 @@ -1848,7 +2015,7 @@ let construct_header ~root ~contract_path ~contract_version let* initial_cash, initial_portfolio = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state @@ -1918,7 +2085,7 @@ let construct_header ~root ~contract_path ~contract_version | _, _ -> Ok Financing.legacy_policy in let financing = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then Some financing else None in diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index e85661d..e324722 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -72,7 +72,7 @@ let common ~root ~contract_version fields = let initial_field = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -81,19 +81,19 @@ let common ~root ~contract_version fields = let venue_calendars = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then List.assoc_opt "venue_calendars" fields else None in let* risk = field ~root fields "risk" in let* execution = field ~root fields "execution" in let financing = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then List.assoc_opt "financing" fields else None in let settlement = - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then List.assoc_opt "settlement" fields else None in @@ -126,14 +126,14 @@ let batch json = let calendar_fields = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -154,11 +154,11 @@ let batch json = "slices"; ] @ calendar_fields - @ (if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then - [ "financing" ] + @ (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] + then [ "financing" ] else []) @ - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then [ "settlement" ] else []) json @@ -174,14 +174,14 @@ let stream_header ~contract_version json = let calendar_fields = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -199,11 +199,11 @@ let stream_header ~contract_version json = "max_internal_events"; ] @ calendar_fields - @ (if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then - [ "financing" ] + @ (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] + then [ "financing" ] else []) @ - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then [ "settlement" ] else []) json diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 45032db..d09f3b7 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -50,7 +50,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments let* () = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Ok () else Account.create ~base_currency ~initial_cash @@ -71,7 +71,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments let* () = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -91,7 +91,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments (child root (if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio.cash" else "initial_cash")) "initial cash must contain every scenario currency exactly once" diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index 120f8cb..ed1c9c5 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -103,7 +103,7 @@ let group_kind_to_string = function let nullable render = Option.fold ~none:`Null ~some:render let modern_protocol protocol_version = - List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] let financing_to_yojson policy = `Assoc @@ -253,7 +253,7 @@ let execution_to_yojson ~protocol_version model execution = ] in if - List.mem protocol_version [ "12"; "11" ] + List.mem protocol_version [ "13"; "12"; "11" ] && List.mem (Execution_model.name model) [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] @@ -291,7 +291,7 @@ let execution_to_yojson ~protocol_version model execution = ] ); ] else if - String.equal protocol_version "12" + List.mem protocol_version [ "13"; "12" ] && String.equal (Execution_model.name model) "quote_trade_v1" then `Assoc @@ -308,7 +308,28 @@ let execution_to_yojson ~protocol_version model execution = (Execution.fee_schedules execution)) ); ] ); ] - else if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7" ] then + else if + String.equal protocol_version "13" + && String.equal (Execution_model.name model) "order_book_v1" + then + `Assoc + [ + ("model", string (Execution_model.name model)); + ( "configuration", + `Assoc + [ + ("version", string "1"); + ("participation_bps", `Int (Execution.participation_bps execution)); + ( "fee_schedules", + `List + (List.map fee_schedule_to_yojson + (Execution.fee_schedules execution)) ); + ( "max_depth_levels", + `Int (Option.get (Execution.book_depth_limit execution)) ); + ] ); + ] + else if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7" ] + then `Assoc [ ("model", string (Execution_model.name model)); @@ -347,6 +368,7 @@ let execution_to_yojson ~protocol_version model execution = let protocol_version initialization = match initialization.scenario_contract_version with + | "15" -> "13" | "14" -> "12" | "13" -> "11" | "12" -> "10" @@ -395,7 +417,8 @@ let initialize_message ~sequence:message_sequence initialization = ] in let fields = - if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then + if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + then let initial_portfolio = Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson initialization.initial_portfolio @@ -408,14 +431,15 @@ let initialize_message ~sequence:message_sequence initialization = ( "venue_calendars", `List (List.map venue_calendar_to_yojson venue_calendars) ); ]; - (if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8" ] then + (if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8" ] + then [ ( "financing", Option.fold ~none:`Null ~some:financing_to_yojson initialization.financing ); ] else []); - (if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then + (if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9" ] then [ ( "settlement", Option.fold ~none:`Null ~some:settlement_to_yojson @@ -449,14 +473,14 @@ let cash_attribution_to_yojson ~protocol_version ("fx_rate", price balance.fx_rate); ("base_value", money balance.base_value); ] - @ (if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8" ] then + @ (if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8" ] then [ ("interest", money balance.interest); ("base_interest", money balance.base_interest); ] else []) @ - if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then + if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9" ] then [ ("settled_amount", money balance.settled_amount); ("unsettled_amount", money balance.unsettled_amount); @@ -476,7 +500,7 @@ let marked_position_to_yojson ~protocol_version ("weight", Option.fold ~none:`Null ~some:weight position.weight); ] @ - if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then + if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9" ] then [ ("settled_quantity", quantity position.settled_quantity); ("unsettled_quantity", quantity position.unsettled_quantity); @@ -561,7 +585,7 @@ let context_to_yojson ~protocol_version context = (List.map (if List.mem protocol_version - [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 else Codec.order_to_yojson) working_orders) ); @@ -574,7 +598,9 @@ let event_to_yojson ~protocol_version = function [ ("type", string "market_slice_closed"); ( "market_slice", - if String.equal protocol_version "12" then + if String.equal protocol_version "13" then + Codec.market_slice_to_yojson_v15 market_slice + else if String.equal protocol_version "12" then Codec.market_slice_to_yojson_v14 market_slice else if String.equal protocol_version "11" then Codec.market_slice_to_yojson_v13 market_slice @@ -591,7 +617,9 @@ let event_to_yojson ~protocol_version = function [ ("type", string "fill_received"); ( "fill", - if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7" ] + if + List.mem protocol_version + [ "13"; "12"; "11"; "10"; "9"; "8"; "7" ] then Codec.fill_to_yojson_v9 fill else Codec.fill_to_yojson fill ); ] @@ -601,7 +629,8 @@ let event_to_yojson ~protocol_version = function ("type", string "order_updated"); ( "order", if - List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + List.mem protocol_version + [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 order else Codec.order_to_yojson order ); ] @@ -689,7 +718,8 @@ let parse_intents_payload ~protocol_version json = let* intent = Scenario.intent_of_yojson ~contract_version: - (if String.equal protocol_version "12" then "14" + (if String.equal protocol_version "13" then "15" + else if String.equal protocol_version "12" then "14" else if String.equal protocol_version "11" then "13" else if String.equal protocol_version "10" then "12" else if String.equal protocol_version "9" then "11" diff --git a/mkdocs.yml b/mkdocs.yml index 81af8aa..2f04317 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,14 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v14: contracts/v14/README.md + - Current v15: contracts/v15/README.md - Transitional v5: contracts/v5/README.md - Transitional v4: contracts/v4/README.md - Transitional v3: contracts/v3/README.md - Frozen v2: contracts/v2/README.md - Historical v1: contracts/v1/README.md - External strategy: - - Current v12: contracts/strategy/v12/README.md + - Current v13: contracts/strategy/v13/README.md - Historical v3: contracts/strategy/v3/README.md - Historical v2: contracts/strategy/v2/README.md - Historical v1: contracts/strategy/v1/README.md diff --git a/scripts/check-deterministic-journals b/scripts/check-deterministic-journals index 5966154..74a05c0 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -110,3 +110,19 @@ compare_journal \ v14-quote-trade \ contracts/v14/fixtures/quote-trade.scenario.json \ contracts/v14/fixtures/quote-trade.journal.jsonl +compare_journal \ + v15-demo \ + contracts/v15/fixtures/demo.scenario.json \ + contracts/v15/fixtures/demo.journal.jsonl +compare_journal \ + v15-fill-clipped \ + contracts/v15/fixtures/fill-clipped.scenario.json \ + contracts/v15/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v15-quote-trade \ + contracts/v15/fixtures/quote-trade.scenario.json \ + contracts/v15/fixtures/quote-trade.journal.jsonl +compare_journal \ + v15-order-book \ + contracts/v15/fixtures/order-book.scenario.json \ + contracts/v15/fixtures/order-book.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index 1e73038..240086b 100644 --- a/scripts/check-documentation.py +++ b/scripts/check-documentation.py @@ -26,13 +26,13 @@ "docs/persistra.md", "SECURITY.md", "contracts/conformance/README.md", - "contracts/v14/README.md", + "contracts/v15/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v12/README.md", + "contracts/strategy/v13/README.md", "contracts/strategy/v3/README.md", "contracts/strategy/v2/README.md", "contracts/strategy/v1/README.md", diff --git a/scripts/release_artifacts.py b/scripts/release_artifacts.py index 62fd115..baa6858 100644 --- a/scripts/release_artifacts.py +++ b/scripts/release_artifacts.py @@ -376,8 +376,8 @@ def verify_release( ( "bin/trading-engine", "lib/trading_engine/opam", - "share/trading_engine/contracts/v14/scenario.schema.json", - "share/trading_engine/contracts/v14/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v15/scenario.schema.json", + "share/trading_engine/contracts/v15/fixtures/demo.scenario.json", "doc/trading_engine/README.md", ), epoch, @@ -388,7 +388,7 @@ def verify_release( ( "trading_engine.opam", "contracts/v1/scenario.schema.json", - "contracts/v14/fixtures/demo.scenario.json", + "contracts/v15/fixtures/demo.scenario.json", "docs/architecture.md", ".github/workflows/release-candidate.yml", ), @@ -400,8 +400,8 @@ def verify_release( ( "contracts/conformance/manifest.json", "contracts/v1/scenario.schema.json", - "contracts/v14/fixtures/demo.scenario.json", - "contracts/strategy/v12/message.schema.json", + "contracts/v15/fixtures/demo.scenario.json", + "contracts/strategy/v13/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v14/scenario.schema.json", + "contracts/v15/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index 9291ade..dff75b2 100644 --- a/test/cli.t +++ b/test/cli.t @@ -2,7 +2,7 @@ 1.0.0 $ ../bin/main.exe --capabilities - {"engine_version":"1.0.0","scenario_contract_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"2":["version","participation_bps","fee_schedules"],"1":["version","participation_bps","fixed_fee","fee_bps"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"fee_bps":{"minimum":0,"maximum":10000},"fixed_fee":{"minimum":"0","unit":"money"}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_adverse_touch_v1","configuration_versions":["1"],"scenario_contract_versions":["14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["14"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}}],"strategy_protocol_versions":["12","11","10","9","8","7","6","5","4","3"],"resource_limits":{"version":"1","scenario_record_bytes":1048576,"strategy_message_bytes":1048576,"internal_events":100000,"catalog_instruments":4096,"intents_per_batch":4096,"artifact_record_bytes":2097152}} + {"engine_version":"1.0.0","scenario_contract_versions":["15","14","13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["15","14","13","12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1","order_book_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["15","14","13","12","11","10","9","8","7","6","5","4","3"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"2":["version","participation_bps","fee_schedules"],"1":["version","participation_bps","fixed_fee","fee_bps"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"fee_bps":{"minimum":0,"maximum":10000},"fixed_fee":{"minimum":"0","unit":"money"}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_adverse_touch_v1","configuration_versions":["1"],"scenario_contract_versions":["15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["15","14"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"order_book_v1","configuration_versions":["1"],"scenario_contract_versions":["15"],"required_fields":["version","participation_bps","fee_schedules","max_depth_levels"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","max_depth_levels"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["slice_open_level_two_snapshot","contiguous_absolute_level_updates","aggressor_classified_depth_consuming_trades","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"max_depth_levels":{"minimum":1,"maximum":1024}}}],"strategy_protocol_versions":["13","12","11","10","9","8","7","6","5","4","3"],"resource_limits":{"version":"1","scenario_record_bytes":1048576,"strategy_message_bytes":1048576,"internal_events":100000,"catalog_instruments":4096,"intents_per_batch":4096,"artifact_record_bytes":2097152}} $ ../bin/main.exe --validate-only --input ../contracts/v8/fixtures/demo.scenario.json valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=85f7c99e0666159579c79256b3d0dc5f9c328e1275b79465fe1d4c93883e68f1 diff --git a/test/dune b/test/dune index 2c57ecb..1d45f1a 100644 --- a/test/dune +++ b/test/dune @@ -78,17 +78,20 @@ ../contracts/v13/journal.schema.json ../contracts/v13/scenario-stream.schema.json ../contracts/v13/scenario.schema.json - ../contracts/v14/fixtures/demo.journal.jsonl - ../contracts/v14/fixtures/demo.scenario.json - ../contracts/v14/fixtures/demo.scenario.jsonl - ../contracts/v14/fixtures/fill-clipped.journal.jsonl - ../contracts/v14/fixtures/fill-clipped.scenario.json - ../contracts/v14/fixtures/quote-trade.journal.jsonl - ../contracts/v14/fixtures/quote-trade.scenario.json - ../contracts/v14/fixtures/quote-trade.scenario.jsonl - ../contracts/v14/journal.schema.json - ../contracts/v14/scenario-stream.schema.json - ../contracts/v14/scenario.schema.json + ../contracts/v15/fixtures/demo.journal.jsonl + ../contracts/v15/fixtures/demo.scenario.json + ../contracts/v15/fixtures/demo.scenario.jsonl + ../contracts/v15/fixtures/fill-clipped.journal.jsonl + ../contracts/v15/fixtures/fill-clipped.scenario.json + ../contracts/v15/fixtures/quote-trade.journal.jsonl + ../contracts/v15/fixtures/quote-trade.scenario.json + ../contracts/v15/fixtures/quote-trade.scenario.jsonl + ../contracts/v15/fixtures/order-book.journal.jsonl + ../contracts/v15/fixtures/order-book.scenario.json + ../contracts/v15/fixtures/order-book.scenario.jsonl + ../contracts/v15/journal.schema.json + ../contracts/v15/scenario-stream.schema.json + ../contracts/v15/scenario.schema.json ../contracts/v6/fixtures/demo.scenario.json ../contracts/v6/fixtures/demo.scenario.jsonl ../contracts/v5/fixtures/demo.scenario.json @@ -105,7 +108,7 @@ ../contracts/strategy/v9/fixtures/external.strategy.jsonl ../contracts/strategy/v10/fixtures/external.strategy.jsonl ../contracts/strategy/v11/fixtures/external.strategy.jsonl - ../contracts/strategy/v12/fixtures/external.strategy.jsonl + ../contracts/strategy/v13/fixtures/external.strategy.jsonl ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries @@ -128,64 +131,85 @@ (alias runtest) (deps validate_schemas.py - ../contracts/v14/fixtures/demo.journal.jsonl - ../contracts/v14/fixtures/demo.scenario.json - ../contracts/v14/fixtures/demo.scenario.jsonl - ../contracts/v14/journal.schema.json - ../contracts/v14/scenario-stream.schema.json - ../contracts/v14/scenario.schema.json) + ../contracts/v15/fixtures/demo.journal.jsonl + ../contracts/v15/fixtures/demo.scenario.json + ../contracts/v15/fixtures/demo.scenario.jsonl + ../contracts/v15/journal.schema.json + ../contracts/v15/scenario-stream.schema.json + ../contracts/v15/scenario.schema.json) (action (run python3 %{dep:validate_schemas.py} - %{dep:../contracts/v14/scenario.schema.json} - %{dep:../contracts/v14/scenario-stream.schema.json} - %{dep:../contracts/v14/journal.schema.json} - %{dep:../contracts/v14/fixtures/demo.scenario.json} - %{dep:../contracts/v14/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v14/fixtures/demo.journal.jsonl}))) + %{dep:../contracts/v15/scenario.schema.json} + %{dep:../contracts/v15/scenario-stream.schema.json} + %{dep:../contracts/v15/journal.schema.json} + %{dep:../contracts/v15/fixtures/demo.scenario.json} + %{dep:../contracts/v15/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v15/fixtures/demo.journal.jsonl}))) (rule (alias runtest) (deps validate_schemas.py - ../contracts/v14/fixtures/quote-trade.journal.jsonl - ../contracts/v14/fixtures/quote-trade.scenario.json - ../contracts/v14/fixtures/quote-trade.scenario.jsonl - ../contracts/v14/journal.schema.json - ../contracts/v14/scenario-stream.schema.json - ../contracts/v14/scenario.schema.json) + ../contracts/v15/fixtures/quote-trade.journal.jsonl + ../contracts/v15/fixtures/quote-trade.scenario.json + ../contracts/v15/fixtures/quote-trade.scenario.jsonl + ../contracts/v15/journal.schema.json + ../contracts/v15/scenario-stream.schema.json + ../contracts/v15/scenario.schema.json) (action (run python3 %{dep:validate_schemas.py} - %{dep:../contracts/v14/scenario.schema.json} - %{dep:../contracts/v14/scenario-stream.schema.json} - %{dep:../contracts/v14/journal.schema.json} - %{dep:../contracts/v14/fixtures/quote-trade.scenario.json} - %{dep:../contracts/v14/fixtures/quote-trade.scenario.jsonl} - %{dep:../contracts/v14/fixtures/quote-trade.journal.jsonl}))) + %{dep:../contracts/v15/scenario.schema.json} + %{dep:../contracts/v15/scenario-stream.schema.json} + %{dep:../contracts/v15/journal.schema.json} + %{dep:../contracts/v15/fixtures/quote-trade.scenario.json} + %{dep:../contracts/v15/fixtures/quote-trade.scenario.jsonl} + %{dep:../contracts/v15/fixtures/quote-trade.journal.jsonl}))) (rule (alias runtest) (deps validate_strategy_schema.py - ../contracts/v14/scenario.schema.json - ../contracts/v14/journal.schema.json + ../contracts/v15/scenario.schema.json + ../contracts/v15/journal.schema.json ../contracts/diagnostic/v1/diagnostic.schema.json - ../contracts/strategy/v12/message.schema.json - ../contracts/strategy/v12/transcript.schema.json - ../contracts/strategy/v12/fixtures/external.strategy.jsonl) + ../contracts/strategy/v13/message.schema.json + ../contracts/strategy/v13/transcript.schema.json + ../contracts/strategy/v13/fixtures/external.strategy.jsonl) (action (run python3 %{dep:validate_strategy_schema.py} - %{dep:../contracts/v14/scenario.schema.json} - %{dep:../contracts/v14/journal.schema.json} + %{dep:../contracts/v15/scenario.schema.json} + %{dep:../contracts/v15/journal.schema.json} %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} - %{dep:../contracts/strategy/v12/message.schema.json} - %{dep:../contracts/strategy/v12/transcript.schema.json} - %{dep:../contracts/strategy/v12/fixtures/external.strategy.jsonl}))) + %{dep:../contracts/strategy/v13/message.schema.json} + %{dep:../contracts/strategy/v13/transcript.schema.json} + %{dep:../contracts/strategy/v13/fixtures/external.strategy.jsonl}))) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v15/fixtures/order-book.journal.jsonl + ../contracts/v15/fixtures/order-book.scenario.json + ../contracts/v15/fixtures/order-book.scenario.jsonl + ../contracts/v15/journal.schema.json + ../contracts/v15/scenario-stream.schema.json + ../contracts/v15/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v15/scenario.schema.json} + %{dep:../contracts/v15/scenario-stream.schema.json} + %{dep:../contracts/v15/journal.schema.json} + %{dep:../contracts/v15/fixtures/order-book.scenario.json} + %{dep:../contracts/v15/fixtures/order-book.scenario.jsonl} + %{dep:../contracts/v15/fixtures/order-book.journal.jsonl}))) (rule (alias runtest) diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 6c487f9..a75dd33 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -113,6 +113,7 @@ let capabilities_describe_execution_contracts () = "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1"; "quote_trade_v1"; + "order_book_v1"; ] names; let model = List.hd models in @@ -136,7 +137,7 @@ let capabilities_describe_execution_contracts () = (strings "configuration_versions"); Alcotest.(check (list string)) "scenario contracts" - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] (strings "scenario_contract_versions"); Alcotest.(check (list string)) "required fields" diff --git a/test/test_domain.ml b/test/test_domain.ml index e3a50fd..8143222 100644 --- a/test/test_domain.ml +++ b/test/test_domain.ml @@ -181,8 +181,8 @@ let market_event_validation () = let base = market_slice 2L in Alcotest.(check string) "slice rendering includes market-event count" - "slice[2] bars=1 events=0 fx=1 actions=0 lifecycle=0 borrow=0 cash_rates=0 \ - failures=0" + "slice[2] bars=1 events=0 book_events=0 fx=1 actions=0 lifecycle=0 \ + borrow=0 cash_rates=0 failures=0" (Format.asprintf "%a" T.Market_slice.pp base); Alcotest.(check bool) "nonmonotonic ingest rejected" true @@ -198,6 +198,115 @@ let market_event_validation () = ~lifecycle_events:base.lifecycle_events ~market_events:[ first; second ])) +let order_book_event_validation () = + let instrument_id = instrument_id "book-validation" in + let event_at = timestamp "2026-01-03T14:30:00Z" in + let available_at = timestamp "2026-01-03T14:30:01Z" in + let received_at = timestamp "2026-01-03T14:30:02Z" in + let level price_value quantity_value = + T.Order_book_event.level ~price:(price price_value) + ~quantity:(quantity quantity_value) + |> ok + in + let snapshot bids asks = + T.Order_book_event.snapshot ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:1L ~book_sequence:1L ~bids ~asks + in + Alcotest.(check bool) + "locked snapshot accepted" true + (Result.is_ok (snapshot [ level "100" "2" ] [ level "100" "3" ])); + Alcotest.(check bool) + "crossed snapshot rejected" true + (Result.is_error (snapshot [ level "101" "2" ] [ level "100" "3" ])); + Alcotest.(check bool) + "unordered duplicate depth rejected" true + (Result.is_error + (snapshot [ level "100" "2"; level "100" "3" ] [ level "101" "2" ])); + Alcotest.(check bool) + "empty side rejected" true + (Result.is_error (snapshot [] [ level "101" "2" ])); + Alcotest.(check bool) + "zero level rejected" true + (Result.is_error + (T.Order_book_event.level ~price:(price "100") + ~quantity:T.Scalar.Quantity.zero)); + Alcotest.(check bool) + "zero ingest sequence rejected" true + (Result.is_error + (T.Order_book_event.snapshot ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:0L ~book_sequence:1L + ~bids:[ level "99" "1" ] + ~asks:[ level "101" "1" ])); + Alcotest.(check bool) + "zero book sequence rejected" true + (Result.is_error + (T.Order_book_event.snapshot ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:1L ~book_sequence:0L + ~bids:[ level "99" "1" ] + ~asks:[ level "101" "1" ])); + Alcotest.(check bool) + "availability before book event rejected" true + (Result.is_error + (T.Order_book_event.delete ~instrument_id ~event_at + ~available_at:(timestamp "2026-01-03T14:29:59Z") + ~received_at ~ingest_sequence:1L ~book_sequence:1L + ~side:T.Order_book_event.Bid ~price:(price "99"))); + Alcotest.(check bool) + "receipt before book availability rejected" true + (Result.is_error + (T.Order_book_event.delete ~instrument_id ~event_at ~available_at + ~received_at:event_at ~ingest_sequence:1L ~book_sequence:1L + ~side:T.Order_book_event.Bid ~price:(price "99"))); + Alcotest.(check bool) + "zero set rejected" true + (Result.is_error + (T.Order_book_event.set ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:1L ~book_sequence:1L + ~side:T.Order_book_event.Bid ~price:(price "99") + ~quantity:T.Scalar.Quantity.zero)); + Alcotest.(check bool) + "zero book trade rejected" true + (Result.is_error + (T.Order_book_event.trade ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:1L ~book_sequence:1L ~price:(price "99") + ~quantity:T.Scalar.Quantity.zero ~aggressor_side:T.Market_event.Sell)); + Alcotest.(check bool) + "unknown book side rejected" true + (Result.is_error (T.Order_book_event.side_of_string "offer")); + let earlier = snapshot [ level "99" "1" ] [ level "101" "1" ] |> ok in + let later = + T.Order_book_event.delete ~instrument_id ~event_at ~available_at + ~received_at:(timestamp "2026-01-03T14:30:03Z") + ~ingest_sequence:2L ~book_sequence:2L ~side:T.Order_book_event.Bid + ~price:(price "99") + |> ok + in + Alcotest.(check bool) + "book receipt orders replay" true + (T.Order_book_event.compare_replay_order earlier later < 0); + let same_time_later_sequence = + T.Order_book_event.delete ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:2L ~book_sequence:2L + ~side:T.Order_book_event.Bid ~price:(price "99") + |> ok + in + Alcotest.(check bool) + "book ingest sequence orders final replay tie" true + (T.Order_book_event.compare_replay_order earlier same_time_later_sequence + < 0); + List.iter + (fun (wire, side) -> + Alcotest.(check string) + (wire ^ " book side round trip") + wire + (T.Order_book_event.side_of_string wire + |> ok |> T.Order_book_event.side_to_string); + Alcotest.(check string) + (wire ^ " book side rendering") + wire + (T.Order_book_event.side_to_string side)) + [ ("bid", T.Order_book_event.Bid); ("ask", T.Order_book_event.Ask) ] + let bar_validation_boundaries () = let instrument_id = instrument_id "bar-validation" in let create ?(open_price = "100") ?(high_price = "110") ?(low_price = "90") @@ -397,6 +506,8 @@ let tests = portfolio_weight_rounds_toward_zero; Alcotest.test_case "market slice validation" `Quick market_slice_validation; Alcotest.test_case "market event validation" `Quick market_event_validation; + Alcotest.test_case "order-book event validation" `Quick + order_book_event_validation; Alcotest.test_case "bar validation boundaries" `Quick bar_validation_boundaries; Alcotest.test_case "corporate action validation boundaries" `Quick diff --git a/test/test_execution.ml b/test/test_execution.ml index 377b8ea..24856fb 100644 --- a/test/test_execution.ml +++ b/test/test_execution.ml @@ -77,6 +77,69 @@ let quote_trade_execution ?(participation_bps = 10_000) () = let fees = conservative_execution () |> T.Execution.fee_schedules in T.Execution.create_v2 ~participation_bps ~fee_schedules:fees |> ok +let book_level price_value quantity_value = + T.Order_book_event.level ~price:(price price_value) + ~quantity:(quantity quantity_value) + |> ok + +let book_snapshot ?(sequence = 1L) ?(second = 1) + ?(bids = [ book_level "99" "10" ]) ?(asks = [ book_level "101" "10" ]) () = + let event_at = market_event_time second in + T.Order_book_event.snapshot + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~book_sequence:sequence ~bids ~asks + |> ok + +let book_set ?(sequence = 2L) ?(second = 2) ?(side = T.Order_book_event.Bid) + ?(price_value = "99") ?(quantity_value = "5") () = + let event_at = market_event_time second in + T.Order_book_event.set + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~book_sequence:sequence ~side + ~price:(price price_value) ~quantity:(quantity quantity_value) + |> ok + +let book_delete ?(sequence = 2L) ?(second = 2) ?(side = T.Order_book_event.Bid) + ?(price_value = "99") () = + let event_at = market_event_time second in + T.Order_book_event.delete + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~book_sequence:sequence ~side + ~price:(price price_value) + |> ok + +let book_trade ?(sequence = 2L) ?(second = 2) ?(price_value = "99") + ?(quantity_value = "5") ?(aggressor_side = T.Market_event.Sell) () = + let event_at = market_event_time second in + T.Order_book_event.trade + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~book_sequence:sequence ~price:(price price_value) + ~quantity:(quantity quantity_value) ~aggressor_side + |> ok + +let order_book_slice events = + let base = market_slice 2L in + T.Market_slice.create_v15 ~slice_sequence:base.slice_sequence + ~start_at:base.start_at ~end_at:base.end_at ~available_at:base.available_at + ~received_at:base.received_at ~bars:base.bars ~fx_rates:base.fx_rates + ~corporate_actions:base.corporate_actions + ~borrow_observations:base.borrow_observations + ~cash_rate_observations:base.cash_rate_observations + ~settlement_failures:base.settlement_failures + ~lifecycle_events:base.lifecycle_events ~market_events:[] + ~order_book_events:events + |> ok + +let order_book_execution ?(max_depth_levels = 10) () = + let fees = conservative_execution () |> T.Execution.fee_schedules in + T.Execution.create_order_book ~participation_bps:10_000 ~fee_schedules:fees + ~max_depth_levels + |> ok + let liquidity_name = function | T.Fee_schedule.Maker -> "maker" | Taker -> "taker" @@ -238,6 +301,337 @@ let quote_trade_stop_and_event_boundaries () = ~oms (quote_trade_slice [ old_event ]))) +let order_book_walks_depth_and_rejects_inconsistent_updates () = + let oms, order = oms_with_order (request ()) in + let snapshot = + book_snapshot ~asks:[ book_level "101" "4"; book_level "102" "6" ] () + in + let cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms + (order_book_slice [ snapshot ]) + |> ok + in + let first, advance = + match T.Execution.next cursor ~oms |> ok with + | T.Execution.Proposed (proposal, advance) -> (proposal, advance) + | _ -> Alcotest.fail "best ask did not produce a fill" + in + Alcotest.check quantity_testable "first level quantity" (quantity "4") + first.quantity; + Alcotest.check price_testable "best ask first" (price "101") first.price; + Alcotest.(check bool) + "over-consumption rejected" true + (Result.is_error (advance (quantity "5"))); + Alcotest.(check bool) + "negative application rejected" true + (Result.is_error (advance (quantity "-1"))); + Alcotest.(check bool) + "off-lot application rejected" true + (Result.is_error (advance (quantity "0.5"))); + let cursor = advance first.quantity |> ok in + let applied = + fill ~quantity_value:"4" ~price_value:"101" ~executed_at:first.executed_at + order + in + let oms, _ = T.Oms.apply_fill oms applied |> ok in + (match T.Execution.next cursor ~oms |> ok with + | T.Execution.Proposed (second, _) -> + Alcotest.check quantity_testable "second level quantity" (quantity "6") + second.quantity; + Alcotest.check price_testable "second ask follows" (price "102") + second.price + | _ -> Alcotest.fail "second ask did not produce a fill"); + let fok_oms, _ = + oms_with_order + (request_v8 ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) + in + let fok_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:fok_oms + (order_book_slice [ snapshot ]) + |> ok + in + (match T.Execution.next fok_cursor ~oms:fok_oms |> ok with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check quantity_testable "FOK sees total book depth" + (quantity "4") proposal.quantity + | _ -> Alcotest.fail "FOK ignored sufficient multi-level depth"); + let rejected events depth = + Result.is_error + (T.Execution.start_slice_order_book + (order_book_execution ~max_depth_levels:depth ()) + ~instruments:[ instrument () ] + ~oms (order_book_slice events)) + in + Alcotest.(check bool) + "sequence gap rejected" true + (rejected [ book_snapshot (); book_set ~sequence:3L () ] 10); + Alcotest.(check bool) + "missing delete rejected" true + (rejected [ book_snapshot (); book_delete ~price_value:"98" () ] 10); + Alcotest.(check bool) + "ask delete is applied" true + (Result.is_ok + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:T.Oms.empty + (order_book_slice + [ + book_snapshot (); + book_delete ~side:T.Order_book_event.Ask ~price_value:"101" (); + ]))); + Alcotest.(check bool) + "crossing update rejected" true + (rejected [ book_snapshot (); book_set ~price_value:"102" () ] 10); + Alcotest.(check bool) + "depth cap rejected" true + (rejected + [ book_snapshot ~bids:[ book_level "99" "1"; book_level "98" "1" ] () ] + 1); + Alcotest.(check bool) + "update before snapshot rejected" true + (rejected [ book_set () ] 10); + Alcotest.(check bool) + "duplicate snapshot rejected" true + (rejected [ book_snapshot (); book_snapshot ~sequence:2L ~second:2 () ] 10); + Alcotest.(check bool) + "trade beyond displayed depth rejected" true + (rejected + [ + book_snapshot ~bids:[ book_level "99" "2" ] (); + book_trade ~quantity_value:"3" (); + ] + 10); + let sell_oms, _ = oms_with_order (request ~side:T.Order.Sell ()) in + let sell_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:sell_oms + (order_book_slice + [ book_snapshot ~bids:[ book_level "99" "4"; book_level "98" "6" ] () ]) + |> ok + in + (match T.Execution.next sell_cursor ~oms:sell_oms |> ok with + | T.Execution.Proposed (proposal, advance) -> ( + Alcotest.check price_testable "sell walks best bid first" (price "99") + proposal.price; + let cursor = advance proposal.quantity |> ok in + let sell_order = T.Oms.find sell_oms proposal.order_id |> Option.get in + let applied = + fill ~quantity_value:"4" ~price_value:"99" + ~executed_at:proposal.executed_at sell_order + in + let sell_oms, _ = T.Oms.apply_fill sell_oms applied |> ok in + match T.Execution.next cursor ~oms:sell_oms |> ok with + | T.Execution.Proposed (next, _) -> + Alcotest.check price_testable "sell consumes next bid" (price "98") + next.price + | _ -> Alcotest.fail "second bid did not produce a sell fill") + | _ -> Alcotest.fail "best bid did not produce a sell fill"); + let added_oms, _ = + oms_with_order (request ~kind:(T.Order.Limit (price "102")) ()) + in + let added_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:added_oms + (order_book_slice + [ + book_snapshot ~asks:[ book_level "105" "10" ] (); + book_set ~side:T.Order_book_event.Ask ~price_value:"101" + ~quantity_value:"3" (); + ]) + |> ok + in + (match T.Execution.next added_cursor ~oms:added_oms |> ok with + | T.Execution.Proposed (proposal, advance) -> ( + Alcotest.check quantity_testable "added ask is bounded" (quantity "3") + proposal.quantity; + Alcotest.check price_testable "added ask becomes marketable" (price "101") + proposal.price; + let cursor = advance (quantity "2") |> ok in + let added_order = T.Oms.find added_oms proposal.order_id |> Option.get in + let applied = + fill ~quantity_value:"2" ~price_value:"101" + ~executed_at:proposal.executed_at added_order + in + let added_oms, _ = T.Oms.apply_fill added_oms applied |> ok in + match T.Execution.next cursor ~oms:added_oms |> ok with + | T.Execution.Proposed (remainder, _) -> + Alcotest.check quantity_testable "added ask remainder" (quantity "1") + remainder.quantity + | _ -> Alcotest.fail "added ask remainder did not execute") + | _ -> Alcotest.fail "added ask did not produce a fill"); + let passive_sell_oms, _ = + oms_with_order + (request ~side:T.Order.Sell ~kind:(T.Order.Limit (price "101")) ()) + in + let buy_trade_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:passive_sell_oms + (order_book_slice + [ + book_snapshot ~asks:[ book_level "101" "5" ] (); + book_trade ~aggressor_side:T.Market_event.Buy ~price_value:"101" + ~quantity_value:"2" (); + ]) + |> ok + in + (match T.Execution.next buy_trade_cursor ~oms:passive_sell_oms |> ok with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "buy trade should remain behind displayed ask queue"); + let unknown_trade_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:T.Oms.empty + (order_book_slice + [ + book_snapshot (); + book_trade ~aggressor_side:T.Market_event.Unknown + ~quantity_value:"100" (); + ]) + in + Alcotest.(check bool) + "unknown trade does not consume book" true + (Result.is_ok unknown_trade_cursor); + let triggered_stop side trigger = + let oms, order = + oms_with_order + (request_v8 ~side + ~kind:(T.Order.Stop (price trigger)) + ~time_in_force:T.Order.Gtc ()) + in + let cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms + (order_book_slice [ book_snapshot () ]) + |> ok + in + match T.Execution.next cursor ~oms |> ok with + | T.Execution.Triggered (order_id, triggered_at, 2L, _) -> + Alcotest.(check string) + "order-book stop identity" + (T.Id.Order.to_string order.id) + (T.Id.Order.to_string order_id); + Alcotest.(check string) + "order-book stop event time" "2026-01-03T14:30:01.000000Z" + (T.Codec.ptime_to_string triggered_at) + | _ -> Alcotest.fail "order-book snapshot did not trigger stop" + in + triggered_stop T.Order.Buy "100"; + triggered_stop T.Order.Sell "100"; + let waiting_stop_oms, _ = + oms_with_order + (request_v8 + ~kind:(T.Order.Stop (price "200")) + ~time_in_force:T.Order.Gtc ()) + in + let waiting_stop_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:waiting_stop_oms + (order_book_slice [ book_snapshot () ]) + |> ok + in + (match T.Execution.next waiting_stop_cursor ~oms:waiting_stop_oms |> ok with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "untriggered order-book stop should remain dormant"); + let shallow_fok_oms, _ = + oms_with_order + (request_v8 ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) + in + let shallow_fok_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:shallow_fok_oms + (order_book_slice [ book_snapshot ~asks:[ book_level "101" "3" ] () ]) + |> ok + in + (match T.Execution.next shallow_fok_cursor ~oms:shallow_fok_oms |> ok with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "FOK should reject insufficient order-book depth"); + Alcotest.(check bool) + "book model requires book configuration" true + (Result.is_error + (T.Execution.start_slice_order_book (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms:T.Oms.empty + (order_book_slice [ book_snapshot () ]))); + Alcotest.(check bool) + "unknown book instrument rejected" true + (Result.is_error + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument ~id:"other" () ] + ~oms:T.Oms.empty + (order_book_slice [ book_snapshot () ]))); + Alcotest.(check bool) + "snapshot coverage must be complete" true + (Result.is_error + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument (); instrument ~id:"other" () ] + ~oms:T.Oms.empty + (order_book_slice [ book_snapshot () ]))); + Alcotest.(check bool) + "off-tick book level rejected" true + (Result.is_error + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument ~tick_size:"1" () ] + ~oms:T.Oms.empty + (order_book_slice + [ + book_snapshot + ~bids:[ book_level "99.5" "2" ] + ~asks:[ book_level "101" "2" ] + (); + ]))); + Alcotest.(check bool) + "off-lot book level rejected" true + (Result.is_error + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument ~lot_size:"2" () ] + ~oms:T.Oms.empty + (order_book_slice + [ + book_snapshot + ~bids:[ book_level "99" "1" ] + ~asks:[ book_level "101" "2" ] + (); + ]))); + let old_at = timestamp "2026-01-02T14:30:00Z" in + let old_snapshot = + T.Order_book_event.snapshot + ~instrument_id:(instrument_id "test-equity") + ~event_at:old_at ~available_at:old_at ~received_at:old_at + ~ingest_sequence:1L ~book_sequence:1L + ~bids:[ book_level "99" "1" ] + ~asks:[ book_level "101" "1" ] + |> ok + in + Alcotest.(check bool) + "book event outside slice rejected" true + (Result.is_error + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:T.Oms.empty + (order_book_slice [ old_snapshot ]))); + Alcotest.(check bool) + "order-book depth must be positive" true + (Result.is_error + (T.Execution.create_order_book ~participation_bps:10_000 + ~fee_schedules:(T.Execution.fee_schedules (conservative_execution ())) + ~max_depth_levels:0)); + Alcotest.(check bool) + "order-book depth is capped" true + (Result.is_error + (T.Execution.create_order_book ~participation_bps:10_000 + ~fee_schedules:(T.Execution.fee_schedules (conservative_execution ())) + ~max_depth_levels:1025)) + let conservative_limit_models_diverge () = let engine = conservative_execution () in let limit = T.Order.Limit (price "100") in @@ -869,6 +1263,8 @@ let tests = quote_trade_limits_fok_and_continuations; Alcotest.test_case "quote replay stops and boundaries" `Quick quote_trade_stop_and_event_boundaries; + Alcotest.test_case "order book walks depth and rejects inconsistent updates" + `Quick order_book_walks_depth_and_rejects_inconsistent_updates; Alcotest.test_case "conservative limit models diverge" `Quick conservative_limit_models_diverge; Alcotest.test_case "conservative costs are attributed" `Quick diff --git a/test/test_scenario.ml b/test/test_scenario.ml index 3c57b8f..bf17455 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -2,16 +2,21 @@ open Test_support module T = Trading_engine let demo_document () = - In_channel.with_open_bin "../contracts/v14/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v15/fixtures/demo.scenario.json" In_channel.input_all let demo () = T.Scenario.of_string (demo_document ()) |> ok let demo_hash () = T.Sha256.digest_string (demo_document ()) -let stream_path = "../contracts/v14/fixtures/demo.scenario.jsonl" -let quote_trade_path = "../contracts/v14/fixtures/quote-trade.scenario.json" +let stream_path = "../contracts/v15/fixtures/demo.scenario.jsonl" +let quote_trade_path = "../contracts/v15/fixtures/quote-trade.scenario.json" let quote_trade_stream_path = - "../contracts/v14/fixtures/quote-trade.scenario.jsonl" + "../contracts/v15/fixtures/quote-trade.scenario.jsonl" + +let order_book_path = "../contracts/v15/fixtures/order-book.scenario.json" + +let order_book_stream_path = + "../contracts/v15/fixtures/order-book.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -79,7 +84,7 @@ let write_large_stream path slice_count = ~effective_at:start_at ~credit_rate_bps:0 ~debit_rate_bps:0 |> ok in - T.Market_slice.create_v14 ~slice_sequence:(Int64.of_int index) + T.Market_slice.create_v15 ~slice_sequence:(Int64.of_int index) ~start_at ~end_at:(add_seconds base (offset + 1)) ~available_at:(add_seconds base (offset + 2)) @@ -93,13 +98,13 @@ let write_large_stream path slice_count = ~fx_rates:[ fx_mark () ] ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] ~cash_rate_observations:[ cash_rate ] ~settlement_failures:[] - ~lifecycle_events:[] ~market_events:[] + ~lifecycle_events:[] ~market_events:[] ~order_book_events:[] |> ok in let payload = `Assoc [ - ("market_slice", T.Codec.market_slice_to_yojson_v14 market_slice); + ("market_slice", T.Codec.market_slice_to_yojson_v15 market_slice); ("intents", `List []); ] in @@ -144,9 +149,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v14/scenario.schema.json"; - check_schema "../contracts/v14/scenario-stream.schema.json"; - check_schema "../contracts/v14/journal.schema.json" + check_schema "../contracts/v15/scenario.schema.json"; + check_schema "../contracts/v15/scenario-stream.schema.json"; + check_schema "../contracts/v15/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -207,7 +212,7 @@ let v12_distributions_and_lifecycle_parse () = |> ok in let market_slice = - T.Market_slice.create_v14 ~slice_sequence:1L + T.Market_slice.create_v15 ~slice_sequence:1L ~start_at:(timestamp "2026-01-02T14:30:00Z") ~end_at:(timestamp "2026-01-02T20:55:00Z") ~available_at:(timestamp "2026-01-02T21:00:00Z") @@ -249,7 +254,7 @@ let v12_distributions_and_lifecycle_parse () = reason = "acquisition"; }); ] - ~market_events:[] + ~market_events:[] ~order_book_events:[] |> ok in let document = @@ -346,7 +351,7 @@ let v12_distributions_and_lifecycle_parse () = | _ -> Alcotest.fail "demo slice must be an object" in `List - (T.Codec.market_slice_to_yojson_v14 market_slice + (T.Codec.market_slice_to_yojson_v15 market_slice :: List.map add_child_bar rest) | _ -> Alcotest.fail "demo slices must be nonempty" in @@ -436,8 +441,8 @@ let contract_version_is_required_and_supported () = let unsupported_diagnostic = T.Scenario.of_yojson unsupported |> error in Alcotest.(check string) "unsupported version diagnosed" - "unsupported scenario contract_version \"2\" (expected one of 14, 13, 12, \ - 11, 10, 9, 8, 7, 6, 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 15, 14, 13, \ + 12, 11, 10, 9, 8, 7, 6, 5, 4, 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -587,7 +592,7 @@ let dense_schedule_document slice_count = ~effective_at:start_at ~credit_rate_bps:100 ~debit_rate_bps:200 |> ok in - T.Market_slice.create_v14 ~slice_sequence:(Int64.of_int index) ~start_at + T.Market_slice.create_v15 ~slice_sequence:(Int64.of_int index) ~start_at ~end_at:(add_seconds base (time_offset + 1)) ~available_at:(add_seconds base (time_offset + 2)) ~received_at:(add_seconds base (time_offset + 3)) @@ -601,7 +606,8 @@ let dense_schedule_document slice_count = ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] ~cash_rate_observations:[ cash_rate_observation ] ~settlement_failures:[] ~lifecycle_events:[] ~market_events:[] - |> ok |> T.Codec.market_slice_to_yojson_v14) + ~order_book_events:[] + |> ok |> T.Codec.market_slice_to_yojson_v15) in let schedule = List.init slice_count (fun offset -> @@ -1185,7 +1191,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v14/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v15/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -1213,7 +1219,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v14/fixtures/fill-clipped.scenario.json" + "../contracts/v15/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in @@ -1227,7 +1233,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v14/fixtures/fill-clipped.journal.jsonl" + "../contracts/v15/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual @@ -1247,7 +1253,7 @@ let quote_trade_replay_is_causal_and_stream_equivalent () = in let golden = In_channel.with_open_bin - "../contracts/v14/fixtures/quote-trade.journal.jsonl" In_channel.input_all + "../contracts/v15/fixtures/quote-trade.journal.jsonl" In_channel.input_all in Alcotest.(check string) "quote/trade golden journal" golden batch_journal; let fills = @@ -1290,6 +1296,64 @@ let quote_trade_replay_is_causal_and_stream_equivalent () = "quote/trade stream and batch journals agree" expected (In_channel.with_open_bin journal In_channel.input_all)) +let order_book_replay_is_bounded_and_stream_equivalent () = + let document = + In_channel.with_open_bin order_book_path In_channel.input_all + in + let scenario = T.Scenario.of_string document |> ok in + let batch = + T.Replay.run ~scenario_sha256:(T.Sha256.digest_string document) scenario + |> ok + in + let actual = + batch.audits |> List.map T.Codec.audit_to_string |> String.concat "\n" + |> fun value -> value ^ "\n" + in + let golden = + In_channel.with_open_bin + "../contracts/v15/fixtures/order-book.journal.jsonl" In_channel.input_all + in + Alcotest.(check string) "order-book golden journal" golden actual; + let fills = + List.filter_map + (fun (audit : T.Audit.t) -> + match audit.event with + | T.Audit.Fill_applied fill -> Some fill + | _ -> None) + batch.audits + in + Alcotest.(check (list string)) + "queue reduction precedes deterministic partial maker fills" + [ "4@100@2026-02-03T14:35:00.000000Z"; "6@100@2026-02-03T14:36:00.000000Z" ] + (List.map + (fun (fill : T.Fill.t) -> + Printf.sprintf "%s@%s@%s" + (T.Scalar.Quantity.to_decimal_string fill.quantity) + (T.Scalar.Price.to_decimal_string fill.price) + (T.Codec.ptime_to_string fill.executed_at)) + fills); + let stream_hash = T.Sha256.digest_file order_book_stream_path |> ok in + let expected = + T.Replay.run ~scenario_sha256:stream_hash scenario |> ok |> fun result -> + result.audits |> List.map T.Codec.audit_to_string |> String.concat "\n" + |> fun value -> value ^ "\n" + in + let journal = Filename.temp_file "trading-engine-order-book" ".jsonl" in + Sys.remove journal; + Fun.protect + ~finally:(fun () -> + if Sys.file_exists journal then Sys.remove journal; + if Sys.file_exists (journal ^ ".partial") then + Sys.remove (journal ^ ".partial")) + (fun () -> + let streamed = + T.Replay.run_stream ~journal_path:journal order_book_stream_path |> ok + in + Alcotest.(check int64) "two streamed slices" 2L streamed.slice_count; + Alcotest.(check string) + "order-book stream and batch journals agree" expected + (In_channel.with_open_bin journal In_channel.input_all)) + let journal_is_created_exclusively () = let scenario = demo () in let existing = Filename.temp_file "trading-engine" ".jsonl" in @@ -1634,6 +1698,8 @@ let tests = fill_clipping_fixture_reconciles; Alcotest.test_case "quote/trade replay is causal and stream equivalent" `Quick quote_trade_replay_is_causal_and_stream_equivalent; + Alcotest.test_case "order-book replay is bounded and stream equivalent" + `Quick order_book_replay_is_bounded_and_stream_equivalent; Alcotest.test_case "exclusive journal creation" `Quick journal_is_created_exclusively; Alcotest.test_case "exclusive journal finalization" `Quick diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index 9ad4f27..f63b960 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -45,7 +45,7 @@ let initialize_message_is_complete () = T.Strategy_protocol.initialize_message ~sequence:1L (initialization ()) in Alcotest.(check string) - "protocol version" "12" + "protocol version" "13" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -265,7 +265,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "12"); + ("strategy_protocol_version", `String "13"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); From b0aa36710bc8effc09f64b3de82b9b9b2b2ebd9a Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 17:56:08 -0400 Subject: [PATCH 50/57] feat: add typed strategy metrics --- CHANGELOG.md | 3 + README.md | 24 +- contracts/conformance/cases.json | 119 + contracts/conformance/manifest.json | 103 + contracts/strategy/v14/README.md | 64 + contracts/strategy/v14/dune | 15 + .../v14/fixtures/external.scenario.json | 308 +++ .../v14/fixtures/external.scenario.jsonl | 4 + .../v14/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v14/message.schema.json | 302 ++ contracts/strategy/v14/transcript.schema.json | 82 + contracts/v16/README.md | 122 + contracts/v16/dune | 36 + contracts/v16/fixtures/demo.journal.jsonl | 29 + contracts/v16/fixtures/demo.scenario.json | 468 ++++ contracts/v16/fixtures/demo.scenario.jsonl | 6 + .../v16/fixtures/fill-clipped.journal.jsonl | 13 + .../v16/fixtures/fill-clipped.scenario.json | 273 ++ .../v16/fixtures/order-book.journal.jsonl | 13 + .../v16/fixtures/order-book.scenario.json | 378 +++ .../v16/fixtures/order-book.scenario.jsonl | 4 + .../v16/fixtures/quote-trade.journal.jsonl | 13 + .../v16/fixtures/quote-trade.scenario.json | 319 +++ .../v16/fixtures/quote-trade.scenario.jsonl | 4 + contracts/v16/journal.schema.json | 2441 +++++++++++++++++ contracts/v16/scenario-stream.schema.json | 78 + contracts/v16/scenario.schema.json | 888 ++++++ docs/api-reference.md | 2 +- docs/continuous-integration.md | 2 +- docs/execution-model.md | 4 +- docs/persistra.md | 6 +- docs/scenario.md | 22 +- lib/audit.ml | 2 +- lib/audit.mli | 2 +- lib/codec.ml | 92 +- lib/codec.mli | 1 + lib/contract.ml | 10 +- lib/engine.ml | 11 +- lib/engine.mli | 11 + lib/execution_model.ml | 23 +- lib/external_replay.ml | 4 +- lib/market_slice.ml | 2 + lib/market_slice.mli | 17 + lib/metric.ml | 101 + lib/metric.mli | 28 + lib/replay.ml | 4 +- lib/resource_limits.ml | 12 + lib/resource_limits.mli | 6 + lib/scenario.ml | 131 +- lib/scenario_shape.ml | 30 +- lib/scenario_validation.ml | 20 +- lib/strategy.ml | 2 +- lib/strategy.mli | 2 +- lib/strategy_protocol.ml | 44 +- mkdocs.yml | 5 +- scripts/check-deterministic-journals | 20 + scripts/check-documentation.py | 4 +- scripts/release_artifacts.py | 12 +- test/cli.t | 2 +- test/dune | 15 + test/test_diagnostic.ml | 17 +- test/test_domain.ml | 46 + test/test_reducer.ml | 4 +- test/test_reducer_properties.ml | 7 +- test/test_scenario.ml | 45 +- test/test_strategy_protocol.ml | 54 +- 66 files changed, 6775 insertions(+), 170 deletions(-) create mode 100644 contracts/strategy/v14/README.md create mode 100644 contracts/strategy/v14/dune create mode 100644 contracts/strategy/v14/fixtures/external.scenario.json create mode 100644 contracts/strategy/v14/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v14/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v14/message.schema.json create mode 100644 contracts/strategy/v14/transcript.schema.json create mode 100644 contracts/v16/README.md create mode 100644 contracts/v16/dune create mode 100644 contracts/v16/fixtures/demo.journal.jsonl create mode 100644 contracts/v16/fixtures/demo.scenario.json create mode 100644 contracts/v16/fixtures/demo.scenario.jsonl create mode 100644 contracts/v16/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v16/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v16/fixtures/order-book.journal.jsonl create mode 100644 contracts/v16/fixtures/order-book.scenario.json create mode 100644 contracts/v16/fixtures/order-book.scenario.jsonl create mode 100644 contracts/v16/fixtures/quote-trade.journal.jsonl create mode 100644 contracts/v16/fixtures/quote-trade.scenario.json create mode 100644 contracts/v16/fixtures/quote-trade.scenario.jsonl create mode 100644 contracts/v16/journal.schema.json create mode 100644 contracts/v16/scenario-stream.schema.json create mode 100644 contracts/v16/scenario.schema.json create mode 100644 lib/metric.ml create mode 100644 lib/metric.mli diff --git a/CHANGELOG.md b/CHANGELOG.md index d9352a3..125542c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Publish scenario/journal contract v16 and strategy protocol v14 with typed, dimensioned strategy + metrics while retaining string-only metric compatibility through v15 and protocol v13. + - Add bounded level-two order-book replay with fresh snapshots, contiguous absolute updates, multi-level marketable depth, deterministic passive queue position, and locked-book support. - Publish scenario/journal contract v15 and external strategy protocol v13 while preserving v14 diff --git a/README.md b/README.md index 00905de..1a1b207 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ scenario slices and scheduled or external intents - Current-equity weight sizing at synchronized closing marks with lot rounding - Persistent target reconciliation through bounded market-order attempts - Direct market and limit orders, cancellations, and metrics +- Typed strategy metrics with exact numeric, string, and boolean values, bounded dimensions, + units, and aggregation metadata - Signed long/short position, order, lot, tick, gross-exposure, leverage, and margin risk - Deterministic liquidation-first matching, then sell-before-buy and FIFO priority - Shared per-instrument volume participation, partial fills, and GTC limits @@ -98,7 +100,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v15/fixtures/demo.scenario.json \ + --input contracts/v16/fixtures/demo.scenario.json \ --validate-only ``` @@ -106,7 +108,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v15/fixtures/demo.scenario.json \ + --input contracts/v16/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -114,7 +116,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v15/fixtures/demo.scenario.jsonl \ + --input contracts/v16/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -123,7 +125,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v13/fixtures/external.scenario.json \ + --input contracts/strategy/v14/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -238,19 +240,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 v15 and conformance fixtures](contracts/v15/README.md) +- [Current contract v16 and conformance fixtures](contracts/v16/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v15/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v15/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v15/journal.schema.json) -- [External strategy protocol v13](contracts/strategy/v13/README.md) +- [Scenario JSON Schema](contracts/v16/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v16/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v16/journal.schema.json) +- [External strategy protocol v14](contracts/strategy/v14/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/v13/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v13/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v14/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v14/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 0430ba0..ded2c74 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -983,6 +983,68 @@ "schema_expectation": "accept", "runtime_expectation": "accept", "rule": "structural" + }, + { + "name": "scenario-v16-valid", + "artifact": "scenario-v16", + "kind": "scenario", + "source": "v16/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-v16-order-book-valid", + "artifact": "scenario-v16", + "kind": "scenario", + "source": "v16/fixtures/order-book.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-stream-v16-valid", + "artifact": "scenario-stream-v16", + "kind": "scenario_stream", + "source": "v16/fixtures/order-book.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-ready-valid-v14", + "artifact": "strategy-message-v14", + "kind": "strategy_response", + "source": "strategy/v14/fixtures/external.strategy.jsonl", + "record": 2, + "extract": [ + "message" + ], + "expected_sequence": "1", + "protocol_version": "14", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-intents-valid-v14", + "artifact": "strategy-message-v14", + "kind": "strategy_response", + "source": "strategy/v14/fixtures/external.strategy.jsonl", + "record": 4, + "extract": [ + "message" + ], + "expected_sequence": "2", + "protocol_version": "14", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" } ], "schema_only_cases": [ @@ -1625,6 +1687,63 @@ }, "mutations": [], "schema_expectation": "accept" + }, + { + "name": "strategy-stopped-valid-v14", + "artifact": "strategy-message-v14", + "instance": { + "strategy_protocol_version": "14", + "strategy_sequence": "7", + "message_type": "stopped", + "payload": {} + }, + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept", + "parser_expected": "stopped" + }, + { + "name": "strategy-error-valid-v14", + "artifact": "strategy-message-v14", + "instance": { + "strategy_protocol_version": "14", + "strategy_sequence": "7", + "message_type": "error", + "payload": { + "message": "fixture failure" + } + }, + "mutations": [], + "schema_expectation": "accept" + }, + { + "name": "strategy-v14-rejected-response-branch", + "artifact": "strategy-transcript-v14", + "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 c68ce46..6e7d864 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -1139,6 +1139,109 @@ "format": "jsonl" } ] + }, + { + "name": "scenario-v16", + "schema": "v16/scenario.schema.json", + "version_field": "contract_version", + "version": "16", + "sources": [ + { + "path": "v16/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v16/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "v16/fixtures/quote-trade.scenario.json", + "format": "json" + }, + { + "path": "v16/fixtures/order-book.scenario.json", + "format": "json" + }, + { + "path": "strategy/v14/fixtures/external.scenario.json", + "format": "json" + } + ] + }, + { + "name": "scenario-stream-v16", + "schema": "v16/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "16", + "sources": [ + { + "path": "v16/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "v16/fixtures/quote-trade.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "v16/fixtures/order-book.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v14/fixtures/external.scenario.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "journal-v16", + "schema": "v16/journal.schema.json", + "version_field": "contract_version", + "version": "16", + "sources": [ + { + "path": "v16/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v16/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v16/fixtures/quote-trade.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v16/fixtures/order-book.journal.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "strategy-message-v14", + "schema": "strategy/v14/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "14", + "sources": [ + { + "path": "strategy/v14/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } + ] + }, + { + "name": "strategy-transcript-v14", + "schema": "strategy/v14/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "14", + "sources": [ + { + "path": "strategy/v14/fixtures/external.strategy.jsonl", + "format": "jsonl" + } + ] } ] } diff --git a/contracts/strategy/v14/README.md b/contracts/strategy/v14/README.md new file mode 100644 index 0000000..1164118 --- /dev/null +++ b/contracts/strategy/v14/README.md @@ -0,0 +1,64 @@ +# External strategy protocol v14 + +Version 14 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 v13 remains available for scenario contract v15; earlier versions retain their frozen +shapes. + +Every message repeats `strategy_protocol_version: "14"` 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. + +Intent batches use the scenario v16 typed metric contract. `emit_metric.value` declares a numeric, +string, or boolean value, with optional unit, aggregation, and bounded dimensions. + +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 v16 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 v16 intent shapes. Market-slice events include lifecycle transitions and +the expanded corporate-action catalog, plus causally ordered quote/trade market events. +Protocol v14 also carries bounded order-book snapshots and incrementals and advertises the +`order_book_v1` configuration, including its maximum depth. + +External replay requires an empty batch schedule and empty streamed intent batches. The engine +records accepted messages in both directions in a deterministic transcript. A response rejected +for invalid JSON, fields, version, sequence, EOF, or size is never stored as an accepted exchange. +Instead, the partial transcript ends with a `rejected_strategy_response` diagnostic record. Version +1 rejection diagnostics use the shared +[`diagnostic/v1`](../../diagnostic/v1/README.md) contract. The transcript schema narrows that +contract to the `strategy.protocol` and `resource.limit` codes in the `strategy` phase. The record +includes the structured rejection diagnostic and at most the first 256 raw response bytes encoded +as lowercase hexadecimal. `observed_bytes` counts bytes available when the engine rejected the +response, and `truncated` reports whether the prefix omits observed bytes. The transcript and audit +journal retain partial files after failure and finalize only after their respective success checks. + +- `message.schema.json` validates individual requests and responses. +- `transcript.schema.json` validates accepted exchanges and rejected-response diagnostics. +- `fixtures/external.scenario.json` is the batch replay fixture. +- `fixtures/external.scenario.jsonl` is its bounded-memory stream form. +- `fixtures/external.strategy.jsonl` is the canonical protocol transcript. diff --git a/contracts/strategy/v14/dune b/contracts/strategy/v14/dune new file mode 100644 index 0000000..999eb39 --- /dev/null +++ b/contracts/strategy/v14/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v14/message.schema.json) + (transcript.schema.json as contracts/strategy/v14/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v14/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v14/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v14/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v14/fixtures/external.scenario.json b/contracts/strategy/v14/fixtures/external.scenario.json new file mode 100644 index 0000000..9fa4d9c --- /dev/null +++ b/contracts/strategy/v14/fixtures/external.scenario.json @@ -0,0 +1,308 @@ +{ + "contract_version": "16", + "metadata": { + "producer": "strategy-protocol-fixture" + }, + "run_id": "external-demo", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "10000" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "demo-equity-acme", + "symbol": "ACME", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "demo-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "demo-equity-acme" + ], + "sessions": [ + { + "session_date": "2026-01-01", + "policy": "holiday", + "phases": [] + }, + { + "session_date": "2026-01-02", + "policy": "regular", + "phases": [ + { + "phase": "premarket", + "opens_at": "2026-01-02T09:00:00Z", + "closes_at": "2026-01-02T14:25:00Z" + }, + { + "phase": "opening_auction", + "opens_at": "2026-01-02T14:25:00Z", + "closes_at": "2026-01-02T14:30:00Z" + }, + { + "phase": "regular", + "opens_at": "2026-01-02T14:30:00Z", + "closes_at": "2026-01-02T20:55:00Z" + }, + { + "phase": "closing_auction", + "opens_at": "2026-01-02T20:55:00Z", + "closes_at": "2026-01-02T21:00:00Z" + }, + { + "phase": "postmarket", + "opens_at": "2026-01-02T21:00:00Z", + "closes_at": "2026-01-03T01:00:00Z" + } + ] + }, + { + "session_date": "2026-01-05", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-05T14:30:00Z", + "closes_at": "2026-01-05T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-06", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-06T14:30:00Z", + "closes_at": "2026-01-06T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-07", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-07T14:30:00Z", + "closes_at": "2026-01-07T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-08", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-08T14:30:00Z", + "closes_at": "2026-01-08T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000", + "max_leverage": "2", + "short_borrow_bps": 0, + "instrument_policies": [ + { + "instrument_id": "demo-equity-acme", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "2", + "participation_bps": 5000, + "fee_schedules": [ + { + "schedule_id": "external-acme-fees-v1", + "instrument_id": "demo-equity-acme", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "0.25", + "rounding": "up", + "applies_to": "any" + }, + { + "name": "exchange", + "currency": "USD", + "kind": "notional_bps", + "value": 10, + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-01-02T14:30:00Z", + "end_at": "2026-01-02T21:00:00Z", + "available_at": "2026-01-02T21:00:01Z", + "received_at": "2026-01-02T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "100", + "high": "105", + "low": "99", + "close": "104", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-02T14:30:00Z", + "credit_rate_bps": 0, + "debit_rate_bps": 0 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-01-05T14:30:00Z", + "end_at": "2026-01-05T21:00:00Z", + "available_at": "2026-01-05T21:00:01Z", + "received_at": "2026-01-05T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "103", + "high": "108", + "low": "102", + "close": "107", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-05T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-05T14:30:00Z", + "credit_rate_bps": 0, + "debit_rate_bps": 0 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "demo-equity-acme", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/strategy/v14/fixtures/external.scenario.jsonl b/contracts/strategy/v14/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..894bf5a --- /dev/null +++ b/contracts/strategy/v14/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"16","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":"16","payload":{"intents":[],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"16","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"16","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v14/fixtures/external.strategy.jsonl b/contracts/strategy/v14/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..ceed5d3 --- /dev/null +++ b/contracts/strategy/v14/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"14","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"16","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":"14","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"14","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","strategy_sequence":"2","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} +{"strategy_protocol_version":"14","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","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":{"type":"numeric","value":"2"},"unit":"score","dimensions":{"source":"fixture"},"aggregation":"last"}]}}} +{"strategy_protocol_version":"14","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","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":"14","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"14","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","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":"14","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"14","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","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":"14","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"14","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","strategy_sequence":"6","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} +{"strategy_protocol_version":"14","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"14","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"14","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v14/message.schema.json b/contracts/strategy/v14/message.schema.json new file mode 100644 index 0000000..97d8a98 --- /dev/null +++ b/contracts/strategy/v14/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/v14/message.schema.json", + "title": "Trading Engine external strategy protocol v14 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": "14" }, + "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": "16" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/venueCalendar" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/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/v16/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/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/v16/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/signedDecimal" }, + "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/signedDecimal" }, + "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/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/v16/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/v16/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/v16/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/v14/transcript.schema.json b/contracts/strategy/v14/transcript.schema.json new file mode 100644 index 0000000..4635866 --- /dev/null +++ b/contracts/strategy/v14/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/v14/transcript.schema.json", + "title": "Trading Engine external strategy protocol v14 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": "14" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v14/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/v16/README.md b/contracts/v16/README.md new file mode 100644 index 0000000..03ac9da --- /dev/null +++ b/contracts/v16/README.md @@ -0,0 +1,122 @@ +# Trading Engine contract v16 + +This directory is the authoritative v16 process and file contract shared by Trading Engine and its +clients. Versions 14 through 3 remain readable during their client transitions. + +- `scenario.schema.json` validates batch replay inputs. +- `scenario-stream.schema.json` validates each JSON Lines scenario-stream record. +- `journal.schema.json` validates each JSON Lines audit record. +- The files under `fixtures/` form the canonical valid conformance corpus. +- `fill-clipped.scenario.json` and its journal exercise a leverage-limited partial fill. + +Version 7 requires exactly one explicit risk policy per catalog instrument. Each policy defines +order, signed-position, notional, initial-margin, maintenance-margin, and shorting limits. Versioned +risk groups have explicit membership, may overlap, and can constrain gross, long, short, absolute +net, and gross-to-equity concentration exposure. + +Runtime validation requires exact currency, position-mark, and FX coverage; known instruments; +lot-aligned quantities; tick-aligned positive marks; basis with the same sign as quantity; +nonnegative fee histories; the base FX rate equal to one; instrument, group, aggregate exposure, +leverage, and initial-margin limits. Signed cash is valid. A successful v16 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 v16 scenario, stream record, and journal record carries `"contract_version": "16"`. + +Version 8 adds explicit `market`, `limit`, `stop`, and `stop_limit` orders with `gtc`, `ioc`, +`fok`, `day`, and `gtd` time-in-force policies. `day` orders identify both their venue and the +exact versioned calendar; `gtd` orders carry an absolute expiry timestamp. Older contracts retain +their frozen mapping: market orders are IOC and limit orders are GTC. + +Stops evaluate only completed OHLCV bars. A gap through the trigger records the bar start as the +trigger time; an intrabar touch records the bar end. Trigger state and slice sequence are journaled, +and an activated order cannot execute before the following slice. A stop becomes a market order; +a stop-limit becomes its configured limit order. Splits adjust both trigger and limit prices. + +IOC orders cancel any remainder after their first eligible slice. FOK orders fill only when the +full remaining quantity fits both execution capacity and risk capacity, otherwise they cancel with +no fill. DAY orders cancel after matching the slice that reaches the selected session's final +phase close. GTD orders cancel before matching any completed bar whose end reaches or passes the +expiry, avoiding ambiguous partial-bar execution. + +The v10 `execution` object uses `completed_bar_v1` configuration version `"2"`: participation basis +points plus exactly one composable fee schedule per instrument. Named fixed, notional-basis-point, +and per-unit components declare currency, rounding, and maker/taker applicability. Optional +per-fill minimums and caps use the schedule settlement currency; negative components represent +rebates. Fills and valuations retain every native, quote, and base-currency attribution. Runtime +capabilities also advertise frozen configuration version `"1"` for older scenario contracts. + +Version 10 adds a required `financing` policy and effective-time observations on every market +slice. Borrow observations provide per-instrument locate availability, signed annual rates, and +recall state. Cash observations provide separate annual credit and debit rates per currency. +Policies select Actual/365 or Actual/360 day count, simple or daily compounding, missing-data +handling, locate rejection or fill clipping, and recall rejection or deterministic close-out. + +Borrow availability is enforced when a fill would create or increase a short. Recalls cancel +active sells and may submit priority IOC covers until the position is flat. Borrow charges and cash +interest use the exact slice interval, update native ledgers deterministically, and emit dedicated +journal records. Valuations report cash interest separately and include it in aggregate realized +P&L. Version 9 and earlier retain their frozen fixed-borrow behavior and wire shapes. + +Version 12 separates trade-date economic accounting from settlement-date availability. A required +settlement policy selects total or settled cash buying power and total or settled position +availability. Versioned calendars enumerate canonical business dates, and each instrument has an +explicit business-day lag. Every fill creates a deterministic settlement instruction containing +its cash and position movements, trade date, and due date. A due instruction either settles on the +first eligible slice or records a named failure supplied by that slice. + +Valuations and strategy contexts report settled and unsettled cash and quantities without changing +economic equity. Journals include instruction-created, completed, and failed events. Scenario v10 +and strategy protocol v8 retain their frozen immediate-settlement wire behavior. + +Version 12 adds exact stock-dividend, rights, and spin-off distributions. Each distribution names +its destination instrument, exact entitlement ratio, basis allocation in basis points, and either +rejects fractional entitlements or converts them to cash at an explicit price and currency. +Stock dividends adjust persistent targets and eligible working orders; every distribution journals +delivered quantity, fractional quantity, allocated basis, fractional basis, and cash in lieu. + +Lifecycle events keep stable instrument identity separate from mutable symbol and provider +mappings. Halt and resume transitions control tradability. Expiration and delisting are terminal, +cancel active orders, clear target exposure, and require an explicit hold or cash-out policy. +Cash-out specifies its terminal price and currency. Every transition journals the source event, +resulting listing state, provider provenance, liquidated quantity, and cash attribution. + +Version 14 adds `completed_bar_next_open_v1` and `completed_bar_adverse_touch_v1` without changing +the frozen `completed_bar_v1` semantics. Next-open limits require a marketable later open; +adverse-touch limits require a one-tick trade-through before a maker fill is eligible. Both models +declare fixed half-spread and linear participation-impact catalogs, including an explicit policy +for missing bar volume. Price costs round away from the reference price to instrument ticks and +cannot violate a limit. An `execution_price_selected` audit record attributes the reference price, +spread adjustment, impact adjustment, and final executable price before each fill. + +Version 14 adds `quote_trade_v1` and causally ordered `market_events`. Quotes expose bid/ask price +and displayed size. Trades expose price, size, and buy, sell, or unknown aggressor side. Each event +records economic, availability, and receipt timestamps plus a positive ingest sequence. Replay +orders events by availability, receipt, and ingest sequence. Marketable orders consume only +displayed quote liquidity; passive orders require appropriately aggressed trade evidence, and an +unknown aggressor never fills them. Event capacity is shared deterministically across order +priority and fills retain the event's economic timestamp. Completed bars remain the valuation +boundary. The `quote-trade` batch, stream, and journal fixtures demonstrate equivalent replay. + +Version 15 added `order_book_v1` and bounded level-two `order_book_events`. Every per-instrument +slice bundle starts with a complete snapshot and continues with contiguous absolute set, delete, +and aggressor-classified trade updates. Snapshots and updates reject crossed books, missing +deletes, sequence gaps, tick or lot misalignment, and depth beyond the configured limit; locked +books are valid. State is rebuilt from each slice snapshot, so replay never depends on hidden data +from a prior slice. + +Marketable orders walk observable opposite-side depth in price priority. Passive limit orders join +behind displayed same-price quantity and earlier engine orders. Reductions decrease quantity ahead, +adds join behind, and only appropriately aggressed trades consume the queue and fill the order. +Partial fills and cancellations therefore remain deterministic. Book liquidity is independent of +bar and quote/trade execution semantics, while completed bars remain the valuation boundary. The +`order-book` batch, stream, and journal fixtures demonstrate cancellation, queue depletion, maker +fills, bounded state, and batch/stream equivalence. + +Version 16 replaces string-only metrics with typed observations. Numeric values use canonical +decimal strings; string and boolean values retain their JSON types. Optional units, a closed +aggregation enum, and up to 16 unique dimensions are bounded at ingestion. Dimension keys are +sorted before journal encoding for deterministic downstream reconciliation. diff --git a/contracts/v16/dune b/contracts/v16/dune new file mode 100644 index 0000000..0e7e927 --- /dev/null +++ b/contracts/v16/dune @@ -0,0 +1,36 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v16/journal.schema.json) + (scenario-stream.schema.json as contracts/v16/scenario-stream.schema.json) + (scenario.schema.json as contracts/v16/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v16/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v16/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl + as + contracts/v16/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v16/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v16/fixtures/fill-clipped.scenario.json) + (fixtures/quote-trade.journal.jsonl + as + contracts/v16/fixtures/quote-trade.journal.jsonl) + (fixtures/quote-trade.scenario.json + as + contracts/v16/fixtures/quote-trade.scenario.json) + (fixtures/quote-trade.scenario.jsonl + as + contracts/v16/fixtures/quote-trade.scenario.jsonl) + (fixtures/order-book.journal.jsonl + as + contracts/v16/fixtures/order-book.journal.jsonl) + (fixtures/order-book.scenario.json + as + contracts/v16/fixtures/order-book.scenario.json) + (fixtures/order-book.scenario.jsonl + as + contracts/v16/fixtures/order-book.scenario.jsonl))) diff --git a/contracts/v16/fixtures/demo.journal.jsonl b/contracts/v16/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..2590d37 --- /dev/null +++ b/contracts/v16/fixtures/demo.journal.jsonl @@ -0,0 +1,29 @@ +{"contract_version":"16","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":"a56b9b38f18d93e2953f90c8b052026d174e91c01a9465f8070a22040820a78d","execution_model":"completed_bar_adverse_touch_v1"}} +{"contract_version":"16","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":"16","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":"16","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"16","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":"16","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":"16","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":{"type":"numeric","value":"0.1"},"unit":"ratio","dimensions":{"instrument":"demo-equity-acme","source":"strategy"},"aggregation":"last"}} +{"contract_version":"16","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":"16","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":"16","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"13"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"16","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":"16","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":"16","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":"16","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":"16","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":"16","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":"16","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"3","start_at":"2026-01-06T14:30:00.000000Z","end_at":"2026-01-06T21:00:00.000000Z","available_at":"2026-01-06T21:00:01.000000Z","received_at":"2026-01-06T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"107","high":"109","low":"104","close":"105","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"16","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":"16","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":"16","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":"16","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":"16","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":"16","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":"16","engine_sequence":"24","event_id":"demo-event-000000000024","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"4","start_at":"2026-01-07T14:30:00.000000Z","end_at":"2026-01-07T21:00:00.000000Z","available_at":"2026-01-07T21:00:01.000000Z","received_at":"2026-01-07T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"105","high":"107","low":"103","close":"106","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"16","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":"16","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":"16","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":"16","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":"16","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":"a56b9b38f18d93e2953f90c8b052026d174e91c01a9465f8070a22040820a78d","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/v16/fixtures/demo.scenario.json b/contracts/v16/fixtures/demo.scenario.json new file mode 100644 index 0000000..b8b8fb3 --- /dev/null +++ b/contracts/v16/fixtures/demo.scenario.json @@ -0,0 +1,468 @@ +{ + "contract_version": "16", + "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": { "type": "numeric", "value": "0.1" }, + "unit": "ratio", + "dimensions": { "source": "strategy", "instrument": "demo-equity-acme" }, + "aggregation": "last" + } + ] + }, + { + "after_slice_sequence": "3", + "intents": [ + { + "type": "target_quantities", + "targets": [ + { + "instrument_id": "demo-equity-acme", + "quantity": "2.5" + } + ] + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-01-02T14:30:00Z", + "end_at": "2026-01-02T21:00:00Z", + "available_at": "2026-01-02T21:00:01Z", + "received_at": "2026-01-02T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "100", + "high": "105", + "low": "99", + "close": "104", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-01-05T14:30:00Z", + "end_at": "2026-01-05T21:00:00Z", + "available_at": "2026-01-05T21:00:01Z", + "received_at": "2026-01-05T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "103", + "high": "108", + "low": "102", + "close": "107", + "volume": "13" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-05T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-05T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "3", + "start_at": "2026-01-06T14:30:00Z", + "end_at": "2026-01-06T21:00:00Z", + "available_at": "2026-01-06T21:00:01Z", + "received_at": "2026-01-06T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "107", + "high": "109", + "low": "104", + "close": "105", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-06T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-06T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "4", + "start_at": "2026-01-07T14:30:00Z", + "end_at": "2026-01-07T21:00:00Z", + "available_at": "2026-01-07T21:00:01Z", + "received_at": "2026-01-07T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "105", + "high": "107", + "low": "103", + "close": "106", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-07T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-07T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "demo-equity-acme", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v16/fixtures/demo.scenario.jsonl b/contracts/v16/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..28012c0 --- /dev/null +++ b/contracts/v16/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"16","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":"16","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","weight":"0.1"}],"type":"target_weights"},{"aggregation":"last","dimensions":{"instrument":"demo-equity-acme","source":"strategy"},"name":"desired_weight","type":"emit_metric","unit":"ratio","value":{"type":"numeric","value":"0.1"}}],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"16","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"13"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"16","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","quantity":"2.5"}],"type":"target_quantities"}],"market_slice":{"available_at":"2026-01-06T21:00:01Z","bars":[{"close":"105","high":"109","instrument_id":"demo-equity-acme","low":"104","open":"107","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-06T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-06T21:00:02Z","slice_sequence":"3","start_at":"2026-01-06T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"16","payload":{"intents":[],"market_slice":{"available_at":"2026-01-07T21:00:01Z","bars":[{"close":"106","high":"107","instrument_id":"demo-equity-acme","low":"103","open":"105","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-07T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-07T21:00:02Z","slice_sequence":"4","start_at":"2026-01-07T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"16","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v16/fixtures/fill-clipped.journal.jsonl b/contracts/v16/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..3419c5a --- /dev/null +++ b/contracts/v16/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"16","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":"dd1ca1913eb2ef4e0070887065763fb75e81a1e27b4497bf2afd09546975bd07","execution_model":"completed_bar_v1"}} +{"contract_version":"16","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":"16","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":"16","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"16","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":"16","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":"16","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":"16","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"16","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":"16","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":"16","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":"16","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":"16","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":"dd1ca1913eb2ef4e0070887065763fb75e81a1e27b4497bf2afd09546975bd07","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/v16/fixtures/fill-clipped.scenario.json b/contracts/v16/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..eea4e6d --- /dev/null +++ b/contracts/v16/fixtures/fill-clipped.scenario.json @@ -0,0 +1,273 @@ +{ + "contract_version": "16", + "metadata": { + "producer": "trading-engine", + "purpose": "fill clipping conformance fixture" + }, + "run_id": "fill-clipped", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "550" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "clip-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "clip-equity" + ], + "sessions": [ + { + "session_date": "2026-02-02", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-02T14:30:00Z", + "closes_at": "2026-02-02T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-03", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-03T14:30:00Z", + "closes_at": "2026-02-03T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-04", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-04T14:30:00Z", + "closes_at": "2026-02-04T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "clip-equity", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "2", + "participation_bps": 10000, + "fee_schedules": [ + { + "schedule_id": "clip-fees-v1", + "instrument_id": "clip-equity", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "10", + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "market", + "trigger_price": null, + "limit_price": null, + "time_in_force": "ioc", + "venue_id": null, + "calendar_id": null, + "expires_at": null + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-02-02T14:30:00Z", + "end_at": "2026-02-02T21:00:00Z", + "available_at": "2026-02-02T21:00:01Z", + "received_at": "2026-02-02T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "50", + "high": "50", + "low": "50", + "close": "50", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-02-03T14:30:00Z", + "end_at": "2026-02-03T21:00:00Z", + "available_at": "2026-02-03T21:00:01Z", + "received_at": "2026-02-03T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "100", + "high": "100", + "low": "100", + "close": "100", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-03T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-03T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "clip-equity", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v16/fixtures/order-book.journal.jsonl b/contracts/v16/fixtures/order-book.journal.jsonl new file mode 100644 index 0000000..684543d --- /dev/null +++ b/contracts/v16/fixtures/order-book.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"16","engine_sequence":"1","event_id":"order-book-event-000000000001","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"10f1592feb856e3acc16a2eb3ebd23ff2c7641e092b88307e44efcd9f2072d74","execution_model":"order_book_v1"}} +{"contract_version":"16","engine_sequence":"2","event_id":"order-book-event-000000000002","causation_ids":["order-book-event-000000000001"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} +{"contract_version":"16","engine_sequence":"3","event_id":"order-book-event-000000000003","causation_ids":["order-book-event-000000000002"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} +{"contract_version":"16","engine_sequence":"4","event_id":"order-book-event-000000000004","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00.000000Z","available_at":"2026-02-02T14:31:01.000000Z","received_at":"2026-02-02T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}} +{"contract_version":"16","engine_sequence":"5","event_id":"order-book-event-000000000005","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} +{"contract_version":"16","engine_sequence":"6","event_id":"order-book-event-000000000006","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"order-book-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"order-book-event-000000000006","updated_event_id":"order-book-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"16","engine_sequence":"7","event_id":"order-book-event-000000000007","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} +{"contract_version":"16","engine_sequence":"8","event_id":"order-book-event-000000000008","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00.000000Z","available_at":"2026-02-03T14:35:01.000000Z","received_at":"2026-02-03T14:35:02.000000Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00.000000Z","available_at":"2026-02-03T14:36:01.000000Z","received_at":"2026-02-03T14:36:02.000000Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}} +{"contract_version":"16","engine_sequence":"9","event_id":"order-book-event-000000000009","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} +{"contract_version":"16","engine_sequence":"10","event_id":"order-book-event-000000000010","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000001","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"100","notional":"400","fee":"10","executed_at":"2026-02-03T14:35:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"16","engine_sequence":"11","event_id":"order-book-event-000000000011","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000002","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:36:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"16","engine_sequence":"12","event_id":"order-book-event-000000000012","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"980.02968","fx_rate":"1","base_value":"980.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"980.02968","unsettled_amount":"0","base_settled_value":"980.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"980.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]}} +{"contract_version":"16","engine_sequence":"13","event_id":"order-book-event-000000000013","causation_ids":["order-book-event-000000000012"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"10f1592feb856e3acc16a2eb3ebd23ff2c7641e092b88307e44efcd9f2072d74","execution_model":"order_book_v1","valuation":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"980.02968","fx_rate":"1","base_value":"980.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"980.02968","unsettled_amount":"0","base_settled_value":"980.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"980.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v16/fixtures/order-book.scenario.json b/contracts/v16/fixtures/order-book.scenario.json new file mode 100644 index 0000000..83e22e0 --- /dev/null +++ b/contracts/v16/fixtures/order-book.scenario.json @@ -0,0 +1,378 @@ +{ + "contract_version": "16", + "metadata": { + "producer": "trading-engine", + "purpose": "bounded order-book replay conformance fixture" + }, + "run_id": "order-book", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "2000" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "clip-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "clip-equity" + ], + "sessions": [ + { + "session_date": "2026-02-02", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-02T14:30:00Z", + "closes_at": "2026-02-02T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-03", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-03T14:30:00Z", + "closes_at": "2026-02-03T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-04", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-04T14:30:00Z", + "closes_at": "2026-02-04T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "clip-equity", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "order_book_v1", + "configuration": { + "version": "1", + "participation_bps": 10000, + "fee_schedules": [ + { + "schedule_id": "clip-fees-v1", + "instrument_id": "clip-equity", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "10", + "rounding": "up", + "applies_to": "any" + } + ] + } + ], + "max_depth_levels": 10 + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "limit", + "trigger_price": null, + "limit_price": "100", + "time_in_force": "gtc", + "venue_id": null, + "calendar_id": null, + "expires_at": null + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-02-02T14:30:00Z", + "end_at": "2026-02-02T21:00:00Z", + "available_at": "2026-02-02T21:00:01Z", + "received_at": "2026-02-02T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "50", + "high": "50", + "low": "50", + "close": "50", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [ + { + "type": "snapshot", + "instrument_id": "clip-equity", + "event_at": "2026-02-02T14:31:00Z", + "available_at": "2026-02-02T14:31:01Z", + "received_at": "2026-02-02T14:31:02Z", + "ingest_sequence": "1", + "book_sequence": "1", + "bids": [ + { + "price": "49", + "quantity": "20" + } + ], + "asks": [ + { + "price": "51", + "quantity": "20" + } + ] + } + ] + }, + { + "slice_sequence": "2", + "start_at": "2026-02-03T14:30:00Z", + "end_at": "2026-02-03T21:00:00Z", + "available_at": "2026-02-03T21:00:01Z", + "received_at": "2026-02-03T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "100", + "high": "100", + "low": "100", + "close": "100", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-03T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-03T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [ + { + "type": "snapshot", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:31:00Z", + "available_at": "2026-02-03T14:31:01Z", + "received_at": "2026-02-03T14:31:02Z", + "ingest_sequence": "1", + "book_sequence": "1", + "bids": [ + { + "price": "100", + "quantity": "5" + } + ], + "asks": [ + { + "price": "101", + "quantity": "20" + } + ] + }, + { + "type": "set", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:32:00Z", + "available_at": "2026-02-03T14:32:01Z", + "received_at": "2026-02-03T14:32:02Z", + "ingest_sequence": "2", + "book_sequence": "2", + "side": "bid", + "price": "100", + "quantity": "3" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:33:00Z", + "available_at": "2026-02-03T14:33:01Z", + "received_at": "2026-02-03T14:33:02Z", + "ingest_sequence": "3", + "book_sequence": "3", + "price": "100", + "quantity": "3", + "aggressor_side": "sell" + }, + { + "type": "set", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:34:00Z", + "available_at": "2026-02-03T14:34:01Z", + "received_at": "2026-02-03T14:34:02Z", + "ingest_sequence": "4", + "book_sequence": "4", + "side": "bid", + "price": "100", + "quantity": "10" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:35:00Z", + "available_at": "2026-02-03T14:35:01Z", + "received_at": "2026-02-03T14:35:02Z", + "ingest_sequence": "5", + "book_sequence": "5", + "price": "100", + "quantity": "4", + "aggressor_side": "sell" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:36:00Z", + "available_at": "2026-02-03T14:36:01Z", + "received_at": "2026-02-03T14:36:02Z", + "ingest_sequence": "6", + "book_sequence": "6", + "price": "100", + "quantity": "6", + "aggressor_side": "sell" + } + ] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "clip-equity", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v16/fixtures/order-book.scenario.jsonl b/contracts/v16/fixtures/order-book.scenario.jsonl new file mode 100644 index 0000000..e8aa5d3 --- /dev/null +++ b/contracts/v16/fixtures/order-book.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"16","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded order-book replay conformance fixture"},"run_id":"order-book","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"order_book_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}],"max_depth_levels":10}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"16","scenario_sequence":"2","record_type":"market_slice","payload":{"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}],"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00Z","available_at":"2026-02-02T14:31:01Z","received_at":"2026-02-02T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}}} +{"contract_version":"16","scenario_sequence":"3","record_type":"market_slice","payload":{"intents":[],"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00Z","available_at":"2026-02-03T14:35:01Z","received_at":"2026-02-03T14:35:02Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00Z","available_at":"2026-02-03T14:36:01Z","received_at":"2026-02-03T14:36:02Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}}} +{"contract_version":"16","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v16/fixtures/quote-trade.journal.jsonl b/contracts/v16/fixtures/quote-trade.journal.jsonl new file mode 100644 index 0000000..b6e48b2 --- /dev/null +++ b/contracts/v16/fixtures/quote-trade.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"16","engine_sequence":"1","event_id":"quote-trade-event-000000000001","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"0e57b52243f3fc9fe37b94dc8fbde9a8b69c6c680bd5369e4d77c63060b853e8","execution_model":"quote_trade_v1"}} +{"contract_version":"16","engine_sequence":"2","event_id":"quote-trade-event-000000000002","causation_ids":["quote-trade-event-000000000001"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} +{"contract_version":"16","engine_sequence":"3","event_id":"quote-trade-event-000000000003","causation_ids":["quote-trade-event-000000000002"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} +{"contract_version":"16","engine_sequence":"4","event_id":"quote-trade-event-000000000004","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"16","engine_sequence":"5","event_id":"quote-trade-event-000000000005","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} +{"contract_version":"16","engine_sequence":"6","event_id":"quote-trade-event-000000000006","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"quote-trade-event-000000000006","updated_event_id":"quote-trade-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"16","engine_sequence":"7","event_id":"quote-trade-event-000000000007","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} +{"contract_version":"16","engine_sequence":"8","event_id":"quote-trade-event-000000000008","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]}} +{"contract_version":"16","engine_sequence":"9","event_id":"quote-trade-event-000000000009","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} +{"contract_version":"16","engine_sequence":"10","event_id":"quote-trade-event-000000000010","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000001","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"99","notional":"396","fee":"10","executed_at":"2026-02-03T14:33:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"16","engine_sequence":"11","event_id":"quote-trade-event-000000000011","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000002","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:34:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"16","engine_sequence":"12","event_id":"quote-trade-event-000000000012","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]}} +{"contract_version":"16","engine_sequence":"13","event_id":"quote-trade-event-000000000013","causation_ids":["quote-trade-event-000000000012"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"0e57b52243f3fc9fe37b94dc8fbde9a8b69c6c680bd5369e4d77c63060b853e8","execution_model":"quote_trade_v1","valuation":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v16/fixtures/quote-trade.scenario.json b/contracts/v16/fixtures/quote-trade.scenario.json new file mode 100644 index 0000000..baf05f8 --- /dev/null +++ b/contracts/v16/fixtures/quote-trade.scenario.json @@ -0,0 +1,319 @@ +{ + "contract_version": "16", + "metadata": { + "producer": "trading-engine", + "purpose": "bounded quote and trade replay fixture" + }, + "run_id": "quote-trade", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "2000" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "clip-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "clip-equity" + ], + "sessions": [ + { + "session_date": "2026-02-02", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-02T14:30:00Z", + "closes_at": "2026-02-02T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-03", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-03T14:30:00Z", + "closes_at": "2026-02-03T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-04", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-04T14:30:00Z", + "closes_at": "2026-02-04T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "clip-equity", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "quote_trade_v1", + "configuration": { + "version": "1", + "participation_bps": 10000, + "fee_schedules": [ + { + "schedule_id": "clip-fees-v1", + "instrument_id": "clip-equity", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "10", + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "limit", + "trigger_price": null, + "limit_price": "100", + "time_in_force": "gtc", + "venue_id": null, + "calendar_id": null, + "expires_at": null + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-02-02T14:30:00Z", + "end_at": "2026-02-02T21:00:00Z", + "available_at": "2026-02-02T21:00:01Z", + "received_at": "2026-02-02T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "50", + "high": "50", + "low": "50", + "close": "50", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-02-03T14:30:00Z", + "end_at": "2026-02-03T21:00:00Z", + "available_at": "2026-02-03T21:00:01Z", + "received_at": "2026-02-03T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "100", + "high": "100", + "low": "100", + "close": "100", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-03T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-03T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [ + { + "type": "quote", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:31:00Z", + "available_at": "2026-02-03T14:31:01Z", + "received_at": "2026-02-03T14:31:02Z", + "ingest_sequence": "1", + "bid_price": "99", + "bid_quantity": "20", + "ask_price": "101", + "ask_quantity": "20" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:32:00Z", + "available_at": "2026-02-03T14:32:01Z", + "received_at": "2026-02-03T14:32:02Z", + "ingest_sequence": "2", + "price": "100", + "quantity": "5", + "aggressor_side": "unknown" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:33:00Z", + "available_at": "2026-02-03T14:33:01Z", + "received_at": "2026-02-03T14:33:02Z", + "ingest_sequence": "3", + "price": "99", + "quantity": "4", + "aggressor_side": "sell" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:34:00Z", + "available_at": "2026-02-03T14:34:01Z", + "received_at": "2026-02-03T14:34:02Z", + "ingest_sequence": "4", + "price": "100", + "quantity": "10", + "aggressor_side": "sell" + } + ], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "clip-equity", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v16/fixtures/quote-trade.scenario.jsonl b/contracts/v16/fixtures/quote-trade.scenario.jsonl new file mode 100644 index 0000000..842d0e7 --- /dev/null +++ b/contracts/v16/fixtures/quote-trade.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"16","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded quote and trade replay fixture"},"run_id":"quote-trade","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"quote_trade_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"16","scenario_sequence":"2","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]},"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}]}} +{"contract_version":"16","scenario_sequence":"3","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]},"intents":[]}} +{"contract_version":"16","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v16/journal.schema.json b/contracts/v16/journal.schema.json new file mode 100644 index 0000000..2315fa1 --- /dev/null +++ b/contracts/v16/journal.schema.json @@ -0,0 +1,2441 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json", + "title": "Trading Engine v16 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": "16" + }, + "engine_sequence": { + "$ref": "#/$defs/sequence" + }, + "event_id": { + "$ref": "#/$defs/identifier" + }, + "causation_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/identifier" + } + }, + "run_id": { + "$ref": "#/$defs/identifier" + }, + "recorded_at": { + "$ref": "#/$defs/timestamp" + }, + "event_type": { + "enum": [ + "run_started", + "initial_state", + "market_slice_received", + "target_portfolio_requested", + "order_accepted", + "order_rejected", + "order_triggered", + "order_cancelled", + "split_applied", + "cash_dividend_applied", + "distribution_applied", + "lifecycle_applied", + "order_adjusted", + "execution_price_selected", + "fill_applied", + "settlement_instruction_created", + "settlement_completed", + "settlement_failed", + "fill_clipped", + "borrow_fee_applied", + "borrow_charge_applied", + "borrow_recall_received", + "cash_interest_applied", + "margin_call", + "margin_restored", + "intent_rejected", + "metric_emitted", + "valuation", + "run_completed" + ] + }, + "payload": { + "type": "object" + } + }, + "allOf": [ + { + "if": { "properties": { "event_type": { "const": "execution_price_selected" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/executionPriceSelected" } } } + }, + { + "if": { + "properties": { + "event_type": { + "const": "run_started" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/runStarted" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "initial_state" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/initialState" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "market_slice_received" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/marketSlice" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "target_portfolio_requested" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/targetPortfolio" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "order_accepted", + "order_rejected", + "order_triggered" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/order" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "order_cancelled" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/orderCancelled" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "split_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/splitApplied" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "cash_dividend_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/dividendApplied" + } + } + } + }, + { + "if": { "properties": { "event_type": { "const": "distribution_applied" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/distributionApplied" } } } + }, + { + "if": { "properties": { "event_type": { "const": "lifecycle_applied" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/lifecycleApplied" } } } + }, + { + "if": { + "properties": { + "event_type": { + "const": "order_adjusted" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/orderAdjusted" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "fill_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/fill" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "settlement_instruction_created", + "settlement_completed", + "settlement_failed" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/settlementInstruction" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "fill_clipped" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/fillClipped" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_fee_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowFee" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_charge_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowCharge" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_recall_received" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowRecall" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "cash_interest_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/cashInterest" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "margin_call", + "margin_restored", + "valuation" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/valuation" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "intent_rejected" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/intentRejected" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "metric_emitted" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/metric" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "run_completed" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/runCompleted" + } + } + } + } + ], + "$defs": { + "identifier": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" + }, + "signedDecimal": { + "type": "string", + "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" + }, + "unsignedDecimal": { + "type": "string", + "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "positiveDecimal": { + "type": "string", + "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "sequence": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "nonnegativeSequence": { + "type": "string", + "pattern": "^(?:0|[1-9][0-9]*)$" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" + }, + "runStarted": { + "type": "object", + "additionalProperties": false, + "required": [ + "scenario_sha256", + "execution_model" + ], + "properties": { + "scenario_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "execution_model": { + "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] + } + } + }, + "initialState": { + "type": "object", + "additionalProperties": false, + "required": [ + "portfolio", + "valuation" + ], + "properties": { + "portfolio": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/scenario.schema.json#/$defs/fractionalPolicy" } + } + } + ] + }, + "marketSlice": { + "type": "object", + "additionalProperties": false, + "required": [ + "slice_sequence", + "start_at", + "end_at", + "available_at", + "received_at", + "bars", + "fx_rates", + "corporate_actions", + "borrow_observations", + "cash_rate_observations", + "settlement_failures", + "lifecycle_events", + "market_events", + "order_book_events" + ], + "properties": { + "slice_sequence": { + "$ref": "#/$defs/sequence" + }, + "start_at": { + "$ref": "#/$defs/timestamp" + }, + "end_at": { + "$ref": "#/$defs/timestamp" + }, + "available_at": { + "$ref": "#/$defs/timestamp" + }, + "received_at": { + "$ref": "#/$defs/timestamp" + }, + "bars": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/bar" + } + }, + "fx_rates": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/fxRate" + } + }, + "corporate_actions": { + "type": "array", + "items": { + "$ref": "#/$defs/corporateAction" + } + }, + "borrow_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/borrowObservation" + } + }, + "cash_rate_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/cashRateObservation" + } + }, + "settlement_failures": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/settlementFailure" + } + }, + "lifecycle_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/lifecycleEvent" + } + }, + "market_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/marketEvent" + } + }, + "order_book_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/orderBookEvent" + } + } + } + }, + "settlementInstruction": { + "type": "object", + "additionalProperties": false, + "required": ["instruction_id", "fill_id", "instrument_id", "currency", "cash_movement", "position_movement", "trade_date", "due_date", "status", "settled_at", "failed_at", "failure_reason"], + "properties": { + "instruction_id": { "$ref": "#/$defs/identifier" }, + "fill_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "currency": { "$ref": "#/$defs/identifier" }, + "cash_movement": { "$ref": "#/$defs/signedDecimal" }, + "position_movement": { "$ref": "#/$defs/signedDecimal" }, + "trade_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "due_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "status": { "enum": ["pending", "settled", "failed"] }, + "settled_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, + "failed_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, + "failure_reason": { "oneOf": [{ "type": "null" }, { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" }] } + } + }, + "targetPortfolio": { + "type": "object", + "additionalProperties": false, + "required": [ + "basis", + "targets" + ], + "properties": { + "basis": { + "enum": [ + "weights", + "quantities" + ] + }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "weight", + "quantity", + "reference_price" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "weight": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/signedDecimal" + } + ] + }, + "quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "reference_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + } + } + } + } + } + }, + "order": { + "type": "object", + "additionalProperties": false, + "required": [ + "order_id", + "instrument_id", + "side", + "quantity", + "order_kind", + "trigger_price", + "limit_price", + "time_in_force", + "venue_id", + "calendar_id", + "expires_at", + "origin", + "created_event_id", + "updated_event_id", + "created_sequence", + "created_at", + "eligible_after_slice_sequence", + "triggered_at", + "triggered_slice_sequence", + "filled_quantity", + "filled_notional", + "status", + "rejection_reason" + ], + "properties": { + "order_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "side": { + "enum": [ + "buy", + "sell" + ] + }, + "quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "order_kind": { + "enum": [ + "market", + "limit", + "stop", + "stop_limit" + ] + }, + "trigger_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + }, + "limit_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + }, + "time_in_force": { + "enum": [ + "gtc", + "ioc", + "fok", + "day", + "gtd" + ] + }, + "venue_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/identifier" + } + ] + }, + "calendar_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/identifier" + } + ] + }, + "expires_at": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/timestamp" + } + ] + }, + "origin": { + "enum": [ + "direct", + "target_rebalance", + "margin_liquidation", + "borrow_recall", + "instrument_halt", + "instrument_terminal" + ] + }, + "created_event_id": { + "$ref": "#/$defs/identifier" + }, + "updated_event_id": { + "$ref": "#/$defs/identifier" + }, + "created_sequence": { + "$ref": "#/$defs/sequence" + }, + "created_at": { + "$ref": "#/$defs/timestamp" + }, + "eligible_after_slice_sequence": { + "$ref": "#/$defs/nonnegativeSequence" + }, + "triggered_at": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/timestamp" + } + ] + }, + "triggered_slice_sequence": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/sequence" + } + ] + }, + "filled_quantity": { + "$ref": "#/$defs/unsignedDecimal" + }, + "filled_notional": { + "$ref": "#/$defs/unsignedDecimal" + }, + "status": { + "enum": [ + "working", + "partially_filled", + "filled", + "cancelled", + "rejected" + ] + }, + "rejection_reason": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "minLength": 1 + } + ] + } + } + }, + "orderCancelled": { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "reason" + ], + "properties": { + "order": { + "$ref": "#/$defs/order" + }, + "reason": { + "enum": [ + "strategy_requested", + "target_replaced", + "market_ioc", + "immediate_or_cancel", + "fill_or_kill", + "day_expired", + "gtd_expired", + "margin_call", + "borrow_recall" + ] + } + } + }, + "splitApplied": { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "previous_quantity", + "adjusted_quantity" + ], + "properties": { + "action": { + "$ref": "#/$defs/corporateAction" + }, + "previous_quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "adjusted_quantity": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "dividendApplied": { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "quantity", + "cash_amount" + ], + "properties": { + "action": { + "$ref": "#/$defs/corporateAction" + }, + "quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "cash_amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "distributionApplied": { + "type": "object", + "additionalProperties": false, + "required": ["action", "source_quantity", "destination_quantity", "fractional_quantity", "allocated_basis", "fractional_basis", "cash_in_lieu"], + "properties": { + "action": { "$ref": "#/$defs/corporateAction" }, + "source_quantity": { "$ref": "#/$defs/signedDecimal" }, + "destination_quantity": { "$ref": "#/$defs/signedDecimal" }, + "fractional_quantity": { "$ref": "#/$defs/signedDecimal" }, + "allocated_basis": { "$ref": "#/$defs/signedDecimal" }, + "fractional_basis": { "$ref": "#/$defs/signedDecimal" }, + "cash_in_lieu": { "$ref": "#/$defs/signedDecimal" } + } + }, + "lifecycleApplied": { + "type": "object", + "additionalProperties": false, + "required": ["lifecycle_event", "listing", "liquidated_quantity", "cash_amount"], + "properties": { + "lifecycle_event": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/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/v16/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/v16/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", "minLength": 1, "maxLength": 128, "pattern": "^\\S(?:.*\\S)?$" }, + "value": { "$ref": "#/$defs/metricValue" }, + "unit": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, + "dimensions": { + "type": "object", + "maxProperties": 16, + "propertyNames": { "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, + "additionalProperties": { "type": "string", "maxLength": 128 } + }, + "aggregation": { "enum": ["last", "sum", "minimum", "maximum", "mean"] } + }, + "allOf": [ + { "if": { "properties": { "value": { "properties": { "type": { "enum": ["string", "boolean"] } } } } }, "then": { "properties": { "aggregation": { "enum": ["last"] } } } } + ] + }, + "metricValue": { + "oneOf": [ + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "numeric" }, "value": { "type": "string", "pattern": "^(0|[1-9][0-9]*|-[1-9][0-9]*)(\\.[0-9]*[1-9])?$|^-0\\.[0-9]*[1-9]$" } } }, + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "string" }, "value": { "type": "string", "maxLength": 1024 } } }, + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "boolean" }, "value": { "type": "boolean" } } } + ] + }, + "runCompleted": { + "type": "object", + "additionalProperties": false, + "required": [ + "scenario_sha256", + "execution_model", + "valuation", + "order_counts" + ], + "properties": { + "scenario_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "execution_model": { + "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] + }, + "valuation": { + "$ref": "#/$defs/valuation" + }, + "order_counts": { + "type": "object", + "additionalProperties": false, + "required": [ + "total", + "active", + "filled", + "rejected", + "cancelled" + ], + "properties": { + "total": { + "type": "integer", + "minimum": 0 + }, + "active": { + "type": "integer", + "minimum": 0 + }, + "filled": { + "type": "integer", + "minimum": 0 + }, + "rejected": { + "type": "integer", + "minimum": 0 + }, + "cancelled": { + "type": "integer", + "minimum": 0 + } + } + } + } + } + } +} diff --git a/contracts/v16/scenario-stream.schema.json b/contracts/v16/scenario-stream.schema.json new file mode 100644 index 0000000..e3b1694 --- /dev/null +++ b/contracts/v16/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/v16/scenario-stream.schema.json", + "title": "Trading Engine v16 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": "16" }, + "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": "16" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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": "16" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v16/scenario.schema.json b/contracts/v16/scenario.schema.json new file mode 100644 index 0000000..1af81e1 --- /dev/null +++ b/contracts/v16/scenario.schema.json @@ -0,0 +1,888 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json", + "title": "Trading Engine v16 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": "16" }, + "metadata": { "type": "object" }, + "run_id": { "$ref": "#/$defs/identifier" }, + "base_currency": { "$ref": "#/$defs/identifier" }, + "initial_portfolio": { "$ref": "#/$defs/initialPortfolio" }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/venueCalendar" } + }, + "risk": { "$ref": "#/$defs/risk" }, + "execution": { "$ref": "#/$defs/execution" }, + "financing": { "$ref": "#/$defs/financing" }, + "settlement": { "$ref": "#/$defs/settlement" }, + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 }, + "schedule": { "type": "array", "items": { "$ref": "#/$defs/scheduleItem" } }, + "slices": { + "description": "Slices are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-item rule is enforced semantically.", + "type": "array", + "items": { "$ref": "#/$defs/marketSlice" } + } + }, + "$defs": { + "settlement": { + "type": "object", + "additionalProperties": false, + "required": ["cash_buying_power", "position_availability", "calendars", "rules"], + "properties": { + "cash_buying_power": { "enum": ["total_cash", "settled_cash"] }, + "position_availability": { "enum": ["total_positions", "settled_positions"] }, + "calendars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementCalendar" } }, + "rules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementRule" } } + } + }, + "settlementCalendar": { + "type": "object", + "additionalProperties": false, + "required": ["calendar_id", "version", "business_dates"], + "properties": { + "calendar_id": { "$ref": "#/$defs/identifier" }, + "version": { "const": "1" }, + "business_dates": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" } } + } + }, + "settlementRule": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "calendar_id", "lag_business_days"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "calendar_id": { "$ref": "#/$defs/identifier" }, + "lag_business_days": { "type": "integer", "minimum": 0, "maximum": 30 } + } + }, + "settlementFailure": { + "type": "object", + "additionalProperties": false, + "required": ["instruction_id", "reason"], + "properties": { + "instruction_id": { "$ref": "#/$defs/identifier" }, + "reason": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } + } + }, + "financing": { + "type": "object", + "additionalProperties": false, + "required": ["day_count", "compounding", "borrow_missing_data", "cash_missing_data", "locate_policy", "recall_policy"], + "properties": { + "day_count": { "enum": ["actual_365", "actual_360"] }, + "compounding": { "enum": ["simple", "daily"] }, + "borrow_missing_data": { "enum": ["reject", "zero"] }, + "cash_missing_data": { "enum": ["reject", "zero"] }, + "locate_policy": { "enum": ["reject_order", "clip_fill"] }, + "recall_policy": { "enum": ["reject_new_shorts", "close_out"] } + } + }, + "borrowObservation": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "effective_at", "available_quantity", "annual_rate_bps", "recalled"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "effective_at": { "$ref": "#/$defs/timestamp" }, + "available_quantity": { "$ref": "#/$defs/unsignedDecimal" }, + "annual_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, + "recalled": { "type": "boolean" } + } + }, + "cashRateObservation": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "effective_at", "credit_rate_bps", "debit_rate_bps"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "effective_at": { "$ref": "#/$defs/timestamp" }, + "credit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, + "debit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 } + } + }, + "identifier": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" + }, + "signedDecimal": { + "type": "string", + "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" + }, + "unsignedDecimal": { + "type": "string", + "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "positiveDecimal": { + "type": "string", + "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "sequence": { "type": "string", "pattern": "^[1-9][0-9]*$" }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" + }, + "cashBalance": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "amount"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "amount": { "$ref": "#/$defs/signedDecimal" } + } + }, + "initialPortfolio": { + "type": "object", + "additionalProperties": false, + "required": ["cash", "positions", "marks", "fx_rates"], + "properties": { + "cash": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashBalance" } }, + "positions": { "type": "array", "items": { "$ref": "#/$defs/initialPosition" } }, + "marks": { "type": "array", "items": { "$ref": "#/$defs/initialMark" } }, + "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } } + } + }, + "initialPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "cost_basis", "realized_pnl", "dividend_pnl", "execution_fees", "borrow_fees"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "quantity": { "$ref": "#/$defs/signedDecimal" }, + "cost_basis": { "$ref": "#/$defs/signedDecimal" }, + "realized_pnl": { "$ref": "#/$defs/signedDecimal" }, + "dividend_pnl": { "$ref": "#/$defs/signedDecimal" }, + "execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, + "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "initialMark": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "price"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "price": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "instrument": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "symbol", "quote_currency", "tick_size", "lot_size"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "quote_currency": { "$ref": "#/$defs/identifier" }, + "tick_size": { "$ref": "#/$defs/positiveDecimal" }, + "lot_size": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "venueCalendar": { + "type": "object", + "additionalProperties": false, + "required": ["calendar_id", "calendar_version", "venue_id", "instrument_ids", "sessions"], + "properties": { + "calendar_id": { "$ref": "#/$defs/identifier" }, + "calendar_version": { "const": "1" }, + "venue_id": { "$ref": "#/$defs/identifier" }, + "instrument_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/identifier" } + }, + "sessions": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/venueSession" } + } + } + }, + "venueSession": { + "oneOf": [ + { "$ref": "#/$defs/openVenueSession" }, + { "$ref": "#/$defs/holidayVenueSession" } + ] + }, + "openVenueSession": { + "type": "object", + "additionalProperties": false, + "required": ["session_date", "policy", "phases"], + "properties": { + "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "policy": { "enum": ["regular", "early_close"] }, + "phases": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "items": { "$ref": "#/$defs/venuePhase" } + } + } + }, + "holidayVenueSession": { + "type": "object", + "additionalProperties": false, + "required": ["session_date", "policy", "phases"], + "properties": { + "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "policy": { "const": "holiday" }, + "phases": { "type": "array", "maxItems": 0 } + } + }, + "venuePhase": { + "type": "object", + "additionalProperties": false, + "required": ["phase", "opens_at", "closes_at"], + "properties": { + "phase": { "enum": ["premarket", "opening_auction", "regular", "closing_auction", "postmarket"] }, + "opens_at": { "$ref": "#/$defs/timestamp" }, + "closes_at": { "$ref": "#/$defs/timestamp" } + } + }, + "risk": { + "type": "object", + "additionalProperties": false, + "required": ["max_gross_exposure", "max_leverage", "short_borrow_bps", "instrument_policies", "groups"], + "properties": { + "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, + "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "instrument_policies": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/instrumentRiskPolicy" } + }, + "groups": { + "type": "array", + "items": { "$ref": "#/$defs/riskGroup" } + } + } + }, + "instrumentRiskPolicy": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "max_order_quantity", "max_long_position", "max_short_position", "max_notional_exposure", "initial_margin_bps", "maintenance_margin_bps", "shorting_allowed"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_notional_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "shorting_allowed": { "type": "boolean" } + } + }, + "nullablePositiveDecimal": { + "oneOf": [ + { "type": "null" }, + { "$ref": "#/$defs/positiveDecimal" } + ] + }, + "riskGroup": { + "type": "object", + "additionalProperties": false, + "required": ["group_id", "group_version", "group_type", "instrument_ids", "limits"], + "properties": { + "group_id": { "$ref": "#/$defs/identifier" }, + "group_version": { "const": "1" }, + "group_type": { "enum": ["issuer", "sector", "currency", "country", "asset_class", "custom"] }, + "instrument_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/identifier" } + }, + "limits": { "$ref": "#/$defs/riskGroupLimits" } + } + }, + "riskGroupLimits": { + "type": "object", + "additionalProperties": false, + "required": ["max_gross_exposure", "max_long_exposure", "max_short_exposure", "max_absolute_net_exposure", "max_concentration"], + "properties": { + "max_gross_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_long_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_short_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_absolute_net_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_concentration": { + "oneOf": [ + { "type": "null" }, + { "allOf": [{ "$ref": "#/$defs/positiveDecimal" }, { "pattern": "^(?:0[.][0-9]{0,5}[1-9]|1)$" }] } + ] + } + } + }, + "execution": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "enum": ["completed_bar_next_open_v1", "completed_bar_adverse_touch_v1"] }, + "configuration": { "$ref": "#/$defs/conservativeBarConfiguration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "quote_trade_v1" }, + "configuration": { "$ref": "#/$defs/quoteTradeConfiguration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "order_book_v1" }, + "configuration": { "$ref": "#/$defs/orderBookConfiguration" } + } + } + ] + }, + "completedBarV1Configuration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules"], + "properties": { + "version": { "const": "2" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } + } + }, + "conservativeBarConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules", "spread_model", "impact_model"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, + "spread_model": { "$ref": "#/$defs/fixedSpreadModel" }, + "impact_model": { "$ref": "#/$defs/linearImpactModel" } + } + }, + "quoteTradeConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } + } + }, + "orderBookConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules", "max_depth_levels"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, + "max_depth_levels": { "type": "integer", "minimum": 1, "maximum": 1024 } + } + }, + "fixedSpreadModel": { + "type": "object", + "additionalProperties": false, + "required": ["model", "half_spread_bps"], + "properties": { + "model": { "const": "fixed_half_spread_v1" }, + "half_spread_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "linearImpactModel": { + "type": "object", + "additionalProperties": false, + "required": ["model", "coefficient_bps", "missing_volume_policy"], + "properties": { + "model": { "const": "linear_participation_v1" }, + "coefficient_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "missing_volume_policy": { "enum": ["reject", "zero_impact"] } + } + }, + "feeSchedule": { + "type": "object", + "additionalProperties": false, + "required": ["schedule_id", "instrument_id", "settlement_currency", "minimum", "maximum", "components"], + "properties": { + "schedule_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "settlement_currency": { "$ref": "#/$defs/identifier" }, + "minimum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, + "maximum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, + "components": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeComponent" } } + } + }, + "feeComponent": { + "type": "object", + "additionalProperties": false, + "required": ["name", "currency", "kind", "value", "rounding", "applies_to"], + "properties": { + "name": { "$ref": "#/$defs/identifier" }, + "currency": { "$ref": "#/$defs/identifier" }, + "kind": { "enum": ["fixed", "notional_bps", "per_unit"] }, + "value": { "oneOf": [{ "$ref": "#/$defs/signedDecimal" }, { "type": "integer", "minimum": -10000, "maximum": 10000 }] }, + "rounding": { "enum": ["up", "down", "nearest"] }, + "applies_to": { "enum": ["any", "maker", "taker"] } + }, + "allOf": [ + { "if": { "properties": { "kind": { "const": "notional_bps" } } }, "then": { "properties": { "value": { "type": "integer" } } } }, + { "if": { "properties": { "kind": { "enum": ["fixed", "per_unit"] } } }, "then": { "properties": { "value": { "$ref": "#/$defs/signedDecimal" } } } } + ] + }, + "scheduleItem": { + "type": "object", + "additionalProperties": false, + "required": ["after_slice_sequence", "intents"], + "properties": { + "after_slice_sequence": { "$ref": "#/$defs/sequence" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "#/$defs/intent" } } + } + }, + "intent": { + "oneOf": [ + { "$ref": "#/$defs/targetWeights" }, + { "$ref": "#/$defs/targetQuantities" }, + { "$ref": "#/$defs/submitOrder" }, + { "$ref": "#/$defs/cancelOrder" }, + { "$ref": "#/$defs/metric" } + ] + }, + "targetWeights": { + "type": "object", + "additionalProperties": false, + "required": ["type", "targets"], + "properties": { + "type": { "const": "target_weights" }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "weight"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "weight": { "$ref": "#/$defs/signedDecimal" } + } + } + } + } + }, + "targetQuantities": { + "type": "object", + "additionalProperties": false, + "required": ["type", "targets"], + "properties": { + "type": { "const": "target_quantities" }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "quantity": { "$ref": "#/$defs/signedDecimal" } + } + } + } + } + }, + "submitOrder": { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "side", "quantity", "order_kind", "trigger_price", "limit_price", "time_in_force", "venue_id", "calendar_id", "expires_at"], + "properties": { + "type": { "const": "submit_order" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "order_kind": { "enum": ["market", "limit", "stop", "stop_limit"] }, + "trigger_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, + "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, + "time_in_force": { "enum": ["gtc", "ioc", "fok", "day", "gtd"] }, + "venue_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, + "calendar_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, + "expires_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] } + }, + "allOf": [ + { "if": { "properties": { "order_kind": { "const": "market" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "type": "null" } } } }, + { "if": { "properties": { "order_kind": { "const": "limit" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, + { "if": { "properties": { "order_kind": { "const": "stop" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "type": "null" } } } }, + { "if": { "properties": { "order_kind": { "const": "stop_limit" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, + { "if": { "properties": { "time_in_force": { "const": "day" } } }, "then": { "properties": { "venue_id": { "$ref": "#/$defs/identifier" }, "calendar_id": { "$ref": "#/$defs/identifier" }, "expires_at": { "type": "null" } } } }, + { "if": { "properties": { "time_in_force": { "const": "gtd" } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "$ref": "#/$defs/timestamp" } } } }, + { "if": { "properties": { "time_in_force": { "enum": ["gtc", "ioc", "fok"] } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "type": "null" } } } } + ] + }, + "cancelOrder": { + "type": "object", + "additionalProperties": false, + "required": ["type", "order_id"], + "properties": { + "type": { "const": "cancel_order" }, + "order_id": { "$ref": "#/$defs/identifier" } + } + }, + "metric": { + "type": "object", + "additionalProperties": false, + "required": ["type", "name", "value"], + "properties": { + "type": { "const": "emit_metric" }, + "name": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^\\S(?:.*\\S)?$" }, + "value": { "$ref": "#/$defs/metricValue" }, + "unit": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, + "dimensions": { + "type": "object", + "maxProperties": 16, + "propertyNames": { "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, + "additionalProperties": { "type": "string", "maxLength": 128 } + }, + "aggregation": { "enum": ["last", "sum", "minimum", "maximum", "mean"] } + }, + "allOf": [ + { "if": { "properties": { "value": { "properties": { "type": { "enum": ["string", "boolean"] } } } } }, "then": { "properties": { "aggregation": { "enum": ["last"] } } } } + ] + }, + "metricValue": { + "oneOf": [ + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "numeric" }, "value": { "type": "string", "pattern": "^(0|[1-9][0-9]*|-[1-9][0-9]*)(\\.[0-9]*[1-9])?$|^-0\\.[0-9]*[1-9]$" } } }, + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "string" }, "value": { "type": "string", "maxLength": 1024 } } }, + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "boolean" }, "value": { "type": "boolean" } } } + ] + }, + "marketSlice": { + "type": "object", + "additionalProperties": false, + "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "market_events", "order_book_events", "fx_rates", "corporate_actions", "borrow_observations", "cash_rate_observations", "settlement_failures", "lifecycle_events"], + "properties": { + "slice_sequence": { "$ref": "#/$defs/sequence" }, + "start_at": { "$ref": "#/$defs/timestamp" }, + "end_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "bars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/bar" } }, + "market_events": { "type": "array", "items": { "$ref": "#/$defs/marketEvent" } }, + "order_book_events": { "type": "array", "items": { "$ref": "#/$defs/orderBookEvent" } }, + "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } }, + "corporate_actions": { "type": "array", "items": { "$ref": "#/$defs/corporateAction" } }, + "borrow_observations": { "type": "array", "items": { "$ref": "#/$defs/borrowObservation" } }, + "cash_rate_observations": { "type": "array", "items": { "$ref": "#/$defs/cashRateObservation" } }, + "settlement_failures": { "type": "array", "items": { "$ref": "#/$defs/settlementFailure" } }, + "lifecycle_events": { "type": "array", "items": { "$ref": "#/$defs/lifecycleEvent" } } + } + }, + "bar": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "open", "high", "low", "close", "volume"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "open": { "$ref": "#/$defs/positiveDecimal" }, + "high": { "$ref": "#/$defs/positiveDecimal" }, + "low": { "$ref": "#/$defs/positiveDecimal" }, + "close": { "$ref": "#/$defs/positiveDecimal" }, + "volume": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] } + } + }, + "marketEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "bid_price", "bid_quantity", "ask_price", "ask_quantity"], + "properties": { + "type": { "const": "quote" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "bid_price": { "$ref": "#/$defs/positiveDecimal" }, + "bid_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "ask_price": { "$ref": "#/$defs/positiveDecimal" }, + "ask_quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "price", "quantity", "aggressor_side"], + "properties": { + "type": { "const": "trade" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "aggressor_side": { "enum": ["buy", "sell", "unknown"] } + } + } + ] + }, + "orderBookLevel": { + "type": "object", + "additionalProperties": false, + "required": ["price", "quantity"], + "properties": { + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "orderBookEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "bids", "asks"], + "properties": { + "type": { "const": "snapshot" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "bids": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } }, + "asks": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price", "quantity"], + "properties": { + "type": { "const": "set" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "side": { "enum": ["bid", "ask"] }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price"], + "properties": { + "type": { "const": "delete" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "side": { "enum": ["bid", "ask"] }, + "price": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "price", "quantity", "aggressor_side"], + "properties": { + "type": { "const": "trade" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "aggressor_side": { "enum": ["buy", "sell", "unknown"] } + } + } + ] + }, + "fxRate": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "rate"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "rate": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "corporateAction": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "numerator", "denominator"], + "properties": { + "type": { "const": "split" }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "amount_per_unit"], + "properties": { + "type": { "const": "cash_dividend" }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "amount_per_unit": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], + "properties": { + "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "destination_instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" }, + "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fractional_policy": { "$ref": "#/$defs/fractionalPolicy" } + } + } + ] + }, + "fractionalPolicy": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["policy"], + "properties": { "policy": { "const": "reject" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["policy", "price", "currency"], + "properties": { + "policy": { "const": "cash_in_lieu" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "currency": { "$ref": "#/$defs/identifier" } + } + } + ] + }, + "terminalPolicy": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["policy"], + "properties": { "policy": { "const": "hold" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["policy", "price", "currency"], + "properties": { + "policy": { "const": "cash_out" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "currency": { "$ref": "#/$defs/identifier" } + } + } + ] + }, + "lifecycleEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "reason"], + "properties": { + "type": { "const": "halt" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "reason": { "type": "string", "minLength": 1 } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id"], + "properties": { + "type": { "const": "resume" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "symbol", "provider", "provider_instrument_id"], + "properties": { + "type": { "const": "identifier_change" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "provider": { "$ref": "#/$defs/identifier" }, + "provider_instrument_id": { "$ref": "#/$defs/identifier" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "terminal_policy"], + "properties": { + "type": { "const": "expiration" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "terminal_policy": { "$ref": "#/$defs/terminalPolicy" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "terminal_policy", "reason"], + "properties": { + "type": { "const": "delisting" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "terminal_policy": { "$ref": "#/$defs/terminalPolicy" }, + "reason": { "type": "string", "minLength": 1 } + } + } + ] + } + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index a5d0aed..d7b9192 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/v15/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v16/README.md) remain authoritative for process boundaries. diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index aff839c..dbe9e62 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 v15 batch, stream, journal, and +The full test suite additionally validates and replays the current v16 batch, stream, journal, and strategy-v12 fixtures, including quote/trade causality and the reconciled first valuation. Coverage runs once in the exact locked Ubuntu environment. The required Persistra job also runs diff --git a/docs/execution-model.md b/docs/execution-model.md index 53d70d6..157fcaf 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,12 +1,12 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v15 advertises `completed_bar_v1`, `completed_bar_next_open_v1`, +Contract v16 advertises `completed_bar_v1`, `completed_bar_next_open_v1`, `completed_bar_adverse_touch_v1`, `quote_trade_v1`, and `order_book_v1`; embedders can inject another module through the typed engine configuration without introducing runtime shared-library loading. The selected name is repeated in both terminal audit records. -Each compiled model owns a strict configuration contract. The v15 envelope separates selection from +Each compiled model owns a strict configuration contract. The v16 envelope separates selection from model-specific parameters: ```json diff --git a/docs/persistra.md b/docs/persistra.md index f66b6be..8ebacb5 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 v15 while retaining v14 through v3 and +structural checks. The engine advertises current contract v16 while retaining v15 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 v13](../contracts/strategy/v13/README.md). Persistra's host turns protocol +[strategy protocol v14](../contracts/strategy/v14/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,7 +79,7 @@ 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 v15 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v16 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, diff --git a/docs/scenario.md b/docs/scenario.md index 64b50cb..c4c7292 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 v15 demo](../contracts/v15/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v15/scenario.schema.json) provides structural validation. +Use [the v16 demo](../contracts/v16/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v16/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/v15/scenario-stream.schema.json) validates each line, -and [the v15 stream fixture](../contracts/v15/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v16/scenario-stream.schema.json) validates each line, +and [the v16 stream fixture](../contracts/v16/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; v15 is `"15"` | +| `contract_version` | Required string identifying this file contract; v16 is `"16"` | | `metadata` | Required arbitrary JSON object preserved for provenance and ignored by execution | | `run_id` | Stable identity used in generated IDs | | `base_currency` | Reporting currency used for aggregate risk and valuation | @@ -167,7 +167,11 @@ Supported intents are: - `target_quantities` with a `targets` array of `instrument_id` and `quantity` - `submit_order` with instrument, side, quantity, kind, and nullable limit price - `cancel_order` with a deterministic `order_id` -- `emit_metric` with string `name` and `value` +- `emit_metric` with a bounded string `name` and typed `value`. The value object declares + `numeric` (a canonical decimal string), `string`, or `boolean`. Optional `unit`, `aggregation` + (`last`, `sum`, `minimum`, `maximum`, or `mean`), and up to 16 string dimensions carry + reconciliation metadata. Dimension keys are unique and journal encoding sorts them + lexicographically. Contracts through v15 retain the legacy string-only shape. Both target forms contain every configured instrument exactly once. Weights and quantities are signed. Gross absolute weight must not exceed `max_leverage`; quantity targets align to their @@ -268,7 +272,7 @@ records `event_at`, `available_at`, `received_at`, and a positive `ingest_sequen strictly ordered by availability, receipt, and ingest sequence; economic time cannot follow availability, and no event may escape its containing slice's time or observability boundary. Prices and quantities align to the instrument tick and lot. The -[`quote-trade` fixture](../contracts/v15/fixtures/quote-trade.scenario.json) demonstrates passive +[`quote-trade` fixture](../contracts/v16/fixtures/quote-trade.scenario.json) demonstrates passive fills and has an equivalent bounded JSON Lines replay. Version 15 slices add `order_book_events`. Every configured instrument supplies a fresh full @@ -278,7 +282,7 @@ books are accepted. Runtime validation enforces the configured `max_depth_levels delete, sequence continuity, slice observability, and tick/lot alignment. Marketable orders walk the visible book; passive limits queue behind displayed same-price depth, with reductions moving them forward and additions joining behind. The -[`order-book` fixture](../contracts/v15/fixtures/order-book.scenario.json) demonstrates bounded +[`order-book` fixture](../contracts/v16/fixtures/order-book.scenario.json) demonstrates bounded queue replay and has equivalent JSON Lines and journal artifacts. For causal next-open execution, an order-changing schedule entry's anchor `received_at` is no later @@ -286,7 +290,7 @@ than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v15/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v16/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 0734f09..6145ce3 100644 --- a/lib/audit.ml +++ b/lib/audit.ml @@ -128,7 +128,7 @@ type event = | Margin_call_triggered of valuation | Margin_restored of valuation | Intent_rejected of string - | Metric_emitted of { name : string; value : string } + | Metric_emitted of Metric.t | Valuation of valuation | Run_completed of { scenario_sha256 : string; diff --git a/lib/audit.mli b/lib/audit.mli index 30328af..07708df 100644 --- a/lib/audit.mli +++ b/lib/audit.mli @@ -130,7 +130,7 @@ type event = | Margin_call_triggered of valuation | Margin_restored of valuation | Intent_rejected of string - | Metric_emitted of { name : string; value : string } + | Metric_emitted of Metric.t | Valuation of valuation | Run_completed of { scenario_sha256 : string; diff --git a/lib/codec.ml b/lib/codec.ml index 238a9d5..fb5f935 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -457,9 +457,10 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ] |> function | `Assoc fields - when List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] -> + when List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] + -> let settlement = - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then [ ( "settlement_failures", `List @@ -469,7 +470,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = else [] in let lifecycle = - if List.mem contract_version [ "15"; "14"; "13"; "12" ] then + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12" ] then [ ( "lifecycle_events", `List @@ -479,7 +480,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = else [] in let market_events = - if List.mem contract_version [ "15"; "14" ] then + if List.mem contract_version [ "16"; "15"; "14" ] then [ ( "market_events", `List @@ -489,7 +490,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = else [] in let order_book_events = - if String.equal contract_version "15" then + if List.mem contract_version [ "16"; "15" ] then [ ( "order_book_events", `List @@ -534,6 +535,9 @@ let market_slice_to_yojson_v14 market_slice = let market_slice_to_yojson_v15 market_slice = versioned_market_slice_to_yojson ~contract_version:"15" market_slice +let market_slice_to_yojson_v16 market_slice = + versioned_market_slice_to_yojson ~contract_version:"16" market_slice + let request_fields request = let kind, limit_price = match request.Order.kind with @@ -637,7 +641,9 @@ let order_to_yojson_v8 order = ]) let versioned_order_to_yojson ~contract_version order = - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + if + List.mem contract_version + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] then order_to_yojson_v8 order else order_to_yojson order @@ -835,8 +841,9 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "cash_balances", `List (List.map - (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then - cash_attribution_to_yojson_v11 + (if + List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] + then cash_attribution_to_yojson_v11 else if String.equal contract_version "10" then cash_attribution_to_yojson_v10 else cash_attribution_to_yojson) @@ -844,8 +851,9 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "positions", `List (List.map - (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then - position_attribution_to_yojson_v11 + (if + List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] + then position_attribution_to_yojson_v11 else if String.equal contract_version "9" || String.equal contract_version "10" @@ -855,15 +863,16 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ] |> function | `Assoc fields - when List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9" ] - -> + when List.mem contract_version + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9" ] -> let financing = - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then - [ ("cash_interest", money valuation.Account.cash_interest) ] + if + List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] + then [ ("cash_interest", money valuation.Account.cash_interest) ] else [] in let settlement = - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then [ ("settled_cash", money valuation.Account.settled_cash); ("unsettled_cash", money valuation.unsettled_cash); @@ -912,7 +921,7 @@ let valuation_to_yojson ~contract_version valuation = let fields = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] then fields @ [ @@ -970,6 +979,41 @@ let requested_target_to_yojson target = Option.fold ~none:`Null ~some:price target.reference_price ); ] +let metric_to_yojson metric = + let value = + match metric.Metric.value with + | Metric.Numeric value -> + `Assoc + [ + ("type", string "numeric"); + ("value", string (Metric.numeric_to_string value)); + ] + | Metric.String value -> + `Assoc [ ("type", string "string"); ("value", string value) ] + | Metric.Boolean value -> + `Assoc [ ("type", string "boolean"); ("value", `Bool value) ] + in + `Assoc + ([ ("name", string metric.name); ("value", value) ] + @ (match metric.unit_ with + | None -> [] + | Some unit_ -> [ ("unit", string unit_) ]) + @ (if metric.dimensions = [] then [] + else + [ + ( "dimensions", + `Assoc + (List.map + (fun dimension -> + (dimension.Metric.key, string dimension.value)) + metric.dimensions) ); + ]) + @ + match metric.aggregation with + | None -> [] + | Some aggregation -> + [ ("aggregation", string (Metric.aggregation_to_string aggregation)) ]) + let payload_to_yojson ~contract_version = function | Audit.Run_started { scenario_sha256; execution_model } -> `Assoc @@ -1054,7 +1098,9 @@ let payload_to_yojson ~contract_version = function ("final_price", price attribution.final_price); ] | Audit.Fill_applied fill -> - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9" ] + if + List.mem contract_version + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9" ] then fill_to_yojson_v9 fill else fill_to_yojson fill | Audit.Settlement_instruction_created instruction @@ -1183,8 +1229,16 @@ let payload_to_yojson ~contract_version = function | Audit.Margin_call_triggered valuation | Audit.Margin_restored valuation -> valuation_to_yojson ~contract_version valuation | Audit.Intent_rejected reason -> `Assoc [ ("reason", string reason) ] - | Audit.Metric_emitted { name; value } -> - `Assoc [ ("name", string name); ("value", string value) ] + | Audit.Metric_emitted metric -> + if String.equal contract_version "16" then metric_to_yojson metric + else + let value = + match metric.Metric.value with + | Metric.String value -> value + | Metric.Numeric value -> Metric.numeric_to_string value + | Metric.Boolean value -> string_of_bool value + in + `Assoc [ ("name", string metric.name); ("value", string value) ] | Audit.Valuation valuation -> valuation_to_yojson ~contract_version valuation | Audit.Run_completed { scenario_sha256; execution_model; valuation; order_counts } -> diff --git a/lib/codec.mli b/lib/codec.mli index 12ad70c..f48c608 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -10,6 +10,7 @@ val market_slice_to_yojson_v12 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v13 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v14 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v15 : Market_slice.t -> Yojson.Safe.t +val market_slice_to_yojson_v16 : 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 f901c21..d8682a4 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,11 +1,12 @@ -let version = "15" -let previous_version = "14" +let version = "16" +let previous_version = "15" let legacy_journal_version = "3" let supported_versions = [ version; previous_version; + "14"; "13"; "12"; "11"; @@ -20,8 +21,8 @@ let supported_versions = ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "13" -let previous_strategy_protocol_version = "12" +let strategy_protocol_version = "14" +let previous_strategy_protocol_version = "13" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -40,6 +41,7 @@ let capabilities_to_yojson () = [ strategy_protocol_version; previous_strategy_protocol_version; + "12"; "11"; "10"; "9"; diff --git a/lib/engine.ml b/lib/engine.ml index 49af7ba..c88493e 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -75,6 +75,7 @@ let config_v12 = config_v11 let config_v13 = config_v12 let config_v14 = config_v13 let config_v15 = config_v14 +let config_v16 = config_v15 let valid_sha256 value = String.length value = 64 @@ -1499,15 +1500,13 @@ module Interactive = struct | Ok (desired, requested) -> replace_targets reduction Audit.Weights desired requested - let metric reduction name value = - if String.length name = 0 || String.trim name <> name then - reject_intent reduction "metric name must be a nonempty trimmed string" - else emit reduction (Audit.Metric_emitted { name; value }) + let emit_metric reduction observation = + emit reduction (Audit.Metric_emitted observation) let handle_intent reduction = function | intent when reduction.state.liquidation_pending -> ( match intent with - | Strategy.Emit_metric { name; value } -> metric reduction name value + | Strategy.Emit_metric observation -> emit_metric reduction observation | _ -> reject_intent reduction "margin liquidation is in progress") | Strategy.Target_weights targets -> set_weight_targets reduction targets | Strategy.Target_quantities targets -> @@ -1519,7 +1518,7 @@ module Interactive = struct else submit_order reduction request | Strategy.Cancel_order order_id -> cancel_order reduction ~reason:Audit.Strategy_requested order_id - | Strategy.Emit_metric { name; value } -> metric reduction name value + | Strategy.Emit_metric observation -> emit_metric reduction observation type drain_result = | Drained of reduction diff --git a/lib/engine.mli b/lib/engine.mli index ca85780..dc47f1f 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -84,6 +84,17 @@ val config_v15 : max_internal_events:int -> (config, string) result +val config_v16 : + 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_model.ml b/lib/execution_model.ml index eef0033..64c1712 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -66,7 +66,22 @@ let completed_bar_v1_contract = version = "2"; previous_versions = [ "1" ]; scenario_contract_versions = - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; + [ + "16"; + "15"; + "14"; + "13"; + "12"; + "11"; + "10"; + "9"; + "8"; + "7"; + "6"; + "5"; + "4"; + "3"; + ]; required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; legacy_required_fields = [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ]; @@ -87,7 +102,7 @@ let conservative_contract = { version = "1"; previous_versions = []; - scenario_contract_versions = [ "15"; "14"; "13" ]; + scenario_contract_versions = [ "16"; "15"; "14"; "13" ]; required_fields = [ "version"; @@ -116,7 +131,7 @@ let quote_trade_contract = { version = "1"; previous_versions = []; - scenario_contract_versions = [ "15"; "14" ]; + scenario_contract_versions = [ "16"; "15"; "14" ]; required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; legacy_required_fields = []; supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; @@ -138,7 +153,7 @@ let order_book_contract = { version = "1"; previous_versions = []; - scenario_contract_versions = [ "15" ]; + scenario_contract_versions = [ "16"; "15" ]; required_fields = [ "version"; "participation_bps"; "fee_schedules"; "max_depth_levels" ]; legacy_required_fields = []; diff --git a/lib/external_replay.ml b/lib/external_replay.ml index 1c95491..1c446ae 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -105,8 +105,8 @@ 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 "15" then - Engine.config_v15 ~contract_version ~risk ~venue_calendars + if List.mem contract_version [ "16"; "15" ] then + Engine.config_v16 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events else if String.equal contract_version "14" then diff --git a/lib/market_slice.ml b/lib/market_slice.ml index 130efe0..f3dcd27 100644 --- a/lib/market_slice.ml +++ b/lib/market_slice.ml @@ -186,6 +186,8 @@ let create_v15 ~slice_sequence ~start_at ~end_at ~available_at ~received_at settlement_failures; } +let create_v16 = create_v15 + let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations ~settlement_failures ~lifecycle_events diff --git a/lib/market_slice.mli b/lib/market_slice.mli index a1e0ded..7182f96 100644 --- a/lib/market_slice.mli +++ b/lib/market_slice.mli @@ -123,6 +123,23 @@ val create_v15 : order_book_events:Order_book_event.t list -> (t, string) result +val create_v16 : + slice_sequence:int64 -> + start_at:Ptime.t -> + end_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + bars:Bar.t list -> + fx_rates:fx_mark list -> + corporate_actions:Corporate_action.t list -> + borrow_observations:Financing.borrow_observation list -> + cash_rate_observations:Financing.cash_rate_observation list -> + settlement_failures:Settlement.failure list -> + lifecycle_events:Instrument_lifecycle.event list -> + market_events:Market_event.t list -> + order_book_events:Order_book_event.t list -> + (t, string) result + val bar : t -> Id.Instrument.t -> Bar.t option val fx_rate : t -> string -> Scalar.Price.t option val compare_replay_order : t -> t -> int diff --git a/lib/metric.ml b/lib/metric.ml new file mode 100644 index 0000000..08e086e --- /dev/null +++ b/lib/metric.ml @@ -0,0 +1,101 @@ +type numeric = string +type value = Numeric of numeric | String of string | Boolean of bool +type aggregation = Last | Sum | Minimum | Maximum | Mean +type dimension = { key : string; value : string } + +type t = { + name : string; + value : value; + unit_ : string option; + dimensions : dimension list; + aggregation : aggregation option; +} + +let max_name_bytes = Resource_limits.metric_name_bytes +let max_string_value_bytes = Resource_limits.metric_string_value_bytes +let max_unit_bytes = Resource_limits.metric_unit_bytes +let max_dimensions = Resource_limits.metric_dimensions +let max_dimension_key_bytes = Resource_limits.metric_dimension_key_bytes +let max_dimension_value_bytes = Resource_limits.metric_dimension_value_bytes + +let valid_trimmed ~maximum value = + String.length value > 0 + && String.length value <= maximum + && String.equal value (String.trim value) + +let numeric_to_string value = value + +let numeric_of_string value = + let length = String.length value in + let start = if length > 0 && value.[0] = '-' then 1 else 0 in + let decimal = ref None in + let valid = ref (start < length) in + for index = start to length - 1 do + match value.[index] with + | '0' .. '9' -> () + | '.' when !decimal = None && index > start && index + 1 < length -> + decimal := Some index + | _ -> valid := false + done; + let integer_end = Option.value !decimal ~default:length in + if integer_end - start > 1 && value.[start] = '0' then valid := false; + if Option.is_some !decimal && value.[length - 1] = '0' then valid := false; + if String.equal value "-0" then valid := false; + if !valid then Ok value + else Error "metric numeric value must be a canonical decimal string" + +let aggregation_to_string = function + | Last -> "last" + | Sum -> "sum" + | Minimum -> "minimum" + | Maximum -> "maximum" + | Mean -> "mean" + +let aggregation_of_string = function + | "last" -> Ok Last + | "sum" -> Ok Sum + | "minimum" -> Ok Minimum + | "maximum" -> Ok Maximum + | "mean" -> Ok Mean + | _ -> Error "metric aggregation must be last, sum, minimum, maximum, or mean" + +let create ~name ~value ?unit_ ?(dimensions = []) ?aggregation () = + if not (valid_trimmed ~maximum:max_name_bytes name) then + Error "metric name must be a nonempty trimmed string of at most 128 bytes" + else if + match value with + | String value -> String.length value > max_string_value_bytes + | Numeric _ | Boolean _ -> false + then Error "metric string value must contain at most 1024 bytes" + else if + match (value, aggregation) with + | (String _ | Boolean _), Some (Sum | Minimum | Maximum | Mean) -> true + | _ -> false + then Error "non-numeric metrics only support last aggregation" + else if + match unit_ with + | Some unit_ -> not (valid_trimmed ~maximum:max_unit_bytes unit_) + | None -> false + then Error "metric unit must be a nonempty trimmed string of at most 64 bytes" + else if List.length dimensions > max_dimensions then + Error "metric dimensions must contain at most 16 entries" + else + let dimensions = + List.sort + (fun (left, _) (right, _) -> String.compare left right) + dimensions + in + let rec validate prior acc = function + | [] -> Ok { name; value; unit_; dimensions = List.rev acc; aggregation } + | (key, value) :: remaining -> + if not (valid_trimmed ~maximum:max_dimension_key_bytes key) then + Error + "metric dimension key must be a nonempty trimmed string of at \ + most 64 bytes" + else if String.length value > max_dimension_value_bytes then + Error "metric dimension value must contain at most 128 bytes" + else if Option.equal String.equal prior (Some key) then + Error "metric dimension keys must be unique" + else validate (Some key) ({ key; value } :: acc) remaining + in + validate None [] dimensions diff --git a/lib/metric.mli b/lib/metric.mli new file mode 100644 index 0000000..a529fae --- /dev/null +++ b/lib/metric.mli @@ -0,0 +1,28 @@ +(** Typed, dimensioned strategy observations. *) + +type numeric +type value = Numeric of numeric | String of string | Boolean of bool +type aggregation = Last | Sum | Minimum | Maximum | Mean +type dimension = { key : string; value : string } + +type t = private { + name : string; + value : value; + unit_ : string option; + dimensions : dimension list; + aggregation : aggregation option; +} + +val create : + name:string -> + value:value -> + ?unit_:string -> + ?dimensions:(string * string) list -> + ?aggregation:aggregation -> + unit -> + (t, string) result + +val aggregation_to_string : aggregation -> string +val aggregation_of_string : string -> (aggregation, string) result +val numeric_of_string : string -> (numeric, string) result +val numeric_to_string : numeric -> string diff --git a/lib/replay.ml b/lib/replay.ml index 3378072..aa9c7bb 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -78,8 +78,8 @@ 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 "15" then - Engine.config_v15 ~contract_version ~risk ~venue_calendars + if List.mem contract_version [ "16"; "15" ] then + Engine.config_v16 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events else if String.equal contract_version "14" then diff --git a/lib/resource_limits.ml b/lib/resource_limits.ml index e873a82..e3df489 100644 --- a/lib/resource_limits.ml +++ b/lib/resource_limits.ml @@ -5,6 +5,12 @@ let internal_events = 100_000 let catalog_instruments = 4_096 let intents_per_batch = 4_096 let artifact_record_bytes = 2_097_152 +let metric_name_bytes = 128 +let metric_string_value_bytes = 1_024 +let metric_unit_bytes = 64 +let metric_dimensions = 16 +let metric_dimension_key_bytes = 64 +let metric_dimension_value_bytes = 128 let to_yojson () = `Assoc @@ -16,4 +22,10 @@ let to_yojson () = ("catalog_instruments", `Int catalog_instruments); ("intents_per_batch", `Int intents_per_batch); ("artifact_record_bytes", `Int artifact_record_bytes); + ("metric_name_bytes", `Int metric_name_bytes); + ("metric_string_value_bytes", `Int metric_string_value_bytes); + ("metric_unit_bytes", `Int metric_unit_bytes); + ("metric_dimensions", `Int metric_dimensions); + ("metric_dimension_key_bytes", `Int metric_dimension_key_bytes); + ("metric_dimension_value_bytes", `Int metric_dimension_value_bytes); ] diff --git a/lib/resource_limits.mli b/lib/resource_limits.mli index e0f4f78..682453c 100644 --- a/lib/resource_limits.mli +++ b/lib/resource_limits.mli @@ -10,4 +10,10 @@ val internal_events : int val catalog_instruments : int val intents_per_batch : int val artifact_record_bytes : int +val metric_name_bytes : int +val metric_string_value_bytes : int +val metric_unit_bytes : int +val metric_dimensions : int +val metric_dimension_key_bytes : int +val metric_dimension_value_bytes : int val to_yojson : unit -> Yojson.Safe.t diff --git a/lib/scenario.ml b/lib/scenario.ml index 81877d0..86a10f8 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -556,7 +556,7 @@ let parse_v7_risk base_currency instruments json = let parse_risk ~contract_version base_currency instruments json = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] then parse_v7_risk base_currency instruments json else parse_legacy_risk base_currency instruments json @@ -829,7 +829,7 @@ let parse_versioned_execution ~contract_version ~instruments json = let parse_execution ~contract_version ~instruments json = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then parse_versioned_execution ~contract_version ~instruments json else parse_legacy_execution ~contract_version json @@ -879,7 +879,8 @@ let parse_portfolio_intent ~name ~parse_target make json = let parse_submit_intent ~contract_version json = let versioned = - List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + List.mem contract_version + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] in let* fields = object_fields ~name:"submit_order intent" @@ -979,17 +980,86 @@ let parse_cancel_intent json = let* order_id = parse_id Id.Order.of_string ~name:"order_id" order_json in Ok (Strategy.Cancel_order order_id) -let parse_metric_intent json = - let* fields = - object_fields ~name:"emit_metric intent" - ~expected:[ "type"; "name"; "value" ] - json - in - let* name_json = field fields "name" in - let* name = string ~name:"metric name" name_json in - let* value_json = field fields "value" in - let* value = string ~name:"metric value" value_json in - Ok (Strategy.Emit_metric { name; value }) +let parse_metric_intent ~contract_version json = + if not (String.equal contract_version "16") then + let* fields = + object_fields ~name:"emit_metric intent" + ~expected:[ "type"; "name"; "value" ] + json + in + let* name_json = field fields "name" in + let* name = string ~name:"metric name" name_json in + let* value_json = field fields "value" in + let* value = string ~name:"metric value" value_json in + let* metric = Metric.create ~name ~value:(Metric.String value) () in + Ok (Strategy.Emit_metric metric) + else + let* fields = + match json with + | `Assoc fields -> + let names = List.map fst fields in + let unique = List.sort_uniq String.compare names in + let allowed = + [ "aggregation"; "dimensions"; "name"; "type"; "unit"; "value" ] + in + if List.length names <> List.length unique then + Error "emit_metric intent must not contain duplicate fields" + else if + not + (List.for_all (fun name -> List.mem name allowed) unique + && List.for_all + (fun name -> List.mem name unique) + [ "type"; "name"; "value" ]) + then Error "emit_metric intent has unknown or missing fields" + else Ok fields + | _ -> Error "emit_metric intent must be a JSON object" + in + let* name_json = field fields "name" in + let* name = string ~name:"metric name" name_json in + let* value = + let* json = field fields "value" in + let* value_fields = + object_fields ~name:"metric value" ~expected:[ "type"; "value" ] json + in + let* type_json = field value_fields "type" in + let* value_type = string ~name:"metric value type" type_json in + let* value_json = field value_fields "value" in + match (value_type, value_json) with + | "numeric", `String value -> + Metric.numeric_of_string value + |> Result.map (fun value -> Metric.Numeric value) + | "string", `String value -> Ok (Metric.String value) + | "boolean", `Bool value -> Ok (Metric.Boolean value) + | _ -> Error "metric value does not match its declared type" + in + let* unit_ = + match List.assoc_opt "unit" fields with + | None -> Ok None + | Some json -> string ~name:"metric unit" json |> Result.map Option.some + in + let* dimensions = + match List.assoc_opt "dimensions" fields with + | None -> Ok [] + | Some (`Assoc dimensions) -> + List.fold_left + (fun result (key, json) -> + let* values = result in + let* value = string ~name:"metric dimension value" json in + Ok ((key, value) :: values)) + (Ok []) dimensions + | Some _ -> Error "metric dimensions must be an object" + in + let* aggregation = + match List.assoc_opt "aggregation" fields with + | None -> Ok None + | Some json -> + let* value = string ~name:"metric aggregation" json in + Metric.aggregation_of_string value |> Result.map Option.some + in + let* metric = + Metric.create ~name ~value ?unit_ ~dimensions ?aggregation () + in + Ok (Strategy.Emit_metric metric) let parse_intent ~contract_version json = match json with @@ -1008,7 +1078,8 @@ let parse_intent ~contract_version json = | Some (`String "submit_order") -> parse_submit_intent ~contract_version json | Some (`String "cancel_order") -> parse_cancel_intent json - | Some (`String "emit_metric") -> parse_metric_intent json + | Some (`String "emit_metric") -> + parse_metric_intent ~contract_version json | Some _ -> Error "unsupported intent type" | None -> Error "intent is missing type") | _ -> Error "intent must be a JSON object" @@ -1850,25 +1921,27 @@ let parse_order_book_event json = let parse_slice ~contract_version json = let financing_fields = - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then - [ "borrow_observations"; "cash_rate_observations" ] + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] + then [ "borrow_observations"; "cash_rate_observations" ] else [] in let settlement_fields = - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then [ "settlement_failures" ] else [] in let lifecycle_fields = - if List.mem contract_version [ "15"; "14"; "13"; "12" ] then + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12" ] then [ "lifecycle_events" ] else [] in let market_event_fields = - if List.mem contract_version [ "15"; "14" ] then [ "market_events" ] else [] + if List.mem contract_version [ "16"; "15"; "14" ] then [ "market_events" ] + else [] in let order_book_event_fields = - if String.equal contract_version "15" then [ "order_book_events" ] else [] + if List.mem contract_version [ "16"; "15" ] then [ "order_book_events" ] + else [] in let* fields = object_fields ~name:"market slice" @@ -1906,7 +1979,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 [ "15"; "14"; "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] then let* borrow_json = Result.bind (field fields "borrow_observations") @@ -1921,7 +1994,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 [ "15"; "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then let* failures_json = Result.bind (field fields "settlement_failures") @@ -1930,21 +2003,21 @@ let parse_slice ~contract_version json = let* settlement_failures = map_list parse_settlement_failure failures_json in - if List.mem contract_version [ "15"; "14"; "13"; "12" ] then + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12" ] then let* lifecycle_json = Result.bind (field fields "lifecycle_events") (list ~name:"lifecycle_events") in let* lifecycle_events = map_list parse_lifecycle_event lifecycle_json in - if List.mem contract_version [ "15"; "14" ] then + if List.mem contract_version [ "16"; "15"; "14" ] then let* events_json = Result.bind (field fields "market_events") (list ~name:"market_events") in let* market_events = map_list parse_market_event events_json in - if String.equal contract_version "15" then + if List.mem contract_version [ "16"; "15" ] then let* book_events_json = Result.bind (field fields "order_book_events") @@ -2015,7 +2088,7 @@ let construct_header ~root ~contract_path ~contract_version let* initial_cash, initial_portfolio = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state @@ -2085,8 +2158,8 @@ let construct_header ~root ~contract_path ~contract_version | _, _ -> Ok Financing.legacy_policy in let financing = - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then - Some financing + if List.mem contract_version [ "16"; "15"; "14"; "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 e324722..71de814 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -72,7 +72,7 @@ let common ~root ~contract_version fields = let initial_field = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -81,19 +81,19 @@ let common ~root ~contract_version fields = let venue_calendars = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then List.assoc_opt "venue_calendars" fields else None in let* risk = field ~root fields "risk" in let* execution = field ~root fields "execution" in let financing = - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then - List.assoc_opt "financing" fields + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] + then List.assoc_opt "financing" fields else None in let settlement = - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then List.assoc_opt "settlement" fields else None in @@ -126,14 +126,14 @@ let batch json = let calendar_fields = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -154,11 +154,13 @@ let batch json = "slices"; ] @ calendar_fields - @ (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] + @ (if + List.mem contract_version + [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] then [ "financing" ] else []) @ - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then [ "settlement" ] else []) json @@ -174,14 +176,14 @@ let stream_header ~contract_version json = let calendar_fields = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -199,11 +201,13 @@ let stream_header ~contract_version json = "max_internal_events"; ] @ calendar_fields - @ (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] + @ (if + List.mem contract_version + [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] then [ "financing" ] else []) @ - if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then [ "settlement" ] else []) json diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index d09f3b7..06bed98 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -50,7 +50,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments let* () = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Ok () else Account.create ~base_currency ~initial_cash @@ -71,7 +71,9 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments let* () = if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ + "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; + ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -91,7 +93,19 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments (child root (if List.mem contract_version - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ + "16"; + "15"; + "14"; + "13"; + "12"; + "11"; + "10"; + "9"; + "8"; + "7"; + "6"; + ] then "initial_portfolio.cash" else "initial_cash")) "initial cash must contain every scenario currency exactly once" diff --git a/lib/strategy.ml b/lib/strategy.ml index 8cdd8b5..85eadf0 100644 --- a/lib/strategy.ml +++ b/lib/strategy.ml @@ -50,7 +50,7 @@ type intent = | Target_quantities of quantity_target list | Submit_order of Order.request | Cancel_order of Id.Order.t - | Emit_metric of { name : string; value : string } + | Emit_metric of Metric.t let ( let* ) result function_ = match result with Ok value -> function_ value | Error _ as error -> error diff --git a/lib/strategy.mli b/lib/strategy.mli index 071e23e..ece4bd7 100644 --- a/lib/strategy.mli +++ b/lib/strategy.mli @@ -47,7 +47,7 @@ type intent = | Target_quantities of quantity_target list | Submit_order of Order.request | Cancel_order of Id.Order.t - | Emit_metric of { name : string; value : string } + | Emit_metric of Metric.t val context : now:Ptime.t -> diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index ed1c9c5..8cbc699 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -103,7 +103,8 @@ let group_kind_to_string = function let nullable render = Option.fold ~none:`Null ~some:render let modern_protocol protocol_version = - List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + List.mem protocol_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] let financing_to_yojson policy = `Assoc @@ -253,7 +254,7 @@ let execution_to_yojson ~protocol_version model execution = ] in if - List.mem protocol_version [ "13"; "12"; "11" ] + List.mem protocol_version [ "14"; "13"; "12"; "11" ] && List.mem (Execution_model.name model) [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] @@ -291,7 +292,7 @@ let execution_to_yojson ~protocol_version model execution = ] ); ] else if - List.mem protocol_version [ "13"; "12" ] + List.mem protocol_version [ "14"; "13"; "12" ] && String.equal (Execution_model.name model) "quote_trade_v1" then `Assoc @@ -309,7 +310,7 @@ let execution_to_yojson ~protocol_version model execution = ] ); ] else if - String.equal protocol_version "13" + List.mem protocol_version [ "14"; "13" ] && String.equal (Execution_model.name model) "order_book_v1" then `Assoc @@ -328,7 +329,8 @@ let execution_to_yojson ~protocol_version model execution = `Int (Option.get (Execution.book_depth_limit execution)) ); ] ); ] - else if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7" ] + else if + List.mem protocol_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] then `Assoc [ @@ -368,6 +370,7 @@ let execution_to_yojson ~protocol_version model execution = let protocol_version initialization = match initialization.scenario_contract_version with + | "16" -> "14" | "15" -> "13" | "14" -> "12" | "13" -> "11" @@ -417,7 +420,9 @@ let initialize_message ~sequence:message_sequence initialization = ] in let fields = - if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + if + List.mem protocol_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then let initial_portfolio = Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson @@ -431,7 +436,9 @@ let initialize_message ~sequence:message_sequence initialization = ( "venue_calendars", `List (List.map venue_calendar_to_yojson venue_calendars) ); ]; - (if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8" ] + (if + List.mem protocol_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] then [ ( "financing", @@ -439,7 +446,8 @@ let initialize_message ~sequence:message_sequence initialization = initialization.financing ); ] else []); - (if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9" ] then + (if List.mem protocol_version [ "14"; "13"; "12"; "11"; "10"; "9" ] + then [ ( "settlement", Option.fold ~none:`Null ~some:settlement_to_yojson @@ -473,14 +481,15 @@ let cash_attribution_to_yojson ~protocol_version ("fx_rate", price balance.fx_rate); ("base_value", money balance.base_value); ] - @ (if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8" ] then + @ (if List.mem protocol_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + then [ ("interest", money balance.interest); ("base_interest", money balance.base_interest); ] else []) @ - if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9" ] then + if List.mem protocol_version [ "14"; "13"; "12"; "11"; "10"; "9" ] then [ ("settled_amount", money balance.settled_amount); ("unsettled_amount", money balance.unsettled_amount); @@ -500,7 +509,7 @@ let marked_position_to_yojson ~protocol_version ("weight", Option.fold ~none:`Null ~some:weight position.weight); ] @ - if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9" ] then + if List.mem protocol_version [ "14"; "13"; "12"; "11"; "10"; "9" ] then [ ("settled_quantity", quantity position.settled_quantity); ("unsettled_quantity", quantity position.unsettled_quantity); @@ -585,7 +594,7 @@ let context_to_yojson ~protocol_version context = (List.map (if List.mem protocol_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 else Codec.order_to_yojson) working_orders) ); @@ -598,7 +607,9 @@ let event_to_yojson ~protocol_version = function [ ("type", string "market_slice_closed"); ( "market_slice", - if String.equal protocol_version "13" then + if String.equal protocol_version "14" then + Codec.market_slice_to_yojson_v16 market_slice + else if String.equal protocol_version "13" then Codec.market_slice_to_yojson_v15 market_slice else if String.equal protocol_version "12" then Codec.market_slice_to_yojson_v14 market_slice @@ -619,7 +630,7 @@ let event_to_yojson ~protocol_version = function ( "fill", if List.mem protocol_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] then Codec.fill_to_yojson_v9 fill else Codec.fill_to_yojson fill ); ] @@ -630,7 +641,7 @@ let event_to_yojson ~protocol_version = function ( "order", if List.mem protocol_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 order else Codec.order_to_yojson order ); ] @@ -718,7 +729,8 @@ let parse_intents_payload ~protocol_version json = let* intent = Scenario.intent_of_yojson ~contract_version: - (if String.equal protocol_version "13" then "15" + (if String.equal protocol_version "14" then "16" + else if List.mem protocol_version [ "14"; "13" ] then "15" else if String.equal protocol_version "12" then "14" else if String.equal protocol_version "11" then "13" else if String.equal protocol_version "10" then "12" diff --git a/mkdocs.yml b/mkdocs.yml index 2f04317..0b4fb2f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,15 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v15: contracts/v15/README.md + - Current v16: contracts/v16/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 v13: contracts/strategy/v13/README.md + - Current v14: contracts/strategy/v14/README.md + - Historical v13: contracts/strategy/v13/README.md - Historical v3: contracts/strategy/v3/README.md - Historical v2: contracts/strategy/v2/README.md - Historical v1: contracts/strategy/v1/README.md diff --git a/scripts/check-deterministic-journals b/scripts/check-deterministic-journals index 74a05c0..b05c7df 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -126,3 +126,23 @@ compare_journal \ v15-order-book \ contracts/v15/fixtures/order-book.scenario.json \ contracts/v15/fixtures/order-book.journal.jsonl + +compare_journal \ + v16-demo \ + contracts/v16/fixtures/demo.scenario.json \ + contracts/v16/fixtures/demo.journal.jsonl + +compare_journal \ + v16-fill-clipped \ + contracts/v16/fixtures/fill-clipped.scenario.json \ + contracts/v16/fixtures/fill-clipped.journal.jsonl + +compare_journal \ + v16-quote-trade \ + contracts/v16/fixtures/quote-trade.scenario.json \ + contracts/v16/fixtures/quote-trade.journal.jsonl + +compare_journal \ + v16-order-book \ + contracts/v16/fixtures/order-book.scenario.json \ + contracts/v16/fixtures/order-book.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index 240086b..39a2038 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/v15/README.md", + "contracts/v16/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v13/README.md", + "contracts/strategy/v14/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 baa6858..6da23e8 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/v15/scenario.schema.json", - "share/trading_engine/contracts/v15/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v16/scenario.schema.json", + "share/trading_engine/contracts/v16/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/v15/fixtures/demo.scenario.json", + "contracts/v16/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/v15/fixtures/demo.scenario.json", - "contracts/strategy/v13/message.schema.json", + "contracts/v16/fixtures/demo.scenario.json", + "contracts/strategy/v14/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v15/scenario.schema.json", + "contracts/v16/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index dff75b2..f127929 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":["15","14","13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["15","14","13","12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1","order_book_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["15","14","13","12","11","10","9","8","7","6","5","4","3"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"2":["version","participation_bps","fee_schedules"],"1":["version","participation_bps","fixed_fee","fee_bps"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"fee_bps":{"minimum":0,"maximum":10000},"fixed_fee":{"minimum":"0","unit":"money"}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_adverse_touch_v1","configuration_versions":["1"],"scenario_contract_versions":["15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["15","14"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"order_book_v1","configuration_versions":["1"],"scenario_contract_versions":["15"],"required_fields":["version","participation_bps","fee_schedules","max_depth_levels"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","max_depth_levels"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["slice_open_level_two_snapshot","contiguous_absolute_level_updates","aggressor_classified_depth_consuming_trades","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"max_depth_levels":{"minimum":1,"maximum":1024}}}],"strategy_protocol_versions":["13","12","11","10","9","8","7","6","5","4","3"],"resource_limits":{"version":"1","scenario_record_bytes":1048576,"strategy_message_bytes":1048576,"internal_events":100000,"catalog_instruments":4096,"intents_per_batch":4096,"artifact_record_bytes":2097152}} + {"engine_version":"1.0.0","scenario_contract_versions":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1","order_book_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"2":["version","participation_bps","fee_schedules"],"1":["version","participation_bps","fixed_fee","fee_bps"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"fee_bps":{"minimum":0,"maximum":10000},"fixed_fee":{"minimum":"0","unit":"money"}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_adverse_touch_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"order_book_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15"],"required_fields":["version","participation_bps","fee_schedules","max_depth_levels"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","max_depth_levels"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["slice_open_level_two_snapshot","contiguous_absolute_level_updates","aggressor_classified_depth_consuming_trades","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"max_depth_levels":{"minimum":1,"maximum":1024}}}],"strategy_protocol_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"resource_limits":{"version":"1","scenario_record_bytes":1048576,"strategy_message_bytes":1048576,"internal_events":100000,"catalog_instruments":4096,"intents_per_batch":4096,"artifact_record_bytes":2097152,"metric_name_bytes":128,"metric_string_value_bytes":1024,"metric_unit_bytes":64,"metric_dimensions":16,"metric_dimension_key_bytes":64,"metric_dimension_value_bytes":128}} $ ../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 1d45f1a..e68f723 100644 --- a/test/dune +++ b/test/dune @@ -109,6 +109,21 @@ ../contracts/strategy/v10/fixtures/external.strategy.jsonl ../contracts/strategy/v11/fixtures/external.strategy.jsonl ../contracts/strategy/v13/fixtures/external.strategy.jsonl + ../contracts/strategy/v14/fixtures/external.strategy.jsonl + ../contracts/v16/fixtures/demo.journal.jsonl + ../contracts/v16/fixtures/demo.scenario.json + ../contracts/v16/fixtures/demo.scenario.jsonl + ../contracts/v16/fixtures/fill-clipped.journal.jsonl + ../contracts/v16/fixtures/fill-clipped.scenario.json + ../contracts/v16/fixtures/order-book.journal.jsonl + ../contracts/v16/fixtures/order-book.scenario.json + ../contracts/v16/fixtures/order-book.scenario.jsonl + ../contracts/v16/fixtures/quote-trade.journal.jsonl + ../contracts/v16/fixtures/quote-trade.scenario.json + ../contracts/v16/fixtures/quote-trade.scenario.jsonl + ../contracts/v16/journal.schema.json + ../contracts/v16/scenario-stream.schema.json + ../contracts/v16/scenario.schema.json ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index a75dd33..13ebe5b 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -137,7 +137,22 @@ let capabilities_describe_execution_contracts () = (strings "configuration_versions"); Alcotest.(check (list string)) "scenario contracts" - [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] + [ + "16"; + "15"; + "14"; + "13"; + "12"; + "11"; + "10"; + "9"; + "8"; + "7"; + "6"; + "5"; + "4"; + "3"; + ] (strings "scenario_contract_versions"); Alcotest.(check (list string)) "required fields" diff --git a/test/test_domain.ml b/test/test_domain.ml index 8143222..2a607ec 100644 --- a/test/test_domain.ml +++ b/test/test_domain.ml @@ -495,6 +495,51 @@ let risk_limits_cover_lots () = ~initial_margin_bps:5000 ~maintenance_margin_bps:2500 ~short_borrow_bps:100)) +let typed_metric_validation () = + let numeric = T.Metric.numeric_of_string "-12.5" |> ok in + Alcotest.(check string) + "negative fractional numeric" "-0.5" + (T.Metric.numeric_of_string "-0.5" |> ok |> T.Metric.numeric_to_string); + let metric = + T.Metric.create ~name:"strategy.signal" ~value:(T.Metric.Numeric numeric) + ~unit_:"ratio" + ~dimensions:[ ("venue", "XNYS"); ("asset", "ACME") ] + ~aggregation:T.Metric.Mean () + |> ok + in + Alcotest.(check string) + "numeric round trip" "-12.5" + (match metric.value with + | T.Metric.Numeric value -> T.Metric.numeric_to_string value + | _ -> Alcotest.fail "expected numeric metric"); + Alcotest.(check (list string)) + "dimensions sort canonically" [ "asset"; "venue" ] + (List.map (fun dimension -> dimension.T.Metric.key) metric.dimensions); + List.iter + (fun invalid -> + Alcotest.(check bool) + (invalid ^ " rejected") true + (Result.is_error (T.Metric.numeric_of_string invalid))) + [ ""; "01"; "1.0"; "-0"; "+1"; "1e3" ]; + Alcotest.(check bool) + "duplicate dimensions rejected" true + (Result.is_error + (T.Metric.create ~name:"duplicate" ~value:(T.Metric.Boolean true) + ~dimensions:[ ("side", "buy"); ("side", "sell") ] + ())); + Alcotest.(check bool) + "nonnumeric aggregation rejected" true + (Result.is_error + (T.Metric.create ~name:"state" ~value:(T.Metric.String "risk-on") + ~aggregation:T.Metric.Mean ())); + Alcotest.(check bool) + "dimension count bounded" true + (Result.is_error + (T.Metric.create ~name:"bounded" ~value:(T.Metric.String "ok") + ~dimensions: + (List.init 17 (fun index -> (Printf.sprintf "key-%02d" index, "x"))) + ())) + let tests = [ Alcotest.test_case "identifier validation" `Quick identifier_validation; @@ -523,4 +568,5 @@ let tests = Alcotest.test_case "risk accepts multiple currencies" `Quick risk_accepts_multiple_currencies; Alcotest.test_case "risk limits cover lots" `Quick risk_limits_cover_lots; + Alcotest.test_case "typed metric validation" `Quick typed_metric_validation; ] diff --git a/test/test_reducer.ml b/test/test_reducer.ml index 38899d0..46bbd74 100644 --- a/test/test_reducer.ml +++ b/test/test_reducer.ml @@ -796,7 +796,9 @@ let explicit_phase_order_is_stable () = |> ok in let metric = - T.Strategy.Emit_metric { name = "phase.boundary"; value = "reached" } + T.Metric.create ~name:"phase.boundary" ~value:(T.Metric.String "reached") () + |> ok + |> fun metric -> T.Strategy.Emit_metric metric in let state = runner [ (1L, [ target "2" ]); (2L, [ metric; target "0" ]) ] in let state, _ = Runner.process_slice state (market_slice 1L) |> ok in diff --git a/test/test_reducer_properties.ml b/test/test_reducer_properties.ml index 0ca0c95..9135a12 100644 --- a/test/test_reducer_properties.ml +++ b/test/test_reducer_properties.ml @@ -320,8 +320,11 @@ let command_intents context command = ] | Emit_metric value -> [ - T.Strategy.Emit_metric - { name = "generated.reducer.metric"; value = string_of_int value }; + ( T.Metric.create ~name:"generated.reducer.metric" + ~value:(T.Metric.String (string_of_int value)) + () + |> Result.get_ok + |> fun metric -> T.Strategy.Emit_metric metric ); ] module Asset_set = Set.Make (struct diff --git a/test/test_scenario.ml b/test/test_scenario.ml index bf17455..eebadba 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -2,21 +2,21 @@ open Test_support module T = Trading_engine let demo_document () = - In_channel.with_open_bin "../contracts/v15/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v16/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/v15/fixtures/demo.scenario.jsonl" -let quote_trade_path = "../contracts/v15/fixtures/quote-trade.scenario.json" +let stream_path = "../contracts/v16/fixtures/demo.scenario.jsonl" +let quote_trade_path = "../contracts/v16/fixtures/quote-trade.scenario.json" let quote_trade_stream_path = - "../contracts/v15/fixtures/quote-trade.scenario.jsonl" + "../contracts/v16/fixtures/quote-trade.scenario.jsonl" -let order_book_path = "../contracts/v15/fixtures/order-book.scenario.json" +let order_book_path = "../contracts/v16/fixtures/order-book.scenario.json" let order_book_stream_path = - "../contracts/v15/fixtures/order-book.scenario.jsonl" + "../contracts/v16/fixtures/order-book.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -104,7 +104,7 @@ let write_large_stream path slice_count = let payload = `Assoc [ - ("market_slice", T.Codec.market_slice_to_yojson_v15 market_slice); + ("market_slice", T.Codec.market_slice_to_yojson_v16 market_slice); ("intents", `List []); ] in @@ -149,9 +149,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v15/scenario.schema.json"; - check_schema "../contracts/v15/scenario-stream.schema.json"; - check_schema "../contracts/v15/journal.schema.json" + check_schema "../contracts/v16/scenario.schema.json"; + check_schema "../contracts/v16/scenario-stream.schema.json"; + check_schema "../contracts/v16/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -351,7 +351,7 @@ let v12_distributions_and_lifecycle_parse () = | _ -> Alcotest.fail "demo slice must be an object" in `List - (T.Codec.market_slice_to_yojson_v15 market_slice + (T.Codec.market_slice_to_yojson_v16 market_slice :: List.map add_child_bar rest) | _ -> Alcotest.fail "demo slices must be nonempty" in @@ -441,8 +441,8 @@ let contract_version_is_required_and_supported () = let unsupported_diagnostic = T.Scenario.of_yojson unsupported |> error in Alcotest.(check string) "unsupported version diagnosed" - "unsupported scenario contract_version \"2\" (expected one of 15, 14, 13, \ - 12, 11, 10, 9, 8, 7, 6, 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 16, 15, 14, \ + 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -607,7 +607,7 @@ let dense_schedule_document slice_count = ~cash_rate_observations:[ cash_rate_observation ] ~settlement_failures:[] ~lifecycle_events:[] ~market_events:[] ~order_book_events:[] - |> ok |> T.Codec.market_slice_to_yojson_v15) + |> ok |> T.Codec.market_slice_to_yojson_v16) in let schedule = List.init slice_count (fun offset -> @@ -622,7 +622,12 @@ let dense_schedule_document slice_count = [ ("type", `String "emit_metric"); ("name", `String "dense_schedule"); - ("value", `String (string_of_int sequence)); + ( "value", + `Assoc + [ + ("type", `String "numeric"); + ("value", `String (string_of_int sequence)); + ] ); ]; ] ); ]) @@ -1191,7 +1196,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v15/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v16/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -1219,7 +1224,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v15/fixtures/fill-clipped.scenario.json" + "../contracts/v16/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in @@ -1233,7 +1238,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v15/fixtures/fill-clipped.journal.jsonl" + "../contracts/v16/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual @@ -1253,7 +1258,7 @@ let quote_trade_replay_is_causal_and_stream_equivalent () = in let golden = In_channel.with_open_bin - "../contracts/v15/fixtures/quote-trade.journal.jsonl" In_channel.input_all + "../contracts/v16/fixtures/quote-trade.journal.jsonl" In_channel.input_all in Alcotest.(check string) "quote/trade golden journal" golden batch_journal; let fills = @@ -1311,7 +1316,7 @@ let order_book_replay_is_bounded_and_stream_equivalent () = in let golden = In_channel.with_open_bin - "../contracts/v15/fixtures/order-book.journal.jsonl" In_channel.input_all + "../contracts/v16/fixtures/order-book.journal.jsonl" In_channel.input_all in Alcotest.(check string) "order-book golden journal" golden actual; let fills = diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index f63b960..bec7710 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" "13" + "protocol version" "14" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -265,7 +265,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "13"); + ("strategy_protocol_version", `String "14"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); @@ -301,7 +301,31 @@ let responses_are_strict_and_typed () = [ ("type", `String "emit_metric"); ("name", `String "signal"); - ("value", `String "0.5"); + ( "value", + `Assoc + [ + ("type", `String "numeric"); ("value", `String "0.5"); + ] ); + ]; + `Assoc + [ + ("type", `String "emit_metric"); + ("name", `String "regime"); + ( "value", + `Assoc + [ + ("type", `String "string"); + ("value", `String "risk-on"); + ] ); + ]; + `Assoc + [ + ("type", `String "emit_metric"); + ("name", `String "healthy"); + ( "value", + `Assoc + [ ("type", `String "boolean"); ("value", `Bool true) ] + ); ]; ] ); ]) @@ -309,9 +333,27 @@ let responses_are_strict_and_typed () = (match T.Strategy_protocol.response_of_yojson ~expected_sequence:3L intents |> ok with - | T.Strategy_protocol.Intents [ T.Strategy.Emit_metric { name; value } ] -> - Alcotest.(check string) "metric name" "signal" name; - Alcotest.(check string) "metric value" "0.5" value + | T.Strategy_protocol.Intents + [ + T.Strategy.Emit_metric metric; + T.Strategy.Emit_metric string_metric; + T.Strategy.Emit_metric boolean_metric; + ] -> ( + Alcotest.(check string) "metric name" "signal" metric.name; + (match metric.value with + | T.Metric.Numeric value -> + Alcotest.(check string) + "metric value" "0.5" + (T.Metric.numeric_to_string value) + | _ -> Alcotest.fail "expected numeric metric value"); + (match string_metric.value with + | T.Metric.String value -> + Alcotest.(check string) "string metric value" "risk-on" value + | _ -> Alcotest.fail "expected string metric value"); + match boolean_metric.value with + | T.Metric.Boolean value -> + Alcotest.(check bool) "boolean metric value" true value + | _ -> Alcotest.fail "expected boolean metric value") | _ -> Alcotest.fail "expected metric intent"); let wrong_sequence = T.Strategy_protocol.response_of_yojson ~expected_sequence:4L ready From c8a877eace239a34089fd78de0eb4dab4f56d738 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 18:01:00 -0400 Subject: [PATCH 51/57] fix: preserve resource capability compatibility --- lib/resource_limits.ml | 6 ------ test/cli.t | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/resource_limits.ml b/lib/resource_limits.ml index e3df489..6ba78ac 100644 --- a/lib/resource_limits.ml +++ b/lib/resource_limits.ml @@ -22,10 +22,4 @@ let to_yojson () = ("catalog_instruments", `Int catalog_instruments); ("intents_per_batch", `Int intents_per_batch); ("artifact_record_bytes", `Int artifact_record_bytes); - ("metric_name_bytes", `Int metric_name_bytes); - ("metric_string_value_bytes", `Int metric_string_value_bytes); - ("metric_unit_bytes", `Int metric_unit_bytes); - ("metric_dimensions", `Int metric_dimensions); - ("metric_dimension_key_bytes", `Int metric_dimension_key_bytes); - ("metric_dimension_value_bytes", `Int metric_dimension_value_bytes); ] diff --git a/test/cli.t b/test/cli.t index f127929..6ce3ee0 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":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1","order_book_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"2":["version","participation_bps","fee_schedules"],"1":["version","participation_bps","fixed_fee","fee_bps"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"fee_bps":{"minimum":0,"maximum":10000},"fixed_fee":{"minimum":"0","unit":"money"}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_adverse_touch_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"order_book_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15"],"required_fields":["version","participation_bps","fee_schedules","max_depth_levels"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","max_depth_levels"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["slice_open_level_two_snapshot","contiguous_absolute_level_updates","aggressor_classified_depth_consuming_trades","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"max_depth_levels":{"minimum":1,"maximum":1024}}}],"strategy_protocol_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"resource_limits":{"version":"1","scenario_record_bytes":1048576,"strategy_message_bytes":1048576,"internal_events":100000,"catalog_instruments":4096,"intents_per_batch":4096,"artifact_record_bytes":2097152,"metric_name_bytes":128,"metric_string_value_bytes":1024,"metric_unit_bytes":64,"metric_dimensions":16,"metric_dimension_key_bytes":64,"metric_dimension_value_bytes":128}} + {"engine_version":"1.0.0","scenario_contract_versions":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1","order_book_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"2":["version","participation_bps","fee_schedules"],"1":["version","participation_bps","fixed_fee","fee_bps"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"fee_bps":{"minimum":0,"maximum":10000},"fixed_fee":{"minimum":"0","unit":"money"}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_adverse_touch_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"order_book_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15"],"required_fields":["version","participation_bps","fee_schedules","max_depth_levels"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","max_depth_levels"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["slice_open_level_two_snapshot","contiguous_absolute_level_updates","aggressor_classified_depth_consuming_trades","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"max_depth_levels":{"minimum":1,"maximum":1024}}}],"strategy_protocol_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"resource_limits":{"version":"1","scenario_record_bytes":1048576,"strategy_message_bytes":1048576,"internal_events":100000,"catalog_instruments":4096,"intents_per_batch":4096,"artifact_record_bytes":2097152}} $ ../bin/main.exe --validate-only --input ../contracts/v8/fixtures/demo.scenario.json valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=85f7c99e0666159579c79256b3d0dc5f9c328e1275b79465fe1d4c93883e68f1 From 797213243606c6e9e7ef848dd8f849b50004ee32 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 18:23:28 -0400 Subject: [PATCH 52/57] feat: add CLI automation pipelines --- CHANGELOG.md | 2 + README.md | 22 + bin/dune | 10 +- bin/main.ml | 681 ++++++++++++++++----- contracts/cli/v1/README.md | 20 + contracts/cli/v1/dune | 7 + contracts/cli/v1/fixtures/demo.result.json | 1 + contracts/cli/v1/result.schema.json | 130 ++++ contracts/conformance/manifest.json | 12 + contracts/diagnostic/v1/README.md | 2 +- docs/diagnostics.md | 3 +- docs/scenario.md | 12 + lib/codec.mli | 4 + lib/resource_limits.ml | 1 + lib/resource_limits.mli | 1 + mkdocs.yml | 2 + scripts/check-documentation.py | 1 + scripts/release_artifacts.py | 4 + test/cli.t | 51 ++ test/dune | 17 + test/validate_cli_result.py | 66 ++ 21 files changed, 897 insertions(+), 152 deletions(-) create mode 100644 contracts/cli/v1/README.md create mode 100644 contracts/cli/v1/dune create mode 100644 contracts/cli/v1/fixtures/demo.result.json create mode 100644 contracts/cli/v1/result.schema.json create mode 100644 test/validate_cli_result.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 125542c..902e9ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add versioned machine-readable CLI success results and JSON failure diagnostics, plus bounded + JSON Lines standard-input spooling and unambiguous completed-journal standard-output pipelines. - Publish scenario/journal contract v16 and strategy protocol v14 with typed, dimensioned strategy metrics while retaining string-only metric compatibility through v15 and protocol v13. diff --git a/README.md b/README.md index 1a1b207..e93feda 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,19 @@ opam exec -- dune exec trading-engine -- \ --journal demo.journal.jsonl ``` +Compose a JSON Lines producer and journal consumer without mixing streams: + +```sh +produce-scenario | trading-engine --input - --input-format jsonl --journal - | consume-journal +``` + +Standard input is spooled to a private temporary file, limited to 1 GiB, then hashed and validated +before replay. Standard output contains only journal records. The engine stages and verifies the +complete journal before copying it to the pipe; its final `run_completed` record and a zero exit +status signal completion. Pipe output cannot provide exclusive no-replace publication, atomic +linking, retained partial files, directory synchronization, or restart-durability guarantees. +`--durable-artifacts` is therefore invalid with `--journal -`. + Run an external strategy against an empty-schedule scenario: ```sh @@ -162,6 +175,13 @@ diagnostic contract identified by each diagnostic's `diagnostic_version`. Human the default. Use `--diagnostic-format json` to receive one JSON diagnostic on standard error with a stable code, phase, typed context, and sanitized underlying cause. +Use `--output-format json` for the versioned +[CLI result contract](contracts/cli/v1/README.md). A success document includes the run identity, +scenario and artifact hashes, replay counts, normalized current valuation, and artifact locations. +This option also selects JSON failure diagnostics. For file journals the success document is written +to standard output. With `--journal -`, the journal owns standard output and the success document +moves to standard error. + The final and `.partial` journal paths must not already exist. Batch JSON hashes the same complete document it parses. JSON Lines input is hashed and validated in a bounded-memory pass before the journal is created, then replayed from the same open file and hashed again before publication. The @@ -238,6 +258,7 @@ do not provide reducer snapshots or restart recovery. - [Contributing](CONTRIBUTING.md) - [Architecture](docs/architecture.md) - [Diagnostic contract](docs/diagnostics.md) +- [CLI result contract](contracts/cli/v1/README.md) - [Scenario contract](docs/scenario.md) - [Contract conformance corpus](contracts/conformance/README.md) - [Current contract v16 and conformance fixtures](contracts/v16/README.md) @@ -246,6 +267,7 @@ do not provide reducer snapshots or restart recovery. - [Scenario JSON Schema](contracts/v16/scenario.schema.json) - [Scenario stream record JSON Schema](contracts/v16/scenario-stream.schema.json) - [Journal record JSON Schema](contracts/v16/journal.schema.json) +- [CLI result JSON Schema](contracts/cli/v1/result.schema.json) - [External strategy protocol v14](contracts/strategy/v14/README.md) - [Historical strategy protocol v3](contracts/strategy/v3/README.md) - [Historical strategy protocol v2](contracts/strategy/v2/README.md) diff --git a/bin/dune b/bin/dune index cb97f95..d1392d9 100644 --- a/bin/dune +++ b/bin/dune @@ -4,4 +4,12 @@ (package trading_engine) (instrumentation (backend bisect_ppx)) - (libraries trading_engine cmdliner eio_main fmt.tty logs.fmt logs.cli)) + (libraries + trading_engine + cmdliner + eio_main + fmt.tty + logs.fmt + logs.cli + yojson + unix)) diff --git a/bin/main.ml b/bin/main.ml index dad181a..14e0fbb 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -1,6 +1,37 @@ open Cmdliner type diagnostic_format = Human | Json +type output_format = Human_output | Json_output + +type counts = { + instruments : int64; + schedule_batches : int64; + slices : int64; + audits : int64; + orders : int64; + active_orders : int64; + filled_orders : int64; + rejected_orders : int64; +} + +type success = { + operation : string; + run_id : Trading_engine.Id.Run.t; + scenario_sha256 : string; + journal_sha256 : string option; + transcript_sha256 : string option; + counts : counts; + valuation : Trading_engine.Account.valuation; + journal : string option; + transcript : string option; +} + +type journal_destination = { + replay_path : string; + public_path : string; + writes_stdout : bool; + cleanup : unit -> unit; +} let cli_error message = Trading_engine.Diagnostic.make @@ -17,65 +48,243 @@ let count predicate values = (fun total value -> total + Bool.to_int (predicate value)) 0 values -let run_replay scenario_sha256 scenario journal durability = +let int64 value = `Intlit (Int64.to_string value) +let option_string = function None -> `Null | Some value -> `String value + +let success_to_yojson success = + let counts = success.counts in + `Assoc + [ + ("result_version", `String "1"); + ("status", `String "success"); + ("operation", `String success.operation); + ("run_id", `String (Trading_engine.Id.Run.to_string success.run_id)); + ( "hashes", + `Assoc + [ + ("scenario_sha256", `String success.scenario_sha256); + ("journal_sha256", option_string success.journal_sha256); + ( "strategy_transcript_sha256", + option_string success.transcript_sha256 ); + ] ); + ( "counts", + `Assoc + [ + ("instruments", int64 counts.instruments); + ("schedule_batches", int64 counts.schedule_batches); + ("slices", int64 counts.slices); + ("audits", int64 counts.audits); + ("orders", int64 counts.orders); + ("active_orders", int64 counts.active_orders); + ("filled_orders", int64 counts.filled_orders); + ("rejected_orders", int64 counts.rejected_orders); + ] ); + ( "valuation", + Trading_engine.Codec.account_valuation_to_yojson ~contract_version:"16" + success.valuation ); + ( "artifacts", + `Assoc + [ + ("journal", option_string success.journal); + ("strategy_transcript", option_string success.transcript); + ] ); + ] + +let order_counts orders = + let active = count Trading_engine.Order.is_active orders in + let filled = + count + (fun order -> + order.Trading_engine.Order.status = Trading_engine.Order.Filled) + orders + in + let rejected = + count + (fun order -> + match order.Trading_engine.Order.status with + | Trading_engine.Order.Rejected _ -> true + | _ -> false) + orders + in + (active, filled, rejected) + +let emit_success format ~to_stderr success = + let formatter = + if to_stderr then Format.err_formatter else Format.std_formatter + in + match format with + | Json_output -> + Fmt.pf formatter "%s@." + (Yojson.Safe.to_string (success_to_yojson success)) + | Human_output -> + let counts = success.counts in + Fmt.pf formatter + "run=%a audits=%Ld orders=%Ld active=%Ld filled=%Ld rejected=%Ld@." + Trading_engine.Id.Run.pp success.run_id counts.audits counts.orders + counts.active_orders counts.filled_orders counts.rejected_orders; + Fmt.pf formatter "%a@." Trading_engine.Account.pp_valuation + success.valuation; + Option.iter (Fmt.pf formatter "journal=%s@.") success.journal; + Option.iter + (Fmt.pf formatter "strategy_transcript=%s@.") + success.transcript + +let digest_file path = Trading_engine.Sha256.digest_file path + +let remove_if_exists path = + try if Sys.file_exists path then Sys.remove path with Sys_error _ -> () + +let temporary_journal_destination () = + try + let directory = + Filename.temp_dir ~perms:0o700 "trading-engine-journal-" "" + in + let path = Filename.concat directory "journal.jsonl" in + Ok + { + replay_path = path; + public_path = "stdout"; + writes_stdout = true; + cleanup = + (fun () -> + remove_if_exists path; + remove_if_exists (path ^ ".partial"); + remove_if_exists (path ^ ".partial.cleanup"); + try Unix.rmdir directory with Unix.Unix_error _ -> ()); + } + with exception_ -> + Error + (Trading_engine.Diagnostic.of_exception + ~code:Trading_engine.Diagnostic.Artifact_io + ~phase:Trading_engine.Diagnostic.Artifact + ~message:"could not create temporary journal spool" exception_) + +let journal_destination path = + if String.equal path "-" then temporary_journal_destination () + else + Ok + { + replay_path = path; + public_path = path; + writes_stdout = false; + cleanup = Fun.id; + } + +let copy_file_to_stdout path = + try + In_channel.with_open_bin path (fun channel -> + let buffer = Bytes.create 65_536 in + let rec loop () = + match input channel buffer 0 (Bytes.length buffer) with + | 0 -> () + | length -> + output stdout buffer 0 length; + loop () + in + loop ()); + flush stdout; + Ok () + with exception_ -> + Error + (Trading_engine.Diagnostic.of_exception + ~code:Trading_engine.Diagnostic.Artifact_io + ~phase:Trading_engine.Diagnostic.Artifact + ~message:"could not write journal to standard output" exception_) + +let finish_success output_format destination success = + let result = + match digest_file destination.replay_path with + | Error _ as error -> error + | Ok journal_sha256 -> + let success = + { + success with + journal_sha256 = Some journal_sha256; + journal = Some destination.public_path; + } + in + if destination.writes_stdout then ( + match copy_file_to_stdout destination.replay_path with + | Error _ as error -> error + | Ok () -> + emit_success output_format ~to_stderr:true success; + Ok ()) + else ( + emit_success output_format ~to_stderr:false success; + Ok ()) + in + destination.cleanup (); + result + +let run_replay scenario_sha256 scenario destination durability output_format = match - Trading_engine.Replay.run ~scenario_sha256 ~journal_path:journal ~durability - scenario + Trading_engine.Replay.run ~scenario_sha256 + ~journal_path:destination.replay_path ~durability scenario with - | Error message -> Error message + | Error message -> + destination.cleanup (); + Error message | Ok result -> - let active = count Trading_engine.Order.is_active result.orders in - let filled = - count - (fun order -> - order.Trading_engine.Order.status = Trading_engine.Order.Filled) - result.orders + let active, filled, rejected = order_counts result.orders in + let success = + { + operation = "replay"; + run_id = scenario.Trading_engine.Scenario.run_id; + scenario_sha256; + journal_sha256 = None; + transcript_sha256 = None; + counts = + { + instruments = Int64.of_int (List.length scenario.instruments); + schedule_batches = Int64.of_int (List.length scenario.schedule); + slices = Int64.of_int (List.length scenario.slices); + audits = Int64.of_int (List.length result.audits); + orders = Int64.of_int (List.length result.orders); + active_orders = Int64.of_int active; + filled_orders = Int64.of_int filled; + rejected_orders = Int64.of_int rejected; + }; + valuation = result.valuation; + journal = None; + transcript = None; + } in - let rejected = - count - (fun order -> - match order.Trading_engine.Order.status with - | Trading_engine.Order.Rejected _ -> true - | _ -> false) - result.orders - in - Fmt.pr "run=%a audits=%d orders=%d active=%d filled=%d rejected=%d@." - Trading_engine.Id.Run.pp scenario.Trading_engine.Scenario.run_id - (List.length result.audits) - (List.length result.orders) - active filled rejected; - Fmt.pr "%a@." Trading_engine.Account.pp_valuation result.valuation; - Fmt.pr "journal=%s@." journal; - Ok () - -let run_stream input journal durability = + finish_success output_format destination success + +let run_stream input destination durability output_format = match - Trading_engine.Replay.run_stream ~journal_path:journal ~durability input + Trading_engine.Replay.run_stream ~journal_path:destination.replay_path + ~durability input with - | Error message -> Error message + | Error message -> + destination.cleanup (); + Error message | Ok result -> - let active = count Trading_engine.Order.is_active result.orders in - let filled = - count - (fun order -> - order.Trading_engine.Order.status = Trading_engine.Order.Filled) - result.orders - in - let rejected = - count - (fun order -> - match order.Trading_engine.Order.status with - | Trading_engine.Order.Rejected _ -> true - | _ -> false) - result.orders + let active, filled, rejected = order_counts result.orders in + let success = + { + operation = "replay"; + run_id = result.run_id; + scenario_sha256 = result.scenario_sha256; + journal_sha256 = None; + transcript_sha256 = None; + counts = + { + instruments = Int64.of_int result.instrument_count; + schedule_batches = result.schedule_count; + slices = result.slice_count; + audits = result.audit_count; + orders = Int64.of_int (List.length result.orders); + active_orders = Int64.of_int active; + filled_orders = Int64.of_int filled; + rejected_orders = Int64.of_int rejected; + }; + valuation = result.valuation; + journal = None; + transcript = None; + } in - Fmt.pr "run=%a audits=%Ld orders=%d active=%d filled=%d rejected=%d@." - Trading_engine.Id.Run.pp result.run_id result.audit_count - (List.length result.orders) - active filled rejected; - Fmt.pr "%a@." Trading_engine.Account.pp_valuation result.valuation; - Fmt.pr "journal=%s@." journal; - Ok () + finish_success output_format destination success type external_strategy = { command : string list; @@ -83,75 +292,117 @@ type external_strategy = { transcript : string; } -let run_external_replay environment scenario_sha256 scenario journal strategy - durability = +let run_external_replay environment scenario_sha256 scenario destination + strategy durability output_format = match Trading_engine.External_replay.run ~durability ~env:environment - ~scenario_sha256 ~journal_path:journal + ~scenario_sha256 ~journal_path:destination.replay_path ~transcript_path:strategy.transcript ~strategy_command:strategy.command ~strategy_timeout:strategy.timeout scenario with - | Error message -> Error message - | Ok result -> - let active = count Trading_engine.Order.is_active result.orders in - let filled = - count - (fun order -> - order.Trading_engine.Order.status = Trading_engine.Order.Filled) - result.orders - in - let rejected = - count - (fun order -> - match order.Trading_engine.Order.status with - | Trading_engine.Order.Rejected _ -> true - | _ -> false) - result.orders - in - Fmt.pr "run=%a audits=%d orders=%d active=%d filled=%d rejected=%d@." - Trading_engine.Id.Run.pp scenario.Trading_engine.Scenario.run_id - (List.length result.audits) - (List.length result.orders) - active filled rejected; - Fmt.pr "%a@." Trading_engine.Account.pp_valuation result.valuation; - Fmt.pr "journal=%s@." journal; - Fmt.pr "strategy_transcript=%s@." strategy.transcript; - Ok () - -let run_external_stream environment input journal strategy durability = + | Error message -> + destination.cleanup (); + Error message + | Ok result -> ( + let active, filled, rejected = order_counts result.orders in + match digest_file strategy.transcript with + | Error _ as error -> + destination.cleanup (); + error + | Ok transcript_sha256 -> + finish_success output_format destination + { + operation = "replay"; + run_id = scenario.Trading_engine.Scenario.run_id; + scenario_sha256; + journal_sha256 = None; + transcript_sha256 = Some transcript_sha256; + counts = + { + instruments = Int64.of_int (List.length scenario.instruments); + schedule_batches = 0L; + slices = Int64.of_int (List.length scenario.slices); + audits = Int64.of_int (List.length result.audits); + orders = Int64.of_int (List.length result.orders); + active_orders = Int64.of_int active; + filled_orders = Int64.of_int filled; + rejected_orders = Int64.of_int rejected; + }; + valuation = result.valuation; + journal = None; + transcript = Some strategy.transcript; + }) + +let run_external_stream environment input destination strategy durability + output_format = match Trading_engine.External_replay.run_stream ~durability ~env:environment - ~journal_path:journal ~transcript_path:strategy.transcript + ~journal_path:destination.replay_path ~transcript_path:strategy.transcript ~strategy_command:strategy.command ~strategy_timeout:strategy.timeout input with - | Error message -> Error message - | Ok result -> - let active = count Trading_engine.Order.is_active result.orders in - let filled = - count - (fun order -> - order.Trading_engine.Order.status = Trading_engine.Order.Filled) - result.orders - in - let rejected = - count - (fun order -> - match order.Trading_engine.Order.status with - | Trading_engine.Order.Rejected _ -> true - | _ -> false) - result.orders - in - Fmt.pr "run=%a audits=%Ld orders=%d active=%d filled=%d rejected=%d@." - Trading_engine.Id.Run.pp result.run_id result.audit_count - (List.length result.orders) - active filled rejected; - Fmt.pr "%a@." Trading_engine.Account.pp_valuation result.valuation; - Fmt.pr "journal=%s@." journal; - Fmt.pr "strategy_transcript=%s@." strategy.transcript; - Ok () - -let execute_json environment input journal validate_only strategy durability = + | Error message -> + destination.cleanup (); + Error message + | Ok result -> ( + let active, filled, rejected = order_counts result.orders in + match digest_file strategy.transcript with + | Error _ as error -> + destination.cleanup (); + error + | Ok transcript_sha256 -> + finish_success output_format destination + { + operation = "replay"; + run_id = result.run_id; + scenario_sha256 = result.scenario_sha256; + journal_sha256 = None; + transcript_sha256 = Some transcript_sha256; + counts = + { + instruments = Int64.of_int result.instrument_count; + schedule_batches = 0L; + slices = result.slice_count; + audits = result.audit_count; + orders = Int64.of_int (List.length result.orders); + active_orders = Int64.of_int active; + filled_orders = Int64.of_int filled; + rejected_orders = Int64.of_int rejected; + }; + valuation = result.valuation; + journal = None; + transcript = Some strategy.transcript; + }) + +let emit_validation output_format ~run_id ~scenario_sha256 ~instrument_count + ~schedule_count ~slice_count ~orders ~valuation ~audit_count = + let active, filled, rejected = order_counts orders in + emit_success output_format ~to_stderr:false + { + operation = "validate"; + run_id; + scenario_sha256; + journal_sha256 = None; + transcript_sha256 = None; + counts = + { + instruments = instrument_count; + schedule_batches = schedule_count; + slices = slice_count; + audits = audit_count; + orders = Int64.of_int (List.length orders); + active_orders = Int64.of_int active; + filled_orders = Int64.of_int filled; + rejected_orders = Int64.of_int rejected; + }; + valuation; + journal = None; + transcript = None; + }; + Ok () + +let execute_json environment input journal validate_only strategy durability + output_format = let document = try Ok (In_channel.with_open_bin input In_channel.input_all) with Sys_error message as exception_ -> @@ -175,16 +426,28 @@ let execute_json environment input journal validate_only strategy durability = | None -> ( match Trading_engine.Replay.run ~scenario_sha256 scenario with | Error message -> Error message - | Ok _ -> - Fmt.pr - "valid run=%a instruments=%d schedule=%d slices=%d \ - scenario_sha256=%s@." - Trading_engine.Id.Run.pp scenario.run_id - (List.length scenario.instruments) - (List.length scenario.schedule) - (List.length scenario.slices) - scenario_sha256; - Ok ()) + | Ok result -> + if output_format = Human_output then ( + Fmt.pr + "valid run=%a instruments=%d schedule=%d slices=%d \ + scenario_sha256=%s@." + Trading_engine.Id.Run.pp scenario.run_id + (List.length scenario.instruments) + (List.length scenario.schedule) + (List.length scenario.slices) + scenario_sha256; + Ok ()) + else + emit_validation output_format ~run_id:scenario.run_id + ~scenario_sha256 + ~instrument_count: + (Int64.of_int (List.length scenario.instruments)) + ~schedule_count: + (Int64.of_int (List.length scenario.schedule)) + ~slice_count: + (Int64.of_int (List.length scenario.slices)) + ~orders:result.orders ~valuation:result.valuation + ~audit_count:(Int64.of_int (List.length result.audits))) else match journal with | None -> @@ -192,13 +455,19 @@ let execute_json environment input journal validate_only strategy durability = (cli_error "--journal is required unless --validate-only is set") | Some path -> ( - match strategy with - | None -> run_replay scenario_sha256 scenario path durability - | Some strategy -> - run_external_replay environment scenario_sha256 scenario - path strategy durability))) + match journal_destination path with + | Error _ as error -> error + | Ok destination -> ( + match strategy with + | None -> + run_replay scenario_sha256 scenario destination + durability output_format + | Some strategy -> + run_external_replay environment scenario_sha256 scenario + destination strategy durability output_format)))) -let execute_jsonl environment input journal validate_only strategy durability = +let execute_jsonl environment input journal validate_only strategy durability + output_format = if validate_only then match journal with | Some _ -> @@ -207,30 +476,44 @@ let execute_jsonl environment input journal validate_only strategy durability = match Trading_engine.Replay.run_stream input with | Error message -> Error message | Ok result -> - Fmt.pr - "valid run=%a instruments=%d schedule=%Ld slices=%Ld \ - scenario_sha256=%s@." - Trading_engine.Id.Run.pp result.run_id result.instrument_count - result.schedule_count result.slice_count result.scenario_sha256; - Ok ()) + if output_format = Human_output then ( + Fmt.pr + "valid run=%a instruments=%d schedule=%Ld slices=%Ld \ + scenario_sha256=%s@." + Trading_engine.Id.Run.pp result.run_id result.instrument_count + result.schedule_count result.slice_count result.scenario_sha256; + Ok ()) + else + emit_validation output_format ~run_id:result.run_id + ~scenario_sha256:result.scenario_sha256 + ~instrument_count:(Int64.of_int result.instrument_count) + ~schedule_count:result.schedule_count + ~slice_count:result.slice_count ~orders:result.orders + ~valuation:result.valuation ~audit_count:result.audit_count) else match journal with | None -> Error (cli_error "--journal is required unless --validate-only is set") | Some path -> ( - match strategy with - | None -> run_stream input path durability - | Some strategy -> - run_external_stream environment input path strategy durability) + match journal_destination path with + | Error _ as error -> error + | Ok destination -> ( + match strategy with + | None -> run_stream input destination durability output_format + | Some strategy -> + run_external_stream environment input destination strategy + durability output_format)) type input_format = Json | Jsonl let execute_scenario environment input journal validate_only strategy durability - = function + output_format = function | Json -> execute_json environment input journal validate_only strategy durability + output_format | Jsonl -> execute_jsonl environment input journal validate_only strategy durability + output_format let external_strategy executable arguments timeout transcript = match (executable, transcript, arguments, timeout) with @@ -250,9 +533,78 @@ let external_strategy executable arguments timeout transcript = Error (cli_error "--strategy-timeout must be finite and positive") else Ok (Some { command = executable :: arguments; timeout; transcript }) +let spool_standard_input () = + let path, channel = + Filename.open_temp_file ~mode:[ Open_binary ] "trading-engine-stdin-" + ".jsonl" + in + let fail diagnostic = + close_out_noerr channel; + remove_if_exists path; + Error diagnostic + in + try + let buffer = Bytes.create 65_536 in + let rec loop total = + match input stdin buffer 0 (Bytes.length buffer) with + | 0 -> Ok total + | length -> + if + total + > Trading_engine.Resource_limits.scenario_stream_bytes - length + then + Error + (Trading_engine.Diagnostic.make + ~code:Trading_engine.Diagnostic.Resource_limit + ~phase:Trading_engine.Diagnostic.Input + (Printf.sprintf + "standard-input scenario stream exceeds %d bytes" + Trading_engine.Resource_limits.scenario_stream_bytes)) + else ( + output channel buffer 0 length; + loop (total + length)) + in + match loop 0 with + | Error diagnostic -> fail diagnostic + | Ok _ -> + close_out channel; + Ok path + with exception_ -> + fail + (Trading_engine.Diagnostic.of_exception + ~code:Trading_engine.Diagnostic.Input_io + ~phase:Trading_engine.Diagnostic.Input + ~message:"could not spool scenario stream from standard input" + exception_) + +let with_input_path input input_format function_ = + if not (String.equal input "-") then function_ input + else + match input_format with + | Json -> + Error + (cli_error + "standard input requires --input-format jsonl; batch JSON is not \ + supported") + | Jsonl -> ( + try + match spool_standard_input () with + | Error _ as error -> error + | Ok path -> + Fun.protect + ~finally:(fun () -> remove_if_exists path) + (fun () -> function_ path) + with exception_ -> + Error + (Trading_engine.Diagnostic.of_exception + ~code:Trading_engine.Diagnostic.Input_io + ~phase:Trading_engine.Diagnostic.Input + ~message:"could not prepare standard-input scenario stream" + exception_)) + let execute environment input journal validate_only capabilities input_format strategy_executable strategy_arguments strategy_timeout strategy_transcript - durable_artifacts = + durable_artifacts output_format = if capabilities then match ( input, @@ -273,6 +625,16 @@ let execute environment input journal validate_only capabilities input_format "--capabilities cannot be combined with replay or strategy options") else if validate_only && durable_artifacts then Error (cli_error "--durable-artifacts cannot be used with --validate-only") + else if durable_artifacts && Option.equal String.equal journal (Some "-") then + Error + (cli_error + "--durable-artifacts cannot be used when --journal writes to standard \ + output") + else if Option.equal String.equal strategy_transcript (Some "-") then + Error + (cli_error + "--strategy-transcript does not support standard output; choose a \ + file path") else match input with | None -> @@ -292,13 +654,16 @@ let execute environment input journal validate_only capabilities input_format if durable_artifacts then Trading_engine.Artifact_writer.Durable else Trading_engine.Artifact_writer.Buffered in - execute_scenario environment path journal validate_only strategy - durability input_format) + with_input_path path input_format (fun input_path -> + execute_scenario environment input_path journal validate_only + strategy durability output_format input_format)) let input = let doc = "Read the replay scenario from $(docv)." in Arg.( - value & opt (some file) None & info [ "input"; "i" ] ~docv:"SCENARIO" ~doc) + value + & opt (some string) None + & info [ "input"; "i" ] ~docv:"SCENARIO|-" ~doc) let input_format = let formats = Arg.enum [ ("json", Json); ("jsonl", Jsonl) ] in @@ -306,7 +671,10 @@ let input_format = Arg.(value & opt formats Json & info [ "input-format" ] ~docv:"FORMAT" ~doc) let journal = - let doc = "Create the append-only JSON Lines audit journal at $(docv)." in + let doc = + "Create the append-only JSON Lines audit journal at $(docv). Use '-' to \ + write a completed journal to standard output." + in Arg.( value & opt (some string) None @@ -333,6 +701,16 @@ let diagnostic_format = Arg.( value & opt formats Human & info [ "diagnostic-format" ] ~docv:"FORMAT" ~doc) +let output_format = + let formats = Arg.enum [ ("human", Human_output); ("json", Json_output) ] in + let doc = + "Render successful validation and replay summaries as $(docv) (default: \ + human). JSON output also selects JSON diagnostics." + in + Arg.( + value & opt formats Human_output + & info [ "output-format" ] ~docv:"FORMAT" ~doc) + let strategy_executable = let doc = "Launch $(docv) as the external strategy process without using a shell." @@ -398,27 +776,32 @@ let command environment = strategy_transcript durable_artifacts diagnostic_format + output_format -> ( diagnostic_format, + output_format, execute environment input journal validate_only capabilities input_format strategy_executable strategy_arguments - strategy_timeout strategy_transcript durable_artifacts )) + strategy_timeout strategy_transcript durable_artifacts + output_format )) $ input $ journal $ validate_only $ capabilities $ input_format $ strategy_executable $ strategy_argument $ strategy_timeout - $ strategy_transcript $ durable_artifacts $ diagnostic_format) + $ strategy_transcript $ durable_artifacts $ diagnostic_format + $ output_format) let () = Fmt_tty.setup_std_outputs (); Eio_main.run @@ fun environment -> match Cmd.eval_value' (command environment) with | `Exit code -> exit code - | `Ok (_, Ok ()) -> exit Cmd.Exit.ok - | `Ok (format, Error diagnostic) -> + | `Ok (_, _, Ok ()) -> exit Cmd.Exit.ok + | `Ok (diagnostic_format, output_format, Error diagnostic) -> let rendered = - match format with - | Human -> + match (diagnostic_format, output_format) with + | Human, Human_output -> "trading-engine: " ^ Trading_engine.Diagnostic.to_human diagnostic - | Json -> Trading_engine.Diagnostic.to_json diagnostic + | Json, _ | _, Json_output -> + Trading_engine.Diagnostic.to_json diagnostic in Fmt.epr "%s@." rendered; exit Cmd.Exit.some_error diff --git a/contracts/cli/v1/README.md b/contracts/cli/v1/README.md new file mode 100644 index 0000000..82a47e4 --- /dev/null +++ b/contracts/cli/v1/README.md @@ -0,0 +1,20 @@ +# CLI result contract v1 + +Pass `--output-format json` to receive one compact JSON success document. The +[`result.schema.json`](result.schema.json) schema defines its stable fields. The document identifies +the operation and run, binds scenario and artifact hashes, reports replay counts and the normalized +current valuation, and names any created artifacts. + +Failures use the existing +[diagnostic contract v1](../../diagnostic/v1/diagnostic.schema.json). Selecting JSON output also +selects JSON diagnostics, so automation does not need to combine two format flags. Diagnostics are +always written to standard error. + +When `--journal -` is selected, standard output contains only the complete JSON Lines journal. The +success document moves to standard error. Its journal artifact is named `stdout`, and the final +`run_completed` record signals successful completion. A consumer must also require a zero process +exit status. Strategy protocol messages remain confined to the supervised child process. + +This result contract is independent of the scenario contract version. Its `valuation` uses the +current v16 valuation shape so consumers receive one stable automation model for older accepted +scenarios. diff --git a/contracts/cli/v1/dune b/contracts/cli/v1/dune new file mode 100644 index 0000000..c85ad1c --- /dev/null +++ b/contracts/cli/v1/dune @@ -0,0 +1,7 @@ +(install + (section share) + (package trading_engine) + (files + (README.md as contracts/cli/v1/README.md) + (result.schema.json as contracts/cli/v1/result.schema.json) + (fixtures/demo.result.json as contracts/cli/v1/fixtures/demo.result.json))) diff --git a/contracts/cli/v1/fixtures/demo.result.json b/contracts/cli/v1/fixtures/demo.result.json new file mode 100644 index 0000000..efb13c6 --- /dev/null +++ b/contracts/cli/v1/fixtures/demo.result.json @@ -0,0 +1 @@ +{"result_version":"1","status":"success","operation":"validate","run_id":"demo","hashes":{"scenario_sha256":"a56b9b38f18d93e2953f90c8b052026d174e91c01a9465f8070a22040820a78d","journal_sha256":null,"strategy_transcript_sha256":null},"counts":{"instruments":1,"schedule_batches":2,"slices":4,"audits":29,"orders":3,"active_orders":0,"filled_orders":2,"rejected_orders":0},"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"},"artifacts":{"journal":null,"strategy_transcript":null}} diff --git a/contracts/cli/v1/result.schema.json b/contracts/cli/v1/result.schema.json new file mode 100644 index 0000000..2aea165 --- /dev/null +++ b/contracts/cli/v1/result.schema.json @@ -0,0 +1,130 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/cli/v1/result.schema.json", + "title": "Trading Engine CLI success result v1", + "type": "object", + "additionalProperties": false, + "required": [ + "result_version", + "status", + "operation", + "run_id", + "hashes", + "counts", + "valuation", + "artifacts" + ], + "properties": { + "result_version": { "const": "1" }, + "status": { "const": "success" }, + "operation": { "enum": ["validate", "replay"] }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/identifier" }, + "hashes": { + "type": "object", + "additionalProperties": false, + "required": ["scenario_sha256", "journal_sha256", "strategy_transcript_sha256"], + "properties": { + "scenario_sha256": { "$ref": "#/$defs/sha256" }, + "journal_sha256": { "oneOf": [{ "$ref": "#/$defs/sha256" }, { "type": "null" }] }, + "strategy_transcript_sha256": { "oneOf": [{ "$ref": "#/$defs/sha256" }, { "type": "null" }] } + } + }, + "counts": { + "type": "object", + "additionalProperties": false, + "required": ["instruments", "schedule_batches", "slices", "audits", "orders", "active_orders", "filled_orders", "rejected_orders"], + "properties": { + "instruments": { "$ref": "#/$defs/count" }, + "schedule_batches": { "$ref": "#/$defs/count" }, + "slices": { "$ref": "#/$defs/count" }, + "audits": { "$ref": "#/$defs/count" }, + "orders": { "$ref": "#/$defs/count" }, + "active_orders": { "$ref": "#/$defs/count" }, + "filled_orders": { "$ref": "#/$defs/count" }, + "rejected_orders": { "$ref": "#/$defs/count" } + } + }, + "valuation": { "$ref": "#/$defs/accountValuation" }, + "artifacts": { + "type": "object", + "additionalProperties": false, + "required": ["journal", "strategy_transcript"], + "properties": { + "journal": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }, + "strategy_transcript": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] } + } + } + }, + "allOf": [ + { + "if": { "properties": { "operation": { "const": "validate" } } }, + "then": { + "properties": { + "hashes": { "properties": { "journal_sha256": { "type": "null" }, "strategy_transcript_sha256": { "type": "null" } } }, + "artifacts": { "properties": { "journal": { "type": "null" }, "strategy_transcript": { "type": "null" } } } + } + } + }, + { + "if": { "properties": { "operation": { "const": "replay" } } }, + "then": { + "properties": { + "hashes": { "properties": { "journal_sha256": { "$ref": "#/$defs/sha256" } } }, + "artifacts": { "properties": { "journal": { "type": "string", "minLength": 1 } } } + } + } + } + ], + "$defs": { + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "count": { "type": "integer", "minimum": 0 }, + "accountValuation": { + "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", + "execution_fee_components" + ], + "properties": { + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "settled_cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "unsettled_cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/unsignedDecimal" }, + "cost_basis": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "realized_pnl": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "unrealized_pnl": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "dividend_pnl": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "execution_fees": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "borrow_fees": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "cash_interest": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "total_fees": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, + "cash_balances": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/cashAttribution" } }, + "positions": { "type": "array", "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/positionAttribution" } }, + "execution_fee_components": { "type": "array", "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/feeComponentAttribution" } } + } + } + } +} diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index 6e7d864..fe01d3c 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -1242,6 +1242,18 @@ "format": "jsonl" } ] + }, + { + "name": "cli-result-v1", + "schema": "cli/v1/result.schema.json", + "version_field": "result_version", + "version": "1", + "sources": [ + { + "path": "cli/v1/fixtures/demo.result.json", + "format": "json" + } + ] } ] } diff --git a/contracts/diagnostic/v1/README.md b/contracts/diagnostic/v1/README.md index 7c62f8b..ecfb773 100644 --- a/contracts/diagnostic/v1/README.md +++ b/contracts/diagnostic/v1/README.md @@ -1,7 +1,7 @@ # Diagnostic contract v1 This directory defines the stable JSON emitted on standard error when the CLI uses -`--diagnostic-format json`. Validate each complete document against +`--diagnostic-format json` or `--output-format json`. Validate each complete document against [`diagnostic.schema.json`](diagnostic.schema.json). The `code` and typed `context` fields are the machine contract. Treat `message`, cause messages, diff --git a/docs/diagnostics.md b/docs/diagnostics.md index dad9b59..537b92e 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -2,7 +2,8 @@ Process and file boundaries return diagnostic contract version `1`. The CLI prints the concise `message` by default. Pass `--diagnostic-format json` to write one machine-readable diagnostic to -standard error. The process exits with status 123 for either format. +standard error. `--output-format json` also selects JSON diagnostics so an automation client needs +only one format option. The process exits with status 123 for either format. The versioned [diagnostic JSON Schema](../contracts/diagnostic/v1/diagnostic.schema.json) is the authoritative structural contract. The adjacent fixture demonstrates every optional context diff --git a/docs/scenario.md b/docs/scenario.md index c4c7292..a15d7a3 100644 --- a/docs/scenario.md +++ b/docs/scenario.md @@ -14,6 +14,10 @@ trading-engine --input scenario.json --validate-only trading-engine --input scenario.jsonl --input-format jsonl --validate-only ``` +Use `--input - --input-format jsonl` to read a stream from standard input. The CLI spools at most +1 GiB to a private temporary file so the same bytes can be hashed, validated, replayed, and hashed +again. Batch JSON cannot use standard input. + ## JSON Lines stream Use the stream for histories that should not be materialized inside the engine. The first record @@ -67,6 +71,14 @@ the scenario contract or the separate strategy protocol, never both. Each JSON Lines record is limited to 1 MiB, excluding its line feed. The reader accepts a final record without a line feed and drains an oversized record without retaining bytes above the limit. +Use `--journal -` to write a journal to standard output. The engine first creates and verifies a +complete temporary journal, then copies only journal bytes to the pipe. The final `run_completed` +record and a zero process exit status signal completeness. Success summaries move to standard error, +and diagnostics always use standard error, so protocol, journal, and summary bytes never share one +stream. Pipes do not provide exclusive no-replace publication, atomic linking, retained partial +artifacts, directory synchronization, or restart durability. They cannot be combined with +`--durable-artifacts`. + ## Instruments, risk, and execution Each instrument contains `instrument_id`, `symbol`, `quote_currency`, `tick_size`, and `lot_size`. diff --git a/lib/codec.mli b/lib/codec.mli index f48c608..ed39a34 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -16,5 +16,9 @@ val order_to_yojson_v8 : Order.t -> Yojson.Safe.t val fill_to_yojson : Fill.t -> Yojson.Safe.t val fill_to_yojson_v9 : Fill.t -> Yojson.Safe.t val initial_portfolio_to_yojson : Initial_portfolio.t -> Yojson.Safe.t + +val account_valuation_to_yojson : + ?contract_version:string -> Account.valuation -> Yojson.Safe.t + val audit_to_yojson : Audit.t -> Yojson.Safe.t val audit_to_string : Audit.t -> string diff --git a/lib/resource_limits.ml b/lib/resource_limits.ml index 6ba78ac..a2e0bd3 100644 --- a/lib/resource_limits.ml +++ b/lib/resource_limits.ml @@ -1,5 +1,6 @@ let version = "1" let scenario_record_bytes = 1_048_576 +let scenario_stream_bytes = 1_073_741_824 let strategy_message_bytes = 1_048_576 let internal_events = 100_000 let catalog_instruments = 4_096 diff --git a/lib/resource_limits.mli b/lib/resource_limits.mli index 682453c..15657a2 100644 --- a/lib/resource_limits.mli +++ b/lib/resource_limits.mli @@ -5,6 +5,7 @@ val version : string val scenario_record_bytes : int +val scenario_stream_bytes : int val strategy_message_bytes : int val internal_events : int val catalog_instruments : int diff --git a/mkdocs.yml b/mkdocs.yml index 0b4fb2f..ec574e8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,8 @@ nav: - Security policy: SECURITY.md - Contracts: - Conformance corpus: contracts/conformance/README.md + - CLI results: + - Current v1: contracts/cli/v1/README.md - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index 39a2038..5ae4a7b 100644 --- a/scripts/check-documentation.py +++ b/scripts/check-documentation.py @@ -26,6 +26,7 @@ "docs/persistra.md", "SECURITY.md", "contracts/conformance/README.md", + "contracts/cli/v1/README.md", "contracts/v16/README.md", "contracts/v5/README.md", "contracts/v4/README.md", diff --git a/scripts/release_artifacts.py b/scripts/release_artifacts.py index 6da23e8..aceeddc 100644 --- a/scripts/release_artifacts.py +++ b/scripts/release_artifacts.py @@ -378,6 +378,7 @@ def verify_release( "lib/trading_engine/opam", "share/trading_engine/contracts/v16/scenario.schema.json", "share/trading_engine/contracts/v16/fixtures/demo.scenario.json", + "share/trading_engine/contracts/cli/v1/result.schema.json", "doc/trading_engine/README.md", ), epoch, @@ -389,6 +390,7 @@ def verify_release( "trading_engine.opam", "contracts/v1/scenario.schema.json", "contracts/v16/fixtures/demo.scenario.json", + "contracts/cli/v1/result.schema.json", "docs/architecture.md", ".github/workflows/release-candidate.yml", ), @@ -402,6 +404,7 @@ def verify_release( "contracts/v1/scenario.schema.json", "contracts/v16/fixtures/demo.scenario.json", "contracts/strategy/v14/message.schema.json", + "contracts/cli/v1/result.schema.json", ), epoch, ) @@ -413,6 +416,7 @@ def verify_release( "docs/architecture/index.html", "contracts/v1/index.html", "contracts/v16/scenario.schema.json", + "contracts/cli/v1/result.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index 6ce3ee0..22bbca7 100644 --- a/test/cli.t +++ b/test/cli.t @@ -10,6 +10,11 @@ $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=786f38d8bd10faac03b6b15c7aa8ae0a867eedc609ca6eaa75cfd93ae3ffdcae + $ ../bin/main.exe --output-format json --validate-only --input ../contracts/v8/fixtures/demo.scenario.json | python3 -c 'import json, sys; result=json.load(sys.stdin); print(result["result_version"], result["status"], result["operation"], result["run_id"]); print(result["counts"]["instruments"], result["counts"]["slices"], result["counts"]["audits"], result["valuation"]["equity"]); print(result["hashes"]["journal_sha256"], result["artifacts"]["journal"])' + 1 success validate demo + 1 4 22 10111.65392 + None None + $ ../bin/main.exe --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts run=demo audits=22 orders=3 active=0 filled=2 rejected=0 cash=9846.65392 equity=10111.65392 gross=265 realized=18.965682 unrealized=7.688238 fees=3.16608 @@ -32,6 +37,12 @@ 1 scenario_stream.invalid validation 6 6 None + $ diagnostic=$(../bin/main.exe --output-format json --validate-only --input-format jsonl --input truncated.scenario.jsonl 2>&1 >/dev/null); status=$?; test "$status" -eq 123; python3 -c 'import json, sys; diagnostic=json.loads(sys.argv[1]); print(diagnostic["diagnostic_version"], diagnostic["code"], diagnostic["phase"])' "$diagnostic" + 1 scenario_stream.invalid validation + + $ ../bin/main.exe --output-format json --validate-only --input missing.scenario.json 2>&1 >/dev/null | python3 -c 'import json, sys; diagnostic=json.load(sys.stdin); print(diagnostic["code"], diagnostic["phase"])' + input.io input + $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v8/fixtures/demo.scenario.json > invalid-tick.json $ ../bin/main.exe --validate-only --input invalid-tick.json trading-engine: market prices and volumes must align with instrument increments @@ -47,6 +58,36 @@ $ test ! -e validation.journal.jsonl + $ ../bin/main.exe --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl --journal piped-file.journal.jsonl >/dev/null + $ cat ../contracts/v8/fixtures/demo.scenario.jsonl | ../bin/main.exe --input-format jsonl --input - --journal - > piped-stdout.journal.jsonl 2> piped-summary.txt + $ cmp piped-file.journal.jsonl piped-stdout.journal.jsonl + $ python3 - piped-summary.txt piped-stdout.journal.jsonl <<'PY' + > import hashlib + > import json + > import sys + > summary_path, journal_path = sys.argv[1:] + > summary = open(summary_path, encoding="utf-8").read() + > journal_bytes = open(journal_path, "rb").read() + > completion = json.loads(journal_bytes.splitlines()[-1]) + > print("journal=stdout" in summary, completion["event_type"]) + > print(len(journal_bytes.splitlines()), hashlib.sha256(journal_bytes).hexdigest() == hashlib.sha256(open("piped-file.journal.jsonl", "rb").read()).hexdigest()) + > PY + True run_completed + 22 True + + $ ../bin/main.exe --output-format json --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl --journal - > piped-json.journal.jsonl 2> piped-json-summary.json + $ python3 -c 'import json; result=json.load(open("piped-json-summary.json")); print(result["result_version"], result["operation"], result["artifacts"]["journal"], result["hashes"]["journal_sha256"] is not None)' + 1 replay stdout True + $ cmp piped-file.journal.jsonl piped-json.journal.jsonl + + $ printf '{}\n' | ../bin/main.exe --input - --journal - + trading-engine: standard input requires --input-format jsonl; batch JSON is not supported + [123] + + $ ../bin/main.exe --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl --journal - --durable-artifacts + trading-engine: --durable-artifacts cannot be used when --journal writes to standard output + [123] + $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v8/fixtures/demo.scenario.json trading-engine: --durable-artifacts cannot be used with --validate-only [123] @@ -67,6 +108,16 @@ 12 14 $ diff -u ../contracts/strategy/v6/fixtures/external.strategy.jsonl external/run.strategy.jsonl + $ mkdir external-json + $ ../bin/main.exe --output-format json --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal external-json/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-json/run.strategy.jsonl --strategy-timeout 5 | python3 -c 'import hashlib, json, sys; result=json.load(sys.stdin); digest=lambda path: hashlib.sha256(open(path, "rb").read()).hexdigest(); print(result["operation"], result["run_id"], result["artifacts"]["strategy_transcript"]); print(result["hashes"]["journal_sha256"] == digest(result["artifacts"]["journal"]), result["hashes"]["strategy_transcript_sha256"] == digest(result["artifacts"]["strategy_transcript"]))' + replay external-demo external-json/run.strategy.jsonl + True True + + $ ../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal ignored-stdout.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript - + trading-engine: --strategy-transcript does not support standard output; choose a file path + [123] + $ test ! -e ignored-stdout.journal.jsonl + $ mkdir callback-ordering $ ../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal callback-ordering/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg cancel-next --strategy-transcript callback-ordering/run.strategy.jsonl --strategy-timeout 5 run=external-demo audits=12 orders=2 active=0 filled=1 rejected=0 diff --git a/test/dune b/test/dune index e68f723..aba6db0 100644 --- a/test/dune +++ b/test/dune @@ -755,3 +755,20 @@ ../contracts/v9/fixtures/demo.scenario.json) (action (run python3 %{dep:test_benchmark_replay.py}))) + +(rule + (alias runtest) + (deps + validate_cli_result.py + ../contracts/cli/v1/result.schema.json + ../contracts/v16/journal.schema.json + ../contracts/v16/fixtures/demo.scenario.json + ../bin/main.exe) + (action + (run + python3 + %{dep:validate_cli_result.py} + %{dep:../contracts/cli/v1/result.schema.json} + %{dep:../contracts/v16/journal.schema.json} + %{dep:../bin/main.exe} + %{dep:../contracts/v16/fixtures/demo.scenario.json}))) diff --git a/test/validate_cli_result.py b/test/validate_cli_result.py new file mode 100644 index 0000000..62664b6 --- /dev/null +++ b/test/validate_cli_result.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Validate live CLI success results against their versioned JSON Schema.""" + +from __future__ import annotations + +import hashlib +import json +import subprocess +import sys +import tempfile +from pathlib import Path + +from jsonschema import Draft202012Validator +from referencing import Registry, Resource + + +def load(path: Path) -> object: + return json.loads(path.read_text(encoding="utf-8")) + + +def run(binary: Path, *arguments: str) -> tuple[object, str]: + completed = subprocess.run( + [str(binary), "--output-format", "json", *arguments], + check=True, + capture_output=True, + text=True, + ) + return json.loads(completed.stdout), completed.stderr + + +def main() -> None: + if len(sys.argv) != 5: + raise SystemExit( + "usage: validate_cli_result.py RESULT_SCHEMA JOURNAL_SCHEMA BINARY SCENARIO" + ) + result_path, journal_path, binary, scenario = map(Path, sys.argv[1:]) + result_schema = load(result_path) + journal_schema = load(journal_path) + Draft202012Validator.check_schema(result_schema) + registry = Registry().with_resource( + journal_schema["$id"], Resource.from_contents(journal_schema) + ) + validator = Draft202012Validator(result_schema, registry=registry) + + validation, validation_stderr = run( + binary, "--validate-only", "--input", str(scenario) + ) + validator.validate(validation) + assert validation_stderr == "" + assert validation["operation"] == "validate" + + with tempfile.TemporaryDirectory() as directory: + journal = Path(directory) / "run.journal.jsonl" + replay, replay_stderr = run( + binary, "--input", str(scenario), "--journal", str(journal) + ) + validator.validate(replay) + assert replay_stderr == "" + assert replay["operation"] == "replay" + assert replay["hashes"]["journal_sha256"] == hashlib.sha256( + journal.read_bytes() + ).hexdigest() + + +if __name__ == "__main__": + main() From 29115345964e8f963f32a7e850e712d770ecdfe1 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Wed, 26 Aug 2026 13:24:59 -0400 Subject: [PATCH 53/57] refactor: reset replay contracts to v1 --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- .github/ISSUE_TEMPLATE/contract-change.yml | 2 +- .github/ISSUE_TEMPLATE/cross-repository.yml | 2 +- .github/labels.json | 9 +- .github/repository.json | 2 +- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 94 +- CONTRIBUTING.md | 2 +- README.md | 279 +- bench/benchmark_batch_schedule.py | 7 +- bench/benchmark_replay.py | 26 +- bin/main.ml | 3 +- contracts/cli/v1/README.md | 4 +- contracts/cli/v1/result.schema.json | 42 +- contracts/conformance/README.md | 35 +- contracts/conformance/cases.json | 1683 +----------- contracts/conformance/dune | 1 - contracts/conformance/frozen.sha256 | 22 - contracts/conformance/manifest.json | 1227 +-------- contracts/strategy/v1/README.md | 33 +- .../v1/fixtures/external.scenario.json | 283 +- .../v1/fixtures/external.scenario.jsonl | 8 +- .../v1/fixtures/external.strategy.jsonl | 14 +- contracts/strategy/v1/message.schema.json | 101 +- contracts/strategy/v1/transcript.schema.json | 82 +- contracts/strategy/v10/README.md | 59 - contracts/strategy/v10/dune | 15 - .../v10/fixtures/external.scenario.json | 302 -- .../v10/fixtures/external.scenario.jsonl | 4 - .../v10/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v10/message.schema.json | 302 -- contracts/strategy/v10/transcript.schema.json | 82 - contracts/strategy/v11/README.md | 59 - contracts/strategy/v11/dune | 15 - .../v11/fixtures/external.scenario.json | 304 -- .../v11/fixtures/external.scenario.jsonl | 4 - .../v11/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v11/message.schema.json | 302 -- contracts/strategy/v11/transcript.schema.json | 82 - contracts/strategy/v12/README.md | 59 - contracts/strategy/v12/dune | 15 - .../v12/fixtures/external.scenario.json | 306 --- .../v12/fixtures/external.scenario.jsonl | 4 - .../v12/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v12/message.schema.json | 302 -- contracts/strategy/v12/transcript.schema.json | 82 - contracts/strategy/v13/README.md | 61 - contracts/strategy/v13/dune | 15 - .../v13/fixtures/external.scenario.json | 308 --- .../v13/fixtures/external.scenario.jsonl | 4 - .../v13/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v13/message.schema.json | 302 -- contracts/strategy/v13/transcript.schema.json | 82 - contracts/strategy/v14/README.md | 64 - contracts/strategy/v14/dune | 15 - .../v14/fixtures/external.scenario.json | 308 --- .../v14/fixtures/external.scenario.jsonl | 4 - .../v14/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v14/message.schema.json | 302 -- contracts/strategy/v14/transcript.schema.json | 82 - contracts/strategy/v2/README.md | 29 - contracts/strategy/v2/dune | 15 - .../v2/fixtures/external.scenario.json | 64 - .../v2/fixtures/external.scenario.jsonl | 4 - .../v2/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v2/message.schema.json | 282 -- contracts/strategy/v2/transcript.schema.json | 22 - contracts/strategy/v3/README.md | 47 - contracts/strategy/v3/dune | 15 - .../v3/fixtures/external.scenario.json | 64 - .../v3/fixtures/external.scenario.jsonl | 4 - .../v3/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v3/message.schema.json | 284 -- contracts/strategy/v3/transcript.schema.json | 82 - contracts/strategy/v4/README.md | 53 - contracts/strategy/v4/dune | 15 - .../v4/fixtures/external.scenario.json | 196 -- .../v4/fixtures/external.scenario.jsonl | 4 - .../v4/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v4/message.schema.json | 290 -- contracts/strategy/v4/transcript.schema.json | 82 - contracts/strategy/v5/README.md | 53 - contracts/strategy/v5/dune | 15 - .../v5/fixtures/external.scenario.json | 204 -- .../v5/fixtures/external.scenario.jsonl | 4 - .../v5/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v5/message.schema.json | 294 -- contracts/strategy/v5/transcript.schema.json | 82 - contracts/strategy/v6/README.md | 54 - contracts/strategy/v6/dune | 15 - .../v6/fixtures/external.scenario.json | 204 -- .../v6/fixtures/external.scenario.jsonl | 4 - .../v6/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v6/message.schema.json | 298 -- contracts/strategy/v6/transcript.schema.json | 82 - contracts/strategy/v7/README.md | 55 - contracts/strategy/v7/dune | 15 - .../v7/fixtures/external.scenario.json | 215 -- .../v7/fixtures/external.scenario.jsonl | 4 - .../v7/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v7/message.schema.json | 299 -- contracts/strategy/v7/transcript.schema.json | 83 - contracts/strategy/v8/README.md | 56 - contracts/strategy/v8/dune | 15 - .../v8/fixtures/external.scenario.json | 271 -- .../v8/fixtures/external.scenario.jsonl | 4 - .../v8/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v8/message.schema.json | 299 -- contracts/strategy/v8/transcript.schema.json | 82 - contracts/strategy/v9/README.md | 58 - contracts/strategy/v9/dune | 15 - .../v9/fixtures/external.scenario.json | 302 -- .../v9/fixtures/external.scenario.jsonl | 4 - .../v9/fixtures/external.strategy.jsonl | 14 - contracts/strategy/v9/message.schema.json | 302 -- contracts/strategy/v9/transcript.schema.json | 82 - contracts/v1/README.md | 28 +- contracts/v1/dune | 26 +- contracts/v1/fixtures/demo.journal.jsonl | 54 +- contracts/v1/fixtures/demo.scenario.json | 363 ++- contracts/v1/fixtures/demo.scenario.jsonl | 10 +- .../v1/fixtures/fill-clipped.journal.jsonl | 13 + .../fixtures/fill-clipped.scenario.json | 5 +- .../v1/fixtures/order-book.journal.jsonl | 15 + .../fixtures/order-book.scenario.json | 3 +- .../v1/fixtures/order-book.scenario.jsonl | 4 + .../v1/fixtures/quote-trade.journal.jsonl | 15 + .../fixtures/quote-trade.scenario.json | 3 +- .../v1/fixtures/quote-trade.scenario.jsonl | 4 + contracts/v1/journal.schema.json | 2336 ++++++++++++++-- contracts/v1/scenario-stream.schema.json | 103 +- contracts/v1/scenario.schema.json | 900 ++++-- contracts/v10/README.md | 62 - contracts/v10/dune | 18 - contracts/v10/fixtures/demo.journal.jsonl | 26 - contracts/v10/fixtures/demo.scenario.json | 411 --- contracts/v10/fixtures/demo.scenario.jsonl | 6 - .../v10/fixtures/fill-clipped.journal.jsonl | 13 - .../v10/fixtures/fill-clipped.scenario.json | 236 -- contracts/v10/journal.schema.json | 2193 --------------- contracts/v10/scenario-stream.schema.json | 77 - contracts/v10/scenario.schema.json | 510 ---- contracts/v11/README.md | 73 - contracts/v11/dune | 18 - contracts/v11/fixtures/demo.journal.jsonl | 31 - contracts/v11/fixtures/demo.scenario.json | 444 --- contracts/v11/fixtures/demo.scenario.jsonl | 6 - .../v11/fixtures/fill-clipped.journal.jsonl | 13 - .../v11/fixtures/fill-clipped.scenario.json | 267 -- contracts/v11/journal.schema.json | 2314 ---------------- contracts/v11/scenario-stream.schema.json | 78 - contracts/v11/scenario.schema.json | 552 ---- contracts/v12/README.md | 85 - contracts/v12/dune | 18 - contracts/v12/fixtures/demo.journal.jsonl | 31 - contracts/v12/fixtures/demo.scenario.json | 444 --- contracts/v12/fixtures/demo.scenario.jsonl | 6 - .../v12/fixtures/fill-clipped.journal.jsonl | 13 - .../v12/fixtures/fill-clipped.scenario.json | 267 -- contracts/v12/journal.schema.json | 2394 ---------------- contracts/v12/scenario-stream.schema.json | 78 - contracts/v12/scenario.schema.json | 669 ----- contracts/v13/README.md | 93 - contracts/v13/dune | 18 - contracts/v13/fixtures/demo.journal.jsonl | 29 - contracts/v13/fixtures/demo.scenario.json | 453 --- contracts/v13/fixtures/demo.scenario.jsonl | 6 - .../v13/fixtures/fill-clipped.journal.jsonl | 13 - .../v13/fixtures/fill-clipped.scenario.json | 267 -- contracts/v13/journal.schema.json | 2413 ---------------- contracts/v13/scenario-stream.schema.json | 78 - contracts/v13/scenario.schema.json | 713 ----- contracts/v14/README.md | 102 - contracts/v14/dune | 27 - contracts/v14/fixtures/demo.journal.jsonl | 29 - contracts/v14/fixtures/demo.scenario.json | 461 ---- contracts/v14/fixtures/demo.scenario.jsonl | 6 - .../v14/fixtures/fill-clipped.journal.jsonl | 13 - .../v14/fixtures/fill-clipped.scenario.json | 271 -- .../v14/fixtures/quote-trade.journal.jsonl | 13 - .../v14/fixtures/quote-trade.scenario.json | 317 --- .../v14/fixtures/quote-trade.scenario.jsonl | 4 - contracts/v14/journal.schema.json | 2420 ---------------- contracts/v14/scenario-stream.schema.json | 78 - contracts/v14/scenario.schema.json | 770 ------ contracts/v15/README.md | 117 - contracts/v15/dune | 36 - contracts/v15/fixtures/demo.journal.jsonl | 29 - contracts/v15/fixtures/demo.scenario.json | 465 ---- contracts/v15/fixtures/demo.scenario.jsonl | 6 - .../v15/fixtures/fill-clipped.journal.jsonl | 13 - .../v15/fixtures/order-book.journal.jsonl | 13 - .../v15/fixtures/order-book.scenario.json | 378 --- .../v15/fixtures/order-book.scenario.jsonl | 4 - .../v15/fixtures/quote-trade.journal.jsonl | 13 - .../v15/fixtures/quote-trade.scenario.jsonl | 4 - contracts/v15/journal.schema.json | 2427 ---------------- contracts/v15/scenario-stream.schema.json | 78 - contracts/v15/scenario.schema.json | 870 ------ contracts/v16/README.md | 122 - contracts/v16/dune | 36 - contracts/v16/fixtures/demo.journal.jsonl | 29 - contracts/v16/fixtures/demo.scenario.json | 468 ---- contracts/v16/fixtures/demo.scenario.jsonl | 6 - .../v16/fixtures/fill-clipped.journal.jsonl | 13 - .../v16/fixtures/fill-clipped.scenario.json | 273 -- .../v16/fixtures/order-book.journal.jsonl | 13 - .../v16/fixtures/order-book.scenario.jsonl | 4 - .../v16/fixtures/quote-trade.journal.jsonl | 13 - .../v16/fixtures/quote-trade.scenario.json | 319 --- .../v16/fixtures/quote-trade.scenario.jsonl | 4 - contracts/v16/journal.schema.json | 2441 ----------------- contracts/v16/scenario-stream.schema.json | 78 - contracts/v16/scenario.schema.json | 888 ------ contracts/v2/README.md | 20 - contracts/v2/dune | 10 - contracts/v2/fixtures/demo.journal.jsonl | 20 - contracts/v2/fixtures/demo.scenario.json | 135 - contracts/v2/fixtures/demo.scenario.jsonl | 6 - contracts/v2/journal.schema.json | 431 --- contracts/v2/scenario-stream.schema.json | 131 - contracts/v2/scenario.schema.json | 316 --- contracts/v3/README.md | 15 - contracts/v3/dune | 10 - contracts/v3/fixtures/demo.journal.jsonl | 20 - contracts/v3/fixtures/demo.scenario.json | 113 - contracts/v3/fixtures/demo.scenario.jsonl | 6 - contracts/v3/journal.schema.json | 151 - contracts/v3/scenario-stream.schema.json | 75 - contracts/v3/scenario.schema.json | 263 -- contracts/v4/README.md | 31 - contracts/v4/dune | 16 - contracts/v4/fixtures/demo.journal.jsonl | 20 - contracts/v4/fixtures/demo.scenario.json | 113 - contracts/v4/fixtures/demo.scenario.jsonl | 6 - .../v4/fixtures/fill-clipped.journal.jsonl | 10 - .../v4/fixtures/fill-clipped.scenario.json | 79 - contracts/v4/journal.schema.json | 177 -- contracts/v4/scenario-stream.schema.json | 75 - contracts/v4/scenario.schema.json | 263 -- contracts/v5/README.md | 30 - contracts/v5/dune | 16 - contracts/v5/fixtures/demo.journal.jsonl | 20 - contracts/v5/fixtures/demo.scenario.json | 166 -- contracts/v5/fixtures/demo.scenario.jsonl | 6 - .../v5/fixtures/fill-clipped.journal.jsonl | 10 - .../v5/fixtures/fill-clipped.scenario.json | 113 - contracts/v5/journal.schema.json | 177 -- contracts/v5/scenario-stream.schema.json | 76 - contracts/v5/scenario.schema.json | 339 --- contracts/v6/README.md | 31 - contracts/v6/dune | 16 - contracts/v6/fixtures/demo.journal.jsonl | 22 - contracts/v6/fixtures/demo.scenario.json | 294 -- contracts/v6/fixtures/demo.scenario.jsonl | 6 - .../v6/fixtures/fill-clipped.journal.jsonl | 12 - .../v6/fixtures/fill-clipped.scenario.json | 164 -- contracts/v6/journal.schema.json | 186 -- contracts/v6/scenario-stream.schema.json | 76 - contracts/v6/scenario.schema.json | 369 --- contracts/v7/README.md | 32 - contracts/v7/dune | 16 - contracts/v7/fixtures/demo.journal.jsonl | 22 - contracts/v7/fixtures/demo.scenario.json | 302 -- contracts/v7/fixtures/demo.scenario.jsonl | 6 - .../v7/fixtures/fill-clipped.journal.jsonl | 11 - .../v7/fixtures/fill-clipped.scenario.json | 172 -- contracts/v7/journal.schema.json | 238 -- contracts/v7/scenario-stream.schema.json | 76 - contracts/v7/scenario.schema.json | 428 --- contracts/v8/README.md | 48 - contracts/v8/dune | 16 - contracts/v8/fixtures/demo.journal.jsonl | 22 - contracts/v8/fixtures/demo.scenario.json | 302 -- contracts/v8/fixtures/demo.scenario.jsonl | 6 - .../v8/fixtures/fill-clipped.journal.jsonl | 11 - .../v8/fixtures/fill-clipped.scenario.json | 177 -- contracts/v8/journal.schema.json | 238 -- contracts/v8/scenario-stream.schema.json | 76 - contracts/v8/scenario.schema.json | 442 --- contracts/v9/README.md | 50 - contracts/v9/dune | 16 - contracts/v9/fixtures/demo.journal.jsonl | 22 - contracts/v9/fixtures/demo.scenario.json | 314 --- contracts/v9/fixtures/demo.scenario.jsonl | 6 - .../v9/fixtures/fill-clipped.journal.jsonl | 11 - .../v9/fixtures/fill-clipped.scenario.json | 187 -- contracts/v9/journal.schema.json | 248 -- contracts/v9/scenario-stream.schema.json | 77 - contracts/v9/scenario.schema.json | 471 ---- docs/api-reference.md | 2 +- docs/architecture.md | 2 +- docs/continuous-integration.md | 44 +- docs/documentation-platform.md | 4 +- docs/execution-model.md | 316 +-- docs/persistra.md | 148 +- docs/scenario.md | 371 +-- lib/audit.ml | 19 - lib/audit.mli | 17 - lib/codec.ml | 444 +-- lib/codec.mli | 14 +- lib/contract.ml | 44 +- lib/contract.mli | 3 - lib/engine.ml | 452 +-- lib/engine.mli | 97 - lib/execution.ml | 67 +- lib/execution.mli | 12 +- lib/execution_model.ml | 54 +- lib/execution_model.mli | 2 - lib/external_replay.ml | 48 +- lib/fill.ml | 18 +- lib/fill.mli | 13 - lib/financing.ml | 5 - lib/financing.mli | 2 - lib/journal.ml | 3 +- lib/market_slice.ml | 47 +- lib/market_slice.mli | 101 - lib/order.ml | 9 +- lib/order.mli | 12 +- lib/replay.ml | 51 +- lib/risk.ml | 576 ++-- lib/risk.mli | 19 +- lib/scenario.ml | 638 ++--- lib/scenario.mli | 20 +- lib/scenario_shape.ml | 141 +- lib/scenario_shape.mli | 5 +- lib/scenario_validation.ml | 44 +- lib/scenario_validation.mli | 1 - lib/strategy_process.ml | 11 +- lib/strategy_protocol.ml | 487 +--- lib/strategy_protocol.mli | 23 +- mkdocs.yml | 15 +- scripts/check-deterministic-journals | 127 +- scripts/check-documentation.py | 11 - scripts/release_artifacts.py | 12 +- test/cli.t | 72 +- test/dune | 672 +---- test/fake_strategy.py | 7 +- test/fuzz_protocol.ml | 2 +- test/test_accounting.ml | 2 +- test/test_boundary_failures.ml | 7 +- test/test_checkpoint4.ml | 139 +- test/test_contract_conformance.ml | 8 +- test/test_corporate_lifecycle.ml | 9 +- test/test_diagnostic.ml | 20 +- test/test_domain.ml | 24 +- test/test_execution.ml | 32 +- test/test_fee_schedules.ml | 2 +- test/test_financing.ml | 33 +- test/test_order_lifetimes.ml | 82 +- test/test_reducer.ml | 177 +- test/test_reducer_properties.ml | 33 +- test/test_repository_metadata.py | 17 +- test/test_risk_groups.ml | 113 +- test/test_scenario.ml | 104 +- test/test_settlement.ml | 13 +- test/test_strategy_protocol.ml | 56 +- test/test_support.ml | 122 +- test/test_venue_calendar.ml | 13 +- test/validate_contract_conformance.py | 44 - test/validate_schemas.py | 120 +- test/validate_strategy_schema.py | 2 +- 362 files changed, 5612 insertions(+), 55553 deletions(-) delete mode 100644 contracts/conformance/frozen.sha256 delete mode 100644 contracts/strategy/v10/README.md delete mode 100644 contracts/strategy/v10/dune delete mode 100644 contracts/strategy/v10/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v10/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v10/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v10/message.schema.json delete mode 100644 contracts/strategy/v10/transcript.schema.json delete mode 100644 contracts/strategy/v11/README.md delete mode 100644 contracts/strategy/v11/dune delete mode 100644 contracts/strategy/v11/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v11/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v11/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v11/message.schema.json delete mode 100644 contracts/strategy/v11/transcript.schema.json delete mode 100644 contracts/strategy/v12/README.md delete mode 100644 contracts/strategy/v12/dune delete mode 100644 contracts/strategy/v12/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v12/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v12/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v12/message.schema.json delete mode 100644 contracts/strategy/v12/transcript.schema.json delete mode 100644 contracts/strategy/v13/README.md delete mode 100644 contracts/strategy/v13/dune delete mode 100644 contracts/strategy/v13/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v13/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v13/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v13/message.schema.json delete mode 100644 contracts/strategy/v13/transcript.schema.json delete mode 100644 contracts/strategy/v14/README.md delete mode 100644 contracts/strategy/v14/dune delete mode 100644 contracts/strategy/v14/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v14/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v14/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v14/message.schema.json delete mode 100644 contracts/strategy/v14/transcript.schema.json delete mode 100644 contracts/strategy/v2/README.md delete mode 100644 contracts/strategy/v2/dune delete mode 100644 contracts/strategy/v2/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v2/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v2/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v2/message.schema.json delete mode 100644 contracts/strategy/v2/transcript.schema.json delete mode 100644 contracts/strategy/v3/README.md delete mode 100644 contracts/strategy/v3/dune delete mode 100644 contracts/strategy/v3/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v3/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v3/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v3/message.schema.json delete mode 100644 contracts/strategy/v3/transcript.schema.json delete mode 100644 contracts/strategy/v4/README.md delete mode 100644 contracts/strategy/v4/dune delete mode 100644 contracts/strategy/v4/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v4/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v4/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v4/message.schema.json delete mode 100644 contracts/strategy/v4/transcript.schema.json delete mode 100644 contracts/strategy/v5/README.md delete mode 100644 contracts/strategy/v5/dune delete mode 100644 contracts/strategy/v5/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v5/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v5/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v5/message.schema.json delete mode 100644 contracts/strategy/v5/transcript.schema.json delete mode 100644 contracts/strategy/v6/README.md delete mode 100644 contracts/strategy/v6/dune delete mode 100644 contracts/strategy/v6/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v6/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v6/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v6/message.schema.json delete mode 100644 contracts/strategy/v6/transcript.schema.json delete mode 100644 contracts/strategy/v7/README.md delete mode 100644 contracts/strategy/v7/dune delete mode 100644 contracts/strategy/v7/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v7/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v7/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v7/message.schema.json delete mode 100644 contracts/strategy/v7/transcript.schema.json delete mode 100644 contracts/strategy/v8/README.md delete mode 100644 contracts/strategy/v8/dune delete mode 100644 contracts/strategy/v8/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v8/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v8/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v8/message.schema.json delete mode 100644 contracts/strategy/v8/transcript.schema.json delete mode 100644 contracts/strategy/v9/README.md delete mode 100644 contracts/strategy/v9/dune delete mode 100644 contracts/strategy/v9/fixtures/external.scenario.json delete mode 100644 contracts/strategy/v9/fixtures/external.scenario.jsonl delete mode 100644 contracts/strategy/v9/fixtures/external.strategy.jsonl delete mode 100644 contracts/strategy/v9/message.schema.json delete mode 100644 contracts/strategy/v9/transcript.schema.json create mode 100644 contracts/v1/fixtures/fill-clipped.journal.jsonl rename contracts/{v15 => v1}/fixtures/fill-clipped.scenario.json (98%) create mode 100644 contracts/v1/fixtures/order-book.journal.jsonl rename contracts/{v16 => v1}/fixtures/order-book.scenario.json (99%) create mode 100644 contracts/v1/fixtures/order-book.scenario.jsonl create mode 100644 contracts/v1/fixtures/quote-trade.journal.jsonl rename contracts/{v15 => v1}/fixtures/quote-trade.scenario.json (99%) create mode 100644 contracts/v1/fixtures/quote-trade.scenario.jsonl delete mode 100644 contracts/v10/README.md delete mode 100644 contracts/v10/dune delete mode 100644 contracts/v10/fixtures/demo.journal.jsonl delete mode 100644 contracts/v10/fixtures/demo.scenario.json delete mode 100644 contracts/v10/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v10/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v10/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v10/journal.schema.json delete mode 100644 contracts/v10/scenario-stream.schema.json delete mode 100644 contracts/v10/scenario.schema.json delete mode 100644 contracts/v11/README.md delete mode 100644 contracts/v11/dune delete mode 100644 contracts/v11/fixtures/demo.journal.jsonl delete mode 100644 contracts/v11/fixtures/demo.scenario.json delete mode 100644 contracts/v11/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v11/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v11/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v11/journal.schema.json delete mode 100644 contracts/v11/scenario-stream.schema.json delete mode 100644 contracts/v11/scenario.schema.json delete mode 100644 contracts/v12/README.md delete mode 100644 contracts/v12/dune delete mode 100644 contracts/v12/fixtures/demo.journal.jsonl delete mode 100644 contracts/v12/fixtures/demo.scenario.json delete mode 100644 contracts/v12/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v12/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v12/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v12/journal.schema.json delete mode 100644 contracts/v12/scenario-stream.schema.json delete mode 100644 contracts/v12/scenario.schema.json delete mode 100644 contracts/v13/README.md delete mode 100644 contracts/v13/dune delete mode 100644 contracts/v13/fixtures/demo.journal.jsonl delete mode 100644 contracts/v13/fixtures/demo.scenario.json delete mode 100644 contracts/v13/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v13/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v13/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v13/journal.schema.json delete mode 100644 contracts/v13/scenario-stream.schema.json delete mode 100644 contracts/v13/scenario.schema.json delete mode 100644 contracts/v14/README.md delete mode 100644 contracts/v14/dune delete mode 100644 contracts/v14/fixtures/demo.journal.jsonl delete mode 100644 contracts/v14/fixtures/demo.scenario.json delete mode 100644 contracts/v14/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v14/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v14/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v14/fixtures/quote-trade.journal.jsonl delete mode 100644 contracts/v14/fixtures/quote-trade.scenario.json delete mode 100644 contracts/v14/fixtures/quote-trade.scenario.jsonl delete mode 100644 contracts/v14/journal.schema.json delete mode 100644 contracts/v14/scenario-stream.schema.json delete mode 100644 contracts/v14/scenario.schema.json delete mode 100644 contracts/v15/README.md delete mode 100644 contracts/v15/dune delete mode 100644 contracts/v15/fixtures/demo.journal.jsonl delete mode 100644 contracts/v15/fixtures/demo.scenario.json delete mode 100644 contracts/v15/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v15/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v15/fixtures/order-book.journal.jsonl delete mode 100644 contracts/v15/fixtures/order-book.scenario.json delete mode 100644 contracts/v15/fixtures/order-book.scenario.jsonl delete mode 100644 contracts/v15/fixtures/quote-trade.journal.jsonl delete mode 100644 contracts/v15/fixtures/quote-trade.scenario.jsonl delete mode 100644 contracts/v15/journal.schema.json delete mode 100644 contracts/v15/scenario-stream.schema.json delete mode 100644 contracts/v15/scenario.schema.json delete mode 100644 contracts/v16/README.md delete mode 100644 contracts/v16/dune delete mode 100644 contracts/v16/fixtures/demo.journal.jsonl delete mode 100644 contracts/v16/fixtures/demo.scenario.json delete mode 100644 contracts/v16/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v16/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v16/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v16/fixtures/order-book.journal.jsonl delete mode 100644 contracts/v16/fixtures/order-book.scenario.jsonl delete mode 100644 contracts/v16/fixtures/quote-trade.journal.jsonl delete mode 100644 contracts/v16/fixtures/quote-trade.scenario.json delete mode 100644 contracts/v16/fixtures/quote-trade.scenario.jsonl delete mode 100644 contracts/v16/journal.schema.json delete mode 100644 contracts/v16/scenario-stream.schema.json delete mode 100644 contracts/v16/scenario.schema.json delete mode 100644 contracts/v2/README.md delete mode 100644 contracts/v2/dune delete mode 100644 contracts/v2/fixtures/demo.journal.jsonl delete mode 100644 contracts/v2/fixtures/demo.scenario.json delete mode 100644 contracts/v2/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v2/journal.schema.json delete mode 100644 contracts/v2/scenario-stream.schema.json delete mode 100644 contracts/v2/scenario.schema.json delete mode 100644 contracts/v3/README.md delete mode 100644 contracts/v3/dune delete mode 100644 contracts/v3/fixtures/demo.journal.jsonl delete mode 100644 contracts/v3/fixtures/demo.scenario.json delete mode 100644 contracts/v3/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v3/journal.schema.json delete mode 100644 contracts/v3/scenario-stream.schema.json delete mode 100644 contracts/v3/scenario.schema.json delete mode 100644 contracts/v4/README.md delete mode 100644 contracts/v4/dune delete mode 100644 contracts/v4/fixtures/demo.journal.jsonl delete mode 100644 contracts/v4/fixtures/demo.scenario.json delete mode 100644 contracts/v4/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v4/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v4/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v4/journal.schema.json delete mode 100644 contracts/v4/scenario-stream.schema.json delete mode 100644 contracts/v4/scenario.schema.json delete mode 100644 contracts/v5/README.md delete mode 100644 contracts/v5/dune delete mode 100644 contracts/v5/fixtures/demo.journal.jsonl delete mode 100644 contracts/v5/fixtures/demo.scenario.json delete mode 100644 contracts/v5/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v5/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v5/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v5/journal.schema.json delete mode 100644 contracts/v5/scenario-stream.schema.json delete mode 100644 contracts/v5/scenario.schema.json delete mode 100644 contracts/v6/README.md delete mode 100644 contracts/v6/dune delete mode 100644 contracts/v6/fixtures/demo.journal.jsonl delete mode 100644 contracts/v6/fixtures/demo.scenario.json delete mode 100644 contracts/v6/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v6/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v6/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v6/journal.schema.json delete mode 100644 contracts/v6/scenario-stream.schema.json delete mode 100644 contracts/v6/scenario.schema.json delete mode 100644 contracts/v7/README.md delete mode 100644 contracts/v7/dune delete mode 100644 contracts/v7/fixtures/demo.journal.jsonl delete mode 100644 contracts/v7/fixtures/demo.scenario.json delete mode 100644 contracts/v7/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v7/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v7/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v7/journal.schema.json delete mode 100644 contracts/v7/scenario-stream.schema.json delete mode 100644 contracts/v7/scenario.schema.json delete mode 100644 contracts/v8/README.md delete mode 100644 contracts/v8/dune delete mode 100644 contracts/v8/fixtures/demo.journal.jsonl delete mode 100644 contracts/v8/fixtures/demo.scenario.json delete mode 100644 contracts/v8/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v8/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v8/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v8/journal.schema.json delete mode 100644 contracts/v8/scenario-stream.schema.json delete mode 100644 contracts/v8/scenario.schema.json delete mode 100644 contracts/v9/README.md delete mode 100644 contracts/v9/dune delete mode 100644 contracts/v9/fixtures/demo.journal.jsonl delete mode 100644 contracts/v9/fixtures/demo.scenario.json delete mode 100644 contracts/v9/fixtures/demo.scenario.jsonl delete mode 100644 contracts/v9/fixtures/fill-clipped.journal.jsonl delete mode 100644 contracts/v9/fixtures/fill-clipped.scenario.json delete mode 100644 contracts/v9/journal.schema.json delete mode 100644 contracts/v9/scenario-stream.schema.json delete mode 100644 contracts/v9/scenario.schema.json diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 0c62f08..d32f298 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -35,7 +35,7 @@ body: attributes: label: Contract versions description: Include scenario, journal, diagnostic, and strategy versions that apply. - placeholder: scenario v4, journal v4, strategy v3 + placeholder: scenario v1, journal v1, strategy v1 validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/contract-change.yml b/.github/ISSUE_TEMPLATE/contract-change.yml index c195f1b..df7e56f 100644 --- a/.github/ISSUE_TEMPLATE/contract-change.yml +++ b/.github/ISSUE_TEMPLATE/contract-change.yml @@ -20,7 +20,7 @@ body: id: versions attributes: label: Affected versions - placeholder: current v4; frozen v1-v3 unchanged + placeholder: current v1 and proposed change validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/cross-repository.yml b/.github/ISSUE_TEMPLATE/cross-repository.yml index 5660287..bc1e628 100644 --- a/.github/ISSUE_TEMPLATE/cross-repository.yml +++ b/.github/ISSUE_TEMPLATE/cross-repository.yml @@ -22,7 +22,7 @@ body: id: contracts attributes: label: Contract versions - placeholder: scenario v3, journal v3, strategy v3 + placeholder: scenario v1, journal v1, strategy v1 validations: required: true - type: dropdown diff --git a/.github/labels.json b/.github/labels.json index 5a62bca..a1e11d1 100644 --- a/.github/labels.json +++ b/.github/labels.json @@ -19,13 +19,8 @@ {"category": "effort", "name": "effort: medium", "color": "fef2c0", "description": "Multi-file change with moderate design or testing work"}, {"category": "effort", "name": "effort: large", "color": "f9d0c4", "description": "Broad change that should be split into reviewed increments"}, - {"category": "contract", "name": "contract: scenario-v1", "color": "5319e7", "description": "Frozen scenario and journal contract version 1"}, - {"category": "contract", "name": "contract: scenario-v2", "color": "5319e7", "description": "Frozen scenario and journal contract version 2"}, - {"category": "contract", "name": "contract: scenario-v3", "color": "5319e7", "description": "Transitional scenario and journal contract version 3"}, - {"category": "contract", "name": "contract: scenario-v4", "color": "5319e7", "description": "Current scenario and journal contract version 4"}, - {"category": "contract", "name": "contract: strategy-v1", "color": "7057ff", "description": "Historical external strategy protocol version 1"}, - {"category": "contract", "name": "contract: strategy-v2", "color": "7057ff", "description": "Historical external strategy protocol version 2"}, - {"category": "contract", "name": "contract: strategy-v3", "color": "7057ff", "description": "Current external strategy protocol version 3"}, + {"category": "contract", "name": "contract: scenario-v1", "color": "5319e7", "description": "Current scenario and journal contract"}, + {"category": "contract", "name": "contract: strategy-v1", "color": "7057ff", "description": "Current external strategy protocol"}, {"category": "dependency", "name": "dependency: persistra", "color": "006b75", "description": "Requires coordinated behavior or validation in Persistra"}, {"category": "dependency", "name": "dependency: upstream", "color": "006b75", "description": "Depends on an external project or toolchain"}, diff --git a/.github/repository.json b/.github/repository.json index b6b6ac1..0498cd5 100644 --- a/.github/repository.json +++ b/.github/repository.json @@ -1,5 +1,5 @@ { - "description": "Deterministic event-driven OCaml execution engine with versioned replay contracts and causal audit journals", + "description": "Deterministic OCaml trading replay engine", "homepage": "https://fallblu.github.io/trading-engine/", "topics": [ "backtesting", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e48c83..787774c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,7 +142,7 @@ jobs: - working-directory: persistra env: PERSISTRA_TRADING_ENGINE_BINARY: ${{ github.workspace }}/trading-engine/_build/default/bin/main.exe - PERSISTRA_TRADING_ENGINE_CONTRACT_DIR: ${{ github.workspace }}/trading-engine/contracts/v3 + PERSISTRA_TRADING_ENGINE_CONTRACT_DIR: ${{ github.workspace }}/trading-engine/contracts/v1 run: uv run pytest --no-cov tests/integration/test_trading_engine.py persistra-latest-head: @@ -182,5 +182,5 @@ jobs: - working-directory: persistra env: PERSISTRA_TRADING_ENGINE_BINARY: ${{ github.workspace }}/trading-engine/_build/default/bin/main.exe - PERSISTRA_TRADING_ENGINE_CONTRACT_DIR: ${{ github.workspace }}/trading-engine/contracts/v3 + PERSISTRA_TRADING_ENGINE_CONTRACT_DIR: ${{ github.workspace }}/trading-engine/contracts/v1 run: uv run pytest --no-cov tests/integration/test_trading_engine.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 902e9ec..45ff8dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,87 +1,11 @@ # Changelog -## Unreleased - -- Add versioned machine-readable CLI success results and JSON failure diagnostics, plus bounded - JSON Lines standard-input spooling and unambiguous completed-journal standard-output pipelines. -- Publish scenario/journal contract v16 and strategy protocol v14 with typed, dimensioned strategy - metrics while retaining string-only metric compatibility through v15 and protocol v13. - -- Add bounded level-two order-book replay with fresh snapshots, contiguous absolute updates, - multi-level marketable depth, deterministic passive queue position, and locked-book support. -- Publish scenario/journal contract v15 and external strategy protocol v13 while preserving v14 - and protocol v12 as frozen compatibility contracts. -- Add causal quote/trade replay with displayed-liquidity capacity, aggressor-qualified passive - fills, maker/taker fee attribution, and economic event timestamps. -- Publish scenario/journal contract v14 and external strategy protocol v12 while preserving v13 - and protocol v11 as frozen compatibility contracts. -- Add conservative next-open and adverse-touch completed-bar execution models with strict fixed - spread and linear participation-impact configuration, explicit missing-volume policy, - tick-aligned prices, and separate price-component audit attribution. -- 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. -- Add stable-identity instrument lifecycle state for halt, resume, identifier/provider remapping, - expiration, and delisting with deterministic order cancellation and explicit terminal hold or - cash-out policy. -- Publish scenario/journal contract v12 and external strategy protocol v10 while preserving v11 - and protocol v9 as frozen compatibility contracts. -- Add deterministic trade-date and settlement-date accounting, versioned business-date settlement - calendars, settled and unsettled cash and position attribution, explicit settlement buying-power - policies, and auditable settlement completion and failure events. -- Publish scenario/journal contract v11 and external strategy protocol v9 while preserving v10 and - protocol v8 as frozen compatibility contracts. -- Add effective-time borrow availability, signed rates, locate clipping or rejection, recalls, - deterministic close-outs, and explicit missing-data behavior. -- Add effective-time currency credit/debit rates with Actual/365 or Actual/360 day count, simple or - daily compounding, deterministic cash-ledger entries, and realized P&L attribution. -- Publish scenario/journal contract v10 and external strategy protocol v8 while preserving v9 and - protocol v7 as frozen compatibility contracts. -- Added instrument-aware, composable fee schedules with named fixed, notional, and per-unit - components; explicit rounding; maker/taker applicability; per-fill minimums and caps; rebates; - and deterministic multi-currency conversion. -- Added signed fee-component attribution to fills, positions, valuations, journals, and external - strategy events in scenario/journal contract v9 and strategy protocol v7. -- Preserved completed-bar configuration v1, scenario/journal v8, and strategy protocol v6 as - compatibility contracts. - -- Add explicit GTC, IOC, FOK, DAY, and GTD order lifetimes plus completed-bar stop and stop-limit - activation in scenario contract v8 and external strategy protocol v6. - -- Add contract v7 exact per-instrument risk policies, versioned overlapping exposure groups, - reservation-aware admission and fill clipping, group diagnostics, and strategy protocol v5. - -- Add contract v6 explicit initial portfolio snapshots with signed cash and positions, accounting - history, initial marks and FX, strict risk validation, initial-state auditing, and strategy - protocol v4 initialization. -- Publish strict versioned configuration and machine-readable capabilities for each compiled - execution model. -- Add contract v5 venue calendars with explicit venue and calendar identities, regular and - extended trading phases, holidays, early closes, and reducer-independent clock resolution. -- Protect `main` with required integration checks and a no-bypass review policy, and make rebase - merging the only supported repository merge mode. -- Establish a security baseline with private reporting guidance, grouped dependency proposals, - dependency review, and CodeQL analysis for workflows and Python tooling. -- Add structured issue and pull-request intake, reviewed planning-label and repository metadata, - explicit compatibility guarantees, a pinned required Persistra baseline, and a manual - nonrequired latest-head signal. -- Verify exact canonical journal bytes across locked, dependency-bound, and operating-system CI - cells, with safe concurrency cancellation and documented required versus informational gates. -- Publish one strict documentation site for architecture, versioned contracts, and generated OCaml - APIs, with offline topology checks and bounded external-link validation. -- Define a reproducible release-candidate artifact set with install verification, checksums, SPDX - inventory, SLSA provenance, and a manual tag-only signing boundary. - -## 1.0.0 — 2026-08-21 - -- Release the deterministic completed-bar execution engine with exact checked arithmetic, - causal audit journals, and versioned JSON and JSON Lines contracts. -- Support portfolio targets, direct orders, partial fills, fees, multi-currency accounting, - corporate actions, borrow costs, margin controls, and deterministic liquidation. -- Add synchronous external strategies through protocol v3 with current-slice callback state and - strategy responses applied before matching continues. -- Provide strict schemas, conformance fixtures, replay validation, and Persistra compatibility - checks. +## [1.1.0] - 2026-08-26 + +- Reset the scenario, journal, strategy, execution-configuration, CLI, and diagnostic contracts to + one authoritative v1 surface. +- Remove historical schemas, compatibility dispatch, deprecated constructors, frozen fixtures, + and obsolete compatibility tests. +- Require explicit initial portfolios, current risk and fee policies, complete market slices, and + current strategy initialization. +- Simplify repository and contract documentation around the current engine boundary. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 151fd04..14bca8f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,4 +81,4 @@ position. Do not encode delivery commitments in labels. The reviewed label defin repository metadata live under `.github/` and must agree with the GitHub settings. For reciprocal Persistra compatibility guarantees and the pin-advancement procedure, read -[Persistra integration](docs/persistra.md#compatibility-guarantees). +[Persistra integration](docs/persistra.md#compatibility-gate). diff --git a/README.md b/README.md index e93feda..951f565 100644 --- a/README.md +++ b/README.md @@ -1,287 +1,76 @@ # Trading Engine -Trading Engine is a deterministic, event-driven OCaml execution engine. It runs a typed strategy -through pre-trade risk, order management, synchronized completed-bar execution, exact accounting, -valuation, and a hash-bound JSON Lines audit journal. +Trading Engine is a deterministic OCaml engine for replaying trading strategies. -The [documentation site](docs/documentation-platform.md) connects the architecture, execution and -scenario contracts, stable versioned artifacts, and generated OCaml API reference. +It accepts a strict scenario, runs strategy decisions through risk, order management, execution, +financing, settlement, and accounting, then writes a hash-bound JSON Lines audit journal. The same +reducer supports scheduled intents and supervised external strategy processes. -The engine is replay-first. Its pure kernel and explicit source, strategy, execution, and journal -layers keep networking, files, and wall-clock state outside the reducer. +## Highlights -```text -scenario slices and scheduled or external intents - │ - ▼ - deterministic reducer - │ - ┌──────────┼──────────┐ - ▼ ▼ ▼ - strategy risk + OMS slice simulator - │ │ │ - └──────────┴──── fills┘ - │ - ▼ - accounting + valuation - │ - ▼ - JSON Lines journal -``` - -## Implemented scope - -- OCaml 5.5 and Dune 3.24 with a repository-local opam switch -- Opaque IDs and canonical checked fixed-point prices, weights, money, and quantities -- Synchronized market slices with one bar per configured instrument -- Separate market event, availability, receipt, slice, and engine ordering -- Pure strategy callbacks with causal, immutable context snapshots -- Pure suspend/resume strategy requests with equivalent scripted and external reducers -- Explicit immutable reducer phases behind one internal transition contract -- Portfolio weight and quantity targets covering the complete instrument catalog -- Current-equity weight sizing at synchronized closing marks with lot rounding -- Persistent target reconciliation through bounded market-order attempts -- Direct market and limit orders, cancellations, and metrics -- Typed strategy metrics with exact numeric, string, and boolean values, bounded dimensions, - units, and aggregation metadata -- Signed long/short position, order, lot, tick, gross-exposure, leverage, and margin risk -- Deterministic liquidation-first matching, then sell-before-buy and FIFO priority -- Shared per-instrument volume participation, partial fills, and GTC limits -- One-slice IOC market orders -- Risk-aware fractional-lot clipping with structured `fill_clipped` reasons and thresholds -- Instrument-aware fixed, notional, and per-unit fee schedules with explicit rounding, - maker/taker applicability, minimums, caps, rebates, and deterministic FX conversion -- Explicit multi-currency cash ledgers and complete per-slice FX marks in a base currency -- Explicit signed initial portfolios with cost basis, P&L and fee history, marks, and FX state -- Splits, dividends, rights, spin-offs, fractional cash-in-lieu, and exact basis allocation -- Stable instrument identity with halt/resume, identifier changes, expiration, and delisting -- Effective-time short locates, availability clipping, borrow-rate accrual, recalls, and - deterministic close-out orders -- Per-currency credit/debit cash rates with explicit day-count and compounding policies -- Signed average-cost accounting, realized and unrealized P&L, and equity reconciliation -- Per-currency cash and per-instrument quantity, mark, value, basis, P&L, aggregate fee, and named - fee-component attribution -- Deterministic event IDs, ordered causal references, and order-creation attribution -- Contract-selected compiled execution modules with versioned model-owned configuration and - capability descriptors; v13 adds conservative bar models, v14 adds causal quote/trade replay, - and v15 adds bounded level-two order-book replay while freezing `completed_bar_v1` -- Tick-aligned fixed-spread and participation-impact execution costs with separate reference, - spread, impact, and final-price audit attribution -- Causally ordered quotes and aggressor-classified trades with displayed-liquidity limits, - maker/taker attribution, and event-time fills -- Bounded order-book snapshots and contiguous updates with price-time queue simulation, - multi-level depth consumption, partial fills, and locked-book support -- Strict batch JSON and bounded-memory JSON Lines scenario parsing with JSON Schemas -- Versioned synchronous JSON Lines strategy processes with per-request timeouts and strict - lifecycle supervision -- Complete bidirectional strategy transcripts with coordinated no-replace journal publication -- Scenario SHA-256 binding in `run_started` and `run_completed` -- Exclusive partial artifact creation with optional file and directory synchronization -- Unit, schema-conformance, scenario, golden-contract, reducer model-property, and protocol-fuzz tests +- Exact fixed-point prices, quantities, money, weights, and FX rates +- Completed-bar, conservative-bar, quote/trade, and order-book execution models +- Market, limit, stop, stop-limit, IOC, GTC, GTD, DAY, and FOK orders +- Instrument and portfolio risk, margin, short locates, recalls, and liquidation +- Multi-currency accounting, financing, settlement, fees, and corporate actions +- Deterministic event IDs, causal references, transcripts, and durable artifact publication +- Strict v1 JSON Schemas for scenarios, journals, strategy messages, diagnostics, and CLI results ## Quick start -The project uses a local switch and does not modify the default switch. The complete check also -uses Python's `jsonschema` package to validate every committed schema and canonical fixture. It -checks the frozen-artifact hashes and runs the current differential corpus against the OCaml -parsers. +The repository uses a local opam switch. ```sh -cd ~/trading-engine make bootstrap make check ``` -Run the advisory batch, stream, dense-OMS, and external-strategy performance matrix with -`make benchmark`. See [Performance](docs/performance.md) for workload definitions, reported -metrics, and the baseline tolerance policy. - -Validate the included scenario with an in-memory replay: +Validate the canonical scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v16/fixtures/demo.scenario.json \ + --input contracts/v1/fixtures/demo.scenario.json \ --validate-only ``` -Run it and create a journal: +Replay it to a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v16/fixtures/demo.scenario.json \ + --input contracts/v1/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` -For larger histories, validate and replay the equivalent stream one slice at a time: +Use `--input-format jsonl` for bounded-memory stream input. Use `--capabilities` for the +machine-readable runtime surface and `--output-format json` for structured success and failure +output. -```sh -opam exec -- dune exec trading-engine -- \ - --input contracts/v16/fixtures/demo.scenario.jsonl \ - --input-format jsonl \ - --journal demo.journal.jsonl -``` +## External strategies -Compose a JSON Lines producer and journal consumer without mixing streams: - -```sh -produce-scenario | trading-engine --input - --input-format jsonl --journal - | consume-journal -``` - -Standard input is spooled to a private temporary file, limited to 1 GiB, then hashed and validated -before replay. Standard output contains only journal records. The engine stages and verifies the -complete journal before copying it to the pipe; its final `run_completed` record and a zero exit -status signal completion. Pipe output cannot provide exclusive no-replace publication, atomic -linking, retained partial files, directory synchronization, or restart-durability guarantees. -`--durable-artifacts` is therefore invalid with `--journal -`. - -Run an external strategy against an empty-schedule scenario: +External strategies exchange one synchronous JSON Lines message at a time: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v14/fixtures/external.scenario.json \ + --input contracts/strategy/v1/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ - --strategy-arg=config.toml \ - --strategy-timeout 30 \ --strategy-transcript external.strategy.jsonl ``` -The engine launches the program directly without a shell. This supervises the child but does not -sandbox it; run strategy code with the same trust you give the invoking user. Protocol messages -own the child's standard input and output; strategy diagnostics belong on standard error. Only -one request is outstanding. Initialization must return `ready`, each event must return `intents`, -and shutdown must return `stopped`. Wrong versions or sequences, unknown or malformed fields, -oversized responses, EOF, timeout, extra output, and nonzero exit all fail the replay. The journal -and transcript remain partial until both are complete. The engine then closes both, publishes the -complete set without replacement, and removes the partial names. A close or publication failure -rolls back final names created by the transaction. A cleanup failure leaves the complete final set -and restores every partial name for diagnosis. The strategy runs in a dedicated process group. -Failure and cancellation send `SIGTERM` to the complete group, allow one second for graceful exit, -then send `SIGKILL` and allow five seconds to reap the process tree. - -Discover the executable version and machine-readable compatibility surface: - -```sh -opam exec -- dune exec trading-engine -- --version -opam exec -- dune exec trading-engine -- --capabilities -``` +The child process is supervised but not sandboxed. Protocol output belongs on standard output; +strategy logs belong on standard error. -Clients must confirm that both `scenario_contract_versions` and `journal_contract_versions` -contain the scenario's `contract_version` before starting a replay. External clients must also -require their version in `strategy_protocol_versions`. The versioned `resource_limits` object -publishes inclusive limits for scenario records, strategy messages, reducer feedback, catalogs, -intent batches, and artifact records. Runtime failures can use the structured -diagnostic contract identified by each diagnostic's `diagnostic_version`. Human diagnostics remain -the default. Use `--diagnostic-format json` to receive one JSON diagnostic on standard error with a -stable code, phase, typed context, and sanitized underlying cause. +## Documentation -Use `--output-format json` for the versioned -[CLI result contract](contracts/cli/v1/README.md). A success document includes the run identity, -scenario and artifact hashes, replay counts, normalized current valuation, and artifact locations. -This option also selects JSON failure diagnostics. For file journals the success document is written -to standard output. With `--journal -`, the journal owns standard output and the success document -moves to standard error. - -The final and `.partial` journal paths must not already exist. Batch JSON hashes the same complete -document it parses. JSON Lines input is hashed and validated in a bounded-memory pass before the -journal is created, then replayed from the same open file and hashed again before publication. The -CLI binds that exact-byte hash into the journal. It writes to the partial path and publishes the -requested path only after `run_completed` is fully written and the partial file is closed. An -error preserves the partial artifact for diagnosis. - -A protocol-invalid strategy response is not stored as an accepted transcript exchange. The partial -transcript instead ends with a versioned rejection record containing its structured diagnostic and -a hexadecimal prefix of at most 256 raw response bytes. This covers malformed fields and JSON, -wrong versions or sequences, EOF, and oversized output without retaining the complete rejected -payload. - -Journal and transcript records are limited to 2 MiB each, including the terminating line feed. -Limit failures use the stable `resource.limit` diagnostic code. - -Pass `--durable-artifacts` to synchronize each staged file before publication and synchronize each -containing directory after final links and partial cleanup. The default buffered mode flushes every -record but does not make a restart-durability claim. - -## Execution summary - -An order emitted after slice `n` cannot execute on slice `n`. It first becomes eligible on a later -slice whose start is not earlier than its creation time. - -- Market orders attempt the next eligible open and cancel any remainder after that slice. -- Persistent portfolio targets submit a new bounded attempt after each miss until reached or - superseded. -- Limit orders use deterministic gap improvement and optimistic intrabar touch rules. -- Eligible liquidation orders consume capacity before other orders. Within each origin class, - sells precede buys and FIFO creation order breaks ties within a side. -- Corporate actions are applied before matching. Splits adjust positions, persistent targets, and - active orders; distributions allocate basis and fractional cash exactly; cash dividends credit - longs and debit shorts in the quote-currency ledger. -- Lifecycle events update symbols and provider mappings without changing instrument identity. - Halts and terminal events cancel orders; expiration and delisting follow an explicit hold or - cash-out policy. -- Effective-time borrow observations control short availability and rates. New shorts are rejected - or clipped to their locate, recalls reject new shorts or create deterministic close-out orders, - and observed borrow charges accrue before matching. -- Effective-time currency observations credit positive cash and debit negative cash for the slice - interval under the configured day-count, compounding, and missing-data policies. -- Proposed fills are clipped to the largest permitted fractional-lot quantity at the actual fill - price and never exceed the maximum order quantity. Increasing exposure must satisfy position, - gross-exposure, leverage, and initial-margin limits; exposure-reducing fills remain available. -- A maintenance-margin breach cancels active orders, clears portfolio targets, and creates - deterministic market orders that flatten positions in bounded lots across later slices. -- The engine emits exactly one valuation after each complete synchronized slice. - -Read [Execution model](docs/execution-model.md) for the full phase, price, fee, cash, and accounting -rules. - -## Project boundaries - -The current scope omits: - -- Broker and streaming-market-data connectors -- External execution-report ingestion -- Exchange calendars and time-zone databases -- Durable reducer snapshots and broker reconciliation -- Tick, trade, and order-book replay - -Artifact publication requires a filesystem that supports exclusive file creation, hard links, and -atomic unlink. Durable mode additionally requires file and directory synchronization. An -unsupported synchronization operation returns `artifact.io`, never reports success, and preserves -or restores partial names for diagnosis. Durable artifacts strengthen publication persistence; they -do not provide reducer snapshots or restart recovery. - -## Architecture and contracts - -- [Support and issue guidance](.github/SUPPORT.md) -- [Security policy](.github/SECURITY.md) -- [Security maintenance](docs/security-maintenance.md) -- [Contributing](CONTRIBUTING.md) - [Architecture](docs/architecture.md) -- [Diagnostic contract](docs/diagnostics.md) -- [CLI result contract](contracts/cli/v1/README.md) -- [Scenario contract](docs/scenario.md) -- [Contract conformance corpus](contracts/conformance/README.md) -- [Current contract v16 and conformance fixtures](contracts/v16/README.md) -- [Frozen contract v2](contracts/v2/README.md) -- [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v16/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v16/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v16/journal.schema.json) -- [CLI result JSON Schema](contracts/cli/v1/result.schema.json) -- [External strategy protocol v14](contracts/strategy/v14/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/v14/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v14/transcript.schema.json) - [Execution model](docs/execution-model.md) -- [OCaml coverage](docs/coverage.md) -- [Continuous integration and portability matrix](docs/continuous-integration.md) -- [Documentation platform and generated API](docs/documentation-platform.md) -- [Release artifacts and provenance](docs/release-artifacts.md) -- [Performance](docs/performance.md) -- [Reducer property testing](docs/reducer-property-testing.md) -- [Protocol fuzzing](docs/fuzzing.md) +- [Scenario and journal](docs/scenario.md) +- [Replay contract v1](contracts/v1/README.md) +- [Strategy protocol v1](contracts/strategy/v1/README.md) +- [Diagnostics](docs/diagnostics.md) - [Persistra integration](docs/persistra.md) - [Contributing](CONTRIBUTING.md) +- [Security](.github/SECURITY.md) + +The complete documentation site is published at +[fallblu.github.io/trading-engine](https://fallblu.github.io/trading-engine/). diff --git a/bench/benchmark_batch_schedule.py b/bench/benchmark_batch_schedule.py index 0d30361..14d62b6 100644 --- a/bench/benchmark_batch_schedule.py +++ b/bench/benchmark_batch_schedule.py @@ -16,7 +16,7 @@ ROOT = Path(__file__).resolve().parents[1] DEFAULT_EXECUTABLE = ROOT / "_build/default/bin/main.exe" -FIXTURE = ROOT / "contracts/v5/fixtures/demo.scenario.json" +FIXTURE = ROOT / "contracts/v1/fixtures/demo.scenario.json" def timestamp(value: datetime) -> str: @@ -51,7 +51,10 @@ def dense_scenario(size: int) -> dict[str, object]: { "type": "emit_metric", "name": "dense_schedule", - "value": str(sequence), + "value": {"type": "numeric", "value": str(sequence)}, + "unit": None, + "dimensions": {}, + "aggregation": "last", } ], } diff --git a/bench/benchmark_replay.py b/bench/benchmark_replay.py index 899f4b3..8b73970 100644 --- a/bench/benchmark_replay.py +++ b/bench/benchmark_replay.py @@ -23,7 +23,7 @@ ROOT = Path(__file__).resolve().parents[1] DEFAULT_EXECUTABLE = ROOT / "_build/default/bin/main.exe" DEFAULT_BASELINE = ROOT / "bench/baselines/linux-x86_64.json" -FIXTURE = ROOT / "contracts/v10/fixtures/demo.scenario.json" +FIXTURE = ROOT / "contracts/v1/fixtures/demo.scenario.json" STRATEGY = ROOT / "bench/latency_strategy.py" SUMMARY_PATTERN = re.compile( r"\baudits=(?P[0-9]+).*\bactive=(?P[0-9]+)" @@ -219,7 +219,6 @@ def build_scenario(case: BenchmarkCase) -> dict[str, object]: "risk": { "max_gross_exposure": "1000000000", "max_leverage": "1000000", - "short_borrow_bps": 0, "instrument_policies": [ { "instrument_id": instrument["instrument_id"], @@ -240,8 +239,26 @@ def build_scenario(case: BenchmarkCase) -> dict[str, object]: "configuration": { "version": "1", "participation_bps": 10000, - "fixed_fee": "0", - "fee_bps": 0, + "fee_schedules": [ + { + "schedule_id": f"{instrument['instrument_id']}-fees-v1", + "instrument_id": instrument["instrument_id"], + "settlement_currency": "USD", + "minimum": None, + "maximum": None, + "components": [ + { + "name": "benchmark", + "currency": "USD", + "kind": "fixed", + "value": "0", + "rounding": "nearest", + "applies_to": "any", + } + ], + } + for instrument in instruments + ], }, }, "max_internal_events": max(1000, case.active_order_count * 4 + 16), @@ -268,6 +285,7 @@ def stream_records(document: dict[str, object]) -> list[dict[str, object]]: "risk", "execution", "financing", + "settlement", "max_internal_events", ) records = [ diff --git a/bin/main.ml b/bin/main.ml index 14e0fbb..53f83b7 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -80,8 +80,7 @@ let success_to_yojson success = ("rejected_orders", int64 counts.rejected_orders); ] ); ( "valuation", - Trading_engine.Codec.account_valuation_to_yojson ~contract_version:"16" - success.valuation ); + Trading_engine.Codec.account_valuation_to_yojson success.valuation ); ( "artifacts", `Assoc [ diff --git a/contracts/cli/v1/README.md b/contracts/cli/v1/README.md index 82a47e4..110df86 100644 --- a/contracts/cli/v1/README.md +++ b/contracts/cli/v1/README.md @@ -15,6 +15,4 @@ success document moves to standard error. Its journal artifact is named `stdout` `run_completed` record signals successful completion. A consumer must also require a zero process exit status. Strategy protocol messages remain confined to the supervised child process. -This result contract is independent of the scenario contract version. Its `valuation` uses the -current v16 valuation shape so consumers receive one stable automation model for older accepted -scenarios. +The result contract reports the normalized replay contract v1 valuation. diff --git a/contracts/cli/v1/result.schema.json b/contracts/cli/v1/result.schema.json index 2aea165..504489c 100644 --- a/contracts/cli/v1/result.schema.json +++ b/contracts/cli/v1/result.schema.json @@ -18,7 +18,7 @@ "result_version": { "const": "1" }, "status": { "const": "success" }, "operation": { "enum": ["validate", "replay"] }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/identifier" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/identifier" }, "hashes": { "type": "object", "additionalProperties": false, @@ -104,26 +104,26 @@ "execution_fee_components" ], "properties": { - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "settled_cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "unsettled_cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/unsignedDecimal" }, - "cost_basis": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "realized_pnl": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "unrealized_pnl": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "dividend_pnl": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "execution_fees": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "borrow_fees": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "cash_interest": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "total_fees": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/signedDecimal" }, - "cash_balances": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/cashAttribution" } }, - "positions": { "type": "array", "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/positionAttribution" } }, - "execution_fee_components": { "type": "array", "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/feeComponentAttribution" } } + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "settled_cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "unsettled_cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/unsignedDecimal" }, + "cost_basis": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "realized_pnl": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "unrealized_pnl": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "dividend_pnl": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "execution_fees": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "borrow_fees": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "cash_interest": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "total_fees": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/signedDecimal" }, + "cash_balances": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/cashAttribution" } }, + "positions": { "type": "array", "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/positionAttribution" } }, + "execution_fee_components": { "type": "array", "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/feeComponentAttribution" } } } } } diff --git a/contracts/conformance/README.md b/contracts/conformance/README.md index fce76c8..c6efdcf 100644 --- a/contracts/conformance/README.md +++ b/contracts/conformance/README.md @@ -2,34 +2,11 @@ This directory is the machine-readable entry point for contract consumers. -- `manifest.json` maps every versioned schema branch to its canonical fixtures. -- `cases.json` records deterministic schema/runtime differential cases for the - versions accepted by the current OCaml runtime. -- `frozen.sha256` protects the schema and fixture bytes for archived scenario - contract v1 and v2 and strategy protocol v1 and v2. +- `manifest.json` maps each current v1 schema to canonical fixtures. +- `cases.json` defines structural and semantic differential cases. -The artifact manifest treats each schema at each version as a separate branch. -Every branch has positive canonical inputs and generated negative cases for a -missing version, an unsupported version, and an unknown field. Frozen branches -are schema-only: they are never passed to current runtime parsers. +Structural cases must agree between JSON Schema and the OCaml runtime. Semantic cases document +rules that JSON Schema cannot express, such as ordering, uniqueness, and cross-record invariants. -Every top-level `oneOf` alternative also has a positive witness and a derived -unknown-field rejection. `schema_only_cases` supplies variants that do not occur -in a successful canonical run, such as strategy `error` messages and rejected -response transcript records. - -Differential cases label rules as `structural` or `semantic`. Structural cases -must produce the same result from JSON Schema and the OCaml parser. Semantic -cases explicitly document invariants that JSON Schema cannot express, so an -accepted schema result and a rejected runtime result is intentional. - -Run the complete corpus through the repository gate: - -```sh -make check -``` - -When intentionally changing an archived contract, update `frozen.sha256` in the -same review. Adding a contract version requires a manifest branch, canonical -fixtures, positive and negative validation, and current-runtime differential -cases when the new version is advertised by the engine. +Run `make check` to validate the complete corpus. A contract change must update its schema, +fixtures, manifest entries, differential cases, and runtime checks together. diff --git a/contracts/conformance/cases.json b/contracts/conformance/cases.json index ded2c74..ae344ed 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -2,50 +2,10 @@ "format_version": "1", "cases": [ { - "name": "scenario-v7-valid", - "artifact": "scenario-v7", + "name": "scenario-v1-valid", + "artifact": "scenario-v1", "kind": "scenario", - "source": "v7/fixtures/demo.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-stream-v7-valid", - "artifact": "scenario-stream-v7", - "kind": "scenario_stream", - "source": "v7/fixtures/demo.scenario.jsonl", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-v6-valid", - "artifact": "scenario-v6", - "kind": "scenario", - "source": "v6/fixtures/demo.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-v5-valid", - "artifact": "scenario-v5", - "kind": "scenario", - "source": "v5/fixtures/demo.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-v3-valid", - "artifact": "scenario-v3", - "kind": "scenario", - "source": "v3/fixtures/demo.scenario.json", + "source": "v1/fixtures/demo.scenario.json", "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", @@ -53,16 +13,11 @@ }, { "name": "scenario-missing-version", - "artifact": "scenario-v5", + "artifact": "scenario-v1", "kind": "scenario", - "source": "v5/fixtures/demo.scenario.json", + "source": "v1/fixtures/demo.scenario.json", "mutations": [ - { - "op": "remove", - "path": [ - "contract_version" - ] - } + { "op": "remove", "path": ["contract_version"] } ], "schema_expectation": "reject", "runtime_expectation": "reject", @@ -70,37 +25,11 @@ }, { "name": "scenario-unknown-field", - "artifact": "scenario-v5", - "kind": "scenario", - "source": "v5/fixtures/demo.scenario.json", - "mutations": [ - { - "op": "add", - "path": [ - "unexpected_contract_field" - ], - "value": true - } - ], - "schema_expectation": "reject", - "runtime_expectation": "reject", - "rule": "structural" - }, - { - "name": "scenario-invalid-scalar-type", - "artifact": "scenario-v5", + "artifact": "scenario-v1", "kind": "scenario", - "source": "v5/fixtures/demo.scenario.json", + "source": "v1/fixtures/demo.scenario.json", "mutations": [ - { - "op": "replace", - "path": [ - "initial_cash", - 0, - "amount" - ], - "value": 10000 - } + { "op": "add", "path": ["unexpected_contract_field"], "value": true } ], "schema_expectation": "reject", "runtime_expectation": "reject", @@ -108,18 +37,14 @@ }, { "name": "scenario-unsupported-execution-configuration", - "artifact": "scenario-v5", + "artifact": "scenario-v1", "kind": "scenario", - "source": "v5/fixtures/demo.scenario.json", + "source": "v1/fixtures/demo.scenario.json", "mutations": [ { "op": "replace", - "path": [ - "execution", - "configuration", - "version" - ], - "value": "2" + "path": ["execution", "configuration", "version"], + "value": "unsupported" } ], "schema_expectation": "reject", @@ -128,17 +53,11 @@ }, { "name": "scenario-duplicate-instrument", - "artifact": "scenario-v5", + "artifact": "scenario-v1", "kind": "scenario", - "source": "v5/fixtures/demo.scenario.json", + "source": "v1/fixtures/demo.scenario.json", "mutations": [ - { - "op": "append_copy", - "path": [ - "instruments" - ], - "index": 0 - } + { "op": "append_copy", "path": ["instruments"], "index": 0 } ], "schema_expectation": "accept", "runtime_expectation": "reject", @@ -146,17 +65,13 @@ }, { "name": "scenario-overlapping-slices", - "artifact": "scenario-v5", + "artifact": "scenario-v1", "kind": "scenario", - "source": "v5/fixtures/demo.scenario.json", + "source": "v1/fixtures/demo.scenario.json", "mutations": [ { "op": "replace", - "path": [ - "slices", - 1, - "start_at" - ], + "path": ["slices", 1, "start_at"], "value": "2026-01-02T20:00:00Z" } ], @@ -165,30 +80,10 @@ "rule": "semantic" }, { - "name": "scenario-stream-v5-valid", - "artifact": "scenario-stream-v5", - "kind": "scenario_stream", - "source": "v5/fixtures/demo.scenario.jsonl", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-stream-v6-valid", - "artifact": "scenario-stream-v6", - "kind": "scenario_stream", - "source": "v6/fixtures/demo.scenario.jsonl", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-stream-v3-valid", - "artifact": "scenario-stream-v3", + "name": "scenario-stream-v1-valid", + "artifact": "scenario-stream-v1", "kind": "scenario_stream", - "source": "v3/fixtures/demo.scenario.jsonl", + "source": "v1/fixtures/demo.scenario.jsonl", "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", @@ -196,36 +91,12 @@ }, { "name": "scenario-stream-missing-version", - "artifact": "scenario-stream-v5", + "artifact": "scenario-stream-v1", "kind": "scenario_stream", - "source": "v5/fixtures/demo.scenario.jsonl", + "source": "v1/fixtures/demo.scenario.jsonl", "record": 1, "mutations": [ - { - "op": "remove", - "path": [ - "contract_version" - ] - } - ], - "schema_expectation": "reject", - "runtime_expectation": "reject", - "rule": "structural" - }, - { - "name": "scenario-stream-unknown-field", - "artifact": "scenario-stream-v5", - "kind": "scenario_stream", - "source": "v5/fixtures/demo.scenario.jsonl", - "record": 2, - "mutations": [ - { - "op": "add", - "path": [ - "unexpected_contract_field" - ], - "value": true - } + { "op": "remove", "path": ["contract_version"] } ], "schema_expectation": "reject", "runtime_expectation": "reject", @@ -233,18 +104,12 @@ }, { "name": "scenario-stream-out-of-order-sequence", - "artifact": "scenario-stream-v5", + "artifact": "scenario-stream-v1", "kind": "scenario_stream", - "source": "v5/fixtures/demo.scenario.jsonl", + "source": "v1/fixtures/demo.scenario.jsonl", "record": 2, "mutations": [ - { - "op": "replace", - "path": [ - "scenario_sequence" - ], - "value": "3" - } + { "op": "replace", "path": ["scenario_sequence"], "value": "99" } ], "schema_expectation": "accept", "runtime_expectation": "reject", @@ -252,1498 +117,116 @@ }, { "name": "strategy-ready-valid", - "artifact": "strategy-message-v6", + "artifact": "strategy-message-v1", "kind": "strategy_response", - "source": "strategy/v6/fixtures/external.strategy.jsonl", + "source": "strategy/v1/fixtures/external.strategy.jsonl", "record": 2, - "extract": [ - "message" - ], + "extract": ["message"], "expected_sequence": "1", "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "6" + "rule": "structural" }, { "name": "strategy-intents-valid", - "artifact": "strategy-message-v6", + "artifact": "strategy-message-v1", "kind": "strategy_response", - "source": "strategy/v6/fixtures/external.strategy.jsonl", + "source": "strategy/v1/fixtures/external.strategy.jsonl", "record": 4, - "extract": [ - "message" - ], + "extract": ["message"], "expected_sequence": "2", "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "6" + "rule": "structural" }, { "name": "strategy-stopped-valid", - "artifact": "strategy-message-v6", + "artifact": "strategy-message-v1", "kind": "strategy_response", - "source": "strategy/v6/fixtures/external.strategy.jsonl", + "source": "strategy/v1/fixtures/external.strategy.jsonl", "record": 14, - "extract": [ - "message" - ], + "extract": ["message"], "expected_sequence": "7", "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "6" - }, - { - "name": "strategy-error-valid", - "artifact": "strategy-message-v6", - "kind": "strategy_response", - "source": "strategy/v6/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "expected_sequence": "7", - "mutations": [ - { - "op": "replace", - "path": [ - "message_type" - ], - "value": "error" - }, - { - "op": "replace", - "path": [ - "payload" - ], - "value": { - "message": "intentional conformance error" - } - } - ], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "6" + "rule": "structural" }, { "name": "strategy-missing-version", - "artifact": "strategy-message-v6", + "artifact": "strategy-message-v1", "kind": "strategy_response", - "source": "strategy/v6/fixtures/external.strategy.jsonl", + "source": "strategy/v1/fixtures/external.strategy.jsonl", "record": 2, - "extract": [ - "message" - ], + "extract": ["message"], "expected_sequence": "1", "mutations": [ - { - "op": "remove", - "path": [ - "strategy_protocol_version" - ] - } + { "op": "remove", "path": ["strategy_protocol_version"] } ], "schema_expectation": "reject", "runtime_expectation": "reject", - "rule": "structural", - "protocol_version": "6" + "rule": "structural" }, { - "name": "strategy-unknown-field", - "artifact": "strategy-message-v6", + "name": "strategy-wrong-sequence", + "artifact": "strategy-message-v1", "kind": "strategy_response", - "source": "strategy/v6/fixtures/external.strategy.jsonl", + "source": "strategy/v1/fixtures/external.strategy.jsonl", "record": 2, - "extract": [ - "message" - ], + "extract": ["message"], "expected_sequence": "1", "mutations": [ - { - "op": "add", - "path": [ - "unexpected_contract_field" - ], - "value": true - } - ], - "schema_expectation": "reject", - "runtime_expectation": "reject", - "rule": "structural", - "protocol_version": "6" - }, - { - "name": "strategy-wrong-sequence", - "artifact": "strategy-message-v6", - "kind": "strategy_response", - "source": "strategy/v6/fixtures/external.strategy.jsonl", - "record": 2, - "extract": [ - "message" + { "op": "replace", "path": ["strategy_sequence"], "value": "99" } ], - "expected_sequence": "2", - "mutations": [], "schema_expectation": "accept", "runtime_expectation": "reject", - "rule": "semantic", - "protocol_version": "6" - }, - { - "name": "strategy-ready-valid-v5", - "artifact": "strategy-message-v5", - "kind": "strategy_response", - "source": "strategy/v5/fixtures/external.strategy.jsonl", - "record": 2, - "extract": [ - "message" - ], - "expected_sequence": "1", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "5" - }, - { - "name": "strategy-intents-valid-v5", - "artifact": "strategy-message-v5", - "kind": "strategy_response", - "source": "strategy/v5/fixtures/external.strategy.jsonl", - "record": 4, - "extract": [ - "message" - ], - "expected_sequence": "2", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "5" - }, + "rule": "semantic" + } + ], + "schema_only_cases": [ { - "name": "strategy-stopped-valid-v5", - "artifact": "strategy-message-v5", - "kind": "strategy_response", - "source": "strategy/v5/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "expected_sequence": "7", + "name": "strategy-error-valid", + "artifact": "strategy-message-v1", + "instance": { + "strategy_protocol_version": "1", + "strategy_sequence": "1", + "message_type": "error", + "payload": { "message": "fixture failure" } + }, "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "5" + "rule": "structural" }, { - "name": "strategy-error-valid-v5", - "artifact": "strategy-message-v5", - "kind": "strategy_response", - "source": "strategy/v5/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "expected_sequence": "7", - "mutations": [ - { - "op": "replace", - "path": [ - "message_type" - ], - "value": "error" + "name": "strategy-rejected-response-valid", + "artifact": "strategy-transcript-v1", + "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": "invalid strategy response", + "context": { "json_path": "$", "sequence": "1" }, + "cause": null }, - { - "op": "replace", - "path": [ - "payload" - ], - "value": { - "message": "intentional conformance error" - } + "evidence": { + "encoding": "hex", + "prefix": "7b", + "observed_bytes": 1, + "truncated": false } - ], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "5" - }, - { - "name": "strategy-ready-valid-v4", - "artifact": "strategy-message-v4", - "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", - "record": 2, - "extract": [ - "message" - ], - "expected_sequence": "1", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "4" - }, - { - "name": "strategy-intents-valid-v4", - "artifact": "strategy-message-v4", - "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", - "record": 4, - "extract": [ - "message" - ], - "expected_sequence": "2", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "4" - }, - { - "name": "strategy-stopped-valid-v4", - "artifact": "strategy-message-v4", - "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "expected_sequence": "7", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "4" - }, - { - "name": "strategy-error-valid-v4", - "artifact": "strategy-message-v4", - "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "expected_sequence": "7", - "mutations": [ - { - "op": "replace", - "path": [ - "message_type" - ], - "value": "error" - }, - { - "op": "replace", - "path": [ - "payload" - ], - "value": { - "message": "intentional conformance error" - } - } - ], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural", - "protocol_version": "4" - }, - { - "name": "strategy-missing-version-v4", - "artifact": "strategy-message-v4", - "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", - "record": 2, - "extract": [ - "message" - ], - "expected_sequence": "1", - "mutations": [ - { - "op": "remove", - "path": [ - "strategy_protocol_version" - ] - } - ], - "schema_expectation": "reject", - "runtime_expectation": "reject", - "rule": "structural", - "protocol_version": "4" - }, - { - "name": "strategy-unknown-field-v4", - "artifact": "strategy-message-v4", - "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", - "record": 2, - "extract": [ - "message" - ], - "expected_sequence": "1", - "mutations": [ - { - "op": "add", - "path": [ - "unexpected_contract_field" - ], - "value": true - } - ], - "schema_expectation": "reject", - "runtime_expectation": "reject", - "rule": "structural", - "protocol_version": "4" - }, - { - "name": "strategy-wrong-sequence-v4", - "artifact": "strategy-message-v4", - "kind": "strategy_response", - "source": "strategy/v4/fixtures/external.strategy.jsonl", - "record": 2, - "extract": [ - "message" - ], - "expected_sequence": "2", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "reject", - "rule": "semantic", - "protocol_version": "4" - }, - { - "name": "scenario-v9-valid", - "artifact": "scenario-v9", - "kind": "scenario", - "source": "v9/fixtures/demo.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-stream-v9-valid", - "artifact": "scenario-stream-v9", - "kind": "scenario_stream", - "source": "v9/fixtures/demo.scenario.jsonl", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-ready-valid-v7", - "artifact": "strategy-message-v7", - "kind": "strategy_response", - "source": "strategy/v7/fixtures/external.strategy.jsonl", - "record": 2, - "extract": [ - "message" - ], - "expected_sequence": "1", - "protocol_version": "7", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-intents-valid-v7", - "artifact": "strategy-message-v7", - "kind": "strategy_response", - "source": "strategy/v7/fixtures/external.strategy.jsonl", - "record": 4, - "extract": [ - "message" - ], - "expected_sequence": "2", - "protocol_version": "7", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-stopped-valid-v7", - "artifact": "strategy-message-v7", - "kind": "strategy_response", - "source": "strategy/v7/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "expected_sequence": "7", - "protocol_version": "7", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-error-valid-v7", - "artifact": "strategy-message-v7", - "kind": "strategy_response", - "source": "strategy/v7/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "expected_sequence": "7", - "protocol_version": "7", - "mutations": [ - { - "op": "replace", - "path": [ - "message_type" - ], - "value": "error" - }, - { - "op": "replace", - "path": [ - "payload" - ], - "value": { - "message": "fixture failure" - } - } - ], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-v10-valid", - "artifact": "scenario-v10", - "kind": "scenario", - "source": "v10/fixtures/demo.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-stream-v10-valid", - "artifact": "scenario-stream-v10", - "kind": "scenario_stream", - "source": "v10/fixtures/demo.scenario.jsonl", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-ready-valid-v8", - "artifact": "strategy-message-v8", - "kind": "strategy_response", - "source": "strategy/v8/fixtures/external.strategy.jsonl", - "record": 2, - "extract": [ - "message" - ], - "expected_sequence": "1", - "protocol_version": "8", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-intents-valid-v8", - "artifact": "strategy-message-v8", - "kind": "strategy_response", - "source": "strategy/v8/fixtures/external.strategy.jsonl", - "record": 4, - "extract": [ - "message" - ], - "expected_sequence": "2", - "protocol_version": "8", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-stopped-valid-v8", - "artifact": "strategy-message-v8", - "kind": "strategy_response", - "source": "strategy/v8/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "expected_sequence": "7", - "protocol_version": "8", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-error-valid-v8", - "artifact": "strategy-message-v8", - "kind": "strategy_response", - "source": "strategy/v8/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "expected_sequence": "7", - "protocol_version": "8", - "mutations": [ - { - "op": "replace", - "path": [ - "message_type" - ], - "value": "error" - }, - { - "op": "replace", - "path": [ - "payload" - ], - "value": { - "message": "fixture failure" - } - } - ], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-v11-valid", - "artifact": "scenario-v11", - "kind": "scenario", - "source": "v11/fixtures/demo.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-stream-v11-valid", - "artifact": "scenario-stream-v11", - "kind": "scenario_stream", - "source": "v11/fixtures/demo.scenario.jsonl", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-ready-valid-v9", - "artifact": "strategy-message-v9", - "kind": "strategy_response", - "source": "strategy/v9/fixtures/external.strategy.jsonl", - "record": 2, - "extract": [ - "message" - ], - "expected_sequence": "1", - "protocol_version": "9", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-intents-valid-v9", - "artifact": "strategy-message-v9", - "kind": "strategy_response", - "source": "strategy/v9/fixtures/external.strategy.jsonl", - "record": 4, - "extract": [ - "message" - ], - "expected_sequence": "2", - "protocol_version": "9", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-stopped-valid-v9", - "artifact": "strategy-message-v9", - "kind": "strategy_response", - "source": "strategy/v9/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "expected_sequence": "7", - "protocol_version": "9", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-error-valid-v9", - "artifact": "strategy-message-v9", - "kind": "strategy_response", - "source": "strategy/v9/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "expected_sequence": "7", - "protocol_version": "9", - "mutations": [ - { - "op": "replace", - "path": [ - "message_type" - ], - "value": "error" - }, - { - "op": "replace", - "path": [ - "payload" - ], - "value": { - "message": "fixture failure" - } - } - ], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-v15-valid", - "artifact": "scenario-v15", - "kind": "scenario", - "source": "v15/fixtures/demo.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-v15-order-book-valid", - "artifact": "scenario-v15", - "kind": "scenario", - "source": "v15/fixtures/order-book.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-stream-v15-valid", - "artifact": "scenario-stream-v15", - "kind": "scenario_stream", - "source": "v15/fixtures/order-book.scenario.jsonl", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-ready-valid-v13", - "artifact": "strategy-message-v13", - "kind": "strategy_response", - "source": "strategy/v13/fixtures/external.strategy.jsonl", - "record": 2, - "extract": [ - "message" - ], - "expected_sequence": "1", - "protocol_version": "13", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-intents-valid-v13", - "artifact": "strategy-message-v13", - "kind": "strategy_response", - "source": "strategy/v13/fixtures/external.strategy.jsonl", - "record": 4, - "extract": [ - "message" - ], - "expected_sequence": "2", - "protocol_version": "13", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-v16-valid", - "artifact": "scenario-v16", - "kind": "scenario", - "source": "v16/fixtures/demo.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-v16-order-book-valid", - "artifact": "scenario-v16", - "kind": "scenario", - "source": "v16/fixtures/order-book.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "scenario-stream-v16-valid", - "artifact": "scenario-stream-v16", - "kind": "scenario_stream", - "source": "v16/fixtures/order-book.scenario.jsonl", + }, "mutations": [], "schema_expectation": "accept", "runtime_expectation": "accept", "rule": "structural" - }, - { - "name": "strategy-ready-valid-v14", - "artifact": "strategy-message-v14", - "kind": "strategy_response", - "source": "strategy/v14/fixtures/external.strategy.jsonl", - "record": 2, - "extract": [ - "message" - ], - "expected_sequence": "1", - "protocol_version": "14", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - }, - { - "name": "strategy-intents-valid-v14", - "artifact": "strategy-message-v14", - "kind": "strategy_response", - "source": "strategy/v14/fixtures/external.strategy.jsonl", - "record": 4, - "extract": [ - "message" - ], - "expected_sequence": "2", - "protocol_version": "14", - "mutations": [], - "schema_expectation": "accept", - "runtime_expectation": "accept", - "rule": "structural" - } - ], - "schema_only_cases": [ - { - "name": "strategy-v1-error-branch", - "artifact": "strategy-message-v1", - "source": "strategy/v1/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "mutations": [ - { - "op": "replace", - "path": [ - "message_type" - ], - "value": "error" - }, - { - "op": "replace", - "path": [ - "payload" - ], - "value": { - "message": "intentional conformance error" - } - } - ], - "schema_expectation": "accept" - }, - { - "name": "strategy-v2-error-branch", - "artifact": "strategy-message-v2", - "source": "strategy/v2/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "mutations": [ - { - "op": "replace", - "path": [ - "message_type" - ], - "value": "error" - }, - { - "op": "replace", - "path": [ - "payload" - ], - "value": { - "message": "intentional conformance error" - } - } - ], - "schema_expectation": "accept" - }, - { - "name": "strategy-v3-error-branch", - "artifact": "strategy-message-v3", - "source": "strategy/v3/fixtures/external.strategy.jsonl", - "record": 14, - "extract": [ - "message" - ], - "mutations": [ - { - "op": "replace", - "path": [ - "message_type" - ], - "value": "error" - }, - { - "op": "replace", - "path": [ - "payload" - ], - "value": { - "message": "intentional conformance error" - } - } - ], - "schema_expectation": "accept" - }, - { - "name": "strategy-v3-rejected-response-branch", - "artifact": "strategy-transcript-v3", - "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" - }, - { - "name": "strategy-v4-rejected-response-branch", - "artifact": "strategy-transcript-v4", - "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" - }, - { - "name": "strategy-v5-rejected-response-branch", - "artifact": "strategy-transcript-v5", - "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", - "source": "strategy/v5/fixtures/external.strategy.jsonl" - }, - { - "name": "strategy-v6-rejected-response-branch", - "artifact": "strategy-transcript-v6", - "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", - "source": "strategy/v6/fixtures/external.strategy.jsonl" - }, - { - "name": "strategy-v7-rejected-response-branch", - "artifact": "strategy-transcript-v7", - "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", - "source": "strategy/v7/fixtures/external.strategy.jsonl" - }, - { - "name": "strategy-v8-rejected-response-branch", - "artifact": "strategy-transcript-v8", - "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", - "source": "strategy/v8/fixtures/external.strategy.jsonl" - }, - { - "name": "strategy-v9-rejected-response-branch", - "artifact": "strategy-transcript-v9", - "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", - "source": "strategy/v9/fixtures/external.strategy.jsonl" - }, - { - "name": "scenario-v12-valid", - "artifact": "scenario-v12", - "kind": "scenario", - "source": "v12/fixtures/demo.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "parser_expectation": "accept" - }, - { - "name": "scenario-stream-v12-valid", - "artifact": "scenario-stream-v12", - "kind": "scenario_stream", - "source": "v12/fixtures/demo.scenario.jsonl", - "mutations": [], - "schema_expectation": "accept", - "parser_expectation": "accept" - }, - { - "name": "strategy-ready-valid-v10", - "artifact": "strategy-message-v10", - "instance": { - "strategy_protocol_version": "10", - "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-v10", - "artifact": "strategy-message-v10", - "instance": { - "strategy_protocol_version": "10", - "strategy_sequence": "2", - "message_type": "intents", - "payload": { - "intents": [] - } - }, - "mutations": [], - "schema_expectation": "accept", - "parser_expectation": "accept", - "parser_expected": "intents" - }, - { - "name": "strategy-error-valid-v10", - "artifact": "strategy-message-v10", - "instance": { - "strategy_protocol_version": "10", - "strategy_sequence": "7", - "message_type": "error", - "payload": { - "message": "fixture failure" - } - }, - "mutations": [], - "schema_expectation": "accept" - }, - { - "name": "strategy-v10-rejected-response-branch", - "artifact": "strategy-transcript-v10", - "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" - }, - { - "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" - }, - { - "name": "scenario-v14-valid", - "artifact": "scenario-v14", - "kind": "scenario", - "source": "v14/fixtures/demo.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "parser_expectation": "accept" - }, - { - "name": "scenario-v14-quote-trade-valid", - "artifact": "scenario-v14", - "kind": "scenario", - "source": "v14/fixtures/quote-trade.scenario.json", - "mutations": [], - "schema_expectation": "accept", - "parser_expectation": "accept" - }, - { - "name": "scenario-stream-v14-valid", - "artifact": "scenario-stream-v14", - "kind": "scenario_stream", - "source": "v14/fixtures/quote-trade.scenario.jsonl", - "mutations": [], - "schema_expectation": "accept", - "parser_expectation": "accept" - }, - { - "name": "strategy-ready-valid-v12", - "artifact": "strategy-message-v12", - "instance": { - "strategy_protocol_version": "12", - "strategy_sequence": "1", - "message_type": "ready", - "payload": { - "strategy_name": "conformance", - "strategy_version": null - } - }, - "mutations": [], - "schema_expectation": "accept", - "parser_expectation": "accept", - "parser_expected": "ready" - }, - { - "name": "strategy-intents-valid-v12", - "artifact": "strategy-message-v12", - "instance": { - "strategy_protocol_version": "12", - "strategy_sequence": "2", - "message_type": "intents", - "payload": { - "intents": [] - } - }, - "mutations": [], - "schema_expectation": "accept", - "parser_expectation": "accept", - "parser_expected": "intents" - }, - { - "name": "strategy-error-valid-v12", - "artifact": "strategy-message-v12", - "instance": { - "strategy_protocol_version": "12", - "strategy_sequence": "7", - "message_type": "error", - "payload": { - "message": "fixture failure" - } - }, - "mutations": [], - "schema_expectation": "accept" - }, - { - "name": "strategy-v12-rejected-response-branch", - "artifact": "strategy-transcript-v12", - "instance": { - "strategy_diagnostic_version": "1", - "transcript_sequence": "2", - "record_type": "rejected_strategy_response", - "expected_strategy_sequence": "1", - "diagnostic": { - "diagnostic_version": "1", - "code": "strategy.protocol", - "phase": "strategy", - "message": "strategy initialization: invalid strategy response JSON", - "context": { - "json_path": "$", - "sequence": "1" - }, - "cause": null - }, - "evidence": { - "encoding": "hex", - "prefix": "7b", - "observed_bytes": 1, - "truncated": false - } - }, - "mutations": [], - "schema_expectation": "accept" - }, - { - "name": "strategy-stopped-valid-v13", - "artifact": "strategy-message-v13", - "instance": { - "strategy_protocol_version": "13", - "strategy_sequence": "7", - "message_type": "stopped", - "payload": {} - }, - "mutations": [], - "schema_expectation": "accept", - "parser_expectation": "accept", - "parser_expected": "stopped" - }, - { - "name": "strategy-error-valid-v13", - "artifact": "strategy-message-v13", - "instance": { - "strategy_protocol_version": "13", - "strategy_sequence": "7", - "message_type": "error", - "payload": { - "message": "fixture failure" - } - }, - "mutations": [], - "schema_expectation": "accept" - }, - { - "name": "strategy-v13-rejected-response-branch", - "artifact": "strategy-transcript-v13", - "instance": { - "strategy_diagnostic_version": "1", - "transcript_sequence": "2", - "record_type": "rejected_strategy_response", - "expected_strategy_sequence": "1", - "diagnostic": { - "diagnostic_version": "1", - "code": "strategy.protocol", - "phase": "strategy", - "message": "strategy initialization: invalid strategy response JSON", - "context": { - "json_path": "$", - "sequence": "1" - }, - "cause": null - }, - "evidence": { - "encoding": "hex", - "prefix": "7b", - "observed_bytes": 1, - "truncated": false - } - }, - "mutations": [], - "schema_expectation": "accept" - }, - { - "name": "strategy-stopped-valid-v14", - "artifact": "strategy-message-v14", - "instance": { - "strategy_protocol_version": "14", - "strategy_sequence": "7", - "message_type": "stopped", - "payload": {} - }, - "mutations": [], - "schema_expectation": "accept", - "parser_expectation": "accept", - "parser_expected": "stopped" - }, - { - "name": "strategy-error-valid-v14", - "artifact": "strategy-message-v14", - "instance": { - "strategy_protocol_version": "14", - "strategy_sequence": "7", - "message_type": "error", - "payload": { - "message": "fixture failure" - } - }, - "mutations": [], - "schema_expectation": "accept" - }, - { - "name": "strategy-v14-rejected-response-branch", - "artifact": "strategy-transcript-v14", - "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/dune b/contracts/conformance/dune index d1eb67f..7a4e1fa 100644 --- a/contracts/conformance/dune +++ b/contracts/conformance/dune @@ -4,5 +4,4 @@ (files (README.md as contracts/conformance/README.md) (cases.json as contracts/conformance/cases.json) - (frozen.sha256 as contracts/conformance/frozen.sha256) (manifest.json as contracts/conformance/manifest.json))) diff --git a/contracts/conformance/frozen.sha256 b/contracts/conformance/frozen.sha256 deleted file mode 100644 index a17e5ab..0000000 --- a/contracts/conformance/frozen.sha256 +++ /dev/null @@ -1,22 +0,0 @@ -78df448f65b0d784d51951108cacb1db247d9d2dd03d93886f43e4acc0e83e1d contracts/strategy/v1/fixtures/external.scenario.json -135cc392abcf1f9dae9e2f190bb96c68ef9b39c44e23ac9250482d93912e326d contracts/strategy/v1/fixtures/external.scenario.jsonl -c93f085b131c1af6aadd84a8fda6bebe9520e5e11c03945b47577846bca7cced contracts/strategy/v1/fixtures/external.strategy.jsonl -9bba3babb59ec025bbcc159b24b99b2f260341456e12caafc1ea05d95a4c9742 contracts/strategy/v1/message.schema.json -64aedf3ea18319c65ad8c3d5a7775e5acff9712170e9ce2211839573f84ddbfb contracts/strategy/v1/transcript.schema.json -78df448f65b0d784d51951108cacb1db247d9d2dd03d93886f43e4acc0e83e1d contracts/strategy/v2/fixtures/external.scenario.json -135cc392abcf1f9dae9e2f190bb96c68ef9b39c44e23ac9250482d93912e326d contracts/strategy/v2/fixtures/external.scenario.jsonl -c35b69d323a35d1637955034b357d83215a67eb0f89513686e271d61af6a8cb4 contracts/strategy/v2/fixtures/external.strategy.jsonl -4666cc7ee98a1420a4003150f080a3f3a538e4366dfa1b7d80b0595518af97a7 contracts/strategy/v2/message.schema.json -c0c974c7aa57e16179462a5ef2a34924b8593d6823b928b2c2f51ce2817c7db5 contracts/strategy/v2/transcript.schema.json -2b4c0b2608fff14fe32cc1366a307e181993f81e14eaf16632ec72ca4eef8bda contracts/v1/fixtures/demo.journal.jsonl -a782fbbee8b89332ee6491d9d9be36bf7f2aaacbd440d30c20ed033fd7566d19 contracts/v1/fixtures/demo.scenario.json -d83b7c23d5e44793f9b71c542738cf4fed0612117e8399a61059677974f3382c contracts/v1/fixtures/demo.scenario.jsonl -9dd286602774f360b149f28828b56ccb1a8676dad8c5e2843d9df5499697fddf contracts/v1/journal.schema.json -9ee2f673c15c410e7cf9943a93526761a2eead592e5ac6948c9abc9d005d3c4e contracts/v1/scenario-stream.schema.json -cdd3102b873206c882f80496bc50f013b82ae62411025c5aa06fffafc692cb07 contracts/v1/scenario.schema.json -7c04bdff8488de02bf6e95238b91b348b0a36eecbb808030ac9e346552790f62 contracts/v2/fixtures/demo.journal.jsonl -21834e964dd6daab292e6285924384970b3341f7166ead1d38f8edb284541e44 contracts/v2/fixtures/demo.scenario.json -576573a119da2fecb9188cdf309bd21888b34d594c416c69e5d69c55255e1765 contracts/v2/fixtures/demo.scenario.jsonl -e222fb136b636b6281186512078bc781d234cefe3c2ede82b1c792daf66231cf contracts/v2/journal.schema.json -fb7e5c459977d6351f4b40938ce898441b0afc268b048b4e48bb094954186f46 contracts/v2/scenario-stream.schema.json -0340a0f5305810acd102894cf8557e312534bbad6eafcf5c27ae88b1dfaca147 contracts/v2/scenario.schema.json diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index fe01d3c..20721a2 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -7,10 +7,11 @@ "version_field": "contract_version", "version": "1", "sources": [ - { - "path": "v1/fixtures/demo.scenario.json", - "format": "json" - } + { "path": "v1/fixtures/demo.scenario.json", "format": "json" }, + { "path": "v1/fixtures/fill-clipped.scenario.json", "format": "json" }, + { "path": "v1/fixtures/quote-trade.scenario.json", "format": "json" }, + { "path": "v1/fixtures/order-book.scenario.json", "format": "json" }, + { "path": "strategy/v1/fixtures/external.scenario.json", "format": "json" } ] }, { @@ -19,10 +20,10 @@ "version_field": "contract_version", "version": "1", "sources": [ - { - "path": "v1/fixtures/demo.scenario.jsonl", - "format": "jsonl" - } + { "path": "v1/fixtures/demo.scenario.jsonl", "format": "jsonl" }, + { "path": "v1/fixtures/quote-trade.scenario.jsonl", "format": "jsonl" }, + { "path": "v1/fixtures/order-book.scenario.jsonl", "format": "jsonl" }, + { "path": "strategy/v1/fixtures/external.scenario.jsonl", "format": "jsonl" } ] }, { @@ -31,258 +32,10 @@ "version_field": "contract_version", "version": "1", "sources": [ - { - "path": "v1/fixtures/demo.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v2", - "schema": "v2/scenario.schema.json", - "version_field": "contract_version", - "version": "2", - "sources": [ - { - "path": "v2/fixtures/demo.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v2", - "schema": "v2/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "2", - "sources": [ - { - "path": "v2/fixtures/demo.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v2", - "schema": "v2/journal.schema.json", - "version_field": "contract_version", - "version": "2", - "sources": [ - { - "path": "v2/fixtures/demo.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v3", - "schema": "v3/scenario.schema.json", - "version_field": "contract_version", - "version": "3", - "sources": [ - { - "path": "v3/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "strategy/v1/fixtures/external.scenario.json", - "format": "json" - }, - { - "path": "strategy/v2/fixtures/external.scenario.json", - "format": "json" - }, - { - "path": "strategy/v3/fixtures/external.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v3", - "schema": "v3/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "3", - "sources": [ - { - "path": "v3/fixtures/demo.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v1/fixtures/external.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v2/fixtures/external.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v3/fixtures/external.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v3", - "schema": "v3/journal.schema.json", - "version_field": "contract_version", - "version": "3", - "sources": [ - { - "path": "v3/fixtures/demo.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v4", - "schema": "v4/scenario.schema.json", - "version_field": "contract_version", - "version": "4", - "sources": [ - { - "path": "v4/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v4/fixtures/fill-clipped.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v4", - "schema": "v4/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "4", - "sources": [ - { - "path": "v4/fixtures/demo.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v4", - "schema": "v4/journal.schema.json", - "version_field": "contract_version", - "version": "4", - "sources": [ - { - "path": "v4/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v4/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v5", - "schema": "v5/scenario.schema.json", - "version_field": "contract_version", - "version": "5", - "sources": [ - { - "path": "v5/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v5/fixtures/fill-clipped.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v5", - "schema": "v5/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "5", - "sources": [ - { - "path": "v5/fixtures/demo.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v5", - "schema": "v5/journal.schema.json", - "version_field": "contract_version", - "version": "5", - "sources": [ - { - "path": "v5/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v5/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v6", - "schema": "v6/scenario.schema.json", - "version_field": "contract_version", - "version": "6", - "sources": [ - { - "path": "v6/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v6/fixtures/fill-clipped.scenario.json", - "format": "json" - }, - { - "path": "strategy/v4/fixtures/external.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v6", - "schema": "v6/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "6", - "sources": [ - { - "path": "v6/fixtures/demo.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v4/fixtures/external.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v6", - "schema": "v6/journal.schema.json", - "version_field": "contract_version", - "version": "6", - "sources": [ - { - "path": "v6/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v6/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "diagnostic-v1", - "schema": "diagnostic/v1/diagnostic.schema.json", - "version_field": "diagnostic_version", - "version": "1", - "sources": [ - { - "path": "diagnostic/v1/fixtures/strategy-protocol.json", - "format": "json" - } + { "path": "v1/fixtures/demo.journal.jsonl", "format": "jsonl" }, + { "path": "v1/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" }, + { "path": "v1/fixtures/quote-trade.journal.jsonl", "format": "jsonl" }, + { "path": "v1/fixtures/order-book.journal.jsonl", "format": "jsonl" } ] }, { @@ -294,9 +47,7 @@ { "path": "strategy/v1/fixtures/external.strategy.jsonl", "format": "jsonl", - "extract": [ - "message" - ] + "extract": ["message"] } ] }, @@ -306,953 +57,25 @@ "version_field": "strategy_protocol_version", "version": "1", "sources": [ - { - "path": "strategy/v1/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "strategy-message-v2", - "schema": "strategy/v2/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "2", - "sources": [ - { - "path": "strategy/v2/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v2", - "schema": "strategy/v2/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "2", - "sources": [ - { - "path": "strategy/v2/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "strategy-message-v3", - "schema": "strategy/v3/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "3", - "sources": [ - { - "path": "strategy/v3/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v3", - "schema": "strategy/v3/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "3", - "sources": [ - { - "path": "strategy/v3/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "strategy-message-v4", - "schema": "strategy/v4/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "4", - "sources": [ - { - "path": "strategy/v4/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v4", - "schema": "strategy/v4/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "4", - "sources": [ - { - "path": "strategy/v4/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v7", - "schema": "v7/scenario.schema.json", - "version_field": "contract_version", - "version": "7", - "sources": [ - { - "path": "v7/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v7/fixtures/fill-clipped.scenario.json", - "format": "json" - }, - { - "path": "strategy/v5/fixtures/external.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v7", - "schema": "v7/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "7", - "sources": [ - { - "path": "v7/fixtures/demo.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v5/fixtures/external.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v7", - "schema": "v7/journal.schema.json", - "version_field": "contract_version", - "version": "7", - "sources": [ - { - "path": "v7/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v7/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "strategy-message-v5", - "schema": "strategy/v5/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "5", - "sources": [ - { - "path": "strategy/v5/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v5", - "schema": "strategy/v5/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "5", - "sources": [ - { - "path": "strategy/v5/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v8", - "schema": "v8/scenario.schema.json", - "version_field": "contract_version", - "version": "8", - "sources": [ - { - "path": "v8/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v8/fixtures/fill-clipped.scenario.json", - "format": "json" - }, - { - "path": "strategy/v6/fixtures/external.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v8", - "schema": "v8/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "8", - "sources": [ - { - "path": "v8/fixtures/demo.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v6/fixtures/external.scenario.jsonl", - "format": "jsonl" - } + { "path": "strategy/v1/fixtures/external.strategy.jsonl", "format": "jsonl" } ] }, { - "name": "journal-v8", - "schema": "v8/journal.schema.json", - "version_field": "contract_version", - "version": "8", + "name": "diagnostic-v1", + "schema": "diagnostic/v1/diagnostic.schema.json", + "version_field": "diagnostic_version", + "version": "1", "sources": [ - { - "path": "v8/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v8/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - } + { "path": "diagnostic/v1/fixtures/strategy-protocol.json", "format": "json" } ] }, { - "name": "strategy-message-v6", - "schema": "strategy/v6/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "6", + "name": "cli-result-v1", + "schema": "cli/v1/result.schema.json", + "version_field": "result_version", + "version": "1", "sources": [ - { - "path": "strategy/v6/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v6", - "schema": "strategy/v6/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "6", - "sources": [ - { - "path": "strategy/v6/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v9", - "schema": "v9/scenario.schema.json", - "version_field": "contract_version", - "version": "9", - "sources": [ - { - "path": "v9/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v9/fixtures/fill-clipped.scenario.json", - "format": "json" - }, - { - "path": "strategy/v7/fixtures/external.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v9", - "schema": "v9/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "9", - "sources": [ - { - "path": "v9/fixtures/demo.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v7/fixtures/external.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v9", - "schema": "v9/journal.schema.json", - "version_field": "contract_version", - "version": "9", - "sources": [ - { - "path": "v9/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v9/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "strategy-message-v7", - "schema": "strategy/v7/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "7", - "sources": [ - { - "path": "strategy/v7/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v7", - "schema": "strategy/v7/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "7", - "sources": [ - { - "path": "strategy/v7/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v10", - "schema": "v10/scenario.schema.json", - "version_field": "contract_version", - "version": "10", - "sources": [ - { - "path": "v10/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v10/fixtures/fill-clipped.scenario.json", - "format": "json" - }, - { - "path": "strategy/v8/fixtures/external.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v10", - "schema": "v10/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "10", - "sources": [ - { - "path": "v10/fixtures/demo.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v8/fixtures/external.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v10", - "schema": "v10/journal.schema.json", - "version_field": "contract_version", - "version": "10", - "sources": [ - { - "path": "v10/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v10/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "strategy-message-v8", - "schema": "strategy/v8/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "8", - "sources": [ - { - "path": "strategy/v8/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v8", - "schema": "strategy/v8/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "8", - "sources": [ - { - "path": "strategy/v8/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v11", - "schema": "v11/scenario.schema.json", - "version_field": "contract_version", - "version": "11", - "sources": [ - { - "path": "v11/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v11/fixtures/fill-clipped.scenario.json", - "format": "json" - }, - { - "path": "strategy/v9/fixtures/external.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v11", - "schema": "v11/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "11", - "sources": [ - { - "path": "v11/fixtures/demo.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v9/fixtures/external.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v11", - "schema": "v11/journal.schema.json", - "version_field": "contract_version", - "version": "11", - "sources": [ - { - "path": "v11/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v11/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "strategy-message-v9", - "schema": "strategy/v9/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "9", - "sources": [ - { - "path": "strategy/v9/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v9", - "schema": "strategy/v9/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "9", - "sources": [ - { - "path": "strategy/v9/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v12", - "schema": "v12/scenario.schema.json", - "version_field": "contract_version", - "version": "12", - "sources": [ - { - "path": "v12/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v12/fixtures/fill-clipped.scenario.json", - "format": "json" - }, - { - "path": "strategy/v10/fixtures/external.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v12", - "schema": "v12/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "12", - "sources": [ - { - "path": "v12/fixtures/demo.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v10/fixtures/external.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v12", - "schema": "v12/journal.schema.json", - "version_field": "contract_version", - "version": "12", - "sources": [ - { - "path": "v12/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v12/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "strategy-message-v10", - "schema": "strategy/v10/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "10", - "sources": [ - { - "path": "strategy/v10/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v10", - "schema": "strategy/v10/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "10", - "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" - } - ] - }, - { - "name": "scenario-v14", - "schema": "v14/scenario.schema.json", - "version_field": "contract_version", - "version": "14", - "sources": [ - { - "path": "v14/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v14/fixtures/fill-clipped.scenario.json", - "format": "json" - }, - { - "path": "v14/fixtures/quote-trade.scenario.json", - "format": "json" - }, - { - "path": "strategy/v12/fixtures/external.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v14", - "schema": "v14/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "14", - "sources": [ - { - "path": "v14/fixtures/demo.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "v14/fixtures/quote-trade.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v12/fixtures/external.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v14", - "schema": "v14/journal.schema.json", - "version_field": "contract_version", - "version": "14", - "sources": [ - { - "path": "v14/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v14/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v14/fixtures/quote-trade.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "strategy-message-v12", - "schema": "strategy/v12/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "12", - "sources": [ - { - "path": "strategy/v12/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v12", - "schema": "strategy/v12/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "12", - "sources": [ - { - "path": "strategy/v12/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v15", - "schema": "v15/scenario.schema.json", - "version_field": "contract_version", - "version": "15", - "sources": [ - { - "path": "v15/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v15/fixtures/fill-clipped.scenario.json", - "format": "json" - }, - { - "path": "v15/fixtures/quote-trade.scenario.json", - "format": "json" - }, - { - "path": "v15/fixtures/order-book.scenario.json", - "format": "json" - }, - { - "path": "strategy/v13/fixtures/external.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v15", - "schema": "v15/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "15", - "sources": [ - { - "path": "v15/fixtures/demo.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "v15/fixtures/quote-trade.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "v15/fixtures/order-book.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v13/fixtures/external.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v15", - "schema": "v15/journal.schema.json", - "version_field": "contract_version", - "version": "15", - "sources": [ - { - "path": "v15/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v15/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v15/fixtures/quote-trade.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v15/fixtures/order-book.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "strategy-message-v13", - "schema": "strategy/v13/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "13", - "sources": [ - { - "path": "strategy/v13/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v13", - "schema": "strategy/v13/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "13", - "sources": [ - { - "path": "strategy/v13/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "scenario-v16", - "schema": "v16/scenario.schema.json", - "version_field": "contract_version", - "version": "16", - "sources": [ - { - "path": "v16/fixtures/demo.scenario.json", - "format": "json" - }, - { - "path": "v16/fixtures/fill-clipped.scenario.json", - "format": "json" - }, - { - "path": "v16/fixtures/quote-trade.scenario.json", - "format": "json" - }, - { - "path": "v16/fixtures/order-book.scenario.json", - "format": "json" - }, - { - "path": "strategy/v14/fixtures/external.scenario.json", - "format": "json" - } - ] - }, - { - "name": "scenario-stream-v16", - "schema": "v16/scenario-stream.schema.json", - "version_field": "contract_version", - "version": "16", - "sources": [ - { - "path": "v16/fixtures/demo.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "v16/fixtures/quote-trade.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "v16/fixtures/order-book.scenario.jsonl", - "format": "jsonl" - }, - { - "path": "strategy/v14/fixtures/external.scenario.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "journal-v16", - "schema": "v16/journal.schema.json", - "version_field": "contract_version", - "version": "16", - "sources": [ - { - "path": "v16/fixtures/demo.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v16/fixtures/fill-clipped.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v16/fixtures/quote-trade.journal.jsonl", - "format": "jsonl" - }, - { - "path": "v16/fixtures/order-book.journal.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "strategy-message-v14", - "schema": "strategy/v14/message.schema.json", - "version_field": "strategy_protocol_version", - "version": "14", - "sources": [ - { - "path": "strategy/v14/fixtures/external.strategy.jsonl", - "format": "jsonl", - "extract": [ - "message" - ] - } - ] - }, - { - "name": "strategy-transcript-v14", - "schema": "strategy/v14/transcript.schema.json", - "version_field": "strategy_protocol_version", - "version": "14", - "sources": [ - { - "path": "strategy/v14/fixtures/external.strategy.jsonl", - "format": "jsonl" - } - ] - }, - { - "name": "cli-result-v1", - "schema": "cli/v1/result.schema.json", - "version_field": "result_version", - "version": "1", - "sources": [ - { - "path": "cli/v1/fixtures/demo.result.json", - "format": "json" - } + { "path": "cli/v1/fixtures/demo.result.json", "format": "json" } ] } ] diff --git a/contracts/strategy/v1/README.md b/contracts/strategy/v1/README.md index 4fa306f..8f72849 100644 --- a/contracts/strategy/v1/README.md +++ b/contracts/strategy/v1/README.md @@ -1,26 +1,17 @@ # External strategy protocol v1 -Version 1 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`. +Trading Engine supervises external strategies over synchronous JSON Lines on standard input and +output. `message.schema.json` defines protocol messages and `transcript.schema.json` defines the +durable exchange log. -Every message repeats `strategy_protocol_version: "1"` 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. +Every message carries `"strategy_protocol_version": "1"` and a positive decimal-string sequence. +The engine sends `initialize`, ordered `event` messages, and `shutdown`; the strategy replies with +`ready`, matching `intents`, and `stopped`. The runtime enforces direction, sequence pairing, +canonical values, message limits, and lifecycle order. -The event context contains the replay clock, every cash ledger and configured position, all -working orders, and the latest available bar for each instrument. Event payloads cover completed -market slices, fills, order updates, and rejected intents. Response intents use the scenario v3 -intent shapes. +Initialization binds the strategy to replay contract v1, the scenario hash, initial portfolio, +instruments, risk, execution, financing, settlement, venue calendars, and metadata. Event contexts +include the current portfolio, working orders, bars, and group exposures. -External replay requires an empty batch schedule and empty streamed intent batches. The engine -records both directions in a deterministic transcript. 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 retained transcript records. -- `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. +The fixtures contain a complete accepted session. Run `make check` to validate both schemas and +runtime behavior. diff --git a/contracts/strategy/v1/fixtures/external.scenario.json b/contracts/strategy/v1/fixtures/external.scenario.json index 5fe2d5a..04961cf 100644 --- a/contracts/strategy/v1/fixtures/external.scenario.json +++ b/contracts/strategy/v1/fixtures/external.scenario.json @@ -1,13 +1,26 @@ { - "contract_version": "3", + "contract_version": "1", "metadata": { "producer": "strategy-protocol-fixture" }, "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" + } + ] + }, "instruments": [ { "instrument_id": "demo-equity-acme", @@ -17,21 +30,148 @@ "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_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", "max_gross_exposure": "1000000", "max_leverage": "2", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "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", - "participation_bps": 10000, - "fixed_fee": "0", - "fee_bps": 0 + "configuration": { + "version": "1", + "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": [], @@ -43,10 +183,43 @@ "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" } + { + "instrument_id": "demo-equity-acme", + "open": "100", + "high": "105", + "low": "99", + "close": "104", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } ], - "fx_rates": [{ "currency": "USD", "rate": "1" }], - "corporate_actions": [] + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-02T14:30:00Z", + "credit_rate_bps": 0, + "debit_rate_bps": 0 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] }, { "slice_sequence": "2", @@ -55,10 +228,80 @@ "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" } + { + "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 + } ], - "fx_rates": [{ "currency": "USD", "rate": "1" }], - "corporate_actions": [] + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-05T14:30:00Z", + "credit_rate_bps": 0, + "debit_rate_bps": 0 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] } - ] + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "demo-equity-acme", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } } diff --git a/contracts/strategy/v1/fixtures/external.scenario.jsonl b/contracts/strategy/v1/fixtures/external.scenario.jsonl index 1e168bf..d0de398 100644 --- a/contracts/strategy/v1/fixtures/external.scenario.jsonl +++ b/contracts/strategy/v1/fixtures/external.scenario.jsonl @@ -1,4 +1,4 @@ -{"contract_version":"3","payload":{"base_currency":"USD","execution":{"fee_bps":0,"fixed_fee":"0","model":"completed_bar_v1","participation_bps":10000},"initial_cash":[{"amount":"10000","currency":"USD"}],"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"1","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"max_internal_events":1000,"metadata":{"producer":"strategy-protocol-fixture"},"risk":{"initial_margin_bps":5000,"maintenance_margin_bps":2500,"max_gross_exposure":"1000000","max_leverage":"2","max_long_position":"1000","max_order_quantity":"1000","max_short_position":"1000","short_borrow_bps":0},"run_id":"external-demo"},"record_type":"scenario_header","scenario_sequence":"1"} -{"contract_version":"3","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"3","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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"3","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} +{"contract_version":"1","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","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":"1","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":"1","payload":{"intents":[],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"1","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"1","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v1/fixtures/external.strategy.jsonl b/contracts/strategy/v1/fixtures/external.strategy.jsonl index 46d40e5..a74e5d5 100644 --- a/contracts/strategy/v1/fixtures/external.strategy.jsonl +++ b/contracts/strategy/v1/fixtures/external.strategy.jsonl @@ -1,14 +1,14 @@ -{"strategy_protocol_version":"1","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"0.1.0-dev","scenario_contract_version":"3","scenario_sha256":"78df448f65b0d784d51951108cacb1db247d9d2dd03d93886f43e4acc0e83e1d","run_id":"external-demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"risk":{"max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_gross_exposure":"1000000","max_leverage":"2","initial_margin_bps":5000,"maintenance_margin_bps":2500,"short_borrow_bps":0},"execution":{"model":"completed_bar_v1","participation_bps":10000,"fixed_fee":"0","fee_bps":0},"metadata":{"producer":"strategy-protocol-fixture"}}}} +{"strategy_protocol_version":"1","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"1","scenario_sha256":"7c1991b8f4662c51faf8b3436999fdce8f0295ee6c0ef5d20a0658c960d04c4d","run_id":"external-demo","base_currency":"USD","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","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":"1","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":"1","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"1","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"1","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"2","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","cash_balances":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0"}],"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":[]}}}}} -{"strategy_protocol_version":"1","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"1","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":"1","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"3","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","cash_balances":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0"}],"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000005","updated_event_id":"external-demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","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","limit_price":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000005","updated_event_id":"external-demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} +{"strategy_protocol_version":"1","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"2","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} +{"strategy_protocol_version":"1","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"1","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":{"type":"numeric","value":"2"},"unit":"score","dimensions":{"source":"fixture"},"aggregation":"last"}]}}} +{"strategy_protocol_version":"1","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","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":"1","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"1","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"1","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"4","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","cash_balances":[{"currency":"USD","amount":"9794"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2"}],"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","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","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}}}}} +{"strategy_protocol_version":"1","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","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","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"}],"executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}}}}} {"strategy_protocol_version":"1","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"1","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"1","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"5","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","cash_balances":[{"currency":"USD","amount":"9794"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2"}],"working_orders":[],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000005","updated_event_id":"external-demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} +{"strategy_protocol_version":"1","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","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":"1","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"1","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"1","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"6","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","cash_balances":[{"currency":"USD","amount":"9794"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2"}],"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":[]}}}}} +{"strategy_protocol_version":"1","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"6","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} {"strategy_protocol_version":"1","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"1","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} {"strategy_protocol_version":"1","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"7","message_type":"shutdown","payload":{}}} {"strategy_protocol_version":"1","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"1","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v1/message.schema.json b/contracts/strategy/v1/message.schema.json index 576c7a5..29ca3ff 100644 --- a/contracts/strategy/v1/message.schema.json +++ b/contracts/strategy/v1/message.schema.json @@ -42,25 +42,33 @@ "initializePayload": { "type": "object", "additionalProperties": false, - "required": ["engine_version", "scenario_contract_version", "scenario_sha256", "run_id", "base_currency", "initial_cash", "instruments", "risk", "execution", "metadata"], + "required": ["engine_version", "scenario_contract_version", "scenario_sha256", "run_id", "base_currency", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "metadata"], "properties": { "engine_version": { "type": "string", "minLength": 1 }, - "scenario_contract_version": { "type": "string", "minLength": 1 }, + "scenario_contract_version": { "const": "1" }, "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/cashBalance" } + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/initialPortfolio" } + ] }, "instruments": { "type": "array", "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/instrument" } + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/execution" }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/settlement" }, "metadata": { "type": "object" } } }, @@ -80,7 +88,7 @@ "additionalProperties": false, "required": ["strategy_name", "strategy_version"], "properties": { - "strategy_name": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, + "strategy_name": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/identifier" }, "strategy_version": { "oneOf": [ { "type": "null" }, @@ -112,36 +120,68 @@ "context": { "type": "object", "additionalProperties": false, - "required": ["now", "cash_balances", "positions", "working_orders", "latest_bars"], + "required": ["now", "portfolio", "working_orders", "latest_bars"], "properties": { - "now": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/timestamp" }, + "now": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/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/v1/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/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/v3/scenario.schema.json#/$defs/cashBalance" } + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/cashAttribution" } }, "positions": { "type": "array", "minItems": 1, - "items": { "$ref": "#/$defs/position" } - }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/journal.schema.json#/$defs/order" } + "items": { "$ref": "#/$defs/markedPosition" } }, - "latest_bars": { + "group_exposures": { "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/bar" } + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/groupExposure" } } } }, - "position": { + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/signedDecimal" } + ] + }, + "markedPosition": { "type": "object", "additionalProperties": false, - "required": ["instrument_id", "quantity"], + "required": ["instrument_id", "quantity", "settled_quantity", "unsettled_quantity", "mark", "base_market_value", "weight"], "properties": { - "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/signedDecimal" } + "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/signedDecimal" }, + "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/signedDecimal" }, + "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/signedDecimal" }, + "weight": { "$ref": "#/$defs/optionalWeight" } } }, "strategyEvent": { @@ -152,7 +192,7 @@ "required": ["type", "market_slice"], "properties": { "type": { "const": "market_slice_closed" }, - "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/marketSlice" } + "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/marketSlice" } } }, { @@ -161,7 +201,7 @@ "required": ["type", "fill"], "properties": { "type": { "const": "fill_received" }, - "fill": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/journal.schema.json#/$defs/fill" } + "fill": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/fill" } } }, { @@ -170,7 +210,7 @@ "required": ["type", "order"], "properties": { "type": { "const": "order_updated" }, - "order": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/journal.schema.json#/$defs/order" } + "order": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/journal.schema.json#/$defs/order" } } }, { @@ -202,7 +242,8 @@ "properties": { "intents": { "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/intent" } + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/intent" } } } }, diff --git a/contracts/strategy/v1/transcript.schema.json b/contracts/strategy/v1/transcript.schema.json index c62a889..791e9cf 100644 --- a/contracts/strategy/v1/transcript.schema.json +++ b/contracts/strategy/v1/transcript.schema.json @@ -2,21 +2,81 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v1/transcript.schema.json", "title": "Trading Engine external strategy protocol v1 transcript record", - "description": "One ordered request or response retained from a supervised stdio strategy session.", - "type": "object", - "additionalProperties": false, - "required": ["strategy_protocol_version", "transcript_sequence", "direction", "message"], - "properties": { - "strategy_protocol_version": { "const": "1" }, - "transcript_sequence": { + "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]*$" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] + "exchange": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_protocol_version", "transcript_sequence", "direction", "message"], + "properties": { + "strategy_protocol_version": { "const": "1" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v1/message.schema.json" + } + } }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v1/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/strategy/v10/README.md b/contracts/strategy/v10/README.md deleted file mode 100644 index 17c89a1..0000000 --- a/contracts/strategy/v10/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# External strategy protocol v10 - -Version 10 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 v9 remains available for scenario contract v11; earlier versions retain their frozen -shapes. - -Every message repeats `strategy_protocol_version: "10"` 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 v12 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 v12 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/v10/dune b/contracts/strategy/v10/dune deleted file mode 100644 index 7fa7432..0000000 --- a/contracts/strategy/v10/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v10/message.schema.json) - (transcript.schema.json as contracts/strategy/v10/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v10/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v10/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v10/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v10/fixtures/external.scenario.json b/contracts/strategy/v10/fixtures/external.scenario.json deleted file mode 100644 index c37e23b..0000000 --- a/contracts/strategy/v10/fixtures/external.scenario.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "contract_version": "12", - "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/v10/fixtures/external.scenario.jsonl b/contracts/strategy/v10/fixtures/external.scenario.jsonl deleted file mode 100644 index e428012..0000000 --- a/contracts/strategy/v10/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"12","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":"12","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":"12","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":"12","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v10/fixtures/external.strategy.jsonl b/contracts/strategy/v10/fixtures/external.strategy.jsonl deleted file mode 100644 index 0b07034..0000000 --- a/contracts/strategy/v10/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"10","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"12","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":"10","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"10","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"10","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"10","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"10","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","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":"10","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"10","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"10","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"10","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"10","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v10/message.schema.json b/contracts/strategy/v10/message.schema.json deleted file mode 100644 index cf722e9..0000000 --- a/contracts/strategy/v10/message.schema.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v10/message.schema.json", - "title": "Trading Engine external strategy protocol v10 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": "10" }, - "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": "12" }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/cashBalance" } - }, - "initial_portfolio": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/initialPortfolio" } - ] - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/instrument" } - }, - "venue_calendars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/venueCalendar" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/financing" }, - "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/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/v12/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/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/v12/journal.schema.json#/$defs/groupExposure" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/signedDecimal" }, - "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/signedDecimal" }, - "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/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/v12/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/v12/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/v12/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/v10/transcript.schema.json b/contracts/strategy/v10/transcript.schema.json deleted file mode 100644 index 02d3ca0..0000000 --- a/contracts/strategy/v10/transcript.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v10/transcript.schema.json", - "title": "Trading Engine external strategy protocol v10 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": "10" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v10/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/strategy/v11/README.md b/contracts/strategy/v11/README.md deleted file mode 100644 index 11d712c..0000000 --- a/contracts/strategy/v11/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# 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 deleted file mode 100644 index 977dbb1..0000000 --- a/contracts/strategy/v11/dune +++ /dev/null @@ -1,15 +0,0 @@ -(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 deleted file mode 100644 index d6f9bab..0000000 --- a/contracts/strategy/v11/fixtures/external.scenario.json +++ /dev/null @@ -1,304 +0,0 @@ -{ - "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 deleted file mode 100644 index c1ced72..0000000 --- a/contracts/strategy/v11/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"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 deleted file mode 100644 index 712ebc6..0000000 --- a/contracts/strategy/v11/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"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 deleted file mode 100644 index 4d26106..0000000 --- a/contracts/strategy/v11/message.schema.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "$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 deleted file mode 100644 index bbbca4b..0000000 --- a/contracts/strategy/v11/transcript.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$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/strategy/v12/README.md b/contracts/strategy/v12/README.md deleted file mode 100644 index 5aa17ef..0000000 --- a/contracts/strategy/v12/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# External strategy protocol v12 - -Version 12 is a synchronous JSON Lines protocol over child-process standard input and output. -Trading Engine sends `initialize`, ordered `event` requests, and `shutdown`. The strategy answers -with `ready`, `intents`, and `stopped`. It may answer any request with `error`. -Protocol v11 remains available for scenario contract v13; earlier versions retain their frozen -shapes. - -Every message repeats `strategy_protocol_version: "12"` and a positive canonical -`strategy_sequence`. A response must repeat the sequence of its request. Only one request is -outstanding. Trading Engine rejects unknown or duplicate fields, invalid canonical values, -oversized lines, a wrong version or sequence, unexpected response types, EOF, timeout, and a -nonzero process exit. - -The event context contains the replay clock, a marked base-currency portfolio, deterministic group -exposure snapshots, all working orders, and the latest available bar for each instrument. Every -callback emitted for a market slice uses -that slice's `received_at` as `now` and uses its complete bars and FX vector. The portfolio reports -cash, equity, net, long, short, and gross market value plus every attributed cash ledger and -configured position. Position quantities and weights reflect applied fills. Weights are truncated -toward zero to six decimal places. `weights_available` is false and all weights are null when -equity is zero or negative. - -The `initialize` request identifies scenario contract v14 and includes the exact `initial_portfolio` -snapshot alongside the legacy cash projection. It also carries the complete versioned venue -calendars, nested execution configuration, financing policy, and settlement policy, so a strategy -can construct DAY orders and reject incompatible execution, financing, or settlement state before -replay. - -Matching pauses after each strategy callback. The engine applies the response against the exact -account and OMS state exposed by that callback before delivering another callback or considering -the next eligible order. Later same-slice contexts include the effects of earlier responses. The -eligible-order sequence is fixed at the start of matching, so newly submitted orders wait for a -later slice. Cancelling an order before its turn leaves its unused slice capacity available to the -next eligible order. - -Event payloads cover completed market slices with effective-time borrow and cash-rate observations -plus explicit settlement failures, fills, order updates, and rejected intents. Portfolio contexts -include cash-interest attribution and settled and unsettled cash and position quantities. Response -intents use the scenario v14 intent shapes. Market-slice events include lifecycle transitions and -the expanded corporate-action catalog, plus causally ordered quote/trade market events. - -External replay requires an empty batch schedule and empty streamed intent batches. The engine -records accepted messages in both directions in a deterministic transcript. A response rejected -for invalid JSON, fields, version, sequence, EOF, or size is never stored as an accepted exchange. -Instead, the partial transcript ends with a `rejected_strategy_response` diagnostic record. Version -1 rejection diagnostics use the shared -[`diagnostic/v1`](../../diagnostic/v1/README.md) contract. The transcript schema narrows that -contract to the `strategy.protocol` and `resource.limit` codes in the `strategy` phase. The record -includes the structured rejection diagnostic and at most the first 256 raw response bytes encoded -as lowercase hexadecimal. `observed_bytes` counts bytes available when the engine rejected the -response, and `truncated` reports whether the prefix omits observed bytes. The transcript and audit -journal retain partial files after failure and finalize only after their respective success checks. - -- `message.schema.json` validates individual requests and responses. -- `transcript.schema.json` validates accepted exchanges and rejected-response diagnostics. -- `fixtures/external.scenario.json` is the batch replay fixture. -- `fixtures/external.scenario.jsonl` is its bounded-memory stream form. -- `fixtures/external.strategy.jsonl` is the canonical protocol transcript. diff --git a/contracts/strategy/v12/dune b/contracts/strategy/v12/dune deleted file mode 100644 index 3351a8f..0000000 --- a/contracts/strategy/v12/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v12/message.schema.json) - (transcript.schema.json as contracts/strategy/v12/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v12/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v12/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v12/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v12/fixtures/external.scenario.json b/contracts/strategy/v12/fixtures/external.scenario.json deleted file mode 100644 index 724125a..0000000 --- a/contracts/strategy/v12/fixtures/external.scenario.json +++ /dev/null @@ -1,306 +0,0 @@ -{ - "contract_version": "14", - "metadata": { - "producer": "strategy-protocol-fixture" - }, - "run_id": "external-demo", - "base_currency": "USD", - "initial_portfolio": { - "cash": [ - { - "currency": "USD", - "amount": "10000" - } - ], - "positions": [], - "marks": [], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ] - }, - "instruments": [ - { - "instrument_id": "demo-equity-acme", - "symbol": "ACME", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "venue_calendars": [ - { - "calendar_id": "demo-xnas-2026", - "calendar_version": "1", - "venue_id": "XNAS", - "instrument_ids": [ - "demo-equity-acme" - ], - "sessions": [ - { - "session_date": "2026-01-01", - "policy": "holiday", - "phases": [] - }, - { - "session_date": "2026-01-02", - "policy": "regular", - "phases": [ - { - "phase": "premarket", - "opens_at": "2026-01-02T09:00:00Z", - "closes_at": "2026-01-02T14:25:00Z" - }, - { - "phase": "opening_auction", - "opens_at": "2026-01-02T14:25:00Z", - "closes_at": "2026-01-02T14:30:00Z" - }, - { - "phase": "regular", - "opens_at": "2026-01-02T14:30:00Z", - "closes_at": "2026-01-02T20:55:00Z" - }, - { - "phase": "closing_auction", - "opens_at": "2026-01-02T20:55:00Z", - "closes_at": "2026-01-02T21:00:00Z" - }, - { - "phase": "postmarket", - "opens_at": "2026-01-02T21:00:00Z", - "closes_at": "2026-01-03T01:00:00Z" - } - ] - }, - { - "session_date": "2026-01-05", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-05T14:30:00Z", - "closes_at": "2026-01-05T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-06", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-06T14:30:00Z", - "closes_at": "2026-01-06T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-07", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-07T14:30:00Z", - "closes_at": "2026-01-07T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-08", - "policy": "early_close", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-08T14:30:00Z", - "closes_at": "2026-01-08T18:00:00Z" - } - ] - } - ] - } - ], - "risk": { - "max_gross_exposure": "1000000", - "max_leverage": "2", - "short_borrow_bps": 0, - "instrument_policies": [ - { - "instrument_id": "demo-equity-acme", - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_notional_exposure": "1000000", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "shorting_allowed": true - } - ], - "groups": [] - }, - "execution": { - "model": "completed_bar_v1", - "configuration": { - "version": "2", - "participation_bps": 5000, - "fee_schedules": [ - { - "schedule_id": "external-acme-fees-v1", - "instrument_id": "demo-equity-acme", - "settlement_currency": "USD", - "minimum": null, - "maximum": null, - "components": [ - { - "name": "broker", - "currency": "USD", - "kind": "fixed", - "value": "0.25", - "rounding": "up", - "applies_to": "any" - }, - { - "name": "exchange", - "currency": "USD", - "kind": "notional_bps", - "value": 10, - "rounding": "up", - "applies_to": "any" - } - ] - } - ] - } - }, - "max_internal_events": 1000, - "schedule": [], - "slices": [ - { - "slice_sequence": "1", - "start_at": "2026-01-02T14:30:00Z", - "end_at": "2026-01-02T21:00:00Z", - "available_at": "2026-01-02T21:00:01Z", - "received_at": "2026-01-02T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "100", - "high": "105", - "low": "99", - "close": "104", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-02T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-02T14:30:00Z", - "credit_rate_bps": 0, - "debit_rate_bps": 0 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [] - }, - { - "slice_sequence": "2", - "start_at": "2026-01-05T14:30:00Z", - "end_at": "2026-01-05T21:00:00Z", - "available_at": "2026-01-05T21:00:01Z", - "received_at": "2026-01-05T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "103", - "high": "108", - "low": "102", - "close": "107", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-05T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-05T14:30:00Z", - "credit_rate_bps": 0, - "debit_rate_bps": 0 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - }, - "settlement": { - "cash_buying_power": "total_cash", - "position_availability": "total_positions", - "calendars": [ - { - "calendar_id": "default-settlement", - "version": "1", - "business_dates": [ - "2026-01-02", - "2026-01-05", - "2026-01-06", - "2026-01-07", - "2026-01-08", - "2026-01-09", - "2026-02-02", - "2026-02-03", - "2026-02-04", - "2026-02-05" - ] - } - ], - "rules": [ - { - "instrument_id": "demo-equity-acme", - "calendar_id": "default-settlement", - "lag_business_days": 1 - } - ] - } -} diff --git a/contracts/strategy/v12/fixtures/external.scenario.jsonl b/contracts/strategy/v12/fixtures/external.scenario.jsonl deleted file mode 100644 index 793db28..0000000 --- a/contracts/strategy/v12/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"14","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"strategy-protocol-fixture"},"run_id":"external-demo","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00Z","closes_at":"2026-01-02T14:25:00Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00Z","closes_at":"2026-01-02T14:30:00Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00Z","closes_at":"2026-01-02T20:55:00Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00Z","closes_at":"2026-01-02T21:00:00Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00Z","closes_at":"2026-01-03T01:00:00Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00Z","closes_at":"2026-01-05T21:00:00Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00Z","closes_at":"2026-01-06T21:00:00Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00Z","closes_at":"2026-01-07T21:00:00Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00Z","closes_at":"2026-01-08T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":0,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_v1","configuration":{"version":"2","participation_bps":5000,"fee_schedules":[{"schedule_id":"external-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]}}} -{"contract_version":"14","payload":{"intents":[],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"14","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"14","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v12/fixtures/external.strategy.jsonl b/contracts/strategy/v12/fixtures/external.strategy.jsonl deleted file mode 100644 index acbe035..0000000 --- a/contracts/strategy/v12/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"12","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"14","scenario_sha256":"6809a3638fe668a2a11e56c42e9bac7e506c0064eb2cb0a3e71ba09d92839ee7","run_id":"external-demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00.000000Z","closes_at":"2026-01-02T14:25:00.000000Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00.000000Z","closes_at":"2026-01-02T14:30:00.000000Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00.000000Z","closes_at":"2026-01-02T20:55:00.000000Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00.000000Z","closes_at":"2026-01-02T21:00:00.000000Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00.000000Z","closes_at":"2026-01-03T01:00:00.000000Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00.000000Z","closes_at":"2026-01-05T21:00:00.000000Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00.000000Z","closes_at":"2026-01-06T21:00:00.000000Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00.000000Z","closes_at":"2026-01-07T21:00:00.000000Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00.000000Z","closes_at":"2026-01-08T18:00:00.000000Z"}]}]}],"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":0,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_v1","configuration":{"version":"2","participation_bps":5000,"fee_schedules":[{"schedule_id":"external-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"any"}]}]}},"metadata":{"producer":"strategy-protocol-fixture"}}}} -{"strategy_protocol_version":"12","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"12","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"2","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}}}}} -{"strategy_protocol_version":"12","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"2","message_type":"intents","payload":{"intents":[{"type":"target_quantities","targets":[{"instrument_id":"demo-equity-acme","quantity":"2"}]},{"type":"emit_metric","name":"fixture_signal","value":"2"}]}}} -{"strategy_protocol_version":"12","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"3","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"order_updated","order":{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} -{"strategy_protocol_version":"12","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"12","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"4","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"fill_received","fill":{"fill_id":"external-demo-fill-000000000001","order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"2","price":"103","notional":"206","fee":"0.456","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.206","quote_amount":"0.206"}]}}}}} -{"strategy_protocol_version":"12","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"12","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"5","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"order_updated","order":{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} -{"strategy_protocol_version":"12","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"12","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"6","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}}}}} -{"strategy_protocol_version":"12","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"12","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"12","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v12/message.schema.json b/contracts/strategy/v12/message.schema.json deleted file mode 100644 index 58c9e0c..0000000 --- a/contracts/strategy/v12/message.schema.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v12/message.schema.json", - "title": "Trading Engine external strategy protocol v12 message", - "description": "One strict request or response in the synchronous JSON Lines strategy protocol. The engine additionally enforces direction, sequence pairing, canonical values, response limits, and lifecycle order.", - "oneOf": [ - { "$ref": "#/$defs/initialize" }, - { "$ref": "#/$defs/ready" }, - { "$ref": "#/$defs/event" }, - { "$ref": "#/$defs/intents" }, - { "$ref": "#/$defs/shutdown" }, - { "$ref": "#/$defs/stopped" }, - { "$ref": "#/$defs/error" } - ], - "$defs": { - "sequence": { - "type": "string", - "pattern": "^[1-9][0-9]*$" - }, - "base": { - "type": "object", - "additionalProperties": false, - "required": ["strategy_protocol_version", "strategy_sequence", "message_type", "payload"], - "properties": { - "strategy_protocol_version": { "const": "12" }, - "strategy_sequence": { "$ref": "#/$defs/sequence" }, - "message_type": { "type": "string" }, - "payload": { "type": "object" } - } - }, - "initialize": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "initialize" }, - "payload": { "$ref": "#/$defs/initializePayload" } - } - } - ] - }, - "initializePayload": { - "type": "object", - "additionalProperties": false, - "required": ["engine_version", "scenario_contract_version", "scenario_sha256", "run_id", "base_currency", "initial_cash", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "metadata"], - "properties": { - "engine_version": { "type": "string", "minLength": 1 }, - "scenario_contract_version": { "const": "14" }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/cashBalance" } - }, - "initial_portfolio": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/initialPortfolio" } - ] - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/instrument" } - }, - "venue_calendars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/venueCalendar" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/financing" }, - "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/settlement" }, - "metadata": { "type": "object" } - } - }, - "ready": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "ready" }, - "payload": { "$ref": "#/$defs/readyPayload" } - } - } - ] - }, - "readyPayload": { - "type": "object", - "additionalProperties": false, - "required": ["strategy_name", "strategy_version"], - "properties": { - "strategy_name": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, - "strategy_version": { - "oneOf": [ - { "type": "null" }, - { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } - ] - } - } - }, - "event": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "event" }, - "payload": { "$ref": "#/$defs/eventPayload" } - } - } - ] - }, - "eventPayload": { - "type": "object", - "additionalProperties": false, - "required": ["context", "event"], - "properties": { - "context": { "$ref": "#/$defs/context" }, - "event": { "$ref": "#/$defs/strategyEvent" } - } - }, - "context": { - "type": "object", - "additionalProperties": false, - "required": ["now", "portfolio", "working_orders", "latest_bars"], - "properties": { - "now": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/bar" } - } - } - }, - "portfolio": { - "type": "object", - "additionalProperties": false, - "required": ["base_currency", "cash", "net_market_value", "long_market_value", "short_market_value", "gross_exposure", "equity", "weights_available", "cash_weight", "cash_balances", "positions", "group_exposures"], - "properties": { - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, - "weights_available": { "type": "boolean" }, - "cash_weight": { "$ref": "#/$defs/optionalWeight" }, - "cash_balances": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json#/$defs/cashAttribution" } - }, - "positions": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/markedPosition" } - }, - "group_exposures": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json#/$defs/groupExposure" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" } - ] - }, - "markedPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "settled_quantity", "unsettled_quantity", "mark", "base_market_value", "weight"], - "properties": { - "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, - "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, - "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, - "weight": { "$ref": "#/$defs/optionalWeight" } - } - }, - "strategyEvent": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "market_slice"], - "properties": { - "type": { "const": "market_slice_closed" }, - "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/marketSlice" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "fill"], - "properties": { - "type": { "const": "fill_received" }, - "fill": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json#/$defs/fill" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "order"], - "properties": { - "type": { "const": "order_updated" }, - "order": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json#/$defs/order" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "reason"], - "properties": { - "type": { "const": "intent_rejected" }, - "reason": { "type": "string", "minLength": 1 } - } - } - ] - }, - "intents": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "intents" }, - "payload": { "$ref": "#/$defs/intentsPayload" } - } - } - ] - }, - "intentsPayload": { - "type": "object", - "additionalProperties": false, - "required": ["intents"], - "properties": { - "intents": { - "type": "array", - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/intent" } - } - } - }, - "shutdown": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "shutdown" }, - "payload": { "$ref": "#/$defs/emptyPayload" } - } - } - ] - }, - "stopped": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "stopped" }, - "payload": { "$ref": "#/$defs/emptyPayload" } - } - } - ] - }, - "emptyPayload": { - "type": "object", - "additionalProperties": false, - "maxProperties": 0 - }, - "error": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "error" }, - "payload": { "$ref": "#/$defs/errorPayload" } - } - } - ] - }, - "errorPayload": { - "type": "object", - "additionalProperties": false, - "required": ["message"], - "properties": { - "message": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } - } - } - } -} diff --git a/contracts/strategy/v12/transcript.schema.json b/contracts/strategy/v12/transcript.schema.json deleted file mode 100644 index f00912f..0000000 --- a/contracts/strategy/v12/transcript.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v12/transcript.schema.json", - "title": "Trading Engine external strategy protocol v12 transcript record", - "description": "One accepted exchange or rejected-response diagnostic retained from a supervised stdio strategy session.", - "oneOf": [ - { "$ref": "#/$defs/exchange" }, - { "$ref": "#/$defs/rejectedResponse" } - ], - "$defs": { - "canonicalSequence": { - "type": "string", - "pattern": "^[1-9][0-9]*$" - }, - "exchange": { - "type": "object", - "additionalProperties": false, - "required": ["strategy_protocol_version", "transcript_sequence", "direction", "message"], - "properties": { - "strategy_protocol_version": { "const": "12" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v12/message.schema.json" - } - } - }, - "rejectedResponse": { - "type": "object", - "additionalProperties": false, - "required": [ - "strategy_diagnostic_version", - "transcript_sequence", - "record_type", - "expected_strategy_sequence", - "diagnostic", - "evidence" - ], - "properties": { - "strategy_diagnostic_version": { "const": "1" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "record_type": { "const": "rejected_strategy_response" }, - "expected_strategy_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "diagnostic": { "$ref": "#/$defs/diagnostic" }, - "evidence": { "$ref": "#/$defs/evidence" } - } - }, - "diagnostic": { - "allOf": [ - { - "$ref": "https://github.com/fallblu/trading-engine/contracts/diagnostic/v1/diagnostic.schema.json" - }, - { - "properties": { - "code": { "enum": ["strategy.protocol", "resource.limit"] }, - "phase": { "const": "strategy" } - } - } - ] - }, - "evidence": { - "type": "object", - "additionalProperties": false, - "required": ["encoding", "prefix", "observed_bytes", "truncated"], - "properties": { - "encoding": { "const": "hex" }, - "prefix": { - "type": "string", - "pattern": "^(?:[0-9a-f]{2}){0,256}$" - }, - "observed_bytes": { - "type": "integer", - "minimum": 0, - "maximum": 1048577 - }, - "truncated": { "type": "boolean" } - } - } - } -} diff --git a/contracts/strategy/v13/README.md b/contracts/strategy/v13/README.md deleted file mode 100644 index bc7a8d5..0000000 --- a/contracts/strategy/v13/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# External strategy protocol v13 - -Version 13 is a synchronous JSON Lines protocol over child-process standard input and output. -Trading Engine sends `initialize`, ordered `event` requests, and `shutdown`. The strategy answers -with `ready`, `intents`, and `stopped`. It may answer any request with `error`. -Protocol v11 remains available for scenario contract v13; earlier versions retain their frozen -shapes. - -Every message repeats `strategy_protocol_version: "13"` and a positive canonical -`strategy_sequence`. A response must repeat the sequence of its request. Only one request is -outstanding. Trading Engine rejects unknown or duplicate fields, invalid canonical values, -oversized lines, a wrong version or sequence, unexpected response types, EOF, timeout, and a -nonzero process exit. - -The event context contains the replay clock, a marked base-currency portfolio, deterministic group -exposure snapshots, all working orders, and the latest available bar for each instrument. Every -callback emitted for a market slice uses -that slice's `received_at` as `now` and uses its complete bars and FX vector. The portfolio reports -cash, equity, net, long, short, and gross market value plus every attributed cash ledger and -configured position. Position quantities and weights reflect applied fills. Weights are truncated -toward zero to six decimal places. `weights_available` is false and all weights are null when -equity is zero or negative. - -The `initialize` request identifies scenario contract v15 and includes the exact `initial_portfolio` -snapshot alongside the legacy cash projection. It also carries the complete versioned venue -calendars, nested execution configuration, financing policy, and settlement policy, so a strategy -can construct DAY orders and reject incompatible execution, financing, or settlement state before -replay. - -Matching pauses after each strategy callback. The engine applies the response against the exact -account and OMS state exposed by that callback before delivering another callback or considering -the next eligible order. Later same-slice contexts include the effects of earlier responses. The -eligible-order sequence is fixed at the start of matching, so newly submitted orders wait for a -later slice. Cancelling an order before its turn leaves its unused slice capacity available to the -next eligible order. - -Event payloads cover completed market slices with effective-time borrow and cash-rate observations -plus explicit settlement failures, fills, order updates, and rejected intents. Portfolio contexts -include cash-interest attribution and settled and unsettled cash and position quantities. Response -intents use the scenario v15 intent shapes. Market-slice events include lifecycle transitions and -the expanded corporate-action catalog, plus causally ordered quote/trade market events. -Protocol v13 also carries bounded order-book snapshots and incrementals and advertises the -`order_book_v1` configuration, including its maximum depth. - -External replay requires an empty batch schedule and empty streamed intent batches. The engine -records accepted messages in both directions in a deterministic transcript. A response rejected -for invalid JSON, fields, version, sequence, EOF, or size is never stored as an accepted exchange. -Instead, the partial transcript ends with a `rejected_strategy_response` diagnostic record. Version -1 rejection diagnostics use the shared -[`diagnostic/v1`](../../diagnostic/v1/README.md) contract. The transcript schema narrows that -contract to the `strategy.protocol` and `resource.limit` codes in the `strategy` phase. The record -includes the structured rejection diagnostic and at most the first 256 raw response bytes encoded -as lowercase hexadecimal. `observed_bytes` counts bytes available when the engine rejected the -response, and `truncated` reports whether the prefix omits observed bytes. The transcript and audit -journal retain partial files after failure and finalize only after their respective success checks. - -- `message.schema.json` validates individual requests and responses. -- `transcript.schema.json` validates accepted exchanges and rejected-response diagnostics. -- `fixtures/external.scenario.json` is the batch replay fixture. -- `fixtures/external.scenario.jsonl` is its bounded-memory stream form. -- `fixtures/external.strategy.jsonl` is the canonical protocol transcript. diff --git a/contracts/strategy/v13/dune b/contracts/strategy/v13/dune deleted file mode 100644 index 70ad1c7..0000000 --- a/contracts/strategy/v13/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v13/message.schema.json) - (transcript.schema.json as contracts/strategy/v13/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v13/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v13/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v13/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v13/fixtures/external.scenario.json b/contracts/strategy/v13/fixtures/external.scenario.json deleted file mode 100644 index 5d45ea2..0000000 --- a/contracts/strategy/v13/fixtures/external.scenario.json +++ /dev/null @@ -1,308 +0,0 @@ -{ - "contract_version": "15", - "metadata": { - "producer": "strategy-protocol-fixture" - }, - "run_id": "external-demo", - "base_currency": "USD", - "initial_portfolio": { - "cash": [ - { - "currency": "USD", - "amount": "10000" - } - ], - "positions": [], - "marks": [], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ] - }, - "instruments": [ - { - "instrument_id": "demo-equity-acme", - "symbol": "ACME", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "venue_calendars": [ - { - "calendar_id": "demo-xnas-2026", - "calendar_version": "1", - "venue_id": "XNAS", - "instrument_ids": [ - "demo-equity-acme" - ], - "sessions": [ - { - "session_date": "2026-01-01", - "policy": "holiday", - "phases": [] - }, - { - "session_date": "2026-01-02", - "policy": "regular", - "phases": [ - { - "phase": "premarket", - "opens_at": "2026-01-02T09:00:00Z", - "closes_at": "2026-01-02T14:25:00Z" - }, - { - "phase": "opening_auction", - "opens_at": "2026-01-02T14:25:00Z", - "closes_at": "2026-01-02T14:30:00Z" - }, - { - "phase": "regular", - "opens_at": "2026-01-02T14:30:00Z", - "closes_at": "2026-01-02T20:55:00Z" - }, - { - "phase": "closing_auction", - "opens_at": "2026-01-02T20:55:00Z", - "closes_at": "2026-01-02T21:00:00Z" - }, - { - "phase": "postmarket", - "opens_at": "2026-01-02T21:00:00Z", - "closes_at": "2026-01-03T01:00:00Z" - } - ] - }, - { - "session_date": "2026-01-05", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-05T14:30:00Z", - "closes_at": "2026-01-05T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-06", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-06T14:30:00Z", - "closes_at": "2026-01-06T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-07", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-07T14:30:00Z", - "closes_at": "2026-01-07T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-08", - "policy": "early_close", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-08T14:30:00Z", - "closes_at": "2026-01-08T18:00:00Z" - } - ] - } - ] - } - ], - "risk": { - "max_gross_exposure": "1000000", - "max_leverage": "2", - "short_borrow_bps": 0, - "instrument_policies": [ - { - "instrument_id": "demo-equity-acme", - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_notional_exposure": "1000000", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "shorting_allowed": true - } - ], - "groups": [] - }, - "execution": { - "model": "completed_bar_v1", - "configuration": { - "version": "2", - "participation_bps": 5000, - "fee_schedules": [ - { - "schedule_id": "external-acme-fees-v1", - "instrument_id": "demo-equity-acme", - "settlement_currency": "USD", - "minimum": null, - "maximum": null, - "components": [ - { - "name": "broker", - "currency": "USD", - "kind": "fixed", - "value": "0.25", - "rounding": "up", - "applies_to": "any" - }, - { - "name": "exchange", - "currency": "USD", - "kind": "notional_bps", - "value": 10, - "rounding": "up", - "applies_to": "any" - } - ] - } - ] - } - }, - "max_internal_events": 1000, - "schedule": [], - "slices": [ - { - "slice_sequence": "1", - "start_at": "2026-01-02T14:30:00Z", - "end_at": "2026-01-02T21:00:00Z", - "available_at": "2026-01-02T21:00:01Z", - "received_at": "2026-01-02T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "100", - "high": "105", - "low": "99", - "close": "104", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-02T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-02T14:30:00Z", - "credit_rate_bps": 0, - "debit_rate_bps": 0 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - }, - { - "slice_sequence": "2", - "start_at": "2026-01-05T14:30:00Z", - "end_at": "2026-01-05T21:00:00Z", - "available_at": "2026-01-05T21:00:01Z", - "received_at": "2026-01-05T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "103", - "high": "108", - "low": "102", - "close": "107", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-05T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-05T14:30:00Z", - "credit_rate_bps": 0, - "debit_rate_bps": 0 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - }, - "settlement": { - "cash_buying_power": "total_cash", - "position_availability": "total_positions", - "calendars": [ - { - "calendar_id": "default-settlement", - "version": "1", - "business_dates": [ - "2026-01-02", - "2026-01-05", - "2026-01-06", - "2026-01-07", - "2026-01-08", - "2026-01-09", - "2026-02-02", - "2026-02-03", - "2026-02-04", - "2026-02-05" - ] - } - ], - "rules": [ - { - "instrument_id": "demo-equity-acme", - "calendar_id": "default-settlement", - "lag_business_days": 1 - } - ] - } -} diff --git a/contracts/strategy/v13/fixtures/external.scenario.jsonl b/contracts/strategy/v13/fixtures/external.scenario.jsonl deleted file mode 100644 index 15ab15e..0000000 --- a/contracts/strategy/v13/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"strategy-protocol-fixture"},"run_id":"external-demo","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00Z","closes_at":"2026-01-02T14:25:00Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00Z","closes_at":"2026-01-02T14:30:00Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00Z","closes_at":"2026-01-02T20:55:00Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00Z","closes_at":"2026-01-02T21:00:00Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00Z","closes_at":"2026-01-03T01:00:00Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00Z","closes_at":"2026-01-05T21:00:00Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00Z","closes_at":"2026-01-06T21:00:00Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00Z","closes_at":"2026-01-07T21:00:00Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00Z","closes_at":"2026-01-08T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":0,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_v1","configuration":{"version":"2","participation_bps":5000,"fee_schedules":[{"schedule_id":"external-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]}}} -{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"15","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v13/fixtures/external.strategy.jsonl b/contracts/strategy/v13/fixtures/external.strategy.jsonl deleted file mode 100644 index 13c41e1..0000000 --- a/contracts/strategy/v13/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"13","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"15","scenario_sha256":"6809a3638fe668a2a11e56c42e9bac7e506c0064eb2cb0a3e71ba09d92839ee7","run_id":"external-demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00.000000Z","closes_at":"2026-01-02T14:25:00.000000Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00.000000Z","closes_at":"2026-01-02T14:30:00.000000Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00.000000Z","closes_at":"2026-01-02T20:55:00.000000Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00.000000Z","closes_at":"2026-01-02T21:00:00.000000Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00.000000Z","closes_at":"2026-01-03T01:00:00.000000Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00.000000Z","closes_at":"2026-01-05T21:00:00.000000Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00.000000Z","closes_at":"2026-01-06T21:00:00.000000Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00.000000Z","closes_at":"2026-01-07T21:00:00.000000Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00.000000Z","closes_at":"2026-01-08T18:00:00.000000Z"}]}]}],"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":0,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_v1","configuration":{"version":"2","participation_bps":5000,"fee_schedules":[{"schedule_id":"external-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"any"}]}]}},"metadata":{"producer":"strategy-protocol-fixture"}}}} -{"strategy_protocol_version":"13","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"13","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"2","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} -{"strategy_protocol_version":"13","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"2","message_type":"intents","payload":{"intents":[{"type":"target_quantities","targets":[{"instrument_id":"demo-equity-acme","quantity":"2"}]},{"type":"emit_metric","name":"fixture_signal","value":"2"}]}}} -{"strategy_protocol_version":"13","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"3","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"order_updated","order":{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} -{"strategy_protocol_version":"13","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"13","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"4","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"fill_received","fill":{"fill_id":"external-demo-fill-000000000001","order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"2","price":"103","notional":"206","fee":"0.456","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.206","quote_amount":"0.206"}]}}}}} -{"strategy_protocol_version":"13","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"13","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"5","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"order_updated","order":{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} -{"strategy_protocol_version":"13","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"13","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"6","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} -{"strategy_protocol_version":"13","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"13","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"13","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v13/message.schema.json b/contracts/strategy/v13/message.schema.json deleted file mode 100644 index ac9b452..0000000 --- a/contracts/strategy/v13/message.schema.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v13/message.schema.json", - "title": "Trading Engine external strategy protocol v13 message", - "description": "One strict request or response in the synchronous JSON Lines strategy protocol. The engine additionally enforces direction, sequence pairing, canonical values, response limits, and lifecycle order.", - "oneOf": [ - { "$ref": "#/$defs/initialize" }, - { "$ref": "#/$defs/ready" }, - { "$ref": "#/$defs/event" }, - { "$ref": "#/$defs/intents" }, - { "$ref": "#/$defs/shutdown" }, - { "$ref": "#/$defs/stopped" }, - { "$ref": "#/$defs/error" } - ], - "$defs": { - "sequence": { - "type": "string", - "pattern": "^[1-9][0-9]*$" - }, - "base": { - "type": "object", - "additionalProperties": false, - "required": ["strategy_protocol_version", "strategy_sequence", "message_type", "payload"], - "properties": { - "strategy_protocol_version": { "const": "13" }, - "strategy_sequence": { "$ref": "#/$defs/sequence" }, - "message_type": { "type": "string" }, - "payload": { "type": "object" } - } - }, - "initialize": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "initialize" }, - "payload": { "$ref": "#/$defs/initializePayload" } - } - } - ] - }, - "initializePayload": { - "type": "object", - "additionalProperties": false, - "required": ["engine_version", "scenario_contract_version", "scenario_sha256", "run_id", "base_currency", "initial_cash", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "metadata"], - "properties": { - "engine_version": { "type": "string", "minLength": 1 }, - "scenario_contract_version": { "const": "15" }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/cashBalance" } - }, - "initial_portfolio": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/initialPortfolio" } - ] - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/instrument" } - }, - "venue_calendars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/venueCalendar" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/financing" }, - "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/settlement" }, - "metadata": { "type": "object" } - } - }, - "ready": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "ready" }, - "payload": { "$ref": "#/$defs/readyPayload" } - } - } - ] - }, - "readyPayload": { - "type": "object", - "additionalProperties": false, - "required": ["strategy_name", "strategy_version"], - "properties": { - "strategy_name": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, - "strategy_version": { - "oneOf": [ - { "type": "null" }, - { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } - ] - } - } - }, - "event": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "event" }, - "payload": { "$ref": "#/$defs/eventPayload" } - } - } - ] - }, - "eventPayload": { - "type": "object", - "additionalProperties": false, - "required": ["context", "event"], - "properties": { - "context": { "$ref": "#/$defs/context" }, - "event": { "$ref": "#/$defs/strategyEvent" } - } - }, - "context": { - "type": "object", - "additionalProperties": false, - "required": ["now", "portfolio", "working_orders", "latest_bars"], - "properties": { - "now": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/bar" } - } - } - }, - "portfolio": { - "type": "object", - "additionalProperties": false, - "required": ["base_currency", "cash", "net_market_value", "long_market_value", "short_market_value", "gross_exposure", "equity", "weights_available", "cash_weight", "cash_balances", "positions", "group_exposures"], - "properties": { - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, - "weights_available": { "type": "boolean" }, - "cash_weight": { "$ref": "#/$defs/optionalWeight" }, - "cash_balances": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/cashAttribution" } - }, - "positions": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/markedPosition" } - }, - "group_exposures": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/groupExposure" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" } - ] - }, - "markedPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "settled_quantity", "unsettled_quantity", "mark", "base_market_value", "weight"], - "properties": { - "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, - "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, - "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, - "weight": { "$ref": "#/$defs/optionalWeight" } - } - }, - "strategyEvent": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "market_slice"], - "properties": { - "type": { "const": "market_slice_closed" }, - "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/marketSlice" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "fill"], - "properties": { - "type": { "const": "fill_received" }, - "fill": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/fill" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "order"], - "properties": { - "type": { "const": "order_updated" }, - "order": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/order" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "reason"], - "properties": { - "type": { "const": "intent_rejected" }, - "reason": { "type": "string", "minLength": 1 } - } - } - ] - }, - "intents": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "intents" }, - "payload": { "$ref": "#/$defs/intentsPayload" } - } - } - ] - }, - "intentsPayload": { - "type": "object", - "additionalProperties": false, - "required": ["intents"], - "properties": { - "intents": { - "type": "array", - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/intent" } - } - } - }, - "shutdown": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "shutdown" }, - "payload": { "$ref": "#/$defs/emptyPayload" } - } - } - ] - }, - "stopped": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "stopped" }, - "payload": { "$ref": "#/$defs/emptyPayload" } - } - } - ] - }, - "emptyPayload": { - "type": "object", - "additionalProperties": false, - "maxProperties": 0 - }, - "error": { - "allOf": [ - { "$ref": "#/$defs/base" }, - { - "properties": { - "message_type": { "const": "error" }, - "payload": { "$ref": "#/$defs/errorPayload" } - } - } - ] - }, - "errorPayload": { - "type": "object", - "additionalProperties": false, - "required": ["message"], - "properties": { - "message": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } - } - } - } -} diff --git a/contracts/strategy/v13/transcript.schema.json b/contracts/strategy/v13/transcript.schema.json deleted file mode 100644 index 031b51b..0000000 --- a/contracts/strategy/v13/transcript.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v13/transcript.schema.json", - "title": "Trading Engine external strategy protocol v13 transcript record", - "description": "One accepted exchange or rejected-response diagnostic retained from a supervised stdio strategy session.", - "oneOf": [ - { "$ref": "#/$defs/exchange" }, - { "$ref": "#/$defs/rejectedResponse" } - ], - "$defs": { - "canonicalSequence": { - "type": "string", - "pattern": "^[1-9][0-9]*$" - }, - "exchange": { - "type": "object", - "additionalProperties": false, - "required": ["strategy_protocol_version", "transcript_sequence", "direction", "message"], - "properties": { - "strategy_protocol_version": { "const": "13" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v13/message.schema.json" - } - } - }, - "rejectedResponse": { - "type": "object", - "additionalProperties": false, - "required": [ - "strategy_diagnostic_version", - "transcript_sequence", - "record_type", - "expected_strategy_sequence", - "diagnostic", - "evidence" - ], - "properties": { - "strategy_diagnostic_version": { "const": "1" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "record_type": { "const": "rejected_strategy_response" }, - "expected_strategy_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "diagnostic": { "$ref": "#/$defs/diagnostic" }, - "evidence": { "$ref": "#/$defs/evidence" } - } - }, - "diagnostic": { - "allOf": [ - { - "$ref": "https://github.com/fallblu/trading-engine/contracts/diagnostic/v1/diagnostic.schema.json" - }, - { - "properties": { - "code": { "enum": ["strategy.protocol", "resource.limit"] }, - "phase": { "const": "strategy" } - } - } - ] - }, - "evidence": { - "type": "object", - "additionalProperties": false, - "required": ["encoding", "prefix", "observed_bytes", "truncated"], - "properties": { - "encoding": { "const": "hex" }, - "prefix": { - "type": "string", - "pattern": "^(?:[0-9a-f]{2}){0,256}$" - }, - "observed_bytes": { - "type": "integer", - "minimum": 0, - "maximum": 1048577 - }, - "truncated": { "type": "boolean" } - } - } - } -} diff --git a/contracts/strategy/v14/README.md b/contracts/strategy/v14/README.md deleted file mode 100644 index 1164118..0000000 --- a/contracts/strategy/v14/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# External strategy protocol v14 - -Version 14 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 v13 remains available for scenario contract v15; earlier versions retain their frozen -shapes. - -Every message repeats `strategy_protocol_version: "14"` 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. - -Intent batches use the scenario v16 typed metric contract. `emit_metric.value` declares a numeric, -string, or boolean value, with optional unit, aggregation, and bounded dimensions. - -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 v16 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 v16 intent shapes. Market-slice events include lifecycle transitions and -the expanded corporate-action catalog, plus causally ordered quote/trade market events. -Protocol v14 also carries bounded order-book snapshots and incrementals and advertises the -`order_book_v1` configuration, including its maximum depth. - -External replay requires an empty batch schedule and empty streamed intent batches. The engine -records accepted messages in both directions in a deterministic transcript. A response rejected -for invalid JSON, fields, version, sequence, EOF, or size is never stored as an accepted exchange. -Instead, the partial transcript ends with a `rejected_strategy_response` diagnostic record. Version -1 rejection diagnostics use the shared -[`diagnostic/v1`](../../diagnostic/v1/README.md) contract. The transcript schema narrows that -contract to the `strategy.protocol` and `resource.limit` codes in the `strategy` phase. The record -includes the structured rejection diagnostic and at most the first 256 raw response bytes encoded -as lowercase hexadecimal. `observed_bytes` counts bytes available when the engine rejected the -response, and `truncated` reports whether the prefix omits observed bytes. The transcript and audit -journal retain partial files after failure and finalize only after their respective success checks. - -- `message.schema.json` validates individual requests and responses. -- `transcript.schema.json` validates accepted exchanges and rejected-response diagnostics. -- `fixtures/external.scenario.json` is the batch replay fixture. -- `fixtures/external.scenario.jsonl` is its bounded-memory stream form. -- `fixtures/external.strategy.jsonl` is the canonical protocol transcript. diff --git a/contracts/strategy/v14/dune b/contracts/strategy/v14/dune deleted file mode 100644 index 999eb39..0000000 --- a/contracts/strategy/v14/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v14/message.schema.json) - (transcript.schema.json as contracts/strategy/v14/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v14/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v14/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v14/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v14/fixtures/external.scenario.json b/contracts/strategy/v14/fixtures/external.scenario.json deleted file mode 100644 index 9fa4d9c..0000000 --- a/contracts/strategy/v14/fixtures/external.scenario.json +++ /dev/null @@ -1,308 +0,0 @@ -{ - "contract_version": "16", - "metadata": { - "producer": "strategy-protocol-fixture" - }, - "run_id": "external-demo", - "base_currency": "USD", - "initial_portfolio": { - "cash": [ - { - "currency": "USD", - "amount": "10000" - } - ], - "positions": [], - "marks": [], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ] - }, - "instruments": [ - { - "instrument_id": "demo-equity-acme", - "symbol": "ACME", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "venue_calendars": [ - { - "calendar_id": "demo-xnas-2026", - "calendar_version": "1", - "venue_id": "XNAS", - "instrument_ids": [ - "demo-equity-acme" - ], - "sessions": [ - { - "session_date": "2026-01-01", - "policy": "holiday", - "phases": [] - }, - { - "session_date": "2026-01-02", - "policy": "regular", - "phases": [ - { - "phase": "premarket", - "opens_at": "2026-01-02T09:00:00Z", - "closes_at": "2026-01-02T14:25:00Z" - }, - { - "phase": "opening_auction", - "opens_at": "2026-01-02T14:25:00Z", - "closes_at": "2026-01-02T14:30:00Z" - }, - { - "phase": "regular", - "opens_at": "2026-01-02T14:30:00Z", - "closes_at": "2026-01-02T20:55:00Z" - }, - { - "phase": "closing_auction", - "opens_at": "2026-01-02T20:55:00Z", - "closes_at": "2026-01-02T21:00:00Z" - }, - { - "phase": "postmarket", - "opens_at": "2026-01-02T21:00:00Z", - "closes_at": "2026-01-03T01:00:00Z" - } - ] - }, - { - "session_date": "2026-01-05", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-05T14:30:00Z", - "closes_at": "2026-01-05T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-06", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-06T14:30:00Z", - "closes_at": "2026-01-06T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-07", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-07T14:30:00Z", - "closes_at": "2026-01-07T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-08", - "policy": "early_close", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-08T14:30:00Z", - "closes_at": "2026-01-08T18:00:00Z" - } - ] - } - ] - } - ], - "risk": { - "max_gross_exposure": "1000000", - "max_leverage": "2", - "short_borrow_bps": 0, - "instrument_policies": [ - { - "instrument_id": "demo-equity-acme", - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_notional_exposure": "1000000", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "shorting_allowed": true - } - ], - "groups": [] - }, - "execution": { - "model": "completed_bar_v1", - "configuration": { - "version": "2", - "participation_bps": 5000, - "fee_schedules": [ - { - "schedule_id": "external-acme-fees-v1", - "instrument_id": "demo-equity-acme", - "settlement_currency": "USD", - "minimum": null, - "maximum": null, - "components": [ - { - "name": "broker", - "currency": "USD", - "kind": "fixed", - "value": "0.25", - "rounding": "up", - "applies_to": "any" - }, - { - "name": "exchange", - "currency": "USD", - "kind": "notional_bps", - "value": 10, - "rounding": "up", - "applies_to": "any" - } - ] - } - ] - } - }, - "max_internal_events": 1000, - "schedule": [], - "slices": [ - { - "slice_sequence": "1", - "start_at": "2026-01-02T14:30:00Z", - "end_at": "2026-01-02T21:00:00Z", - "available_at": "2026-01-02T21:00:01Z", - "received_at": "2026-01-02T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "100", - "high": "105", - "low": "99", - "close": "104", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-02T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-02T14:30:00Z", - "credit_rate_bps": 0, - "debit_rate_bps": 0 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - }, - { - "slice_sequence": "2", - "start_at": "2026-01-05T14:30:00Z", - "end_at": "2026-01-05T21:00:00Z", - "available_at": "2026-01-05T21:00:01Z", - "received_at": "2026-01-05T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "103", - "high": "108", - "low": "102", - "close": "107", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-05T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-05T14:30:00Z", - "credit_rate_bps": 0, - "debit_rate_bps": 0 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - }, - "settlement": { - "cash_buying_power": "total_cash", - "position_availability": "total_positions", - "calendars": [ - { - "calendar_id": "default-settlement", - "version": "1", - "business_dates": [ - "2026-01-02", - "2026-01-05", - "2026-01-06", - "2026-01-07", - "2026-01-08", - "2026-01-09", - "2026-02-02", - "2026-02-03", - "2026-02-04", - "2026-02-05" - ] - } - ], - "rules": [ - { - "instrument_id": "demo-equity-acme", - "calendar_id": "default-settlement", - "lag_business_days": 1 - } - ] - } -} diff --git a/contracts/strategy/v14/fixtures/external.scenario.jsonl b/contracts/strategy/v14/fixtures/external.scenario.jsonl deleted file mode 100644 index 894bf5a..0000000 --- a/contracts/strategy/v14/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"16","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":"16","payload":{"intents":[],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"16","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"16","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v14/fixtures/external.strategy.jsonl b/contracts/strategy/v14/fixtures/external.strategy.jsonl deleted file mode 100644 index ceed5d3..0000000 --- a/contracts/strategy/v14/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"14","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"16","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":"14","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"14","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","strategy_sequence":"2","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} -{"strategy_protocol_version":"14","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","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":{"type":"numeric","value":"2"},"unit":"score","dimensions":{"source":"fixture"},"aggregation":"last"}]}}} -{"strategy_protocol_version":"14","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","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":"14","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"14","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","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":"14","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"14","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","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":"14","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"14","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","strategy_sequence":"6","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} -{"strategy_protocol_version":"14","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"14","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"14","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"14","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"14","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v14/message.schema.json b/contracts/strategy/v14/message.schema.json deleted file mode 100644 index 97d8a98..0000000 --- a/contracts/strategy/v14/message.schema.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v14/message.schema.json", - "title": "Trading Engine external strategy protocol v14 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": "14" }, - "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": "16" }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/cashBalance" } - }, - "initial_portfolio": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/initialPortfolio" } - ] - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/instrument" } - }, - "venue_calendars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/venueCalendar" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/financing" }, - "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/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/v16/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/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/v16/journal.schema.json#/$defs/groupExposure" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/signedDecimal" }, - "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/signedDecimal" }, - "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/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/v16/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/v16/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/v16/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/v14/transcript.schema.json b/contracts/strategy/v14/transcript.schema.json deleted file mode 100644 index 4635866..0000000 --- a/contracts/strategy/v14/transcript.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v14/transcript.schema.json", - "title": "Trading Engine external strategy protocol v14 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": "14" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v14/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/strategy/v2/README.md b/contracts/strategy/v2/README.md deleted file mode 100644 index 0420e34..0000000 --- a/contracts/strategy/v2/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# External strategy protocol v2 - -Version 2 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`. - -Every message repeats `strategy_protocol_version: "2"` 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, all working orders, -and the latest available bar for each instrument. 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. -Event payloads cover completed market slices, fills, order updates, and rejected intents. Response -intents use the scenario v3 intent shapes. - -External replay requires an empty batch schedule and empty streamed intent batches. The engine -records both directions in a deterministic transcript. 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 retained transcript records. -- `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/v2/dune b/contracts/strategy/v2/dune deleted file mode 100644 index cd3cf5a..0000000 --- a/contracts/strategy/v2/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v2/message.schema.json) - (transcript.schema.json as contracts/strategy/v2/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v2/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v2/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v2/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v2/fixtures/external.scenario.json b/contracts/strategy/v2/fixtures/external.scenario.json deleted file mode 100644 index 5fe2d5a..0000000 --- a/contracts/strategy/v2/fixtures/external.scenario.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "contract_version": "3", - "metadata": { - "producer": "strategy-protocol-fixture" - }, - "run_id": "external-demo", - "base_currency": "USD", - "initial_cash": [ - { "currency": "USD", "amount": "10000" } - ], - "instruments": [ - { - "instrument_id": "demo-equity-acme", - "symbol": "ACME", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "risk": { - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_gross_exposure": "1000000", - "max_leverage": "2", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "short_borrow_bps": 0 - }, - "execution": { - "model": "completed_bar_v1", - "participation_bps": 10000, - "fixed_fee": "0", - "fee_bps": 0 - }, - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/strategy/v2/fixtures/external.scenario.jsonl b/contracts/strategy/v2/fixtures/external.scenario.jsonl deleted file mode 100644 index 1e168bf..0000000 --- a/contracts/strategy/v2/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"3","payload":{"base_currency":"USD","execution":{"fee_bps":0,"fixed_fee":"0","model":"completed_bar_v1","participation_bps":10000},"initial_cash":[{"amount":"10000","currency":"USD"}],"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"1","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"max_internal_events":1000,"metadata":{"producer":"strategy-protocol-fixture"},"risk":{"initial_margin_bps":5000,"maintenance_margin_bps":2500,"max_gross_exposure":"1000000","max_leverage":"2","max_long_position":"1000","max_order_quantity":"1000","max_short_position":"1000","short_borrow_bps":0},"run_id":"external-demo"},"record_type":"scenario_header","scenario_sequence":"1"} -{"contract_version":"3","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"3","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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"3","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v2/fixtures/external.strategy.jsonl b/contracts/strategy/v2/fixtures/external.strategy.jsonl deleted file mode 100644 index de2b8e7..0000000 --- a/contracts/strategy/v2/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"2","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"2","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"0.1.0-dev","scenario_contract_version":"3","scenario_sha256":"78df448f65b0d784d51951108cacb1db247d9d2dd03d93886f43e4acc0e83e1d","run_id":"external-demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"risk":{"max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_gross_exposure":"1000000","max_leverage":"2","initial_margin_bps":5000,"maintenance_margin_bps":2500,"short_borrow_bps":0},"execution":{"model":"completed_bar_v1","participation_bps":10000,"fixed_fee":"0","fee_bps":0},"metadata":{"producer":"strategy-protocol-fixture"}}}} -{"strategy_protocol_version":"2","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"2","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"2","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"2","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0"}]},"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":[]}}}}} -{"strategy_protocol_version":"2","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"2","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":"2","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"2","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0"}]},"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000005","updated_event_id":"external-demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","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","limit_price":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000005","updated_event_id":"external-demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} -{"strategy_protocol_version":"2","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"2","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"2","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"2","strategy_sequence":"4","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9794","net_market_value":"208","long_market_value":"208","short_market_value":"0","gross_exposure":"208","equity":"10002","weights_available":true,"cash_weight":"0.979204","cash_balances":[{"currency":"USD","amount":"9794","fx_rate":"1","base_value":"9794"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"104","base_market_value":"208","weight":"0.020795"}]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","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","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}}}}} -{"strategy_protocol_version":"2","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"2","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"2","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"2","strategy_sequence":"5","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9794","net_market_value":"208","long_market_value":"208","short_market_value":"0","gross_exposure":"208","equity":"10002","weights_available":true,"cash_weight":"0.979204","cash_balances":[{"currency":"USD","amount":"9794","fx_rate":"1","base_value":"9794"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"104","base_market_value":"208","weight":"0.020795"}]},"working_orders":[],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000005","updated_event_id":"external-demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} -{"strategy_protocol_version":"2","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"2","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"2","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"2","strategy_sequence":"6","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9794","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10008","weights_available":true,"cash_weight":"0.978617","cash_balances":[{"currency":"USD","amount":"9794","fx_rate":"1","base_value":"9794"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021382"}]},"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":[]}}}}} -{"strategy_protocol_version":"2","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"2","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"2","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"2","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"2","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"2","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v2/message.schema.json b/contracts/strategy/v2/message.schema.json deleted file mode 100644 index a3f9dee..0000000 --- a/contracts/strategy/v2/message.schema.json +++ /dev/null @@ -1,282 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v2/message.schema.json", - "title": "Trading Engine external strategy protocol v2 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": "2" }, - "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", "instruments", "risk", "execution", "metadata"], - "properties": { - "engine_version": { "type": "string", "minLength": 1 }, - "scenario_contract_version": { "type": "string", "minLength": 1 }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/cashBalance" } - }, - "instruments": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/instrument" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/execution" }, - "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/v3/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/v3/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/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"], - "properties": { - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/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/v3/journal.schema.json#/$defs/cashAttribution" } - }, - "positions": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/markedPosition" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/signedDecimal" } - ] - }, - "markedPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], - "properties": { - "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/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/v3/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/v3/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/v3/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", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/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/v2/transcript.schema.json b/contracts/strategy/v2/transcript.schema.json deleted file mode 100644 index bf84b7b..0000000 --- a/contracts/strategy/v2/transcript.schema.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v2/transcript.schema.json", - "title": "Trading Engine external strategy protocol v2 transcript record", - "description": "One ordered request or response retained from a supervised stdio strategy session.", - "type": "object", - "additionalProperties": false, - "required": ["strategy_protocol_version", "transcript_sequence", "direction", "message"], - "properties": { - "strategy_protocol_version": { "const": "2" }, - "transcript_sequence": { - "type": "string", - "pattern": "^[1-9][0-9]*$" - }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v2/message.schema.json" - } - } -} diff --git a/contracts/strategy/v3/README.md b/contracts/strategy/v3/README.md deleted file mode 100644 index ccaad1b..0000000 --- a/contracts/strategy/v3/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# External strategy protocol v3 - -Version 3 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`. - -Every message repeats `strategy_protocol_version: "3"` 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, 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. - -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, fills, order updates, and rejected intents. Response -intents use the scenario v3 intent shapes. - -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/v3/dune b/contracts/strategy/v3/dune deleted file mode 100644 index ec57607..0000000 --- a/contracts/strategy/v3/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v3/message.schema.json) - (transcript.schema.json as contracts/strategy/v3/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v3/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v3/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v3/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v3/fixtures/external.scenario.json b/contracts/strategy/v3/fixtures/external.scenario.json deleted file mode 100644 index 5fe2d5a..0000000 --- a/contracts/strategy/v3/fixtures/external.scenario.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "contract_version": "3", - "metadata": { - "producer": "strategy-protocol-fixture" - }, - "run_id": "external-demo", - "base_currency": "USD", - "initial_cash": [ - { "currency": "USD", "amount": "10000" } - ], - "instruments": [ - { - "instrument_id": "demo-equity-acme", - "symbol": "ACME", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "risk": { - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_gross_exposure": "1000000", - "max_leverage": "2", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "short_borrow_bps": 0 - }, - "execution": { - "model": "completed_bar_v1", - "participation_bps": 10000, - "fixed_fee": "0", - "fee_bps": 0 - }, - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/strategy/v3/fixtures/external.scenario.jsonl b/contracts/strategy/v3/fixtures/external.scenario.jsonl deleted file mode 100644 index 1e168bf..0000000 --- a/contracts/strategy/v3/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"3","payload":{"base_currency":"USD","execution":{"fee_bps":0,"fixed_fee":"0","model":"completed_bar_v1","participation_bps":10000},"initial_cash":[{"amount":"10000","currency":"USD"}],"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"1","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"max_internal_events":1000,"metadata":{"producer":"strategy-protocol-fixture"},"risk":{"initial_margin_bps":5000,"maintenance_margin_bps":2500,"max_gross_exposure":"1000000","max_leverage":"2","max_long_position":"1000","max_order_quantity":"1000","max_short_position":"1000","short_borrow_bps":0},"run_id":"external-demo"},"record_type":"scenario_header","scenario_sequence":"1"} -{"contract_version":"3","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"3","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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"3","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v3/fixtures/external.strategy.jsonl b/contracts/strategy/v3/fixtures/external.strategy.jsonl deleted file mode 100644 index 6839dc6..0000000 --- a/contracts/strategy/v3/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"3","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"3","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"3","scenario_sha256":"78df448f65b0d784d51951108cacb1db247d9d2dd03d93886f43e4acc0e83e1d","run_id":"external-demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"risk":{"max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_gross_exposure":"1000000","max_leverage":"2","initial_margin_bps":5000,"maintenance_margin_bps":2500,"short_borrow_bps":0},"execution":{"model":"completed_bar_v1","participation_bps":10000,"fixed_fee":"0","fee_bps":0},"metadata":{"producer":"strategy-protocol-fixture"}}}} -{"strategy_protocol_version":"3","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"3","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"3","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"3","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0"}]},"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":[]}}}}} -{"strategy_protocol_version":"3","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"3","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":"3","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"3","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0"}]},"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000005","updated_event_id":"external-demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","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","limit_price":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000005","updated_event_id":"external-demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} -{"strategy_protocol_version":"3","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"3","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"3","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"3","strategy_sequence":"4","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9794","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10008","weights_available":true,"cash_weight":"0.978617","cash_balances":[{"currency":"USD","amount":"9794","fx_rate":"1","base_value":"9794"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021382"}]},"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","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}}}}} -{"strategy_protocol_version":"3","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"3","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"3","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"3","strategy_sequence":"5","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9794","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10008","weights_available":true,"cash_weight":"0.978617","cash_balances":[{"currency":"USD","amount":"9794","fx_rate":"1","base_value":"9794"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021382"}]},"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","limit_price":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000005","updated_event_id":"external-demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} -{"strategy_protocol_version":"3","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"3","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"3","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"3","strategy_sequence":"6","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9794","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10008","weights_available":true,"cash_weight":"0.978617","cash_balances":[{"currency":"USD","amount":"9794","fx_rate":"1","base_value":"9794"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021382"}]},"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":[]}}}}} -{"strategy_protocol_version":"3","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"3","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"3","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"3","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"3","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"3","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v3/message.schema.json b/contracts/strategy/v3/message.schema.json deleted file mode 100644 index 75e0ce4..0000000 --- a/contracts/strategy/v3/message.schema.json +++ /dev/null @@ -1,284 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v3/message.schema.json", - "title": "Trading Engine external strategy protocol v3 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": "3" }, - "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", "instruments", "risk", "execution", "metadata"], - "properties": { - "engine_version": { "type": "string", "minLength": 1 }, - "scenario_contract_version": { "type": "string", "minLength": 1 }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/cashBalance" } - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/instrument" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/execution" }, - "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/v3/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/v3/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/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"], - "properties": { - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/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/v3/journal.schema.json#/$defs/cashAttribution" } - }, - "positions": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/markedPosition" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/signedDecimal" } - ] - }, - "markedPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], - "properties": { - "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/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/v3/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/v3/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/v3/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/v3/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/v3/transcript.schema.json b/contracts/strategy/v3/transcript.schema.json deleted file mode 100644 index 0ecb931..0000000 --- a/contracts/strategy/v3/transcript.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v3/transcript.schema.json", - "title": "Trading Engine external strategy protocol v3 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": "3" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v3/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/strategy/v4/README.md b/contracts/strategy/v4/README.md deleted file mode 100644 index 1883476..0000000 --- a/contracts/strategy/v4/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# External strategy protocol v4 - -Version 4 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 v3 remains available for legacy scenario contracts and retains its frozen message and -transcript shapes. - -Every message repeats `strategy_protocol_version: "4"` 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, 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 v6 and includes the exact `initial_portfolio` -snapshot alongside the legacy cash projection. It also carries the nested, versioned execution -configuration, so a strategy can reject incompatible 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, fills, order updates, and rejected intents. Response -intents use the scenario v6 intent shapes. - -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/v4/dune b/contracts/strategy/v4/dune deleted file mode 100644 index b1b6030..0000000 --- a/contracts/strategy/v4/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v4/message.schema.json) - (transcript.schema.json as contracts/strategy/v4/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v4/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v4/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v4/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v4/fixtures/external.scenario.json b/contracts/strategy/v4/fixtures/external.scenario.json deleted file mode 100644 index 3a0e07b..0000000 --- a/contracts/strategy/v4/fixtures/external.scenario.json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "contract_version": "6", - "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_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_gross_exposure": "1000000", - "max_leverage": "2", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "short_borrow_bps": 0 - }, - "execution": { - "model": "completed_bar_v1", - "configuration": { - "version": "1", - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 - } - }, - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/strategy/v4/fixtures/external.scenario.jsonl b/contracts/strategy/v4/fixtures/external.scenario.jsonl deleted file mode 100644 index 2048f33..0000000 --- a/contracts/strategy/v4/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"6","payload":{"metadata":{"producer":"strategy-protocol-fixture"},"run_id":"external-demo","base_currency":"USD","initial_portfolio":{"cash":[{"amount":"10000","currency":"USD"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"1","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"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":{"initial_margin_bps":5000,"maintenance_margin_bps":2500,"max_gross_exposure":"1000000","max_leverage":"2","max_long_position":"1000","max_order_quantity":"1000","max_short_position":"1000","short_borrow_bps":0},"execution":{"model":"completed_bar_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000},"record_type":"scenario_header","scenario_sequence":"1"} -{"contract_version":"6","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"6","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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"6","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v4/fixtures/external.strategy.jsonl b/contracts/strategy/v4/fixtures/external.strategy.jsonl deleted file mode 100644 index fd78fc4..0000000 --- a/contracts/strategy/v4/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"4","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"6","scenario_sha256":"1faa37778736b022309ffc290658161f9e97bb6f7492c7083045922998f237f5","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"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"risk":{"max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_gross_exposure":"1000000","max_leverage":"2","initial_margin_bps":5000,"maintenance_margin_bps":2500,"short_borrow_bps":0},"execution":{"model":"completed_bar_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"metadata":{"producer":"strategy-protocol-fixture"}}}} -{"strategy_protocol_version":"4","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"4","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0"}]},"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":[]}}}}} -{"strategy_protocol_version":"4","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","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":"4","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0"}]},"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","limit_price":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","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","limit_price":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","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} -{"strategy_protocol_version":"4","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"4","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}]},"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"}}}}} -{"strategy_protocol_version":"4","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"4","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}]},"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","limit_price":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","filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} -{"strategy_protocol_version":"4","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"4","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}]},"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":[]}}}}} -{"strategy_protocol_version":"4","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"4","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"4","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"4","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"4","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v4/message.schema.json b/contracts/strategy/v4/message.schema.json deleted file mode 100644 index cb3c259..0000000 --- a/contracts/strategy/v4/message.schema.json +++ /dev/null @@ -1,290 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v4/message.schema.json", - "title": "Trading Engine external strategy protocol v4 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": "4" }, - "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", "risk", "execution", "metadata"], - "properties": { - "engine_version": { "type": "string", "minLength": 1 }, - "scenario_contract_version": { "const": "6" }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/cashBalance" } - }, - "initial_portfolio": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/initialPortfolio" } - ] - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/instrument" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/execution" }, - "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/v6/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/v6/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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"], - "properties": { - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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/v6/journal.schema.json#/$defs/cashAttribution" } - }, - "positions": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/markedPosition" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/signedDecimal" } - ] - }, - "markedPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], - "properties": { - "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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/v6/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/v6/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/v6/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/v6/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/v4/transcript.schema.json b/contracts/strategy/v4/transcript.schema.json deleted file mode 100644 index 0602db3..0000000 --- a/contracts/strategy/v4/transcript.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v4/transcript.schema.json", - "title": "Trading Engine external strategy protocol v4 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": "4" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v4/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/strategy/v5/README.md b/contracts/strategy/v5/README.md deleted file mode 100644 index d5da556..0000000 --- a/contracts/strategy/v5/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# External strategy protocol v5 - -Version 5 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`. -Protocols v4 and v3 remain available for legacy scenario contracts and retain their frozen shapes. - -Every message repeats `strategy_protocol_version: "5"` 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 v7 and includes the exact `initial_portfolio` -snapshot alongside the legacy cash projection. It also carries the nested, versioned execution -configuration, so a strategy can reject incompatible 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, fills, order updates, and rejected intents. Response -intents use the scenario v7 intent shapes. - -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/v5/dune b/contracts/strategy/v5/dune deleted file mode 100644 index 3f3d97d..0000000 --- a/contracts/strategy/v5/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v5/message.schema.json) - (transcript.schema.json as contracts/strategy/v5/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v5/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v5/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v5/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v5/fixtures/external.scenario.json b/contracts/strategy/v5/fixtures/external.scenario.json deleted file mode 100644 index 7a4da01..0000000 --- a/contracts/strategy/v5/fixtures/external.scenario.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "contract_version": "7", - "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": "1", - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 - } - }, - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/strategy/v5/fixtures/external.scenario.jsonl b/contracts/strategy/v5/fixtures/external.scenario.jsonl deleted file mode 100644 index 83383a9..0000000 --- a/contracts/strategy/v5/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"7","payload":{"metadata":{"producer":"strategy-protocol-fixture"},"run_id":"external-demo","base_currency":"USD","initial_portfolio":{"cash":[{"amount":"10000","currency":"USD"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"1","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"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":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000},"record_type":"scenario_header","scenario_sequence":"1"} -{"contract_version":"7","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"7","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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"7","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v5/fixtures/external.strategy.jsonl b/contracts/strategy/v5/fixtures/external.strategy.jsonl deleted file mode 100644 index d24e191..0000000 --- a/contracts/strategy/v5/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"5","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"7","scenario_sha256":"006cea48630b4cd06e7e3908de78065e931c069cac90ae04d2567f20896d413b","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"}]},"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":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"metadata":{"producer":"strategy-protocol-fixture"}}}} -{"strategy_protocol_version":"5","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"5","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":[]}}}}} -{"strategy_protocol_version":"5","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","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":"5","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0"}],"group_exposures":[]},"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","limit_price":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","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","limit_price":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","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} -{"strategy_protocol_version":"5","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"5","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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"}}}}} -{"strategy_protocol_version":"5","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"5","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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","limit_price":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","filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} -{"strategy_protocol_version":"5","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"5","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":[]}}}}} -{"strategy_protocol_version":"5","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"5","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"5","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"5","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"5","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v5/message.schema.json b/contracts/strategy/v5/message.schema.json deleted file mode 100644 index 1e49290..0000000 --- a/contracts/strategy/v5/message.schema.json +++ /dev/null @@ -1,294 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v5/message.schema.json", - "title": "Trading Engine external strategy protocol v5 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": "5" }, - "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", "risk", "execution", "metadata"], - "properties": { - "engine_version": { "type": "string", "minLength": 1 }, - "scenario_contract_version": { "const": "7" }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/cashBalance" } - }, - "initial_portfolio": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/initialPortfolio" } - ] - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/instrument" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/execution" }, - "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/v7/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/v7/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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/v7/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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/v7/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/v7/journal.schema.json#/$defs/groupExposure" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/signedDecimal" } - ] - }, - "markedPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], - "properties": { - "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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/v7/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/v7/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/v7/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/v7/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/v5/transcript.schema.json b/contracts/strategy/v5/transcript.schema.json deleted file mode 100644 index cc6fba1..0000000 --- a/contracts/strategy/v5/transcript.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v5/transcript.schema.json", - "title": "Trading Engine external strategy protocol v5 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": "5" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v5/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/strategy/v6/README.md b/contracts/strategy/v6/README.md deleted file mode 100644 index c0cd6b1..0000000 --- a/contracts/strategy/v6/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# External strategy protocol v6 - -Version 6 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`. -Protocols v4 and v3 remain available for legacy scenario contracts and retain their frozen shapes. - -Every message repeats `strategy_protocol_version: "6"` 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 v8 and includes the exact `initial_portfolio` -snapshot alongside the legacy cash projection. It also carries the complete versioned venue -calendars and nested execution configuration, so a strategy can construct DAY orders and reject -incompatible 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, fills, order updates, and rejected intents. Response -intents use the scenario v8 intent shapes. - -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/v6/dune b/contracts/strategy/v6/dune deleted file mode 100644 index 626dd76..0000000 --- a/contracts/strategy/v6/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v6/message.schema.json) - (transcript.schema.json as contracts/strategy/v6/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v6/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v6/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v6/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v6/fixtures/external.scenario.json b/contracts/strategy/v6/fixtures/external.scenario.json deleted file mode 100644 index 1cd0108..0000000 --- a/contracts/strategy/v6/fixtures/external.scenario.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "contract_version": "8", - "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": "1", - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 - } - }, - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/strategy/v6/fixtures/external.scenario.jsonl b/contracts/strategy/v6/fixtures/external.scenario.jsonl deleted file mode 100644 index 6c1f91c..0000000 --- a/contracts/strategy/v6/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"8","payload":{"metadata":{"producer":"strategy-protocol-fixture"},"run_id":"external-demo","base_currency":"USD","initial_portfolio":{"cash":[{"amount":"10000","currency":"USD"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"1","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"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":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000},"record_type":"scenario_header","scenario_sequence":"1"} -{"contract_version":"8","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"8","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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"8","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v6/fixtures/external.strategy.jsonl b/contracts/strategy/v6/fixtures/external.strategy.jsonl deleted file mode 100644 index 6bb8ac8..0000000 --- a/contracts/strategy/v6/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"6","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"8","scenario_sha256":"9399ec91936b6beff0701c5b730188f238921203553f6f4c1d9d92dc77110afe","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"}]}]}],"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":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"metadata":{"producer":"strategy-protocol-fixture"}}}} -{"strategy_protocol_version":"6","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"6","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":[]}}}}} -{"strategy_protocol_version":"6","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","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":"6","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":"6","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"6","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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"}}}}} -{"strategy_protocol_version":"6","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"6","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":"6","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"6","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":[]}}}}} -{"strategy_protocol_version":"6","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"6","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"6","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"6","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"6","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v6/message.schema.json b/contracts/strategy/v6/message.schema.json deleted file mode 100644 index 143e284..0000000 --- a/contracts/strategy/v6/message.schema.json +++ /dev/null @@ -1,298 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v6/message.schema.json", - "title": "Trading Engine external strategy protocol v6 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": "6" }, - "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", "metadata"], - "properties": { - "engine_version": { "type": "string", "minLength": 1 }, - "scenario_contract_version": { "const": "8" }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/cashBalance" } - }, - "initial_portfolio": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/initialPortfolio" } - ] - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/instrument" } - }, - "venue_calendars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/venueCalendar" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/execution" }, - "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/v8/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/v8/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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/v8/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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/v8/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/v8/journal.schema.json#/$defs/groupExposure" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/signedDecimal" } - ] - }, - "markedPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], - "properties": { - "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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/v8/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/v8/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/v8/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/v8/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/v6/transcript.schema.json b/contracts/strategy/v6/transcript.schema.json deleted file mode 100644 index 3ec1993..0000000 --- a/contracts/strategy/v6/transcript.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v6/transcript.schema.json", - "title": "Trading Engine external strategy protocol v6 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": "6" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v6/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/strategy/v7/README.md b/contracts/strategy/v7/README.md deleted file mode 100644 index 8f89470..0000000 --- a/contracts/strategy/v7/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# External strategy protocol v7 - -Version 7 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 v6 remains available for scenario contract v8; earlier versions retain their frozen -shapes. - -Every message repeats `strategy_protocol_version: "7"` 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 v9 and includes the exact `initial_portfolio` -snapshot alongside the legacy cash projection. It also carries the complete versioned venue -calendars and nested execution configuration, so a strategy can construct DAY orders and reject -incompatible 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, fills, order updates, and rejected intents. Response -intents use the scenario v9 intent shapes. - -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/v7/dune b/contracts/strategy/v7/dune deleted file mode 100644 index fdc117e..0000000 --- a/contracts/strategy/v7/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v7/message.schema.json) - (transcript.schema.json as contracts/strategy/v7/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v7/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v7/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v7/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v7/fixtures/external.scenario.json b/contracts/strategy/v7/fixtures/external.scenario.json deleted file mode 100644 index 75f84da..0000000 --- a/contracts/strategy/v7/fixtures/external.scenario.json +++ /dev/null @@ -1,215 +0,0 @@ -{ - "contract_version": "9", - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/strategy/v7/fixtures/external.scenario.jsonl b/contracts/strategy/v7/fixtures/external.scenario.jsonl deleted file mode 100644 index 34227d4..0000000 --- a/contracts/strategy/v7/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"9","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}} -{"contract_version":"9","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"9","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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"9","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v7/fixtures/external.strategy.jsonl b/contracts/strategy/v7/fixtures/external.strategy.jsonl deleted file mode 100644 index 85cdd80..0000000 --- a/contracts/strategy/v7/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"7","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"9","scenario_sha256":"46f4461cb699182509ef3f7a637cf5a8b33781252da7967a50e7af32399f402e","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"}]}]}],"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":"7","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"7","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":[]}}}}} -{"strategy_protocol_version":"7","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","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":"7","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":"7","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"7","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":"7","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"7","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":"7","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"7","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":[]}}}}} -{"strategy_protocol_version":"7","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"7","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"7","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"7","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"7","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v7/message.schema.json b/contracts/strategy/v7/message.schema.json deleted file mode 100644 index 7939e75..0000000 --- a/contracts/strategy/v7/message.schema.json +++ /dev/null @@ -1,299 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v7/message.schema.json", - "title": "Trading Engine external strategy protocol v7 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": "7" }, - "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", "metadata"], - "properties": { - "engine_version": { "type": "string", "minLength": 1 }, - "scenario_contract_version": { "const": "9" }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/cashBalance" } - }, - "initial_portfolio": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/initialPortfolio" } - ] - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/instrument" } - }, - "venue_calendars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/venueCalendar" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/execution" }, - "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/v9/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/v9/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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/v9/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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/v9/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/v9/journal.schema.json#/$defs/groupExposure" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/signedDecimal" } - ] - }, - "markedPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], - "properties": { - "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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/v9/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/v9/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/v9/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/v9/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/v7/transcript.schema.json b/contracts/strategy/v7/transcript.schema.json deleted file mode 100644 index 03891d3..0000000 --- a/contracts/strategy/v7/transcript.schema.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v7/transcript.schema.json", - "title": "Trading Engine external strategy protocol v7 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": "7" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v7/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/strategy/v8/README.md b/contracts/strategy/v8/README.md deleted file mode 100644 index 5669e19..0000000 --- a/contracts/strategy/v8/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# External strategy protocol v8 - -Version 8 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 v7 remains available for scenario contract v9; earlier versions retain their frozen -shapes. - -Every message repeats `strategy_protocol_version: "8"` 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 v10 and includes the exact `initial_portfolio` -snapshot alongside the legacy cash projection. It also carries the complete versioned venue -calendars, nested execution configuration, and financing policy, so a strategy can construct DAY -orders and reject incompatible execution or financing 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, -fills, order updates, and rejected intents. Portfolio contexts include cash-interest attribution. -Response intents use the scenario v10 intent shapes, including recall-origin order snapshots. - -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/v8/dune b/contracts/strategy/v8/dune deleted file mode 100644 index c613fc4..0000000 --- a/contracts/strategy/v8/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v8/message.schema.json) - (transcript.schema.json as contracts/strategy/v8/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v8/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v8/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v8/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v8/fixtures/external.scenario.json b/contracts/strategy/v8/fixtures/external.scenario.json deleted file mode 100644 index e52dd61..0000000 --- a/contracts/strategy/v8/fixtures/external.scenario.json +++ /dev/null @@ -1,271 +0,0 @@ -{ - "contract_version": "10", - "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 - } - ] - }, - { - "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 - } - ] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - } -} diff --git a/contracts/strategy/v8/fixtures/external.scenario.jsonl b/contracts/strategy/v8/fixtures/external.scenario.jsonl deleted file mode 100644 index d89b702..0000000 --- a/contracts/strategy/v8/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"10","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"}}} -{"contract_version":"10","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}]}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"10","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}]}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"10","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v8/fixtures/external.strategy.jsonl b/contracts/strategy/v8/fixtures/external.strategy.jsonl deleted file mode 100644 index 9622950..0000000 --- a/contracts/strategy/v8/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"8","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"10","scenario_sha256":"008026ed55d30ccabe95d2bb95a843b7631e00ddde2874155459717b53823c5f","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"},"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":"8","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"8","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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}]}}}}} -{"strategy_protocol_version":"8","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","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":"8","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"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":"8","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"8","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":"8","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"8","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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":"8","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"8","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","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"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383"}],"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}]}}}}} -{"strategy_protocol_version":"8","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"8","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"8","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"8","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"8","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v8/message.schema.json b/contracts/strategy/v8/message.schema.json deleted file mode 100644 index dacc035..0000000 --- a/contracts/strategy/v8/message.schema.json +++ /dev/null @@ -1,299 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v8/message.schema.json", - "title": "Trading Engine external strategy protocol v8 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": "8" }, - "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", "metadata"], - "properties": { - "engine_version": { "type": "string", "minLength": 1 }, - "scenario_contract_version": { "const": "10" }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/cashBalance" } - }, - "initial_portfolio": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/initialPortfolio" } - ] - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/instrument" } - }, - "venue_calendars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/venueCalendar" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/financing" }, - "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/v10/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/v10/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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/v10/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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/v10/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/v10/journal.schema.json#/$defs/groupExposure" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/signedDecimal" } - ] - }, - "markedPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "mark", "base_market_value", "weight"], - "properties": { - "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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/v10/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/v10/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/v10/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/v10/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/v8/transcript.schema.json b/contracts/strategy/v8/transcript.schema.json deleted file mode 100644 index 8c66867..0000000 --- a/contracts/strategy/v8/transcript.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v8/transcript.schema.json", - "title": "Trading Engine external strategy protocol v8 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": "8" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v8/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/strategy/v9/README.md b/contracts/strategy/v9/README.md deleted file mode 100644 index c449032..0000000 --- a/contracts/strategy/v9/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# External strategy protocol v9 - -Version 9 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 v8 remains available for scenario contract v10; earlier versions retain their frozen -shapes. - -Every message repeats `strategy_protocol_version: "9"` 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 v11 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 v11 intent shapes, including recall-origin order snapshots. - -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/v9/dune b/contracts/strategy/v9/dune deleted file mode 100644 index 8354cc5..0000000 --- a/contracts/strategy/v9/dune +++ /dev/null @@ -1,15 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (message.schema.json as contracts/strategy/v9/message.schema.json) - (transcript.schema.json as contracts/strategy/v9/transcript.schema.json) - (fixtures/external.scenario.json - as - contracts/strategy/v9/fixtures/external.scenario.json) - (fixtures/external.scenario.jsonl - as - contracts/strategy/v9/fixtures/external.scenario.jsonl) - (fixtures/external.strategy.jsonl - as - contracts/strategy/v9/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v9/fixtures/external.scenario.json b/contracts/strategy/v9/fixtures/external.scenario.json deleted file mode 100644 index 7d1a4a8..0000000 --- a/contracts/strategy/v9/fixtures/external.scenario.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "contract_version": "11", - "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": [] - }, - { - "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": [] - } - ], - "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/v9/fixtures/external.scenario.jsonl b/contracts/strategy/v9/fixtures/external.scenario.jsonl deleted file mode 100644 index 605761a..0000000 --- a/contracts/strategy/v9/fixtures/external.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"11","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":"11","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":[]}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"11","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":[]}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"11","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v9/fixtures/external.strategy.jsonl b/contracts/strategy/v9/fixtures/external.strategy.jsonl deleted file mode 100644 index 0875b1b..0000000 --- a/contracts/strategy/v9/fixtures/external.strategy.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"strategy_protocol_version":"9","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"11","scenario_sha256":"d9b389a906984af1e3170e94863884a938b5c8b86025b1ccc1708e9896520278","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":"9","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} -{"strategy_protocol_version":"9","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","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":[]}}}}} -{"strategy_protocol_version":"9","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","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":"9","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","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":"9","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"9","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","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":"9","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"9","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","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":"9","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"9","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","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":[]}}}}} -{"strategy_protocol_version":"9","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} -{"strategy_protocol_version":"9","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"9","strategy_sequence":"7","message_type":"shutdown","payload":{}}} -{"strategy_protocol_version":"9","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"9","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v9/message.schema.json b/contracts/strategy/v9/message.schema.json deleted file mode 100644 index 3962824..0000000 --- a/contracts/strategy/v9/message.schema.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v9/message.schema.json", - "title": "Trading Engine external strategy protocol v9 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": "9" }, - "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": "11" }, - "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/cashBalance" } - }, - "initial_portfolio": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/initialPortfolio" } - ] - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/instrument" } - }, - "venue_calendars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/venueCalendar" } - }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/financing" }, - "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/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/v11/scenario.schema.json#/$defs/timestamp" }, - "portfolio": { "$ref": "#/$defs/portfolio" }, - "working_orders": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/journal.schema.json#/$defs/order" } - }, - "latest_bars": { - "type": "array", - "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/scenario.schema.json#/$defs/identifier" }, - "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/signedDecimal" }, - "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/signedDecimal" }, - "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/unsignedDecimal" }, - "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/unsignedDecimal" }, - "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/unsignedDecimal" }, - "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/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/v11/journal.schema.json#/$defs/groupExposure" } - } - } - }, - "optionalWeight": { - "oneOf": [ - { "type": "null" }, - { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/scenario.schema.json#/$defs/identifier" }, - "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/signedDecimal" }, - "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/signedDecimal" }, - "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/signedDecimal" }, - "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/positiveDecimal" }, - "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/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/v11/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/v11/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/v11/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/v9/transcript.schema.json b/contracts/strategy/v9/transcript.schema.json deleted file mode 100644 index 8b8fc95..0000000 --- a/contracts/strategy/v9/transcript.schema.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v9/transcript.schema.json", - "title": "Trading Engine external strategy protocol v9 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": "9" }, - "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, - "direction": { - "enum": ["engine_to_strategy", "strategy_to_engine"] - }, - "message": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v9/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/v1/README.md b/contracts/v1/README.md index e3bf5bb..0bd77e3 100644 --- a/contracts/v1/README.md +++ b/contracts/v1/README.md @@ -1,15 +1,19 @@ -# Trading Engine contract v1 +# Replay contract v1 -This frozen directory preserves the historical v1 process and file contract. The current runtime -advertises v4 and v3 only; these artifacts remain available for provenance and schema-only -compatibility testing by older consumers. +This directory is the authoritative replay contract for Trading Engine. -- `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. -- `fixtures/demo.scenario.json`, `fixtures/demo.scenario.jsonl`, and - `fixtures/demo.journal.jsonl` form the canonical valid conformance corpus. +- `scenario.schema.json` defines batch replay input. +- `scenario-stream.schema.json` defines the equivalent JSON Lines stream. +- `journal.schema.json` defines append-only audit records. +- `fixtures/` contains canonical scenarios and journals used by the conformance suite. -Every batch scenario, scenario-stream record, and journal record carries -`"contract_version": "1"`. Consumers must reject missing or unsupported versions before -interpreting the rest of a document. +Every scenario, stream record, and journal record carries `"contract_version": "1"`. +Objects are strict unless a field is explicitly open, decimal values use canonical strings, and +timestamps are bounded RFC 3339 instants. Runtime validation additionally enforces uniqueness, +causal ordering, non-overlapping slices, resource limits, and configuration coverage. + +The contract models an explicit initial portfolio, instrument-level and grouped risk policy, +execution and fee schedules, financing, settlement, venue calendars, lifecycle events, market +data, strategy intents, and causal audit output. A successful replay ends with `run_completed`. + +Run `make check` to validate schemas, fixtures, runtime behavior, and deterministic output. diff --git a/contracts/v1/dune b/contracts/v1/dune index fe01ab1..0f6b798 100644 --- a/contracts/v1/dune +++ b/contracts/v1/dune @@ -7,4 +7,28 @@ (scenario.schema.json as contracts/v1/scenario.schema.json) (fixtures/demo.journal.jsonl as contracts/v1/fixtures/demo.journal.jsonl) (fixtures/demo.scenario.json as contracts/v1/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl as contracts/v1/fixtures/demo.scenario.jsonl))) + (fixtures/demo.scenario.jsonl as contracts/v1/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v1/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v1/fixtures/fill-clipped.scenario.json) + (fixtures/quote-trade.journal.jsonl + as + contracts/v1/fixtures/quote-trade.journal.jsonl) + (fixtures/quote-trade.scenario.json + as + contracts/v1/fixtures/quote-trade.scenario.json) + (fixtures/quote-trade.scenario.jsonl + as + contracts/v1/fixtures/quote-trade.scenario.jsonl) + (fixtures/order-book.journal.jsonl + as + contracts/v1/fixtures/order-book.journal.jsonl) + (fixtures/order-book.scenario.json + as + contracts/v1/fixtures/order-book.scenario.json) + (fixtures/order-book.scenario.jsonl + as + contracts/v1/fixtures/order-book.scenario.jsonl))) diff --git a/contracts/v1/fixtures/demo.journal.jsonl b/contracts/v1/fixtures/demo.journal.jsonl index 4d59346..cdafdd3 100644 --- a/contracts/v1/fixtures/demo.journal.jsonl +++ b/contracts/v1/fixtures/demo.journal.jsonl @@ -1,20 +1,34 @@ -{"contract_version":"1","engine_sequence":"1","run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"a782fbbee8b89332ee6491d9d9be36bf7f2aaacbd440d30c20ed033fd7566d19"}} -{"contract_version":"1","engine_sequence":"2","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"}]}} -{"contract_version":"1","engine_sequence":"3","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","reference_price":"104"}]}} -{"contract_version":"1","engine_sequence":"4","run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} -{"contract_version":"1","engine_sequence":"5","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":"9","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"1","engine_sequence":"6","run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"cash":"10000","market_value":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"10000","total_fees":"0"}} -{"contract_version":"1","engine_sequence":"7","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":"12"}]}} -{"contract_version":"1","engine_sequence":"8","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","side":"buy","quantity":"6","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} -{"contract_version":"1","engine_sequence":"9","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":"9","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"1","engine_sequence":"10","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":"3","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_sequence":"10","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"1","engine_sequence":"11","run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"cash":"9381.132","market_value":"642","cost_basis":"618.868","realized_pnl":"0","unrealized_pnl":"23.132","equity":"10023.132","total_fees":"0.868"}} -{"contract_version":"1","engine_sequence":"12","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"}]}} -{"contract_version":"1","engine_sequence":"13","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","side":"buy","quantity":"3","price":"107","notional":"321","fee":"0.571","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} -{"contract_version":"1","engine_sequence":"14","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","reference_price":null}]}} -{"contract_version":"1","engine_sequence":"15","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","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_sequence":"15","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"1","engine_sequence":"16","run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"cash":"9059.561","market_value":"945","cost_basis":"940.439","realized_pnl":"0","unrealized_pnl":"4.561","equity":"10004.561","total_fees":"1.439"}} -{"contract_version":"1","engine_sequence":"17","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"}]}} -{"contract_version":"1","engine_sequence":"18","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","side":"sell","quantity":"7","price":"105","notional":"735","fee":"0.985","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} -{"contract_version":"1","engine_sequence":"19","run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"cash":"9793.576","market_value":"212","cost_basis":"208.986445","realized_pnl":"2.562445","unrealized_pnl":"3.013555","equity":"10005.576","total_fees":"2.424"}} -{"contract_version":"1","engine_sequence":"20","run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"a782fbbee8b89332ee6491d9d9be36bf7f2aaacbd440d30c20ed033fd7566d19","valuation":{"cash":"9793.576","market_value":"212","cost_basis":"208.986445","realized_pnl":"2.562445","unrealized_pnl":"3.013555","equity":"10005.576","total_fees":"2.424"},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} +{"contract_version":"1","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":"e2227af76072fab8151c3e1bd86f401293d16a736e32040efdaf8761cd397574","execution_model":"completed_bar_adverse_touch_v1"}} +{"contract_version":"1","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","execution_fee_components":[],"borrow_fees":"0.25","total_fees":"0.75","cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"10000","settled_amount":"10000","unsettled_amount":"0","fx_rate":"1","base_value":"10000","base_settled_value":"10000","base_unsettled_value":"0","interest":"0","base_interest":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","settled_quantity":"1","unsettled_quantity":"0","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","execution_fee_components":[],"borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} +{"contract_version":"1","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","execution_fee_components":[],"borrow_fees":"0.25","total_fees":"0.75","cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"10000","settled_amount":"10000","unsettled_amount":"0","fx_rate":"1","base_value":"10000","base_settled_value":"10000","base_unsettled_value":"0","interest":"0","base_interest":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","settled_quantity":"1","unsettled_quantity":"0","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","execution_fee_components":[],"borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"1","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":"1","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":"1","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":{"type":"numeric","value":"0.1"},"unit":"ratio","dimensions":{"instrument":"demo-equity-acme","source":"strategy"},"aggregation":"last"}} +{"contract_version":"1","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":"1","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","execution_fee_components":[],"borrow_fees":"0.25","total_fees":"0.75","cash_interest":"0.074201","settled_cash":"10000.074201","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"10000.074201","settled_amount":"10000.074201","unsettled_amount":"0","fx_rate":"1","base_value":"10000.074201","base_settled_value":"10000.074201","base_unsettled_value":"0","interest":"0.074201","base_interest":"0.074201"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","settled_quantity":"1","unsettled_quantity":"0","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","execution_fee_components":[],"borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75"}],"margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052.074201","maintenance_excess":"10078.074201","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"13"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"1","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":"1","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":"1","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","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"}],"executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} +{"contract_version":"1","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000013"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000001-settlement","fill_id":"demo-fill-000000000001","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-671.655735","position_movement":"6.5","trade_date":"2026-01-05","due_date":"2026-01-06","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"1","engine_sequence":"15","event_id":"demo-event-000000000015","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":"1","engine_sequence":"16","event_id":"demo-event-000000000016","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-000000000016","updated_event_id":"demo-event-000000000016","created_sequence":"16","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":"1","engine_sequence":"17","event_id":"demo-event-000000000017","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","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"}],"borrow_fees":"0.25","total_fees":"1.670735","cash_interest":"0.148402","settled_cash":"10000.148402","unsettled_cash":"-671.655735","cash_balances":[{"currency":"USD","amount":"9328.492667","settled_amount":"10000.148402","unsettled_amount":"-671.655735","fx_rate":"1","base_value":"9328.492667","base_settled_value":"10000.148402","base_unsettled_value":"-671.655735","interest":"0.148402","base_interest":"0.148402"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7.5","settled_quantity":"1","unsettled_quantity":"6.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","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"}],"borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.670735","base_total_fees":"1.670735"}],"margin":{"initial_requirement":"401.25","maintenance_requirement":"200.625","initial_excess":"9729.742667","maintenance_excess":"9930.367667","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"3","start_at":"2026-01-06T14:30:00.000000Z","end_at":"2026-01-06T21:00:00.000000Z","available_at":"2026-01-06T21:00:01.000000Z","received_at":"2026-01-06T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"107","high":"109","low":"104","close":"105","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"1","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000018"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"settlement_completed","payload":{"instruction_id":"demo-fill-000000000001-settlement","fill_id":"demo-fill-000000000001","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-671.655735","position_movement":"6.5","trade_date":"2026-01-05","due_date":"2026-01-06","status":"settled","settled_at":"2026-01-06T14:30:00.000000Z","failed_at":null,"failure_reason":null}} +{"contract_version":"1","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000018"],"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":"1","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000016","demo-event-000000000018"],"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":"1","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000021"],"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","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"}],"executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} +{"contract_version":"1","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000022"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000002-settlement","fill_id":"demo-fill-000000000002","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-237.647211","position_movement":"2.215","trade_date":"2026-01-06","due_date":"2026-01-07","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"1","engine_sequence":"24","event_id":"demo-event-000000000024","causation_ids":["demo-event-000000000018"],"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":"1","engine_sequence":"25","event_id":"demo-event-000000000025","causation_ids":["demo-event-000000000018","demo-event-000000000024"],"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-000000000025","updated_event_id":"demo-event-000000000025","created_sequence":"25","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":"1","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000018"],"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","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"}],"borrow_fees":"0.25","total_fees":"2.157896","cash_interest":"0.21762","settled_cash":"9328.561885","unsettled_cash":"-237.647211","cash_balances":[{"currency":"USD","amount":"9090.914674","settled_amount":"9328.561885","unsettled_amount":"-237.647211","fx_rate":"1","base_value":"9090.914674","base_settled_value":"9328.561885","base_unsettled_value":"-237.647211","interest":"0.21762","base_interest":"0.21762"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"9.715","settled_quantity":"7.5","unsettled_quantity":"2.215","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","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"}],"borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.157896","base_total_fees":"2.157896"}],"margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.952174","maintenance_excess":"9855.970924","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"27","event_id":"demo-event-000000000027","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"4","start_at":"2026-01-07T14:30:00.000000Z","end_at":"2026-01-07T21:00:00.000000Z","available_at":"2026-01-07T21:00:01.000000Z","received_at":"2026-01-07T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"105","high":"107","low":"103","close":"106","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"1","engine_sequence":"28","event_id":"demo-event-000000000028","causation_ids":["demo-event-000000000027"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"settlement_completed","payload":{"instruction_id":"demo-fill-000000000002-settlement","fill_id":"demo-fill-000000000002","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-237.647211","position_movement":"2.215","trade_date":"2026-01-06","due_date":"2026-01-07","status":"settled","settled_at":"2026-01-07T14:30:00.000000Z","failed_at":null,"failure_reason":null}} +{"contract_version":"1","engine_sequence":"29","event_id":"demo-event-000000000029","causation_ids":["demo-event-000000000027"],"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":"1","engine_sequence":"30","event_id":"demo-event-000000000030","causation_ids":["demo-event-000000000025","demo-event-000000000027"],"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":"1","engine_sequence":"31","event_id":"demo-event-000000000031","causation_ids":["demo-event-000000000030"],"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","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"}],"executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} +{"contract_version":"1","engine_sequence":"32","event_id":"demo-event-000000000032","causation_ids":["demo-event-000000000031"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000003-settlement","fill_id":"demo-fill-000000000003","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"755.9978","position_movement":"-7.215","trade_date":"2026-01-07","due_date":"2026-01-08","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"1","engine_sequence":"33","event_id":"demo-event-000000000033","causation_ids":["demo-event-000000000027"],"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","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"}],"borrow_fees":"0.25","total_fees":"3.157896","cash_interest":"0.285075","settled_cash":"9090.982129","unsettled_cash":"755.9978","cash_balances":[{"currency":"USD","amount":"9846.979929","settled_amount":"9090.982129","unsettled_amount":"755.9978","fx_rate":"1","base_value":"9846.979929","base_settled_value":"9090.982129","base_unsettled_value":"755.9978","interest":"0.285075","base_interest":"0.285075"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","settled_quantity":"9.715","unsettled_quantity":"-7.215","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","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"}],"borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.479929","maintenance_excess":"10045.729929","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"34","event_id":"demo-event-000000000034","causation_ids":["demo-event-000000000033"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"e2227af76072fab8151c3e1bd86f401293d16a736e32040efdaf8761cd397574","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","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"}],"borrow_fees":"0.25","total_fees":"3.157896","cash_interest":"0.285075","settled_cash":"9090.982129","unsettled_cash":"755.9978","cash_balances":[{"currency":"USD","amount":"9846.979929","settled_amount":"9090.982129","unsettled_amount":"755.9978","fx_rate":"1","base_value":"9846.979929","base_settled_value":"9090.982129","base_unsettled_value":"755.9978","interest":"0.285075","base_interest":"0.285075"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","settled_quantity":"9.715","unsettled_quantity":"-7.215","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","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"}],"borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896"}],"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/v1/fixtures/demo.scenario.json b/contracts/v1/fixtures/demo.scenario.json index fd0da5d..db90199 100644 --- a/contracts/v1/fixtures/demo.scenario.json +++ b/contracts/v1/fixtures/demo.scenario.json @@ -6,24 +6,205 @@ }, "run_id": "demo", "base_currency": "USD", - "initial_cash": "10000", + "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": "1" + "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_order_quantity": "1000", - "max_position": "1000" + "max_gross_exposure": "1000000", + "max_leverage": "2", + "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": { - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 + "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": [ @@ -42,7 +223,10 @@ { "type": "emit_metric", "name": "desired_weight", - "value": "0.1" + "value": { "type": "numeric", "value": "0.1" }, + "unit": "ratio", + "dimensions": { "source": "strategy", "instrument": "demo-equity-acme" }, + "aggregation": "last" } ] }, @@ -54,7 +238,7 @@ "targets": [ { "instrument_id": "demo-equity-acme", - "quantity": "2" + "quantity": "2.5" } ] } @@ -77,7 +261,35 @@ "close": "104", "volume": "100" } - ] + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] }, { "slice_sequence": "2", @@ -92,9 +304,37 @@ "high": "108", "low": "102", "close": "107", - "volume": "12" + "volume": "13" } - ] + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-05T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-05T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] }, { "slice_sequence": "3", @@ -111,7 +351,35 @@ "close": "105", "volume": "100" } - ] + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-06T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-06T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] }, { "slice_sequence": "4", @@ -128,7 +396,72 @@ "close": "106", "volume": "100" } - ] + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-07T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-07T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] } - ] + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "demo-equity-acme", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } } diff --git a/contracts/v1/fixtures/demo.scenario.jsonl b/contracts/v1/fixtures/demo.scenario.jsonl index 6aa3248..bcb0aa8 100644 --- a/contracts/v1/fixtures/demo.scenario.jsonl +++ b/contracts/v1/fixtures/demo.scenario.jsonl @@ -1,6 +1,6 @@ -{"contract_version":"1","payload":{"base_currency":"USD","execution":{"fee_bps":10,"fixed_fee":"0.25","participation_bps":5000},"initial_cash":"10000","instruments":[{"instrument_id":"demo-equity-acme","lot_size":"1","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"max_internal_events":1000,"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"risk":{"max_order_quantity":"1000","max_position":"1000"},"run_id":"demo"},"record_type":"scenario_header","scenario_sequence":"1"} -{"contract_version":"1","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"}],"end_at":"2026-01-02T21:00:00Z","received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"1","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":"12"}],"end_at":"2026-01-05T21:00:00Z","received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"1","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","quantity":"2"}],"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"}],"end_at":"2026-01-06T21:00:00Z","received_at":"2026-01-06T21:00:02Z","slice_sequence":"3","start_at":"2026-01-06T14:30:00Z"}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"1","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"}],"end_at":"2026-01-07T21:00:00Z","received_at":"2026-01-07T21:00:02Z","slice_sequence":"4","start_at":"2026-01-07T14:30:00Z"}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"1","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","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":"1","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","weight":"0.1"}],"type":"target_weights"},{"aggregation":"last","dimensions":{"instrument":"demo-equity-acme","source":"strategy"},"name":"desired_weight","type":"emit_metric","unit":"ratio","value":{"type":"numeric","value":"0.1"}}],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"1","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"13"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"1","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","quantity":"2.5"}],"type":"target_quantities"}],"market_slice":{"available_at":"2026-01-06T21:00:01Z","bars":[{"close":"105","high":"109","instrument_id":"demo-equity-acme","low":"104","open":"107","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-06T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-06T21:00:02Z","slice_sequence":"3","start_at":"2026-01-06T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"1","payload":{"intents":[],"market_slice":{"available_at":"2026-01-07T21:00:01Z","bars":[{"close":"106","high":"107","instrument_id":"demo-equity-acme","low":"103","open":"105","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-07T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-07T21:00:02Z","slice_sequence":"4","start_at":"2026-01-07T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"5"} {"contract_version":"1","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v1/fixtures/fill-clipped.journal.jsonl b/contracts/v1/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..7318059 --- /dev/null +++ b/contracts/v1/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"1","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":"49451cf495ed00cdcc5330a62814b9b6adeb6e4fc844e74a5ae35f5cbf91c893","execution_model":"completed_bar_v1"}} +{"contract_version":"1","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","execution_fee_components":[],"borrow_fees":"0","total_fees":"0","cash_interest":"0","settled_cash":"550","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"550","settled_amount":"550","unsettled_amount":"0","fx_rate":"1","base_value":"550","base_settled_value":"550","base_unsettled_value":"0","interest":"0","base_interest":"0"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} +{"contract_version":"1","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","execution_fee_components":[],"borrow_fees":"0","total_fees":"0","cash_interest":"0","settled_cash":"550","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"550","settled_amount":"550","unsettled_amount":"0","fx_rate":"1","base_value":"550","base_settled_value":"550","base_unsettled_value":"0","interest":"0","base_interest":"0"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"1","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":"1","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":"1","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","execution_fee_components":[],"borrow_fees":"0","total_fees":"0","cash_interest":"0.004081","settled_cash":"550.004081","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"550.004081","settled_amount":"550.004081","unsettled_amount":"0","fx_rate":"1","base_value":"550.004081","base_settled_value":"550.004081","base_unsettled_value":"0","interest":"0.004081","base_interest":"0.004081"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","settled_quantity":"0","unsettled_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","execution_fee_components":[],"borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.004081","maintenance_excess":"550.004081","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"1","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":"1","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":"1","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":"1","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","execution_fee_components":[],"borrow_fees":"0","total_fees":"0","cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"550.008162","settled_amount":"550.008162","unsettled_amount":"0","fx_rate":"1","base_value":"550.008162","base_settled_value":"550.008162","base_unsettled_value":"0","interest":"0.008162","base_interest":"0.008162"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","settled_quantity":"0","unsettled_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","execution_fee_components":[],"borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","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":"49451cf495ed00cdcc5330a62814b9b6adeb6e4fc844e74a5ae35f5cbf91c893","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","execution_fee_components":[],"borrow_fees":"0","total_fees":"0","cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"550.008162","settled_amount":"550.008162","unsettled_amount":"0","fx_rate":"1","base_value":"550.008162","base_settled_value":"550.008162","base_unsettled_value":"0","interest":"0.008162","base_interest":"0.008162"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","settled_quantity":"0","unsettled_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","execution_fee_components":[],"borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v15/fixtures/fill-clipped.scenario.json b/contracts/v1/fixtures/fill-clipped.scenario.json similarity index 98% rename from contracts/v15/fixtures/fill-clipped.scenario.json rename to contracts/v1/fixtures/fill-clipped.scenario.json index 319e246..e4edda4 100644 --- a/contracts/v15/fixtures/fill-clipped.scenario.json +++ b/contracts/v1/fixtures/fill-clipped.scenario.json @@ -1,5 +1,5 @@ { - "contract_version": "15", + "contract_version": "1", "metadata": { "producer": "trading-engine", "purpose": "fill clipping conformance fixture" @@ -79,7 +79,6 @@ "risk": { "max_gross_exposure": "1000000000", "max_leverage": "1", - "short_borrow_bps": 100, "instrument_policies": [ { "instrument_id": "clip-equity", @@ -97,7 +96,7 @@ "execution": { "model": "completed_bar_v1", "configuration": { - "version": "2", + "version": "1", "participation_bps": 10000, "fee_schedules": [ { diff --git a/contracts/v1/fixtures/order-book.journal.jsonl b/contracts/v1/fixtures/order-book.journal.jsonl new file mode 100644 index 0000000..f016002 --- /dev/null +++ b/contracts/v1/fixtures/order-book.journal.jsonl @@ -0,0 +1,15 @@ +{"contract_version":"1","engine_sequence":"1","event_id":"order-book-event-000000000001","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"d1e8ead9f40facf6b095a785e56ce0dc6f40b8e37cfff91e8de87787bff38c8c","execution_model":"order_book_v1"}} +{"contract_version":"1","engine_sequence":"2","event_id":"order-book-event-000000000002","causation_ids":["order-book-event-000000000001"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","execution_fee_components":[],"borrow_fees":"0","total_fees":"0","cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"2000","settled_amount":"2000","unsettled_amount":"0","fx_rate":"1","base_value":"2000","base_settled_value":"2000","base_unsettled_value":"0","interest":"0","base_interest":"0"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} +{"contract_version":"1","engine_sequence":"3","event_id":"order-book-event-000000000003","causation_ids":["order-book-event-000000000002"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","execution_fee_components":[],"borrow_fees":"0","total_fees":"0","cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"2000","settled_amount":"2000","unsettled_amount":"0","fx_rate":"1","base_value":"2000","base_settled_value":"2000","base_unsettled_value":"0","interest":"0","base_interest":"0"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"4","event_id":"order-book-event-000000000004","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00.000000Z","available_at":"2026-02-02T14:31:01.000000Z","received_at":"2026-02-02T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}} +{"contract_version":"1","engine_sequence":"5","event_id":"order-book-event-000000000005","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} +{"contract_version":"1","engine_sequence":"6","event_id":"order-book-event-000000000006","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"order-book-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"order-book-event-000000000006","updated_event_id":"order-book-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"1","engine_sequence":"7","event_id":"order-book-event-000000000007","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","execution_fee_components":[],"borrow_fees":"0","total_fees":"0","cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","settled_amount":"2000.01484","unsettled_amount":"0","fx_rate":"1","base_value":"2000.01484","base_settled_value":"2000.01484","base_unsettled_value":"0","interest":"0.01484","base_interest":"0.01484"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","settled_quantity":"0","unsettled_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","execution_fee_components":[],"borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"8","event_id":"order-book-event-000000000008","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00.000000Z","available_at":"2026-02-03T14:35:01.000000Z","received_at":"2026-02-03T14:35:02.000000Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00.000000Z","available_at":"2026-02-03T14:36:01.000000Z","received_at":"2026-02-03T14:36:02.000000Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}} +{"contract_version":"1","engine_sequence":"9","event_id":"order-book-event-000000000009","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} +{"contract_version":"1","engine_sequence":"10","event_id":"order-book-event-000000000010","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000001","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"100","notional":"400","fee":"10","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}],"executed_at":"2026-02-03T14:35:00.000000Z","slice_sequence":"2"}} +{"contract_version":"1","engine_sequence":"11","event_id":"order-book-event-000000000011","causation_ids":["order-book-event-000000000010"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"settlement_instruction_created","payload":{"instruction_id":"order-book-fill-000000000001-settlement","fill_id":"order-book-fill-000000000001","instrument_id":"clip-equity","currency":"USD","cash_movement":"-410","position_movement":"4","trade_date":"2026-02-03","due_date":"2026-02-04","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"1","engine_sequence":"12","event_id":"order-book-event-000000000012","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000002","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}],"executed_at":"2026-02-03T14:36:00.000000Z","slice_sequence":"2"}} +{"contract_version":"1","engine_sequence":"13","event_id":"order-book-event-000000000013","causation_ids":["order-book-event-000000000012"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"settlement_instruction_created","payload":{"instruction_id":"order-book-fill-000000000002-settlement","fill_id":"order-book-fill-000000000002","instrument_id":"clip-equity","currency":"USD","cash_movement":"-610","position_movement":"6","trade_date":"2026-02-03","due_date":"2026-02-04","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"1","engine_sequence":"14","event_id":"order-book-event-000000000014","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"borrow_fees":"0","total_fees":"20","cash_interest":"0.02968","settled_cash":"2000.02968","unsettled_cash":"-1020","cash_balances":[{"currency":"USD","amount":"980.02968","settled_amount":"2000.02968","unsettled_amount":"-1020","fx_rate":"1","base_value":"980.02968","base_settled_value":"2000.02968","base_unsettled_value":"-1020","interest":"0.02968","base_interest":"0.02968"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","settled_quantity":"0","unsettled_quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20"}],"margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"15","event_id":"order-book-event-000000000015","causation_ids":["order-book-event-000000000014"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"d1e8ead9f40facf6b095a785e56ce0dc6f40b8e37cfff91e8de87787bff38c8c","execution_model":"order_book_v1","valuation":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"borrow_fees":"0","total_fees":"20","cash_interest":"0.02968","settled_cash":"2000.02968","unsettled_cash":"-1020","cash_balances":[{"currency":"USD","amount":"980.02968","settled_amount":"2000.02968","unsettled_amount":"-1020","fx_rate":"1","base_value":"980.02968","base_settled_value":"2000.02968","base_unsettled_value":"-1020","interest":"0.02968","base_interest":"0.02968"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","settled_quantity":"0","unsettled_quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20"}],"margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v16/fixtures/order-book.scenario.json b/contracts/v1/fixtures/order-book.scenario.json similarity index 99% rename from contracts/v16/fixtures/order-book.scenario.json rename to contracts/v1/fixtures/order-book.scenario.json index 83e22e0..f107f4e 100644 --- a/contracts/v16/fixtures/order-book.scenario.json +++ b/contracts/v1/fixtures/order-book.scenario.json @@ -1,5 +1,5 @@ { - "contract_version": "16", + "contract_version": "1", "metadata": { "producer": "trading-engine", "purpose": "bounded order-book replay conformance fixture" @@ -79,7 +79,6 @@ "risk": { "max_gross_exposure": "1000000000", "max_leverage": "1", - "short_borrow_bps": 100, "instrument_policies": [ { "instrument_id": "clip-equity", diff --git a/contracts/v1/fixtures/order-book.scenario.jsonl b/contracts/v1/fixtures/order-book.scenario.jsonl new file mode 100644 index 0000000..0ef9d37 --- /dev/null +++ b/contracts/v1/fixtures/order-book.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"1","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded order-book replay conformance fixture"},"run_id":"order-book","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"order_book_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}],"max_depth_levels":10}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"1","scenario_sequence":"2","record_type":"market_slice","payload":{"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}],"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00Z","available_at":"2026-02-02T14:31:01Z","received_at":"2026-02-02T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}}} +{"contract_version":"1","scenario_sequence":"3","record_type":"market_slice","payload":{"intents":[],"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00Z","available_at":"2026-02-03T14:35:01Z","received_at":"2026-02-03T14:35:02Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00Z","available_at":"2026-02-03T14:36:01Z","received_at":"2026-02-03T14:36:02Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}}} +{"contract_version":"1","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v1/fixtures/quote-trade.journal.jsonl b/contracts/v1/fixtures/quote-trade.journal.jsonl new file mode 100644 index 0000000..615131a --- /dev/null +++ b/contracts/v1/fixtures/quote-trade.journal.jsonl @@ -0,0 +1,15 @@ +{"contract_version":"1","engine_sequence":"1","event_id":"quote-trade-event-000000000001","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"9c525db135cd830107ffd151024f807261c5e9403ec4e3734109fab1e0da8623","execution_model":"quote_trade_v1"}} +{"contract_version":"1","engine_sequence":"2","event_id":"quote-trade-event-000000000002","causation_ids":["quote-trade-event-000000000001"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","execution_fee_components":[],"borrow_fees":"0","total_fees":"0","cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"2000","settled_amount":"2000","unsettled_amount":"0","fx_rate":"1","base_value":"2000","base_settled_value":"2000","base_unsettled_value":"0","interest":"0","base_interest":"0"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} +{"contract_version":"1","engine_sequence":"3","event_id":"quote-trade-event-000000000003","causation_ids":["quote-trade-event-000000000002"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","execution_fee_components":[],"borrow_fees":"0","total_fees":"0","cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"2000","settled_amount":"2000","unsettled_amount":"0","fx_rate":"1","base_value":"2000","base_settled_value":"2000","base_unsettled_value":"0","interest":"0","base_interest":"0"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"4","event_id":"quote-trade-event-000000000004","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"1","engine_sequence":"5","event_id":"quote-trade-event-000000000005","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} +{"contract_version":"1","engine_sequence":"6","event_id":"quote-trade-event-000000000006","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"quote-trade-event-000000000006","updated_event_id":"quote-trade-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"1","engine_sequence":"7","event_id":"quote-trade-event-000000000007","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","execution_fee_components":[],"borrow_fees":"0","total_fees":"0","cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","settled_amount":"2000.01484","unsettled_amount":"0","fx_rate":"1","base_value":"2000.01484","base_settled_value":"2000.01484","base_unsettled_value":"0","interest":"0.01484","base_interest":"0.01484"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","settled_quantity":"0","unsettled_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","execution_fee_components":[],"borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"8","event_id":"quote-trade-event-000000000008","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]}} +{"contract_version":"1","engine_sequence":"9","event_id":"quote-trade-event-000000000009","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} +{"contract_version":"1","engine_sequence":"10","event_id":"quote-trade-event-000000000010","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000001","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"99","notional":"396","fee":"10","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}],"executed_at":"2026-02-03T14:33:00.000000Z","slice_sequence":"2"}} +{"contract_version":"1","engine_sequence":"11","event_id":"quote-trade-event-000000000011","causation_ids":["quote-trade-event-000000000010"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"settlement_instruction_created","payload":{"instruction_id":"quote-trade-fill-000000000001-settlement","fill_id":"quote-trade-fill-000000000001","instrument_id":"clip-equity","currency":"USD","cash_movement":"-406","position_movement":"4","trade_date":"2026-02-03","due_date":"2026-02-04","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"1","engine_sequence":"12","event_id":"quote-trade-event-000000000012","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000002","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}],"executed_at":"2026-02-03T14:34:00.000000Z","slice_sequence":"2"}} +{"contract_version":"1","engine_sequence":"13","event_id":"quote-trade-event-000000000013","causation_ids":["quote-trade-event-000000000012"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"settlement_instruction_created","payload":{"instruction_id":"quote-trade-fill-000000000002-settlement","fill_id":"quote-trade-fill-000000000002","instrument_id":"clip-equity","currency":"USD","cash_movement":"-610","position_movement":"6","trade_date":"2026-02-03","due_date":"2026-02-04","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} +{"contract_version":"1","engine_sequence":"14","event_id":"quote-trade-event-000000000014","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"borrow_fees":"0","total_fees":"20","cash_interest":"0.02968","settled_cash":"2000.02968","unsettled_cash":"-1016","cash_balances":[{"currency":"USD","amount":"984.02968","settled_amount":"2000.02968","unsettled_amount":"-1016","fx_rate":"1","base_value":"984.02968","base_settled_value":"2000.02968","base_unsettled_value":"-1016","interest":"0.02968","base_interest":"0.02968"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","settled_quantity":"0","unsettled_quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20"}],"margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]}} +{"contract_version":"1","engine_sequence":"15","event_id":"quote-trade-event-000000000015","causation_ids":["quote-trade-event-000000000014"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"9c525db135cd830107ffd151024f807261c5e9403ec4e3734109fab1e0da8623","execution_model":"quote_trade_v1","valuation":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"borrow_fees":"0","total_fees":"20","cash_interest":"0.02968","settled_cash":"2000.02968","unsettled_cash":"-1016","cash_balances":[{"currency":"USD","amount":"984.02968","settled_amount":"2000.02968","unsettled_amount":"-1016","fx_rate":"1","base_value":"984.02968","base_settled_value":"2000.02968","base_unsettled_value":"-1016","interest":"0.02968","base_interest":"0.02968"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","settled_quantity":"0","unsettled_quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20"}],"margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v15/fixtures/quote-trade.scenario.json b/contracts/v1/fixtures/quote-trade.scenario.json similarity index 99% rename from contracts/v15/fixtures/quote-trade.scenario.json rename to contracts/v1/fixtures/quote-trade.scenario.json index 6eb58b8..42e6682 100644 --- a/contracts/v15/fixtures/quote-trade.scenario.json +++ b/contracts/v1/fixtures/quote-trade.scenario.json @@ -1,5 +1,5 @@ { - "contract_version": "15", + "contract_version": "1", "metadata": { "producer": "trading-engine", "purpose": "bounded quote and trade replay fixture" @@ -79,7 +79,6 @@ "risk": { "max_gross_exposure": "1000000000", "max_leverage": "1", - "short_borrow_bps": 100, "instrument_policies": [ { "instrument_id": "clip-equity", diff --git a/contracts/v1/fixtures/quote-trade.scenario.jsonl b/contracts/v1/fixtures/quote-trade.scenario.jsonl new file mode 100644 index 0000000..b02c21c --- /dev/null +++ b/contracts/v1/fixtures/quote-trade.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"1","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded quote and trade replay fixture"},"run_id":"quote-trade","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"quote_trade_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"1","scenario_sequence":"2","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]},"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}]}} +{"contract_version":"1","scenario_sequence":"3","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]},"intents":[]}} +{"contract_version":"1","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v1/journal.schema.json b/contracts/v1/journal.schema.json index 4308c58..a181854 100644 --- a/contracts/v1/journal.schema.json +++ b/contracts/v1/journal.schema.json @@ -7,82 +7,400 @@ "required": [ "contract_version", "engine_sequence", + "event_id", + "causation_ids", "run_id", "recorded_at", "event_type", "payload" ], "properties": { - "contract_version": { "const": "1" }, - "engine_sequence": { "$ref": "#/$defs/sequence" }, - "run_id": { "$ref": "#/$defs/identifier" }, - "recorded_at": { "$ref": "#/$defs/timestamp" }, + "contract_version": { + "const": "1" + }, + "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", - "cash_limited", + "settlement_instruction_created", + "settlement_completed", + "settlement_failed", + "fill_clipped", + "borrow_charge_applied", + "borrow_recall_received", + "cash_interest_applied", + "margin_call", + "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed" ] }, - "payload": { "type": "object" } + "payload": { + "type": "object" + } }, "allOf": [ { - "if": { "properties": { "event_type": { "const": "run_started" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/runStarted" } } } + "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": "market_slice_received" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/marketSlice" } } } + "if": { + "properties": { + "event_type": { + "const": "order_adjusted" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/orderAdjusted" + } + } + } }, { - "if": { "properties": { "event_type": { "const": "target_portfolio_requested" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/targetPortfolio" } } } + "if": { + "properties": { + "event_type": { + "const": "fill_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/fill" + } + } + } }, { - "if": { "properties": { "event_type": { "const": "order_accepted" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/order" } } } + "if": { + "properties": { + "event_type": { + "enum": [ + "settlement_instruction_created", + "settlement_completed", + "settlement_failed" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/settlementInstruction" + } + } + } }, { - "if": { "properties": { "event_type": { "const": "order_rejected" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/order" } } } + "if": { + "properties": { + "event_type": { + "const": "fill_clipped" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/fillClipped" + } + } + } }, { - "if": { "properties": { "event_type": { "const": "order_cancelled" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/orderCancelled" } } } + "if": { + "properties": { + "event_type": { + "const": "borrow_charge_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowCharge" + } + } + } }, { - "if": { "properties": { "event_type": { "const": "fill_applied" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/fill" } } } + "if": { + "properties": { + "event_type": { + "const": "borrow_recall_received" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowRecall" + } + } + } }, { - "if": { "properties": { "event_type": { "const": "cash_limited" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/cashLimited" } } } + "if": { + "properties": { + "event_type": { + "const": "cash_interest_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/cashInterest" + } + } + } }, { - "if": { "properties": { "event_type": { "const": "intent_rejected" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/intentRejected" } } } + "if": { + "properties": { + "event_type": { + "enum": [ + "margin_call", + "margin_restored", + "valuation" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/valuation" + } + } + } }, { - "if": { "properties": { "event_type": { "const": "metric_emitted" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/metric" } } } + "if": { + "properties": { + "event_type": { + "const": "intent_rejected" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/intentRejected" + } + } + } }, { - "if": { "properties": { "event_type": { "const": "valuation" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/valuation" } } } + "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" } } } + "if": { + "properties": { + "event_type": { + "const": "run_completed" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/runCompleted" + } + } + } } ], "$defs": { @@ -91,58 +409,190 @@ "minLength": 1, "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" }, - "unsignedDecimal": { + "signedDecimal": { "type": "string", - "pattern": "^(?:0|[1-9][0-9]*)(?:[.][0-9]{0,5}[1-9])?$" + "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" }, - "signedDecimal": { + "unsignedDecimal": { "type": "string", - "pattern": "^(?:0(?:[.][0-9]{0,5}[1-9])?|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?|-(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" + "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])?)$" }, - "weight": { + "sequence": { "type": "string", - "pattern": "^(?:0(?:[.][0-9]{0,5}[1-9])?|1)$" + "pattern": "^[1-9][0-9]*$" }, - "quantity": { + "nonnegativeSequence": { "type": "string", "pattern": "^(?:0|[1-9][0-9]*)$" }, - "positiveQuantity": { "type": "string", "pattern": "^[1-9][0-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})$" }, - "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "runStarted": { "type": "object", "additionalProperties": false, - "required": ["scenario_sha256"], - "properties": { "scenario_sha256": { "$ref": "#/$defs/sha256" } } + "required": [ + "scenario_sha256", + "execution_model" + ], + "properties": { + "scenario_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "execution_model": { + "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] + } + } + }, + "initialState": { + "type": "object", + "additionalProperties": false, + "required": [ + "portfolio", + "valuation" + ], + "properties": { + "portfolio": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/initialPortfolio" + }, + "valuation": { + "$ref": "#/$defs/valuation" + } + } }, "bar": { "type": "object", "additionalProperties": false, - "required": ["instrument_id", "open", "high", "low", "close", "volume"], + "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" }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "open": { + "$ref": "#/$defs/positiveDecimal" + }, + "high": { + "$ref": "#/$defs/positiveDecimal" + }, + "low": { + "$ref": "#/$defs/positiveDecimal" + }, + "close": { + "$ref": "#/$defs/positiveDecimal" + }, "volume": { "oneOf": [ - { "$ref": "#/$defs/quantity" }, - { "type": "null" } + { + "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/v1/scenario.schema.json#/$defs/fractionalPolicy" } + } + } + ] + }, "marketSlice": { "type": "object", "additionalProperties": false, @@ -152,72 +602,166 @@ "end_at", "available_at", "received_at", - "bars" + "bars", + "fx_rates", + "corporate_actions", + "borrow_observations", + "cash_rate_observations", + "settlement_failures", + "lifecycle_events", + "market_events", + "order_book_events" ], "properties": { - "slice_sequence": { "$ref": "#/$defs/sequence" }, - "start_at": { "$ref": "#/$defs/timestamp" }, - "end_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, + "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" } + "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/v1/scenario.schema.json#/$defs/borrowObservation" + } + }, + "cash_rate_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/cashRateObservation" + } + }, + "settlement_failures": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/settlementFailure" + } + }, + "lifecycle_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/lifecycleEvent" + } + }, + "market_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/marketEvent" + } + }, + "order_book_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/orderBookEvent" + } } } }, - "requestedWeightTarget": { + "settlementInstruction": { "type": "object", "additionalProperties": false, - "required": ["instrument_id", "weight", "quantity", "reference_price"], + "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" }, - "weight": { "$ref": "#/$defs/weight" }, - "quantity": { "$ref": "#/$defs/quantity" }, - "reference_price": { "$ref": "#/$defs/positiveDecimal" } + "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)?$" }] } } }, - "requestedQuantityTarget": { + "targetPortfolio": { "type": "object", "additionalProperties": false, - "required": ["instrument_id", "weight", "quantity", "reference_price"], + "required": [ + "basis", + "targets" + ], "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "weight": { "type": "null" }, - "quantity": { "$ref": "#/$defs/quantity" }, - "reference_price": { "type": "null" } - } - }, - "targetPortfolio": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["basis", "targets"], - "properties": { - "basis": { "const": "weights" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/requestedWeightTarget" } - } - } + "basis": { + "enum": [ + "weights", + "quantities" + ] }, - { - "type": "object", - "additionalProperties": false, - "required": ["basis", "targets"], - "properties": { - "basis": { "const": "quantities" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/requestedQuantityTarget" } + "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", @@ -228,49 +772,321 @@ "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/positiveQuantity" }, - "order_kind": { "enum": ["market", "limit"] }, + "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": [ - { "$ref": "#/$defs/positiveDecimal" }, - { "type": "null" } + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } ] }, - "origin": { "enum": ["direct", "target_rebalance"] }, - "created_sequence": { "$ref": "#/$defs/sequence" }, - "created_at": { "$ref": "#/$defs/timestamp" }, - "eligible_after_slice_sequence": { "$ref": "#/$defs/sequence" }, - "filled_quantity": { "$ref": "#/$defs/quantity" }, - "filled_notional": { "$ref": "#/$defs/unsignedDecimal" }, + "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"] + "enum": [ + "working", + "partially_filled", + "filled", + "cancelled", + "rejected" + ] }, "rejection_reason": { - "oneOf": [{ "type": "string" }, { "type": "null" }] + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "minLength": 1 + } + ] } } }, "orderCancelled": { "type": "object", "additionalProperties": false, - "required": ["order", "reason"], + "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/v1/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": { "$ref": "#/$defs/order" }, - "reason": { "enum": ["strategy_requested", "target_replaced", "market_ioc"] } + "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": { @@ -280,102 +1096,1286 @@ "fill_id", "order_id", "instrument_id", + "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", - "slice_sequence" + "slice_sequence", + "fee_components" ], "properties": { - "fill_id": { "$ref": "#/$defs/identifier" }, - "order_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveQuantity" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "notional": { "$ref": "#/$defs/unsignedDecimal" }, - "fee": { "$ref": "#/$defs/unsignedDecimal" }, - "executed_at": { "$ref": "#/$defs/timestamp" }, - "slice_sequence": { "$ref": "#/$defs/sequence" } + "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" + } } }, - "cashLimited": { + "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": [ - "order_id", "instrument_id", - "requested_quantity", - "affordable_quantity", - "price" + "unit", + "value" ], "properties": { - "order_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "requested_quantity": { "$ref": "#/$defs/positiveQuantity" }, - "affordable_quantity": { "$ref": "#/$defs/quantity" }, - "price": { "$ref": "#/$defs/positiveDecimal" } + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "quantity" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } } }, - "intentRejected": { + "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": ["reason"], - "properties": { "reason": { "type": "string", "minLength": 1 } } + "required": ["currency", "unit", "value"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "unit": { "const": "money" }, + "value": { "$ref": "#/$defs/unsignedDecimal" } + } }, - "metric": { + "settlementPositionThreshold": { "type": "object", "additionalProperties": false, - "required": ["name", "value"], + "required": ["instrument_id", "unit", "value"], "properties": { - "name": { "type": "string", "minLength": 1 }, - "value": { "type": "string" } + "instrument_id": { "$ref": "#/$defs/identifier" }, + "unit": { "const": "quantity" }, + "value": { "$ref": "#/$defs/unsignedDecimal" } } }, - "valuation": { + "instrumentBasisPointsThreshold": { "type": "object", "additionalProperties": false, "required": [ - "cash", - "market_value", - "cost_basis", - "realized_pnl", - "unrealized_pnl", - "equity", - "total_fees" + "instrument_id", + "unit", + "value" ], "properties": { - "cash": { "$ref": "#/$defs/unsignedDecimal" }, - "market_value": { "$ref": "#/$defs/unsignedDecimal" }, - "cost_basis": { "$ref": "#/$defs/unsignedDecimal" }, - "realized_pnl": { "$ref": "#/$defs/signedDecimal" }, - "unrealized_pnl": { "$ref": "#/$defs/signedDecimal" }, - "equity": { "$ref": "#/$defs/unsignedDecimal" }, - "total_fees": { "$ref": "#/$defs/unsignedDecimal" } + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "basis_points" + }, + "value": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + } } }, - "orderCounts": { + "instrumentShortingThreshold": { "type": "object", "additionalProperties": false, - "required": ["total", "active", "filled", "rejected", "cancelled"], + "required": [ + "instrument_id", + "value" + ], "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 } + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "value": { + "const": false + } } }, - "runCompleted": { + "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": ["scenario_sha256", "valuation", "order_counts"], + "required": [ + "group_id", + "unit", + "value" + ], "properties": { - "scenario_sha256": { "$ref": "#/$defs/sha256" }, - "valuation": { "$ref": "#/$defs/valuation" }, - "order_counts": { "$ref": "#/$defs/orderCounts" } + "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" + } + } + }, + "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/v1/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/v1/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/v1/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", "minLength": 1, "maxLength": 128, "pattern": "^\\S(?:.*\\S)?$" }, + "value": { "$ref": "#/$defs/metricValue" }, + "unit": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, + "dimensions": { + "type": "object", + "maxProperties": 16, + "propertyNames": { "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, + "additionalProperties": { "type": "string", "maxLength": 128 } + }, + "aggregation": { "enum": ["last", "sum", "minimum", "maximum", "mean"] } + }, + "allOf": [ + { "if": { "properties": { "value": { "properties": { "type": { "enum": ["string", "boolean"] } } } } }, "then": { "properties": { "aggregation": { "enum": ["last"] } } } } + ] + }, + "metricValue": { + "oneOf": [ + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "numeric" }, "value": { "type": "string", "pattern": "^(0|[1-9][0-9]*|-[1-9][0-9]*)(\\.[0-9]*[1-9])?$|^-0\\.[0-9]*[1-9]$" } } }, + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "string" }, "value": { "type": "string", "maxLength": 1024 } } }, + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "boolean" }, "value": { "type": "boolean" } } } + ] + }, + "runCompleted": { + "type": "object", + "additionalProperties": false, + "required": [ + "scenario_sha256", + "execution_model", + "valuation", + "order_counts" + ], + "properties": { + "scenario_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "execution_model": { + "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] + }, + "valuation": { + "$ref": "#/$defs/valuation" + }, + "order_counts": { + "type": "object", + "additionalProperties": false, + "required": [ + "total", + "active", + "filled", + "rejected", + "cancelled" + ], + "properties": { + "total": { + "type": "integer", + "minimum": 0 + }, + "active": { + "type": "integer", + "minimum": 0 + }, + "filled": { + "type": "integer", + "minimum": 0 + }, + "rejected": { + "type": "integer", + "minimum": 0 + }, + "cancelled": { + "type": "integer", + "minimum": 0 + } + } + } } } } diff --git a/contracts/v1/scenario-stream.schema.json b/contracts/v1/scenario-stream.schema.json index c1c3d91..6743c1a 100644 --- a/contracts/v1/scenario-stream.schema.json +++ b/contracts/v1/scenario-stream.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/fallblu/trading-engine/contracts/v1/scenario-stream.schema.json", "title": "Trading Engine v1 replay scenario stream record", - "description": "The structural contract for one record in a bounded-memory JSON Lines replay scenario. The engine additionally checks record order, terminal counts, catalog coverage, market ordering, causality, risk, tick, lot, time, and OHLC invariants.", + "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" }, @@ -12,12 +12,7 @@ "headerRecord": { "type": "object", "additionalProperties": false, - "required": [ - "contract_version", - "scenario_sequence", - "record_type", - "payload" - ], + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], "properties": { "contract_version": { "const": "1" }, "scenario_sequence": { "const": "1" }, @@ -28,17 +23,10 @@ "sliceRecord": { "type": "object", "additionalProperties": false, - "required": [ - "contract_version", - "scenario_sequence", - "record_type", - "payload" - ], + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], "properties": { "contract_version": { "const": "1" }, - "scenario_sequence": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/sequence" - }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/sequence" }, "record_type": { "const": "market_slice" }, "payload": { "$ref": "#/$defs/slicePayload" } } @@ -46,59 +34,35 @@ "endRecord": { "type": "object", "additionalProperties": false, - "required": [ - "contract_version", - "scenario_sequence", - "record_type", - "payload" - ], + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], "properties": { "contract_version": { "const": "1" }, - "scenario_sequence": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/sequence" - }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/sequence" }, "record_type": { "const": "scenario_end" }, - "payload": { "$ref": "#/$defs/endPayload" } + "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_cash", - "instruments", - "risk", - "execution", - "max_internal_events" - ], + "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/v1/scenario.schema.json#/$defs/identifier" - }, - "base_currency": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/identifier" - }, - "initial_cash": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/unsignedDecimal" - }, - "instruments": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/instrument" - } - }, - "risk": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/risk" - }, - "execution": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/execution" - }, - "max_internal_events": { "type": "integer", "minimum": 1 } + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/settlement" }, + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 } } }, "slicePayload": { @@ -106,25 +70,8 @@ "additionalProperties": false, "required": ["market_slice", "intents"], "properties": { - "market_slice": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/marketSlice" - }, - "intents": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/intent" - } - } - } - }, - "endPayload": { - "type": "object", - "additionalProperties": false, - "required": ["slice_count"], - "properties": { - "slice_count": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/quantity" - } + "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json#/$defs/intent" } } } } } diff --git a/contracts/v1/scenario.schema.json b/contracts/v1/scenario.schema.json index 2d1ffaf..48d09a1 100644 --- a/contracts/v1/scenario.schema.json +++ b/contracts/v1/scenario.schema.json @@ -2,255 +2,549 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/fallblu/trading-engine/contracts/v1/scenario.schema.json", "title": "Trading Engine v1 replay scenario", - "description": "The strict structural input contract. The engine additionally checks catalog coverage, ordering, causality, risk, tick, lot, time, and OHLC invariants.", + "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_cash", - "instruments", - "risk", - "execution", - "max_internal_events", - "schedule", - "slices" - ], + "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": "1" }, "metadata": { "type": "object" }, "run_id": { "$ref": "#/$defs/identifier" }, "base_currency": { "$ref": "#/$defs/identifier" }, - "initial_cash": { "$ref": "#/$defs/unsignedDecimal" }, + "initial_portfolio": { "$ref": "#/$defs/initialPortfolio" }, "instruments": { "type": "array", "minItems": 1, + "maxItems": 4096, "items": { "$ref": "#/$defs/instrument" } }, - "risk": { "$ref": "#/$defs/risk" }, - "execution": { "$ref": "#/$defs/execution" }, - "max_internal_events": { "type": "integer", "minimum": 1 }, - "schedule": { + "venue_calendars": { "type": "array", - "items": { "$ref": "#/$defs/scheduleItem" } + "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|[1-9][0-9]*)(?:[.][0-9]{0,5}[1-9])?$" + "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])?)$" }, - "weight": { - "type": "string", - "pattern": "^(?:0(?:[.][0-9]{0,5}[1-9])?|1)$" - }, - "quantity": { - "type": "string", - "pattern": "^(?:0|[1-9][0-9]*)$" - }, - "positiveQuantity": { - "type": "string", - "pattern": "^[1-9][0-9]*$" - }, - "sequence": { - "type": "string", - "pattern": "^[1-9][0-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})$" }, - "instrument": { + "cashBalance": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "amount"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "amount": { "$ref": "#/$defs/signedDecimal" } + } + }, + "initialPortfolio": { "type": "object", "additionalProperties": false, - "required": [ - "instrument_id", - "symbol", - "quote_currency", - "tick_size", - "lot_size" - ], + "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" }, - "symbol": { "$ref": "#/$defs/identifier" }, - "quote_currency": { "$ref": "#/$defs/identifier" }, - "tick_size": { "$ref": "#/$defs/positiveDecimal" }, - "lot_size": { "$ref": "#/$defs/positiveQuantity" } + "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" } } }, - "risk": { + "initialMark": { "type": "object", "additionalProperties": false, - "required": ["max_order_quantity", "max_position"], + "required": ["instrument_id", "price"], "properties": { - "max_order_quantity": { "$ref": "#/$defs/positiveQuantity" }, - "max_position": { "$ref": "#/$defs/positiveQuantity" } + "instrument_id": { "$ref": "#/$defs/identifier" }, + "price": { "$ref": "#/$defs/positiveDecimal" } } }, - "execution": { + "instrument": { "type": "object", "additionalProperties": false, - "required": ["participation_bps", "fixed_fee", "fee_bps"], + "required": ["instrument_id", "symbol", "quote_currency", "tick_size", "lot_size"], "properties": { - "participation_bps": { - "type": "integer", - "minimum": 0, - "maximum": 10000 - }, - "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, - "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "quote_currency": { "$ref": "#/$defs/identifier" }, + "tick_size": { "$ref": "#/$defs/positiveDecimal" }, + "lot_size": { "$ref": "#/$defs/positiveDecimal" } } }, - "scheduleItem": { + "venueCalendar": { "type": "object", "additionalProperties": false, - "required": ["after_slice_sequence", "intents"], + "required": ["calendar_id", "calendar_version", "venue_id", "instrument_ids", "sessions"], "properties": { - "after_slice_sequence": { "$ref": "#/$defs/sequence" }, - "intents": { + "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", - "items": { "$ref": "#/$defs/intent" } + "minItems": 1, + "items": { "$ref": "#/$defs/venueSession" } } } }, - "intent": { + "venueSession": { "oneOf": [ - { "$ref": "#/$defs/targetWeightsIntent" }, - { "$ref": "#/$defs/targetQuantitiesIntent" }, - { "$ref": "#/$defs/marketOrderIntent" }, - { "$ref": "#/$defs/limitOrderIntent" }, - { "$ref": "#/$defs/cancelOrderIntent" }, - { "$ref": "#/$defs/metricIntent" } + { "$ref": "#/$defs/openVenueSession" }, + { "$ref": "#/$defs/holidayVenueSession" } ] }, - "targetWeightsIntent": { + "openVenueSession": { "type": "object", "additionalProperties": false, - "required": ["type", "targets"], + "required": ["session_date", "policy", "phases"], "properties": { - "type": { "const": "target_weights" }, - "targets": { + "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, - "items": { "$ref": "#/$defs/weightTarget" } + "maxItems": 5, + "items": { "$ref": "#/$defs/venuePhase" } } } }, - "weightTarget": { + "holidayVenueSession": { "type": "object", "additionalProperties": false, - "required": ["instrument_id", "weight"], + "required": ["session_date", "policy", "phases"], "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "weight": { "$ref": "#/$defs/weight" } + "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "policy": { "const": "holiday" }, + "phases": { "type": "array", "maxItems": 0 } } }, - "targetQuantitiesIntent": { + "venuePhase": { "type": "object", "additionalProperties": false, - "required": ["type", "targets"], + "required": ["phase", "opens_at", "closes_at"], "properties": { - "type": { "const": "target_quantities" }, - "targets": { + "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", "instrument_policies", "groups"], + "properties": { + "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, + "instrument_policies": { "type": "array", "minItems": 1, - "items": { "$ref": "#/$defs/quantityTarget" } + "items": { "$ref": "#/$defs/instrumentRiskPolicy" } + }, + "groups": { + "type": "array", + "items": { "$ref": "#/$defs/riskGroup" } } } }, - "quantityTarget": { + "instrumentRiskPolicy": { "type": "object", "additionalProperties": false, - "required": ["instrument_id", "quantity"], + "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" }, - "quantity": { "$ref": "#/$defs/quantity" } + "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" } } }, - "orderFields": { + "nullablePositiveDecimal": { + "oneOf": [ + { "type": "null" }, + { "$ref": "#/$defs/positiveDecimal" } + ] + }, + "riskGroup": { "type": "object", - "required": [ - "type", - "instrument_id", - "side", - "quantity", - "order_kind", - "limit_price" - ], + "additionalProperties": false, + "required": ["group_id", "group_version", "group_type", "instrument_ids", "limits"], "properties": { - "type": { "const": "submit_order" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveQuantity" } + "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" } } }, - "marketOrderIntent": { - "allOf": [ - { "$ref": "#/$defs/orderFields" }, + "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": [ - "type", - "instrument_id", - "side", - "quantity", - "order_kind", - "limit_price" - ], + "required": ["model", "configuration"], "properties": { - "type": true, - "instrument_id": true, - "side": true, - "quantity": true, - "order_kind": { "const": "market" }, - "limit_price": { "type": "null" } + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } } - } - ] - }, - "limitOrderIntent": { - "allOf": [ - { "$ref": "#/$defs/orderFields" }, + }, { "type": "object", "additionalProperties": false, - "required": [ - "type", - "instrument_id", - "side", - "quantity", - "order_kind", - "limit_price" - ], + "required": ["model", "configuration"], "properties": { - "type": true, - "instrument_id": true, - "side": true, - "quantity": true, - "order_kind": { "const": "limit" }, - "limit_price": { "$ref": "#/$defs/positiveDecimal" } + "model": { "enum": ["completed_bar_next_open_v1", "completed_bar_adverse_touch_v1"] }, + "configuration": { "$ref": "#/$defs/conservativeBarConfiguration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "quote_trade_v1" }, + "configuration": { "$ref": "#/$defs/quoteTradeConfiguration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "order_book_v1" }, + "configuration": { "$ref": "#/$defs/orderBookConfiguration" } } } ] }, - "cancelOrderIntent": { + "completedBarV1Configuration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } + } + }, + "conservativeBarConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules", "spread_model", "impact_model"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, + "spread_model": { "$ref": "#/$defs/fixedSpreadModel" }, + "impact_model": { "$ref": "#/$defs/linearImpactModel" } + } + }, + "quoteTradeConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } + } + }, + "orderBookConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules", "max_depth_levels"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, + "max_depth_levels": { "type": "integer", "minimum": 1, "maximum": 1024 } + } + }, + "fixedSpreadModel": { + "type": "object", + "additionalProperties": false, + "required": ["model", "half_spread_bps"], + "properties": { + "model": { "const": "fixed_half_spread_v1" }, + "half_spread_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "linearImpactModel": { + "type": "object", + "additionalProperties": false, + "required": ["model", "coefficient_bps", "missing_volume_policy"], + "properties": { + "model": { "const": "linear_participation_v1" }, + "coefficient_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "missing_volume_policy": { "enum": ["reject", "zero_impact"] } + } + }, + "feeSchedule": { + "type": "object", + "additionalProperties": false, + "required": ["schedule_id", "instrument_id", "settlement_currency", "minimum", "maximum", "components"], + "properties": { + "schedule_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "settlement_currency": { "$ref": "#/$defs/identifier" }, + "minimum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, + "maximum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, + "components": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeComponent" } } + } + }, + "feeComponent": { + "type": "object", + "additionalProperties": false, + "required": ["name", "currency", "kind", "value", "rounding", "applies_to"], + "properties": { + "name": { "$ref": "#/$defs/identifier" }, + "currency": { "$ref": "#/$defs/identifier" }, + "kind": { "enum": ["fixed", "notional_bps", "per_unit"] }, + "value": { "oneOf": [{ "$ref": "#/$defs/signedDecimal" }, { "type": "integer", "minimum": -10000, "maximum": 10000 }] }, + "rounding": { "enum": ["up", "down", "nearest"] }, + "applies_to": { "enum": ["any", "maker", "taker"] } + }, + "allOf": [ + { "if": { "properties": { "kind": { "const": "notional_bps" } } }, "then": { "properties": { "value": { "type": "integer" } } } }, + { "if": { "properties": { "kind": { "enum": ["fixed", "per_unit"] } } }, "then": { "properties": { "value": { "$ref": "#/$defs/signedDecimal" } } } } + ] + }, + "scheduleItem": { + "type": "object", + "additionalProperties": false, + "required": ["after_slice_sequence", "intents"], + "properties": { + "after_slice_sequence": { "$ref": "#/$defs/sequence" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "#/$defs/intent" } } + } + }, + "intent": { + "oneOf": [ + { "$ref": "#/$defs/targetWeights" }, + { "$ref": "#/$defs/targetQuantities" }, + { "$ref": "#/$defs/submitOrder" }, + { "$ref": "#/$defs/cancelOrder" }, + { "$ref": "#/$defs/metric" } + ] + }, + "targetWeights": { + "type": "object", + "additionalProperties": false, + "required": ["type", "targets"], + "properties": { + "type": { "const": "target_weights" }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "weight"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "weight": { "$ref": "#/$defs/signedDecimal" } + } + } + } + } + }, + "targetQuantities": { + "type": "object", + "additionalProperties": false, + "required": ["type", "targets"], + "properties": { + "type": { "const": "target_quantities" }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "quantity": { "$ref": "#/$defs/signedDecimal" } + } + } + } + } + }, + "submitOrder": { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "side", "quantity", "order_kind", "trigger_price", "limit_price", "time_in_force", "venue_id", "calendar_id", "expires_at"], + "properties": { + "type": { "const": "submit_order" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "order_kind": { "enum": ["market", "limit", "stop", "stop_limit"] }, + "trigger_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, + "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, + "time_in_force": { "enum": ["gtc", "ioc", "fok", "day", "gtd"] }, + "venue_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, + "calendar_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, + "expires_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] } + }, + "allOf": [ + { "if": { "properties": { "order_kind": { "const": "market" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "type": "null" } } } }, + { "if": { "properties": { "order_kind": { "const": "limit" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, + { "if": { "properties": { "order_kind": { "const": "stop" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "type": "null" } } } }, + { "if": { "properties": { "order_kind": { "const": "stop_limit" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, + { "if": { "properties": { "time_in_force": { "const": "day" } } }, "then": { "properties": { "venue_id": { "$ref": "#/$defs/identifier" }, "calendar_id": { "$ref": "#/$defs/identifier" }, "expires_at": { "type": "null" } } } }, + { "if": { "properties": { "time_in_force": { "const": "gtd" } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "$ref": "#/$defs/timestamp" } } } }, + { "if": { "properties": { "time_in_force": { "enum": ["gtc", "ioc", "fok"] } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "type": "null" } } } } + ] + }, + "cancelOrder": { "type": "object", "additionalProperties": false, "required": ["type", "order_id"], @@ -259,38 +553,53 @@ "order_id": { "$ref": "#/$defs/identifier" } } }, - "metricIntent": { + "metric": { "type": "object", "additionalProperties": false, "required": ["type", "name", "value"], "properties": { "type": { "const": "emit_metric" }, - "name": { "type": "string" }, - "value": { "type": "string" } - } + "name": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^\\S(?:.*\\S)?$" }, + "value": { "$ref": "#/$defs/metricValue" }, + "unit": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, + "dimensions": { + "type": "object", + "maxProperties": 16, + "propertyNames": { "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, + "additionalProperties": { "type": "string", "maxLength": 128 } + }, + "aggregation": { "enum": ["last", "sum", "minimum", "maximum", "mean"] } + }, + "allOf": [ + { "if": { "properties": { "value": { "properties": { "type": { "enum": ["string", "boolean"] } } } } }, "then": { "properties": { "aggregation": { "enum": ["last"] } } } } + ] + }, + "metricValue": { + "oneOf": [ + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "numeric" }, "value": { "type": "string", "pattern": "^(0|[1-9][0-9]*|-[1-9][0-9]*)(\\.[0-9]*[1-9])?$|^-0\\.[0-9]*[1-9]$" } } }, + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "string" }, "value": { "type": "string", "maxLength": 1024 } } }, + { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "boolean" }, "value": { "type": "boolean" } } } + ] }, "marketSlice": { "type": "object", "additionalProperties": false, - "required": [ - "slice_sequence", - "start_at", - "end_at", - "available_at", - "received_at", - "bars" - ], + "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "market_events", "order_book_events", "fx_rates", "corporate_actions", "borrow_observations", "cash_rate_observations", "settlement_failures", "lifecycle_events"], "properties": { "slice_sequence": { "$ref": "#/$defs/sequence" }, "start_at": { "$ref": "#/$defs/timestamp" }, "end_at": { "$ref": "#/$defs/timestamp" }, "available_at": { "$ref": "#/$defs/timestamp" }, "received_at": { "$ref": "#/$defs/timestamp" }, - "bars": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/bar" } - } + "bars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/bar" } }, + "market_events": { "type": "array", "items": { "$ref": "#/$defs/marketEvent" } }, + "order_book_events": { "type": "array", "items": { "$ref": "#/$defs/orderBookEvent" } }, + "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } }, + "corporate_actions": { "type": "array", "items": { "$ref": "#/$defs/corporateAction" } }, + "borrow_observations": { "type": "array", "items": { "$ref": "#/$defs/borrowObservation" } }, + "cash_rate_observations": { "type": "array", "items": { "$ref": "#/$defs/cashRateObservation" } }, + "settlement_failures": { "type": "array", "items": { "$ref": "#/$defs/settlementFailure" } }, + "lifecycle_events": { "type": "array", "items": { "$ref": "#/$defs/lifecycleEvent" } } } }, "bar": { @@ -303,13 +612,276 @@ "high": { "$ref": "#/$defs/positiveDecimal" }, "low": { "$ref": "#/$defs/positiveDecimal" }, "close": { "$ref": "#/$defs/positiveDecimal" }, - "volume": { - "oneOf": [ - { "$ref": "#/$defs/quantity" }, - { "type": "null" } - ] + "volume": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] } + } + }, + "marketEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "bid_price", "bid_quantity", "ask_price", "ask_quantity"], + "properties": { + "type": { "const": "quote" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "bid_price": { "$ref": "#/$defs/positiveDecimal" }, + "bid_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "ask_price": { "$ref": "#/$defs/positiveDecimal" }, + "ask_quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "price", "quantity", "aggressor_side"], + "properties": { + "type": { "const": "trade" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "aggressor_side": { "enum": ["buy", "sell", "unknown"] } + } } + ] + }, + "orderBookLevel": { + "type": "object", + "additionalProperties": false, + "required": ["price", "quantity"], + "properties": { + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" } } + }, + "orderBookEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "bids", "asks"], + "properties": { + "type": { "const": "snapshot" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "bids": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } }, + "asks": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price", "quantity"], + "properties": { + "type": { "const": "set" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "side": { "enum": ["bid", "ask"] }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price"], + "properties": { + "type": { "const": "delete" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "side": { "enum": ["bid", "ask"] }, + "price": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "price", "quantity", "aggressor_side"], + "properties": { + "type": { "const": "trade" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "aggressor_side": { "enum": ["buy", "sell", "unknown"] } + } + } + ] + }, + "fxRate": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "rate"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "rate": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "corporateAction": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "numerator", "denominator"], + "properties": { + "type": { "const": "split" }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "amount_per_unit"], + "properties": { + "type": { "const": "cash_dividend" }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "amount_per_unit": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], + "properties": { + "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "destination_instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" }, + "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fractional_policy": { "$ref": "#/$defs/fractionalPolicy" } + } + } + ] + }, + "fractionalPolicy": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["policy"], + "properties": { "policy": { "const": "reject" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["policy", "price", "currency"], + "properties": { + "policy": { "const": "cash_in_lieu" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "currency": { "$ref": "#/$defs/identifier" } + } + } + ] + }, + "terminalPolicy": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["policy"], + "properties": { "policy": { "const": "hold" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["policy", "price", "currency"], + "properties": { + "policy": { "const": "cash_out" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "currency": { "$ref": "#/$defs/identifier" } + } + } + ] + }, + "lifecycleEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "reason"], + "properties": { + "type": { "const": "halt" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "reason": { "type": "string", "minLength": 1 } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id"], + "properties": { + "type": { "const": "resume" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "symbol", "provider", "provider_instrument_id"], + "properties": { + "type": { "const": "identifier_change" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "provider": { "$ref": "#/$defs/identifier" }, + "provider_instrument_id": { "$ref": "#/$defs/identifier" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "terminal_policy"], + "properties": { + "type": { "const": "expiration" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "terminal_policy": { "$ref": "#/$defs/terminalPolicy" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "terminal_policy", "reason"], + "properties": { + "type": { "const": "delisting" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "terminal_policy": { "$ref": "#/$defs/terminalPolicy" }, + "reason": { "type": "string", "minLength": 1 } + } + } + ] } } } diff --git a/contracts/v10/README.md b/contracts/v10/README.md deleted file mode 100644 index 09a77f5..0000000 --- a/contracts/v10/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Trading Engine contract v10 - -This directory is the authoritative v10 process and file contract shared by Trading Engine and its -clients. Versions 9, 8, 7, 6, 5, 4, and 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 v10 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 v10 scenario, stream record, and journal record carries `"contract_version": "10"`. - -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. diff --git a/contracts/v10/dune b/contracts/v10/dune deleted file mode 100644 index 07b28ed..0000000 --- a/contracts/v10/dune +++ /dev/null @@ -1,18 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v10/journal.schema.json) - (scenario-stream.schema.json as contracts/v10/scenario-stream.schema.json) - (scenario.schema.json as contracts/v10/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v10/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v10/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl - as - contracts/v10/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v10/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v10/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v10/fixtures/demo.journal.jsonl b/contracts/v10/fixtures/demo.journal.jsonl deleted file mode 100644 index 7d920e6..0000000 --- a/contracts/v10/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,26 +0,0 @@ -{"contract_version":"10","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":"c129c13b6e4273c4d2a784c5b9aaa2630ad67ecbafd6da303da12b2fb6f253bc","execution_model":"completed_bar_v1"}} -{"contract_version":"10","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} -{"contract_version":"10","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} -{"contract_version":"10","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}]}} -{"contract_version":"10","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":"10","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":"10","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":"10","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":"10","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0.074201","margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052.074201","maintenance_excess":"10078.074201","margin_call":false},"group_exposures":[]}} -{"contract_version":"10","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":"12"}],"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}]}} -{"contract_version":"10","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":"10","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":"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","price":"103","notional":"618","fee":"0.868","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.618","quote_amount":"0.618"}]}} -{"contract_version":"10","engine_sequence":"13","event_id":"demo-event-000000000013","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","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"10","engine_sequence":"14","event_id":"demo-event-000000000014","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.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-000000000014","updated_event_id":"demo-event-000000000014","created_sequence":"14","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":"10","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.280402","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5.148402","unrealized_pnl":"40.132","equity":"10130.280402","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.280402","fx_rate":"1","base_value":"9381.280402","interest":"0.148402","base_interest":"0.148402"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618","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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}]}],"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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"cash_interest":"0.148402","margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.780402","maintenance_excess":"9943.030402","margin_call":false},"group_exposures":[]}} -{"contract_version":"10","engine_sequence":"16","event_id":"demo-event-000000000016","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}]}} -{"contract_version":"10","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":["demo-event-000000000016"],"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":"9381.280402","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.06961","closing_balance":"9381.350012"}} -{"contract_version":"10","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000014","demo-event-000000000016"],"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.715","price":"107","notional":"290.505","fee":"0.540505","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.290505","quote_amount":"0.290505"}]}} -{"contract_version":"10","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000016"],"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":"10","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000016","demo-event-000000000019"],"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-000000000020","updated_event_id":"demo-event-000000000020","created_sequence":"20","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":"10","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000016"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.304507","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5.218012","unrealized_pnl":"20.161495","equity":"10110.379507","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.304507","fx_rate":"1","base_value":"9090.304507","interest":"0.218012","base_interest":"0.218012"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505","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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}]}],"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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"cash_interest":"0.218012","margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.342007","maintenance_excess":"9855.360757","margin_call":false},"group_exposures":[]}} -{"contract_version":"10","engine_sequence":"22","event_id":"demo-event-000000000022","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}]}} -{"contract_version":"10","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000022"],"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.304507","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.067451","closing_balance":"9090.371958"}} -{"contract_version":"10","engine_sequence":"24","event_id":"demo-event-000000000024","causation_ids":["demo-event-000000000020","demo-event-000000000022"],"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":"105","notional":"757.575","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.757575","quote_amount":"0.757575"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_amount":"-0.007575"}]}} -{"contract_version":"10","engine_sequence":"25","event_id":"demo-event-000000000025","causation_ids":["demo-event-000000000022"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}]}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]}} -{"contract_version":"10","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000025"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"c129c13b6e4273c4d2a784c5b9aaa2630ad67ecbafd6da303da12b2fb6f253bc","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}]}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v10/fixtures/demo.scenario.json b/contracts/v10/fixtures/demo.scenario.json deleted file mode 100644 index 9f0332b..0000000 --- a/contracts/v10/fixtures/demo.scenario.json +++ /dev/null @@ -1,411 +0,0 @@ -{ - "contract_version": "10", - "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_v1", - "configuration": { - "version": "2", - "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" - } - ] - } - ] - } - }, - "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 - } - ] - }, - { - "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": "12" - } - ], - "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 - } - ] - }, - { - "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 - } - ] - }, - { - "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 - } - ] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - } -} diff --git a/contracts/v10/fixtures/demo.scenario.jsonl b/contracts/v10/fixtures/demo.scenario.jsonl deleted file mode 100644 index 1ce27fc..0000000 --- a/contracts/v10/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"10","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_v1","configuration":{"version":"2","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"}]}]}},"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"}}} -{"contract_version":"10","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}]}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"10","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":"12"}],"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}]}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"10","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}]}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"10","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}]}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"10","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v10/fixtures/fill-clipped.journal.jsonl b/contracts/v10/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index d363391..0000000 --- a/contracts/v10/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"contract_version":"10","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":"19f11d500905b44278fb098678e3a8467dedd02f5b98e126f0ed1a8e5773d53d","execution_model":"completed_bar_v1"}} -{"contract_version":"10","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"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} -{"contract_version":"10","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"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"10","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}]}} -{"contract_version":"10","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":"10","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":"10","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0.004081","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.004081","maintenance_excess":"550.004081","margin_call":false},"group_exposures":[]}} -{"contract_version":"10","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}]}} -{"contract_version":"10","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":"10","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":"10","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":"10","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0.008162","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]}} -{"contract_version":"10","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":"19f11d500905b44278fb098678e3a8467dedd02f5b98e126f0ed1a8e5773d53d","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"}],"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":[]}],"execution_fee_components":[],"cash_interest":"0.008162","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/v10/fixtures/fill-clipped.scenario.json b/contracts/v10/fixtures/fill-clipped.scenario.json deleted file mode 100644 index a408f1f..0000000 --- a/contracts/v10/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,236 +0,0 @@ -{ - "contract_version": "10", - "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 - } - ] - }, - { - "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 - } - ] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - } -} diff --git a/contracts/v10/journal.schema.json b/contracts/v10/journal.schema.json deleted file mode 100644 index fc11e1c..0000000 --- a/contracts/v10/journal.schema.json +++ /dev/null @@ -1,2193 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v10/journal.schema.json", - "title": "Trading Engine v10 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": "10" - }, - "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", - "order_adjusted", - "fill_applied", - "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": "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": "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": { - "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": { - "const": "completed_bar_v1" - } - } - }, - "initialState": { - "type": "object", - "additionalProperties": false, - "required": [ - "portfolio", - "valuation" - ], - "properties": { - "portfolio": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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" - } - } - } - ] - }, - "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" - ], - "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/v10/scenario.schema.json#/$defs/borrowObservation" - } - }, - "cash_rate_observations": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/cashRateObservation" - } - } - } - }, - "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" - ] - }, - "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" - } - } - }, - "orderAdjusted": { - "type": "object", - "additionalProperties": false, - "required": [ - "order", - "action_id" - ], - "properties": { - "order": { - "$ref": "#/$defs/order" - }, - "action_id": { - "$ref": "#/$defs/identifier" - } - } - }, - "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" - } - } - }, - "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": "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/v10/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/v10/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/v10/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" - ], - "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" - } - } - }, - "positionAttribution": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "quote_currency", - "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" - }, - "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", - "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" - }, - "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": { - "const": "completed_bar_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/v10/scenario-stream.schema.json b/contracts/v10/scenario-stream.schema.json deleted file mode 100644 index 5f50c87..0000000 --- a/contracts/v10/scenario-stream.schema.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v10/scenario-stream.schema.json", - "title": "Trading Engine v10 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": "10" }, - "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": "10" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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": "10" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/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", "max_internal_events"], - "properties": { - "metadata": { "type": "object" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/identifier" }, - "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/initialPortfolio" }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/instrument" } }, - "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/financing" }, - "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/v10/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v10/scenario.schema.json b/contracts/v10/scenario.schema.json deleted file mode 100644 index c470212..0000000 --- a/contracts/v10/scenario.schema.json +++ /dev/null @@ -1,510 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v10/scenario.schema.json", - "title": "Trading Engine v10 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", "max_internal_events", "schedule", "slices"], - "properties": { - "contract_version": { "const": "10" }, - "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" }, - "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": { - "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": { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } - } - }, - "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" } } - } - }, - "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"], - "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" } } - } - }, - "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" } - } - } - ] - } - } -} diff --git a/contracts/v11/README.md b/contracts/v11/README.md deleted file mode 100644 index cd3f5ae..0000000 --- a/contracts/v11/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Trading Engine contract v11 - -This directory is the authoritative v11 process and file contract shared by Trading Engine and its -clients. Versions 10 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 v11 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 v11 scenario, stream record, and journal record carries `"contract_version": "11"`. - -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. diff --git a/contracts/v11/dune b/contracts/v11/dune deleted file mode 100644 index a0608ce..0000000 --- a/contracts/v11/dune +++ /dev/null @@ -1,18 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v11/journal.schema.json) - (scenario-stream.schema.json as contracts/v11/scenario-stream.schema.json) - (scenario.schema.json as contracts/v11/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v11/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v11/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl - as - contracts/v11/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v11/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v11/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v11/fixtures/demo.journal.jsonl b/contracts/v11/fixtures/demo.journal.jsonl deleted file mode 100644 index d8fd545..0000000 --- a/contracts/v11/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,31 +0,0 @@ -{"contract_version":"11","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":"eb93b72479e9a6dc49111ba26dce90e04e98538fffc2717095a3f1d98be2a8c1","execution_model":"completed_bar_v1"}} -{"contract_version":"11","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":"11","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":"11","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":[]}} -{"contract_version":"11","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":"11","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":"11","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":"11","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":"11","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":"11","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":"12"}],"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":[]}} -{"contract_version":"11","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":"11","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":"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","price":"103","notional":"618","fee":"0.868","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.618","quote_amount":"0.618"}]}} -{"contract_version":"11","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":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000001-settlement","fill_id":"demo-fill-000000000001","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-618.868","position_movement":"6","trade_date":"2026-01-05","due_date":"2026-01-06","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} -{"contract_version":"11","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","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"11","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.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-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":"11","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":"9381.280402","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5.148402","unrealized_pnl":"40.132","equity":"10130.280402","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.280402","fx_rate":"1","base_value":"9381.280402","interest":"0.148402","base_interest":"0.148402","settled_amount":"10000.148402","unsettled_amount":"-618.868","base_settled_value":"10000.148402","base_unsettled_value":"-618.868"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618","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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"settled_quantity":"1","unsettled_quantity":"6"}],"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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"cash_interest":"0.148402","settled_cash":"10000.148402","unsettled_cash":"-618.868","margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.780402","maintenance_excess":"9943.030402","margin_call":false},"group_exposures":[]}} -{"contract_version":"11","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":[]}} -{"contract_version":"11","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":"settlement_completed","payload":{"instruction_id":"demo-fill-000000000001-settlement","fill_id":"demo-fill-000000000001","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-618.868","position_movement":"6","trade_date":"2026-01-05","due_date":"2026-01-06","status":"settled","settled_at":"2026-01-06T14:30:00.000000Z","failed_at":null,"failure_reason":null}} -{"contract_version":"11","engine_sequence":"19","event_id":"demo-event-000000000019","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":"9381.280402","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.06961","closing_balance":"9381.350012"}} -{"contract_version":"11","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"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.715","price":"107","notional":"290.505","fee":"0.540505","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.290505","quote_amount":"0.290505"}]}} -{"contract_version":"11","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000020"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000002-settlement","fill_id":"demo-fill-000000000002","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-291.045505","position_movement":"2.715","trade_date":"2026-01-06","due_date":"2026-01-07","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} -{"contract_version":"11","engine_sequence":"22","event_id":"demo-event-000000000022","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":"11","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000017","demo-event-000000000022"],"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-000000000023","updated_event_id":"demo-event-000000000023","created_sequence":"23","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":"11","engine_sequence":"24","event_id":"demo-event-000000000024","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.304507","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5.218012","unrealized_pnl":"20.161495","equity":"10110.379507","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.304507","fx_rate":"1","base_value":"9090.304507","interest":"0.218012","base_interest":"0.218012","settled_amount":"9381.350012","unsettled_amount":"-291.045505","base_settled_value":"9381.350012","base_unsettled_value":"-291.045505"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505","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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"settled_quantity":"7","unsettled_quantity":"2.715"}],"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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"cash_interest":"0.218012","settled_cash":"9381.350012","unsettled_cash":"-291.045505","margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.342007","maintenance_excess":"9855.360757","margin_call":false},"group_exposures":[]}} -{"contract_version":"11","engine_sequence":"25","event_id":"demo-event-000000000025","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":[]}} -{"contract_version":"11","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000025"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"settlement_completed","payload":{"instruction_id":"demo-fill-000000000002-settlement","fill_id":"demo-fill-000000000002","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-291.045505","position_movement":"2.715","trade_date":"2026-01-06","due_date":"2026-01-07","status":"settled","settled_at":"2026-01-07T14:30:00.000000Z","failed_at":null,"failure_reason":null}} -{"contract_version":"11","engine_sequence":"27","event_id":"demo-event-000000000027","causation_ids":["demo-event-000000000025"],"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.304507","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.067451","closing_balance":"9090.371958"}} -{"contract_version":"11","engine_sequence":"28","event_id":"demo-event-000000000028","causation_ids":["demo-event-000000000023","demo-event-000000000025"],"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":"105","notional":"757.575","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.757575","quote_amount":"0.757575"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_amount":"-0.007575"}]}} -{"contract_version":"11","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":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000003-settlement","fill_id":"demo-fill-000000000003","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"756.575","position_movement":"-7.215","trade_date":"2026-01-07","due_date":"2026-01-08","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} -{"contract_version":"11","engine_sequence":"30","event_id":"demo-event-000000000030","causation_ids":["demo-event-000000000025"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463","settled_amount":"9090.371958","unsettled_amount":"756.575","base_settled_value":"9090.371958","base_unsettled_value":"756.575"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"settled_quantity":"9.715","unsettled_quantity":"-7.215"}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","settled_cash":"9090.371958","unsettled_cash":"756.575","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]}} -{"contract_version":"11","engine_sequence":"31","event_id":"demo-event-000000000031","causation_ids":["demo-event-000000000030"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"eb93b72479e9a6dc49111ba26dce90e04e98538fffc2717095a3f1d98be2a8c1","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463","settled_amount":"9090.371958","unsettled_amount":"756.575","base_settled_value":"9090.371958","base_unsettled_value":"756.575"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"settled_quantity":"9.715","unsettled_quantity":"-7.215"}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","settled_cash":"9090.371958","unsettled_cash":"756.575","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v11/fixtures/demo.scenario.json b/contracts/v11/fixtures/demo.scenario.json deleted file mode 100644 index 0747373..0000000 --- a/contracts/v11/fixtures/demo.scenario.json +++ /dev/null @@ -1,444 +0,0 @@ -{ - "contract_version": "11", - "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_v1", - "configuration": { - "version": "2", - "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" - } - ] - } - ] - } - }, - "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": [] - }, - { - "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": "12" - } - ], - "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": [] - }, - { - "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": [] - }, - { - "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": [] - } - ], - "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/v11/fixtures/demo.scenario.jsonl b/contracts/v11/fixtures/demo.scenario.jsonl deleted file mode 100644 index bc7d5d0..0000000 --- a/contracts/v11/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"11","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_v1","configuration":{"version":"2","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"}]}]}},"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":"11","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":[]}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"11","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":"12"}],"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":[]}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"11","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":[]}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"11","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":[]}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"11","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v11/fixtures/fill-clipped.journal.jsonl b/contracts/v11/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index 9ad7d70..0000000 --- a/contracts/v11/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"contract_version":"11","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":"8235142ca2e31e8db6f25372b1079adbb218b8b310bfd7a95672f766f43e6808","execution_model":"completed_bar_v1"}} -{"contract_version":"11","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":"11","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":"11","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":[]}} -{"contract_version":"11","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":"11","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":"11","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":"11","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":[]}} -{"contract_version":"11","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":"11","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":"11","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":"11","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":"11","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":"8235142ca2e31e8db6f25372b1079adbb218b8b310bfd7a95672f766f43e6808","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/v11/fixtures/fill-clipped.scenario.json b/contracts/v11/fixtures/fill-clipped.scenario.json deleted file mode 100644 index 5729f37..0000000 --- a/contracts/v11/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,267 +0,0 @@ -{ - "contract_version": "11", - "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": [] - }, - { - "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": [] - } - ], - "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/v11/journal.schema.json b/contracts/v11/journal.schema.json deleted file mode 100644 index d0e9c2d..0000000 --- a/contracts/v11/journal.schema.json +++ /dev/null @@ -1,2314 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v11/journal.schema.json", - "title": "Trading Engine v11 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": "11" - }, - "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", - "order_adjusted", - "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": "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": "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": { - "const": "completed_bar_v1" - } - } - }, - "initialState": { - "type": "object", - "additionalProperties": false, - "required": [ - "portfolio", - "valuation" - ], - "properties": { - "portfolio": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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" - } - } - } - ] - }, - "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" - ], - "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/v11/scenario.schema.json#/$defs/borrowObservation" - } - }, - "cash_rate_observations": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/cashRateObservation" - } - }, - "settlement_failures": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/settlementFailure" - } - } - } - }, - "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" - ] - }, - "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" - } - } - }, - "orderAdjusted": { - "type": "object", - "additionalProperties": false, - "required": [ - "order", - "action_id" - ], - "properties": { - "order": { - "$ref": "#/$defs/order" - }, - "action_id": { - "$ref": "#/$defs/identifier" - } - } - }, - "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/v11/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/v11/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/v11/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": { - "const": "completed_bar_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/v11/scenario-stream.schema.json b/contracts/v11/scenario-stream.schema.json deleted file mode 100644 index ede8140..0000000 --- a/contracts/v11/scenario-stream.schema.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v11/scenario-stream.schema.json", - "title": "Trading Engine v11 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": "11" }, - "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": "11" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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": "11" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/identifier" }, - "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/initialPortfolio" }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/instrument" } }, - "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/financing" }, - "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/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/v11/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v11/scenario.schema.json b/contracts/v11/scenario.schema.json deleted file mode 100644 index 8198197..0000000 --- a/contracts/v11/scenario.schema.json +++ /dev/null @@ -1,552 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v11/scenario.schema.json", - "title": "Trading Engine v11 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": "11" }, - "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": { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } - } - }, - "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" } } - } - }, - "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"], - "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" } } - } - }, - "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" } - } - } - ] - } - } -} diff --git a/contracts/v12/README.md b/contracts/v12/README.md deleted file mode 100644 index 970097d..0000000 --- a/contracts/v12/README.md +++ /dev/null @@ -1,85 +0,0 @@ -# Trading Engine contract v12 - -This directory is the authoritative v12 process and file contract shared by Trading Engine and its -clients. Versions 11 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 v12 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 v12 scenario, stream record, and journal record carries `"contract_version": "12"`. - -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. diff --git a/contracts/v12/dune b/contracts/v12/dune deleted file mode 100644 index 5cff266..0000000 --- a/contracts/v12/dune +++ /dev/null @@ -1,18 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v12/journal.schema.json) - (scenario-stream.schema.json as contracts/v12/scenario-stream.schema.json) - (scenario.schema.json as contracts/v12/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v12/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v12/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl - as - contracts/v12/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v12/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v12/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v12/fixtures/demo.journal.jsonl b/contracts/v12/fixtures/demo.journal.jsonl deleted file mode 100644 index 204d4df..0000000 --- a/contracts/v12/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,31 +0,0 @@ -{"contract_version":"12","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":"ee80423182d458afa2458803c30af1d18f0a8d873bbfe4e16c510920a6aee7d3","execution_model":"completed_bar_v1"}} -{"contract_version":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12"}],"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":"12","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":"12","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":"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","price":"103","notional":"618","fee":"0.868","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.618","quote_amount":"0.618"}]}} -{"contract_version":"12","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":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000001-settlement","fill_id":"demo-fill-000000000001","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-618.868","position_movement":"6","trade_date":"2026-01-05","due_date":"2026-01-06","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} -{"contract_version":"12","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","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"12","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.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-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":"12","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":"9381.280402","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5.148402","unrealized_pnl":"40.132","equity":"10130.280402","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.280402","fx_rate":"1","base_value":"9381.280402","interest":"0.148402","base_interest":"0.148402","settled_amount":"10000.148402","unsettled_amount":"-618.868","base_settled_value":"10000.148402","base_unsettled_value":"-618.868"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618","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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"settled_quantity":"1","unsettled_quantity":"6"}],"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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"cash_interest":"0.148402","settled_cash":"10000.148402","unsettled_cash":"-618.868","margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.780402","maintenance_excess":"9943.030402","margin_call":false},"group_exposures":[]}} -{"contract_version":"12","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":"12","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":"settlement_completed","payload":{"instruction_id":"demo-fill-000000000001-settlement","fill_id":"demo-fill-000000000001","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-618.868","position_movement":"6","trade_date":"2026-01-05","due_date":"2026-01-06","status":"settled","settled_at":"2026-01-06T14:30:00.000000Z","failed_at":null,"failure_reason":null}} -{"contract_version":"12","engine_sequence":"19","event_id":"demo-event-000000000019","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":"9381.280402","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.06961","closing_balance":"9381.350012"}} -{"contract_version":"12","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"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.715","price":"107","notional":"290.505","fee":"0.540505","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.290505","quote_amount":"0.290505"}]}} -{"contract_version":"12","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000020"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000002-settlement","fill_id":"demo-fill-000000000002","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-291.045505","position_movement":"2.715","trade_date":"2026-01-06","due_date":"2026-01-07","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} -{"contract_version":"12","engine_sequence":"22","event_id":"demo-event-000000000022","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":"12","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000017","demo-event-000000000022"],"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-000000000023","updated_event_id":"demo-event-000000000023","created_sequence":"23","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":"12","engine_sequence":"24","event_id":"demo-event-000000000024","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.304507","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5.218012","unrealized_pnl":"20.161495","equity":"10110.379507","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.304507","fx_rate":"1","base_value":"9090.304507","interest":"0.218012","base_interest":"0.218012","settled_amount":"9381.350012","unsettled_amount":"-291.045505","base_settled_value":"9381.350012","base_unsettled_value":"-291.045505"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505","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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"settled_quantity":"7","unsettled_quantity":"2.715"}],"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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"cash_interest":"0.218012","settled_cash":"9381.350012","unsettled_cash":"-291.045505","margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.342007","maintenance_excess":"9855.360757","margin_call":false},"group_exposures":[]}} -{"contract_version":"12","engine_sequence":"25","event_id":"demo-event-000000000025","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":"12","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000025"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"settlement_completed","payload":{"instruction_id":"demo-fill-000000000002-settlement","fill_id":"demo-fill-000000000002","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"-291.045505","position_movement":"2.715","trade_date":"2026-01-06","due_date":"2026-01-07","status":"settled","settled_at":"2026-01-07T14:30:00.000000Z","failed_at":null,"failure_reason":null}} -{"contract_version":"12","engine_sequence":"27","event_id":"demo-event-000000000027","causation_ids":["demo-event-000000000025"],"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.304507","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.067451","closing_balance":"9090.371958"}} -{"contract_version":"12","engine_sequence":"28","event_id":"demo-event-000000000028","causation_ids":["demo-event-000000000023","demo-event-000000000025"],"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":"105","notional":"757.575","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.757575","quote_amount":"0.757575"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_amount":"-0.007575"}]}} -{"contract_version":"12","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":"settlement_instruction_created","payload":{"instruction_id":"demo-fill-000000000003-settlement","fill_id":"demo-fill-000000000003","instrument_id":"demo-equity-acme","currency":"USD","cash_movement":"756.575","position_movement":"-7.215","trade_date":"2026-01-07","due_date":"2026-01-08","status":"pending","settled_at":null,"failed_at":null,"failure_reason":null}} -{"contract_version":"12","engine_sequence":"30","event_id":"demo-event-000000000030","causation_ids":["demo-event-000000000025"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463","settled_amount":"9090.371958","unsettled_amount":"756.575","base_settled_value":"9090.371958","base_unsettled_value":"756.575"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"settled_quantity":"9.715","unsettled_quantity":"-7.215"}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","settled_cash":"9090.371958","unsettled_cash":"756.575","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]}} -{"contract_version":"12","engine_sequence":"31","event_id":"demo-event-000000000031","causation_ids":["demo-event-000000000030"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"ee80423182d458afa2458803c30af1d18f0a8d873bbfe4e16c510920a6aee7d3","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.946958","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"19.25872","unrealized_pnl":"7.688238","equity":"10111.946958","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.946958","fx_rate":"1","base_value":"9846.946958","interest":"0.285463","base_interest":"0.285463","settled_amount":"9090.371958","unsettled_amount":"756.575","base_settled_value":"9090.371958","base_unsettled_value":"756.575"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"settled_quantity":"9.715","unsettled_quantity":"-7.215"}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"cash_interest":"0.285463","settled_cash":"9090.371958","unsettled_cash":"756.575","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.446958","maintenance_excess":"10045.696958","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v12/fixtures/demo.scenario.json b/contracts/v12/fixtures/demo.scenario.json deleted file mode 100644 index cbabef0..0000000 --- a/contracts/v12/fixtures/demo.scenario.json +++ /dev/null @@ -1,444 +0,0 @@ -{ - "contract_version": "12", - "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_v1", - "configuration": { - "version": "2", - "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" - } - ] - } - ] - } - }, - "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": "12" - } - ], - "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/v12/fixtures/demo.scenario.jsonl b/contracts/v12/fixtures/demo.scenario.jsonl deleted file mode 100644 index d4e5fbb..0000000 --- a/contracts/v12/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"12","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_v1","configuration":{"version":"2","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"}]}]}},"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":"12","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":"12","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":"12"}],"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":"12","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":"12","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":"12","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v12/fixtures/fill-clipped.journal.jsonl b/contracts/v12/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index 168bcc2..0000000 --- a/contracts/v12/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"contract_version":"12","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":"24baff6bd716f87e9bb1aab521527ba056c089390c442e6e650ea193df12abfe","execution_model":"completed_bar_v1"}} -{"contract_version":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"12","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":"24baff6bd716f87e9bb1aab521527ba056c089390c442e6e650ea193df12abfe","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/v12/fixtures/fill-clipped.scenario.json b/contracts/v12/fixtures/fill-clipped.scenario.json deleted file mode 100644 index ff34f55..0000000 --- a/contracts/v12/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,267 +0,0 @@ -{ - "contract_version": "12", - "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/v12/journal.schema.json b/contracts/v12/journal.schema.json deleted file mode 100644 index 210f8e3..0000000 --- a/contracts/v12/journal.schema.json +++ /dev/null @@ -1,2394 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v12/journal.schema.json", - "title": "Trading Engine v12 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": "12" - }, - "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", - "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": "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": { - "const": "completed_bar_v1" - } - } - }, - "initialState": { - "type": "object", - "additionalProperties": false, - "required": [ - "portfolio", - "valuation" - ], - "properties": { - "portfolio": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/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/v12/scenario.schema.json#/$defs/borrowObservation" - } - }, - "cash_rate_observations": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/cashRateObservation" - } - }, - "settlement_failures": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/settlementFailure" - } - }, - "lifecycle_events": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/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" - } - } - }, - "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/v12/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/v12/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/v12/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": { - "const": "completed_bar_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/v12/scenario-stream.schema.json b/contracts/v12/scenario-stream.schema.json deleted file mode 100644 index e6aa4dd..0000000 --- a/contracts/v12/scenario-stream.schema.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v12/scenario-stream.schema.json", - "title": "Trading Engine v12 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": "12" }, - "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": "12" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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": "12" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/identifier" }, - "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/initialPortfolio" }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/instrument" } }, - "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/financing" }, - "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/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/v12/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v12/scenario.schema.json b/contracts/v12/scenario.schema.json deleted file mode 100644 index 82c0c8c..0000000 --- a/contracts/v12/scenario.schema.json +++ /dev/null @@ -1,669 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v12/scenario.schema.json", - "title": "Trading Engine v12 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": "12" }, - "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": { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } - } - }, - "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" } } - } - }, - "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/contracts/v13/README.md b/contracts/v13/README.md deleted file mode 100644 index 093283c..0000000 --- a/contracts/v13/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# 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 deleted file mode 100644 index e8c2b5c..0000000 --- a/contracts/v13/dune +++ /dev/null @@ -1,18 +0,0 @@ -(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 deleted file mode 100644 index 4379ef7..0000000 --- a/contracts/v13/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,29 +0,0 @@ -{"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 deleted file mode 100644 index a5d6f3b..0000000 --- a/contracts/v13/fixtures/demo.scenario.json +++ /dev/null @@ -1,453 +0,0 @@ -{ - "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 deleted file mode 100644 index b42027a..0000000 --- a/contracts/v13/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"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 deleted file mode 100644 index 995c19a..0000000 --- a/contracts/v13/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"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 deleted file mode 100644 index 9884ef4..0000000 --- a/contracts/v13/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,267 +0,0 @@ -{ - "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 deleted file mode 100644 index 1c63368..0000000 --- a/contracts/v13/journal.schema.json +++ /dev/null @@ -1,2413 +0,0 @@ -{ - "$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 deleted file mode 100644 index 22d7003..0000000 --- a/contracts/v13/scenario-stream.schema.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "$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 deleted file mode 100644 index 0a072d1..0000000 --- a/contracts/v13/scenario.schema.json +++ /dev/null @@ -1,713 +0,0 @@ -{ - "$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/contracts/v14/README.md b/contracts/v14/README.md deleted file mode 100644 index cf247f4..0000000 --- a/contracts/v14/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Trading Engine contract v14 - -This directory is the authoritative v14 process and file contract shared by Trading Engine and its -clients. Versions 13 through 3 remain readable during their client transitions. - -- `scenario.schema.json` validates batch replay inputs. -- `scenario-stream.schema.json` validates each JSON Lines scenario-stream record. -- `journal.schema.json` validates each JSON Lines audit record. -- The files under `fixtures/` form the canonical valid conformance corpus. -- `fill-clipped.scenario.json` and its journal exercise a leverage-limited partial fill. - -Version 7 requires exactly one explicit risk policy per catalog instrument. Each policy defines -order, signed-position, notional, initial-margin, maintenance-margin, and shorting limits. Versioned -risk groups have explicit membership, may overlap, and can constrain gross, long, short, absolute -net, and gross-to-equity concentration exposure. - -Runtime validation requires exact currency, position-mark, and FX coverage; known instruments; -lot-aligned quantities; tick-aligned positive marks; basis with the same sign as quantity; -nonnegative fee histories; the base FX rate equal to one; instrument, group, aggregate exposure, -leverage, and initial-margin limits. Signed cash is valid. A successful v14 run emits `initial_state` -immediately after `run_started`, followed by a reconciled initial `valuation`, before market data. - -Admission and fill clipping include working-order reservations. When multiple groups limit the same -fill, lexical group identity is the deterministic tie breaker. Valuations and strategy contexts -carry group exposure snapshots, and clipping thresholds identify the exact instrument or group. - -Every v14 scenario, stream record, and journal record carries `"contract_version": "14"`. - -Version 8 adds explicit `market`, `limit`, `stop`, and `stop_limit` orders with `gtc`, `ioc`, -`fok`, `day`, and `gtd` time-in-force policies. `day` orders identify both their venue and the -exact versioned calendar; `gtd` orders carry an absolute expiry timestamp. Older contracts retain -their frozen mapping: market orders are IOC and limit orders are GTC. - -Stops evaluate only completed OHLCV bars. A gap through the trigger records the bar start as the -trigger time; an intrabar touch records the bar end. Trigger state and slice sequence are journaled, -and an activated order cannot execute before the following slice. A stop becomes a market order; -a stop-limit becomes its configured limit order. Splits adjust both trigger and limit prices. - -IOC orders cancel any remainder after their first eligible slice. FOK orders fill only when the -full remaining quantity fits both execution capacity and risk capacity, otherwise they cancel with -no fill. DAY orders cancel after matching the slice that reaches the selected session's final -phase close. GTD orders cancel before matching any completed bar whose end reaches or passes the -expiry, avoiding ambiguous partial-bar execution. - -The v10 `execution` object uses `completed_bar_v1` configuration version `"2"`: participation basis -points plus exactly one composable fee schedule per instrument. Named fixed, notional-basis-point, -and per-unit components declare currency, rounding, and maker/taker applicability. Optional -per-fill minimums and caps use the schedule settlement currency; negative components represent -rebates. Fills and valuations retain every native, quote, and base-currency attribution. Runtime -capabilities also advertise frozen configuration version `"1"` for older scenario contracts. - -Version 10 adds a required `financing` policy and effective-time observations on every market -slice. Borrow observations provide per-instrument locate availability, signed annual rates, and -recall state. Cash observations provide separate annual credit and debit rates per currency. -Policies select Actual/365 or Actual/360 day count, simple or daily compounding, missing-data -handling, locate rejection or fill clipping, and recall rejection or deterministic close-out. - -Borrow availability is enforced when a fill would create or increase a short. Recalls cancel -active sells and may submit priority IOC covers until the position is flat. Borrow charges and cash -interest use the exact slice interval, update native ledgers deterministically, and emit dedicated -journal records. Valuations report cash interest separately and include it in aggregate realized -P&L. Version 9 and earlier retain their frozen fixed-borrow behavior and wire shapes. - -Version 12 separates trade-date economic accounting from settlement-date availability. A required -settlement policy selects total or settled cash buying power and total or settled position -availability. Versioned calendars enumerate canonical business dates, and each instrument has an -explicit business-day lag. Every fill creates a deterministic settlement instruction containing -its cash and position movements, trade date, and due date. A due instruction either settles on the -first eligible slice or records a named failure supplied by that slice. - -Valuations and strategy contexts report settled and unsettled cash and quantities without changing -economic equity. Journals include instruction-created, completed, and failed events. Scenario v10 -and strategy protocol v8 retain their frozen immediate-settlement wire behavior. - -Version 12 adds exact stock-dividend, rights, and spin-off distributions. Each distribution names -its destination instrument, exact entitlement ratio, basis allocation in basis points, and either -rejects fractional entitlements or converts them to cash at an explicit price and currency. -Stock dividends adjust persistent targets and eligible working orders; every distribution journals -delivered quantity, fractional quantity, allocated basis, fractional basis, and cash in lieu. - -Lifecycle events keep stable instrument identity separate from mutable symbol and provider -mappings. Halt and resume transitions control tradability. Expiration and delisting are terminal, -cancel active orders, clear target exposure, and require an explicit hold or cash-out policy. -Cash-out specifies its terminal price and currency. Every transition journals the source event, -resulting listing state, provider provenance, liquidated quantity, and cash attribution. - -Version 13 adds `completed_bar_next_open_v1` and `completed_bar_adverse_touch_v1` without changing -the frozen `completed_bar_v1` semantics. Next-open limits require a marketable later open; -adverse-touch limits require a one-tick trade-through before a maker fill is eligible. Both models -declare fixed half-spread and linear participation-impact catalogs, including an explicit policy -for missing bar volume. Price costs round away from the reference price to instrument ticks and -cannot violate a limit. An `execution_price_selected` audit record attributes the reference price, -spread adjustment, impact adjustment, and final executable price before each fill. - -Version 14 adds `quote_trade_v1` and causally ordered `market_events`. Quotes expose bid/ask price -and displayed size. Trades expose price, size, and buy, sell, or unknown aggressor side. Each event -records economic, availability, and receipt timestamps plus a positive ingest sequence. Replay -orders events by availability, receipt, and ingest sequence. Marketable orders consume only -displayed quote liquidity; passive orders require appropriately aggressed trade evidence, and an -unknown aggressor never fills them. Event capacity is shared deterministically across order -priority and fills retain the event's economic timestamp. Completed bars remain the valuation -boundary. The `quote-trade` batch, stream, and journal fixtures demonstrate equivalent replay. diff --git a/contracts/v14/dune b/contracts/v14/dune deleted file mode 100644 index 17e522a..0000000 --- a/contracts/v14/dune +++ /dev/null @@ -1,27 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v14/journal.schema.json) - (scenario-stream.schema.json as contracts/v14/scenario-stream.schema.json) - (scenario.schema.json as contracts/v14/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v14/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v14/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl - as - contracts/v14/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v14/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v14/fixtures/fill-clipped.scenario.json) - (fixtures/quote-trade.journal.jsonl - as - contracts/v14/fixtures/quote-trade.journal.jsonl) - (fixtures/quote-trade.scenario.json - as - contracts/v14/fixtures/quote-trade.scenario.json) - (fixtures/quote-trade.scenario.jsonl - as - contracts/v14/fixtures/quote-trade.scenario.jsonl))) diff --git a/contracts/v14/fixtures/demo.journal.jsonl b/contracts/v14/fixtures/demo.journal.jsonl deleted file mode 100644 index 3f41591..0000000 --- a/contracts/v14/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,29 +0,0 @@ -{"contract_version":"14","engine_sequence":"1","event_id":"demo-event-000000000001","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"49cac47b2abf617f1610d65a29984ab6cd848cd42c28b5d190fb34369365b18f","execution_model":"completed_bar_adverse_touch_v1"}} -{"contract_version":"14","engine_sequence":"2","event_id":"demo-event-000000000002","causation_ids":["demo-event-000000000001"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"1","cost_basis":"90","realized_pnl":"5","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25"}],"marks":[{"instrument_id":"demo-equity-acme","price":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"10000","net_market_value":"100","long_market_value":"100","short_market_value":"0","gross_exposure":"100","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"10","equity":"10100","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"100","fx_rate":"1","market_value":"100","base_market_value":"100","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"10","base_unrealized_pnl":"10","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} -{"contract_version":"14","engine_sequence":"3","event_id":"demo-event-000000000003","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"10000","net_market_value":"100","long_market_value":"100","short_market_value":"0","gross_exposure":"100","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"10","equity":"10100","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"100","fx_rate":"1","market_value":"100","base_market_value":"100","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"10","base_unrealized_pnl":"10","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} -{"contract_version":"14","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} -{"contract_version":"14","engine_sequence":"5","event_id":"demo-event-000000000005","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"10000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-02T14:30:00.000000Z","period_end":"2026-01-02T21:00:00.000000Z","amount":"0.074201","closing_balance":"10000.074201"}} -{"contract_version":"14","engine_sequence":"6","event_id":"demo-event-000000000006","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} -{"contract_version":"14","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} -{"contract_version":"14","engine_sequence":"8","event_id":"demo-event-000000000008","causation_ids":["demo-event-000000000004","demo-event-000000000006"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"8.715","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000008","updated_event_id":"demo-event-000000000008","created_sequence":"8","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"14","engine_sequence":"9","event_id":"demo-event-000000000009","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"10000.074201","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5.074201","unrealized_pnl":"14","equity":"10104.074201","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000.074201","fx_rate":"1","base_value":"10000.074201","interest":"0.074201","base_interest":"0.074201","settled_amount":"10000.074201","unsettled_amount":"0","base_settled_value":"10000.074201","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"104","fx_rate":"1","market_value":"104","base_market_value":"104","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"14","base_unrealized_pnl":"14","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.074201","settled_cash":"10000.074201","unsettled_cash":"0","margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052.074201","maintenance_excess":"10078.074201","margin_call":false},"group_exposures":[]}} -{"contract_version":"14","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"13"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} -{"contract_version":"14","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"10000.074201","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-05T14:30:00.000000Z","period_end":"2026-01-05T21:00:00.000000Z","amount":"0.074201","closing_balance":"10000.148402"}} -{"contract_version":"14","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000008","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","reference_price":"103","spread_adjustment":"0.06","impact_adjustment":"0.13","final_price":"103.19"}} -{"contract_version":"14","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000012"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000001","order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"6.5","price":"103.19","notional":"670.735","fee":"0.920735","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_amount":"0.670735"}]}} -{"contract_version":"14","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000008","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"order_cancelled","payload":{"order":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"8.715","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000008","updated_event_id":"demo-event-000000000008","created_sequence":"8","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"6.5","filled_notional":"670.735","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"14","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000006","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","side":"buy","quantity":"2.215","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000015","updated_event_id":"demo-event-000000000015","created_sequence":"15","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"14","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9328.492667","net_market_value":"802.5","long_market_value":"802.5","short_market_value":"0","gross_exposure":"802.5","cost_basis":"761.655735","realized_pnl":"5.148402","unrealized_pnl":"40.844265","equity":"10130.992667","dividend_pnl":"1","execution_fees":"1.420735","borrow_fees":"0.25","total_fees":"1.670735","cash_balances":[{"currency":"USD","amount":"9328.492667","fx_rate":"1","base_value":"9328.492667","interest":"0.148402","base_interest":"0.148402","settled_amount":"9328.492667","unsettled_amount":"0","base_settled_value":"9328.492667","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7.5","mark":"107","fx_rate":"1","market_value":"802.5","base_market_value":"802.5","cost_basis":"761.655735","base_cost_basis":"761.655735","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.844265","base_unrealized_pnl":"40.844265","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.420735","base_execution_fees":"1.420735","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.670735","base_total_fees":"1.670735","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_currency":"USD","quote_amount":"0.25","base_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_currency":"USD","quote_amount":"0.670735","base_amount":"0.670735"}],"settled_quantity":"7.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_currency":"USD","quote_amount":"0.25","base_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_currency":"USD","quote_amount":"0.670735","base_amount":"0.670735"}],"cash_interest":"0.148402","settled_cash":"9328.492667","unsettled_cash":"0","margin":{"initial_requirement":"401.25","maintenance_requirement":"200.625","initial_excess":"9729.742667","maintenance_excess":"9930.367667","margin_call":false},"group_exposures":[]}} -{"contract_version":"14","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"3","start_at":"2026-01-06T14:30:00.000000Z","end_at":"2026-01-06T21:00:00.000000Z","available_at":"2026-01-06T21:00:01.000000Z","received_at":"2026-01-06T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"107","high":"109","low":"104","close":"105","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} -{"contract_version":"14","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"9328.492667","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-06T14:30:00.000000Z","period_end":"2026-01-06T21:00:00.000000Z","amount":"0.069218","closing_balance":"9328.561885"}} -{"contract_version":"14","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","side":"buy","reference_price":"107","spread_adjustment":"0.06","impact_adjustment":"0.01","final_price":"107.07"}} -{"contract_version":"14","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000002","order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"2.215","price":"107.07","notional":"237.16005","fee":"0.487161","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.237161","quote_amount":"0.237161"}]}} -{"contract_version":"14","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"target_portfolio_requested","payload":{"basis":"quantities","targets":[{"instrument_id":"demo-equity-acme","weight":null,"quantity":"2.5","reference_price":null}]}} -{"contract_version":"14","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000017","demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","side":"sell","quantity":"7.215","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000022","updated_event_id":"demo-event-000000000022","created_sequence":"22","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"14","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.914674","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.302946","realized_pnl":"5.21762","unrealized_pnl":"20.772054","equity":"10110.989674","dividend_pnl":"1","execution_fees":"1.907896","borrow_fees":"0.25","total_fees":"2.157896","cash_balances":[{"currency":"USD","amount":"9090.914674","fx_rate":"1","base_value":"9090.914674","interest":"0.21762","base_interest":"0.21762","settled_amount":"9090.914674","unsettled_amount":"0","base_settled_value":"9090.914674","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"9.715","mark":"105","fx_rate":"1","market_value":"1020.075","base_market_value":"1020.075","cost_basis":"999.302946","base_cost_basis":"999.302946","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.772054","base_unrealized_pnl":"20.772054","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.907896","base_execution_fees":"1.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.157896","base_total_fees":"2.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.5","quote_currency":"USD","quote_amount":"0.5","base_amount":"0.5"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.907896","quote_currency":"USD","quote_amount":"0.907896","base_amount":"0.907896"}],"settled_quantity":"9.715","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.5","quote_currency":"USD","quote_amount":"0.5","base_amount":"0.5"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.907896","quote_currency":"USD","quote_amount":"0.907896","base_amount":"0.907896"}],"cash_interest":"0.21762","settled_cash":"9090.914674","unsettled_cash":"0","margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.952174","maintenance_excess":"9855.970924","margin_call":false},"group_exposures":[]}} -{"contract_version":"14","engine_sequence":"24","event_id":"demo-event-000000000024","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"4","start_at":"2026-01-07T14:30:00.000000Z","end_at":"2026-01-07T21:00:00.000000Z","available_at":"2026-01-07T21:00:01.000000Z","received_at":"2026-01-07T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"105","high":"107","low":"103","close":"106","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} -{"contract_version":"14","engine_sequence":"25","event_id":"demo-event-000000000025","causation_ids":["demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"9090.914674","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-07T14:30:00.000000Z","period_end":"2026-01-07T21:00:00.000000Z","amount":"0.067455","closing_balance":"9090.982129"}} -{"contract_version":"14","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000022","demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","side":"sell","reference_price":"105","spread_adjustment":"0.06","impact_adjustment":"0.02","final_price":"104.92"}} -{"contract_version":"14","engine_sequence":"27","event_id":"demo-event-000000000027","causation_ids":["demo-event-000000000026"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000003","order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"sell","quantity":"7.215","price":"104.92","notional":"756.9978","fee":"1","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.756998","quote_amount":"0.756998"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_amount":"-0.006998"}]}} -{"contract_version":"14","engine_sequence":"28","event_id":"demo-event-000000000028","causation_ids":["demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.979929","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.154644","realized_pnl":"19.134573","unrealized_pnl":"7.845356","equity":"10111.979929","dividend_pnl":"1","execution_fees":"2.907896","borrow_fees":"0.25","total_fees":"3.157896","cash_balances":[{"currency":"USD","amount":"9846.979929","fx_rate":"1","base_value":"9846.979929","interest":"0.285075","base_interest":"0.285075","settled_amount":"9846.979929","unsettled_amount":"0","base_settled_value":"9846.979929","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","mark":"106","fx_rate":"1","market_value":"265","base_market_value":"265","cost_basis":"257.154644","base_cost_basis":"257.154644","realized_pnl":"18.849498","base_realized_pnl":"18.849498","unrealized_pnl":"7.845356","base_unrealized_pnl":"7.845356","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.907896","base_execution_fees":"2.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"settled_quantity":"2.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"cash_interest":"0.285075","settled_cash":"9846.979929","unsettled_cash":"0","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.479929","maintenance_excess":"10045.729929","margin_call":false},"group_exposures":[]}} -{"contract_version":"14","engine_sequence":"29","event_id":"demo-event-000000000029","causation_ids":["demo-event-000000000028"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"49cac47b2abf617f1610d65a29984ab6cd848cd42c28b5d190fb34369365b18f","execution_model":"completed_bar_adverse_touch_v1","valuation":{"base_currency":"USD","cash":"9846.979929","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.154644","realized_pnl":"19.134573","unrealized_pnl":"7.845356","equity":"10111.979929","dividend_pnl":"1","execution_fees":"2.907896","borrow_fees":"0.25","total_fees":"3.157896","cash_balances":[{"currency":"USD","amount":"9846.979929","fx_rate":"1","base_value":"9846.979929","interest":"0.285075","base_interest":"0.285075","settled_amount":"9846.979929","unsettled_amount":"0","base_settled_value":"9846.979929","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","mark":"106","fx_rate":"1","market_value":"265","base_market_value":"265","cost_basis":"257.154644","base_cost_basis":"257.154644","realized_pnl":"18.849498","base_realized_pnl":"18.849498","unrealized_pnl":"7.845356","base_unrealized_pnl":"7.845356","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.907896","base_execution_fees":"2.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"settled_quantity":"2.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"cash_interest":"0.285075","settled_cash":"9846.979929","unsettled_cash":"0","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.479929","maintenance_excess":"10045.729929","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v14/fixtures/demo.scenario.json b/contracts/v14/fixtures/demo.scenario.json deleted file mode 100644 index 55e7f7b..0000000 --- a/contracts/v14/fixtures/demo.scenario.json +++ /dev/null @@ -1,461 +0,0 @@ -{ - "contract_version": "14", - "metadata": { - "producer": "trading-engine-demo", - "purpose": "deterministic conformance fixture" - }, - "run_id": "demo", - "base_currency": "USD", - "initial_portfolio": { - "cash": [ - { - "currency": "USD", - "amount": "10000" - } - ], - "positions": [ - { - "instrument_id": "demo-equity-acme", - "quantity": "1", - "cost_basis": "90", - "realized_pnl": "5", - "dividend_pnl": "1", - "execution_fees": "0.5", - "borrow_fees": "0.25" - } - ], - "marks": [ - { - "instrument_id": "demo-equity-acme", - "price": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ] - }, - "instruments": [ - { - "instrument_id": "demo-equity-acme", - "symbol": "ACME", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "0.001" - } - ], - "venue_calendars": [ - { - "calendar_id": "demo-xnas-2026", - "calendar_version": "1", - "venue_id": "XNAS", - "instrument_ids": [ - "demo-equity-acme" - ], - "sessions": [ - { - "session_date": "2026-01-01", - "policy": "holiday", - "phases": [] - }, - { - "session_date": "2026-01-02", - "policy": "regular", - "phases": [ - { - "phase": "premarket", - "opens_at": "2026-01-02T09:00:00Z", - "closes_at": "2026-01-02T14:25:00Z" - }, - { - "phase": "opening_auction", - "opens_at": "2026-01-02T14:25:00Z", - "closes_at": "2026-01-02T14:30:00Z" - }, - { - "phase": "regular", - "opens_at": "2026-01-02T14:30:00Z", - "closes_at": "2026-01-02T20:55:00Z" - }, - { - "phase": "closing_auction", - "opens_at": "2026-01-02T20:55:00Z", - "closes_at": "2026-01-02T21:00:00Z" - }, - { - "phase": "postmarket", - "opens_at": "2026-01-02T21:00:00Z", - "closes_at": "2026-01-03T01:00:00Z" - } - ] - }, - { - "session_date": "2026-01-05", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-05T14:30:00Z", - "closes_at": "2026-01-05T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-06", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-06T14:30:00Z", - "closes_at": "2026-01-06T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-07", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-07T14:30:00Z", - "closes_at": "2026-01-07T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-08", - "policy": "early_close", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-08T14:30:00Z", - "closes_at": "2026-01-08T18:00:00Z" - } - ] - } - ] - } - ], - "risk": { - "max_gross_exposure": "1000000", - "max_leverage": "2", - "short_borrow_bps": 100, - "instrument_policies": [ - { - "instrument_id": "demo-equity-acme", - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_notional_exposure": "1000000", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "shorting_allowed": true - } - ], - "groups": [] - }, - "execution": { - "model": "completed_bar_adverse_touch_v1", - "configuration": { - "version": "1", - "participation_bps": 5000, - "fee_schedules": [ - { - "schedule_id": "demo-acme-fees-v1", - "instrument_id": "demo-equity-acme", - "settlement_currency": "USD", - "minimum": "0.3", - "maximum": "1", - "components": [ - { - "name": "broker", - "currency": "USD", - "kind": "fixed", - "value": "0.25", - "rounding": "up", - "applies_to": "any" - }, - { - "name": "exchange", - "currency": "USD", - "kind": "notional_bps", - "value": 10, - "rounding": "up", - "applies_to": "taker" - }, - { - "name": "maker_rebate", - "currency": "USD", - "kind": "notional_bps", - "value": -2, - "rounding": "nearest", - "applies_to": "maker" - } - ] - } - ], - "spread_model": { - "model": "fixed_half_spread_v1", - "half_spread_bps": 5 - }, - "impact_model": { - "model": "linear_participation_v1", - "coefficient_bps": 25, - "missing_volume_policy": "reject" - } - } - }, - "max_internal_events": 1000, - "schedule": [ - { - "after_slice_sequence": "1", - "intents": [ - { - "type": "target_weights", - "targets": [ - { - "instrument_id": "demo-equity-acme", - "weight": "0.1" - } - ] - }, - { - "type": "emit_metric", - "name": "desired_weight", - "value": "0.1" - } - ] - }, - { - "after_slice_sequence": "3", - "intents": [ - { - "type": "target_quantities", - "targets": [ - { - "instrument_id": "demo-equity-acme", - "quantity": "2.5" - } - ] - } - ] - } - ], - "slices": [ - { - "slice_sequence": "1", - "start_at": "2026-01-02T14:30:00Z", - "end_at": "2026-01-02T21:00:00Z", - "available_at": "2026-01-02T21:00:01Z", - "received_at": "2026-01-02T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "100", - "high": "105", - "low": "99", - "close": "104", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-02T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-02T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [] - }, - { - "slice_sequence": "2", - "start_at": "2026-01-05T14:30:00Z", - "end_at": "2026-01-05T21:00:00Z", - "available_at": "2026-01-05T21:00:01Z", - "received_at": "2026-01-05T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "103", - "high": "108", - "low": "102", - "close": "107", - "volume": "13" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-05T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-05T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [] - }, - { - "slice_sequence": "3", - "start_at": "2026-01-06T14:30:00Z", - "end_at": "2026-01-06T21:00:00Z", - "available_at": "2026-01-06T21:00:01Z", - "received_at": "2026-01-06T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "107", - "high": "109", - "low": "104", - "close": "105", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-06T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-06T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [] - }, - { - "slice_sequence": "4", - "start_at": "2026-01-07T14:30:00Z", - "end_at": "2026-01-07T21:00:00Z", - "available_at": "2026-01-07T21:00:01Z", - "received_at": "2026-01-07T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "105", - "high": "107", - "low": "103", - "close": "106", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-07T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-07T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - }, - "settlement": { - "cash_buying_power": "total_cash", - "position_availability": "total_positions", - "calendars": [ - { - "calendar_id": "default-settlement", - "version": "1", - "business_dates": [ - "2026-01-02", - "2026-01-05", - "2026-01-06", - "2026-01-07", - "2026-01-08", - "2026-01-09", - "2026-02-02", - "2026-02-03", - "2026-02-04", - "2026-02-05" - ] - } - ], - "rules": [ - { - "instrument_id": "demo-equity-acme", - "calendar_id": "default-settlement", - "lag_business_days": 1 - } - ] - } -} diff --git a/contracts/v14/fixtures/demo.scenario.jsonl b/contracts/v14/fixtures/demo.scenario.jsonl deleted file mode 100644 index ae6e155..0000000 --- a/contracts/v14/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"14","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"run_id":"demo","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"1","cost_basis":"90","realized_pnl":"5","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25"}],"marks":[{"instrument_id":"demo-equity-acme","price":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"0.001"}],"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00Z","closes_at":"2026-01-02T14:25:00Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00Z","closes_at":"2026-01-02T14:30:00Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00Z","closes_at":"2026-01-02T20:55:00Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00Z","closes_at":"2026-01-02T21:00:00Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00Z","closes_at":"2026-01-03T01:00:00Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00Z","closes_at":"2026-01-05T21:00:00Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00Z","closes_at":"2026-01-06T21:00:00Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00Z","closes_at":"2026-01-07T21:00:00Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00Z","closes_at":"2026-01-08T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_adverse_touch_v1","configuration":{"version":"1","participation_bps":5000,"fee_schedules":[{"schedule_id":"demo-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":"0.3","maximum":"1","components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"taker"},{"name":"maker_rebate","currency":"USD","kind":"notional_bps","value":-2,"rounding":"nearest","applies_to":"maker"}]}],"spread_model":{"model":"fixed_half_spread_v1","half_spread_bps":5},"impact_model":{"model":"linear_participation_v1","coefficient_bps":25,"missing_volume_policy":"reject"}}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]}}} -{"contract_version":"14","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","weight":"0.1"}],"type":"target_weights"},{"name":"desired_weight","type":"emit_metric","value":"0.1"}],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"14","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"13"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"14","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","quantity":"2.5"}],"type":"target_quantities"}],"market_slice":{"available_at":"2026-01-06T21:00:01Z","bars":[{"close":"105","high":"109","instrument_id":"demo-equity-acme","low":"104","open":"107","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-06T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-06T21:00:02Z","slice_sequence":"3","start_at":"2026-01-06T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"14","payload":{"intents":[],"market_slice":{"available_at":"2026-01-07T21:00:01Z","bars":[{"close":"106","high":"107","instrument_id":"demo-equity-acme","low":"103","open":"105","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-07T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-07T21:00:02Z","slice_sequence":"4","start_at":"2026-01-07T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"14","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v14/fixtures/fill-clipped.journal.jsonl b/contracts/v14/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index 1a1c149..0000000 --- a/contracts/v14/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"contract_version":"14","engine_sequence":"1","event_id":"fill-clipped-event-000000000001","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"77457107873a439fdb669f03d65eaf3a7843e1649a4a0036e68a958c7a48baad","execution_model":"completed_bar_v1"}} -{"contract_version":"14","engine_sequence":"2","event_id":"fill-clipped-event-000000000002","causation_ids":["fill-clipped-event-000000000001"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"550"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"550","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"550","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550","fx_rate":"1","base_value":"550","interest":"0","base_interest":"0","settled_amount":"550","unsettled_amount":"0","base_settled_value":"550","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"550","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} -{"contract_version":"14","engine_sequence":"3","event_id":"fill-clipped-event-000000000003","causation_ids":["fill-clipped-event-000000000002"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"550","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550","fx_rate":"1","base_value":"550","interest":"0","base_interest":"0","settled_amount":"550","unsettled_amount":"0","base_settled_value":"550","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"550","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"14","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} -{"contract_version":"14","engine_sequence":"5","event_id":"fill-clipped-event-000000000005","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"550","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.004081","closing_balance":"550.004081"}} -{"contract_version":"14","engine_sequence":"6","event_id":"fill-clipped-event-000000000006","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000006","updated_event_id":"fill-clipped-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"14","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550.004081","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.004081","unrealized_pnl":"0","equity":"550.004081","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.004081","fx_rate":"1","base_value":"550.004081","interest":"0.004081","base_interest":"0.004081","settled_amount":"550.004081","unsettled_amount":"0","base_settled_value":"550.004081","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.004081","settled_cash":"550.004081","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.004081","maintenance_excess":"550.004081","margin_call":false},"group_exposures":[]}} -{"contract_version":"14","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} -{"contract_version":"14","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"550.004081","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.004081","closing_balance":"550.008162"}} -{"contract_version":"14","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000006","fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_clipped","payload":{"reason":{"version":"1","policy":"max_leverage","threshold":{"unit":"ratio","value":"1"}},"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","proposed_quantity":"10","permitted_quantity":"0","price":"100"}} -{"contract_version":"14","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000006","fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"order_cancelled","payload":{"order":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000006","updated_event_id":"fill-clipped-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"14","engine_sequence":"12","event_id":"fill-clipped-event-000000000012","causation_ids":["fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550.008162","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.008162","unrealized_pnl":"0","equity":"550.008162","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.008162","fx_rate":"1","base_value":"550.008162","interest":"0.008162","base_interest":"0.008162","settled_amount":"550.008162","unsettled_amount":"0","base_settled_value":"550.008162","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"100","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]}} -{"contract_version":"14","engine_sequence":"13","event_id":"fill-clipped-event-000000000013","causation_ids":["fill-clipped-event-000000000012"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"77457107873a439fdb669f03d65eaf3a7843e1649a4a0036e68a958c7a48baad","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"550.008162","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.008162","unrealized_pnl":"0","equity":"550.008162","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.008162","fx_rate":"1","base_value":"550.008162","interest":"0.008162","base_interest":"0.008162","settled_amount":"550.008162","unsettled_amount":"0","base_settled_value":"550.008162","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"100","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v14/fixtures/fill-clipped.scenario.json b/contracts/v14/fixtures/fill-clipped.scenario.json deleted file mode 100644 index afee413..0000000 --- a/contracts/v14/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,271 +0,0 @@ -{ - "contract_version": "14", - "metadata": { - "producer": "trading-engine", - "purpose": "fill clipping conformance fixture" - }, - "run_id": "fill-clipped", - "base_currency": "USD", - "initial_portfolio": { - "cash": [ - { - "currency": "USD", - "amount": "550" - } - ], - "positions": [], - "marks": [], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ] - }, - "instruments": [ - { - "instrument_id": "clip-equity", - "symbol": "CLIP", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "venue_calendars": [ - { - "calendar_id": "clip-xnas-2026", - "calendar_version": "1", - "venue_id": "XNAS", - "instrument_ids": [ - "clip-equity" - ], - "sessions": [ - { - "session_date": "2026-02-02", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-02T14:30:00Z", - "closes_at": "2026-02-02T21:00:00Z" - } - ] - }, - { - "session_date": "2026-02-03", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-03T14:30:00Z", - "closes_at": "2026-02-03T21:00:00Z" - } - ] - }, - { - "session_date": "2026-02-04", - "policy": "early_close", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-04T14:30:00Z", - "closes_at": "2026-02-04T18:00:00Z" - } - ] - } - ] - } - ], - "risk": { - "max_gross_exposure": "1000000000", - "max_leverage": "1", - "short_borrow_bps": 100, - "instrument_policies": [ - { - "instrument_id": "clip-equity", - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_notional_exposure": "1000000000", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "shorting_allowed": true - } - ], - "groups": [] - }, - "execution": { - "model": "completed_bar_v1", - "configuration": { - "version": "2", - "participation_bps": 10000, - "fee_schedules": [ - { - "schedule_id": "clip-fees-v1", - "instrument_id": "clip-equity", - "settlement_currency": "USD", - "minimum": null, - "maximum": null, - "components": [ - { - "name": "broker", - "currency": "USD", - "kind": "fixed", - "value": "10", - "rounding": "up", - "applies_to": "any" - } - ] - } - ] - } - }, - "max_internal_events": 1000, - "schedule": [ - { - "after_slice_sequence": "1", - "intents": [ - { - "type": "submit_order", - "instrument_id": "clip-equity", - "side": "buy", - "quantity": "10", - "order_kind": "market", - "trigger_price": null, - "limit_price": null, - "time_in_force": "ioc", - "venue_id": null, - "calendar_id": null, - "expires_at": null - } - ] - } - ], - "slices": [ - { - "slice_sequence": "1", - "start_at": "2026-02-02T14:30:00Z", - "end_at": "2026-02-02T21:00:00Z", - "available_at": "2026-02-02T21:00:01Z", - "received_at": "2026-02-02T21:00:02Z", - "bars": [ - { - "instrument_id": "clip-equity", - "open": "50", - "high": "50", - "low": "50", - "close": "50", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "clip-equity", - "effective_at": "2026-02-02T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-02-02T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [] - }, - { - "slice_sequence": "2", - "start_at": "2026-02-03T14:30:00Z", - "end_at": "2026-02-03T21:00:00Z", - "available_at": "2026-02-03T21:00:01Z", - "received_at": "2026-02-03T21:00:02Z", - "bars": [ - { - "instrument_id": "clip-equity", - "open": "100", - "high": "100", - "low": "100", - "close": "100", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "clip-equity", - "effective_at": "2026-02-03T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-02-03T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - }, - "settlement": { - "cash_buying_power": "total_cash", - "position_availability": "total_positions", - "calendars": [ - { - "calendar_id": "default-settlement", - "version": "1", - "business_dates": [ - "2026-01-02", - "2026-01-05", - "2026-01-06", - "2026-01-07", - "2026-01-08", - "2026-01-09", - "2026-02-02", - "2026-02-03", - "2026-02-04", - "2026-02-05" - ] - } - ], - "rules": [ - { - "instrument_id": "clip-equity", - "calendar_id": "default-settlement", - "lag_business_days": 1 - } - ] - } -} diff --git a/contracts/v14/fixtures/quote-trade.journal.jsonl b/contracts/v14/fixtures/quote-trade.journal.jsonl deleted file mode 100644 index 0f729da..0000000 --- a/contracts/v14/fixtures/quote-trade.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"contract_version":"14","engine_sequence":"1","event_id":"quote-trade-event-000000000001","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"fc4ba762e5464546c4ec601df566c99e6c5995928a18e5b72434775ed20b62ac","execution_model":"quote_trade_v1"}} -{"contract_version":"14","engine_sequence":"2","event_id":"quote-trade-event-000000000002","causation_ids":["quote-trade-event-000000000001"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} -{"contract_version":"14","engine_sequence":"3","event_id":"quote-trade-event-000000000003","causation_ids":["quote-trade-event-000000000002"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} -{"contract_version":"14","engine_sequence":"4","event_id":"quote-trade-event-000000000004","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]}} -{"contract_version":"14","engine_sequence":"5","event_id":"quote-trade-event-000000000005","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} -{"contract_version":"14","engine_sequence":"6","event_id":"quote-trade-event-000000000006","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"quote-trade-event-000000000006","updated_event_id":"quote-trade-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"14","engine_sequence":"7","event_id":"quote-trade-event-000000000007","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} -{"contract_version":"14","engine_sequence":"8","event_id":"quote-trade-event-000000000008","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}]}} -{"contract_version":"14","engine_sequence":"9","event_id":"quote-trade-event-000000000009","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} -{"contract_version":"14","engine_sequence":"10","event_id":"quote-trade-event-000000000010","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000001","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"99","notional":"396","fee":"10","executed_at":"2026-02-03T14:33:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} -{"contract_version":"14","engine_sequence":"11","event_id":"quote-trade-event-000000000011","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000002","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:34:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} -{"contract_version":"14","engine_sequence":"12","event_id":"quote-trade-event-000000000012","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]}} -{"contract_version":"14","engine_sequence":"13","event_id":"quote-trade-event-000000000013","causation_ids":["quote-trade-event-000000000012"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"fc4ba762e5464546c4ec601df566c99e6c5995928a18e5b72434775ed20b62ac","execution_model":"quote_trade_v1","valuation":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v14/fixtures/quote-trade.scenario.json b/contracts/v14/fixtures/quote-trade.scenario.json deleted file mode 100644 index 230f846..0000000 --- a/contracts/v14/fixtures/quote-trade.scenario.json +++ /dev/null @@ -1,317 +0,0 @@ -{ - "contract_version": "14", - "metadata": { - "producer": "trading-engine", - "purpose": "bounded quote and trade replay fixture" - }, - "run_id": "quote-trade", - "base_currency": "USD", - "initial_portfolio": { - "cash": [ - { - "currency": "USD", - "amount": "2000" - } - ], - "positions": [], - "marks": [], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ] - }, - "instruments": [ - { - "instrument_id": "clip-equity", - "symbol": "CLIP", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "venue_calendars": [ - { - "calendar_id": "clip-xnas-2026", - "calendar_version": "1", - "venue_id": "XNAS", - "instrument_ids": [ - "clip-equity" - ], - "sessions": [ - { - "session_date": "2026-02-02", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-02T14:30:00Z", - "closes_at": "2026-02-02T21:00:00Z" - } - ] - }, - { - "session_date": "2026-02-03", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-03T14:30:00Z", - "closes_at": "2026-02-03T21:00:00Z" - } - ] - }, - { - "session_date": "2026-02-04", - "policy": "early_close", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-04T14:30:00Z", - "closes_at": "2026-02-04T18:00:00Z" - } - ] - } - ] - } - ], - "risk": { - "max_gross_exposure": "1000000000", - "max_leverage": "1", - "short_borrow_bps": 100, - "instrument_policies": [ - { - "instrument_id": "clip-equity", - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_notional_exposure": "1000000000", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "shorting_allowed": true - } - ], - "groups": [] - }, - "execution": { - "model": "quote_trade_v1", - "configuration": { - "version": "1", - "participation_bps": 10000, - "fee_schedules": [ - { - "schedule_id": "clip-fees-v1", - "instrument_id": "clip-equity", - "settlement_currency": "USD", - "minimum": null, - "maximum": null, - "components": [ - { - "name": "broker", - "currency": "USD", - "kind": "fixed", - "value": "10", - "rounding": "up", - "applies_to": "any" - } - ] - } - ] - } - }, - "max_internal_events": 1000, - "schedule": [ - { - "after_slice_sequence": "1", - "intents": [ - { - "type": "submit_order", - "instrument_id": "clip-equity", - "side": "buy", - "quantity": "10", - "order_kind": "limit", - "trigger_price": null, - "limit_price": "100", - "time_in_force": "gtc", - "venue_id": null, - "calendar_id": null, - "expires_at": null - } - ] - } - ], - "slices": [ - { - "slice_sequence": "1", - "start_at": "2026-02-02T14:30:00Z", - "end_at": "2026-02-02T21:00:00Z", - "available_at": "2026-02-02T21:00:01Z", - "received_at": "2026-02-02T21:00:02Z", - "bars": [ - { - "instrument_id": "clip-equity", - "open": "50", - "high": "50", - "low": "50", - "close": "50", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "clip-equity", - "effective_at": "2026-02-02T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-02-02T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [] - }, - { - "slice_sequence": "2", - "start_at": "2026-02-03T14:30:00Z", - "end_at": "2026-02-03T21:00:00Z", - "available_at": "2026-02-03T21:00:01Z", - "received_at": "2026-02-03T21:00:02Z", - "bars": [ - { - "instrument_id": "clip-equity", - "open": "100", - "high": "100", - "low": "100", - "close": "100", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "clip-equity", - "effective_at": "2026-02-03T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-02-03T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [ - { - "type": "quote", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:31:00Z", - "available_at": "2026-02-03T14:31:01Z", - "received_at": "2026-02-03T14:31:02Z", - "ingest_sequence": "1", - "bid_price": "99", - "bid_quantity": "20", - "ask_price": "101", - "ask_quantity": "20" - }, - { - "type": "trade", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:32:00Z", - "available_at": "2026-02-03T14:32:01Z", - "received_at": "2026-02-03T14:32:02Z", - "ingest_sequence": "2", - "price": "100", - "quantity": "5", - "aggressor_side": "unknown" - }, - { - "type": "trade", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:33:00Z", - "available_at": "2026-02-03T14:33:01Z", - "received_at": "2026-02-03T14:33:02Z", - "ingest_sequence": "3", - "price": "99", - "quantity": "4", - "aggressor_side": "sell" - }, - { - "type": "trade", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:34:00Z", - "available_at": "2026-02-03T14:34:01Z", - "received_at": "2026-02-03T14:34:02Z", - "ingest_sequence": "4", - "price": "100", - "quantity": "10", - "aggressor_side": "sell" - } - ] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - }, - "settlement": { - "cash_buying_power": "total_cash", - "position_availability": "total_positions", - "calendars": [ - { - "calendar_id": "default-settlement", - "version": "1", - "business_dates": [ - "2026-01-02", - "2026-01-05", - "2026-01-06", - "2026-01-07", - "2026-01-08", - "2026-01-09", - "2026-02-02", - "2026-02-03", - "2026-02-04", - "2026-02-05" - ] - } - ], - "rules": [ - { - "instrument_id": "clip-equity", - "calendar_id": "default-settlement", - "lag_business_days": 1 - } - ] - } -} diff --git a/contracts/v14/fixtures/quote-trade.scenario.jsonl b/contracts/v14/fixtures/quote-trade.scenario.jsonl deleted file mode 100644 index b399827..0000000 --- a/contracts/v14/fixtures/quote-trade.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"14","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded quote and trade replay fixture"},"run_id":"quote-trade","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"quote_trade_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} -{"contract_version":"14","scenario_sequence":"2","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[]},"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}]}} -{"contract_version":"14","scenario_sequence":"3","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}]},"intents":[]}} -{"contract_version":"14","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v14/journal.schema.json b/contracts/v14/journal.schema.json deleted file mode 100644 index 399e678..0000000 --- a/contracts/v14/journal.schema.json +++ /dev/null @@ -1,2420 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json", - "title": "Trading Engine v14 audit journal record", - "type": "object", - "additionalProperties": false, - "required": [ - "contract_version", - "engine_sequence", - "event_id", - "causation_ids", - "run_id", - "recorded_at", - "event_type", - "payload" - ], - "properties": { - "contract_version": { - "const": "14" - }, - "engine_sequence": { - "$ref": "#/$defs/sequence" - }, - "event_id": { - "$ref": "#/$defs/identifier" - }, - "causation_ids": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/$defs/identifier" - } - }, - "run_id": { - "$ref": "#/$defs/identifier" - }, - "recorded_at": { - "$ref": "#/$defs/timestamp" - }, - "event_type": { - "enum": [ - "run_started", - "initial_state", - "market_slice_received", - "target_portfolio_requested", - "order_accepted", - "order_rejected", - "order_triggered", - "order_cancelled", - "split_applied", - "cash_dividend_applied", - "distribution_applied", - "lifecycle_applied", - "order_adjusted", - "execution_price_selected", - "fill_applied", - "settlement_instruction_created", - "settlement_completed", - "settlement_failed", - "fill_clipped", - "borrow_fee_applied", - "borrow_charge_applied", - "borrow_recall_received", - "cash_interest_applied", - "margin_call", - "margin_restored", - "intent_rejected", - "metric_emitted", - "valuation", - "run_completed" - ] - }, - "payload": { - "type": "object" - } - }, - "allOf": [ - { - "if": { "properties": { "event_type": { "const": "execution_price_selected" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/executionPriceSelected" } } } - }, - { - "if": { - "properties": { - "event_type": { - "const": "run_started" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/runStarted" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "initial_state" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/initialState" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "market_slice_received" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/marketSlice" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "target_portfolio_requested" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/targetPortfolio" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "enum": [ - "order_accepted", - "order_rejected", - "order_triggered" - ] - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/order" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "order_cancelled" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/orderCancelled" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "split_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/splitApplied" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "cash_dividend_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/dividendApplied" - } - } - } - }, - { - "if": { "properties": { "event_type": { "const": "distribution_applied" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/distributionApplied" } } } - }, - { - "if": { "properties": { "event_type": { "const": "lifecycle_applied" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/lifecycleApplied" } } } - }, - { - "if": { - "properties": { - "event_type": { - "const": "order_adjusted" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/orderAdjusted" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "fill_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/fill" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "enum": [ - "settlement_instruction_created", - "settlement_completed", - "settlement_failed" - ] - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/settlementInstruction" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "fill_clipped" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/fillClipped" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "borrow_fee_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/borrowFee" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "borrow_charge_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/borrowCharge" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "borrow_recall_received" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/borrowRecall" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "cash_interest_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/cashInterest" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "enum": [ - "margin_call", - "margin_restored", - "valuation" - ] - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/valuation" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "intent_rejected" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/intentRejected" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "metric_emitted" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/metric" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "run_completed" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/runCompleted" - } - } - } - } - ], - "$defs": { - "identifier": { - "type": "string", - "minLength": 1, - "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" - }, - "signedDecimal": { - "type": "string", - "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" - }, - "unsignedDecimal": { - "type": "string", - "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "positiveDecimal": { - "type": "string", - "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "sequence": { - "type": "string", - "pattern": "^[1-9][0-9]*$" - }, - "nonnegativeSequence": { - "type": "string", - "pattern": "^(?:0|[1-9][0-9]*)$" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" - }, - "runStarted": { - "type": "object", - "additionalProperties": false, - "required": [ - "scenario_sha256", - "execution_model" - ], - "properties": { - "scenario_sha256": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, - "execution_model": { - "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1"] - } - } - }, - "initialState": { - "type": "object", - "additionalProperties": false, - "required": [ - "portfolio", - "valuation" - ], - "properties": { - "portfolio": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/initialPortfolio" - }, - "valuation": { - "$ref": "#/$defs/valuation" - } - } - }, - "bar": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "open", - "high", - "low", - "close", - "volume" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "open": { - "$ref": "#/$defs/positiveDecimal" - }, - "high": { - "$ref": "#/$defs/positiveDecimal" - }, - "low": { - "$ref": "#/$defs/positiveDecimal" - }, - "close": { - "$ref": "#/$defs/positiveDecimal" - }, - "volume": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/unsignedDecimal" - } - ] - } - } - }, - "fxRate": { - "type": "object", - "additionalProperties": false, - "required": [ - "currency", - "rate" - ], - "properties": { - "currency": { - "$ref": "#/$defs/identifier" - }, - "rate": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "corporateAction": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": [ - "type", - "action_id", - "instrument_id", - "numerator", - "denominator" - ], - "properties": { - "type": { - "const": "split" - }, - "action_id": { - "$ref": "#/$defs/identifier" - }, - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "numerator": { - "$ref": "#/$defs/sequence" - }, - "denominator": { - "$ref": "#/$defs/sequence" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "type", - "action_id", - "instrument_id", - "amount_per_unit" - ], - "properties": { - "type": { - "const": "cash_dividend" - }, - "action_id": { - "$ref": "#/$defs/identifier" - }, - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "amount_per_unit": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], - "properties": { - "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, - "action_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "destination_instrument_id": { "$ref": "#/$defs/identifier" }, - "numerator": { "$ref": "#/$defs/sequence" }, - "denominator": { "$ref": "#/$defs/sequence" }, - "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fractional_policy": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/fractionalPolicy" } - } - } - ] - }, - "marketSlice": { - "type": "object", - "additionalProperties": false, - "required": [ - "slice_sequence", - "start_at", - "end_at", - "available_at", - "received_at", - "bars", - "fx_rates", - "corporate_actions", - "borrow_observations", - "cash_rate_observations", - "settlement_failures", - "lifecycle_events", - "market_events" - ], - "properties": { - "slice_sequence": { - "$ref": "#/$defs/sequence" - }, - "start_at": { - "$ref": "#/$defs/timestamp" - }, - "end_at": { - "$ref": "#/$defs/timestamp" - }, - "available_at": { - "$ref": "#/$defs/timestamp" - }, - "received_at": { - "$ref": "#/$defs/timestamp" - }, - "bars": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/bar" - } - }, - "fx_rates": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/fxRate" - } - }, - "corporate_actions": { - "type": "array", - "items": { - "$ref": "#/$defs/corporateAction" - } - }, - "borrow_observations": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/borrowObservation" - } - }, - "cash_rate_observations": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/cashRateObservation" - } - }, - "settlement_failures": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/settlementFailure" - } - }, - "lifecycle_events": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/lifecycleEvent" - } - }, - "market_events": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/marketEvent" - } - } - } - }, - "settlementInstruction": { - "type": "object", - "additionalProperties": false, - "required": ["instruction_id", "fill_id", "instrument_id", "currency", "cash_movement", "position_movement", "trade_date", "due_date", "status", "settled_at", "failed_at", "failure_reason"], - "properties": { - "instruction_id": { "$ref": "#/$defs/identifier" }, - "fill_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "currency": { "$ref": "#/$defs/identifier" }, - "cash_movement": { "$ref": "#/$defs/signedDecimal" }, - "position_movement": { "$ref": "#/$defs/signedDecimal" }, - "trade_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "due_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "status": { "enum": ["pending", "settled", "failed"] }, - "settled_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, - "failed_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, - "failure_reason": { "oneOf": [{ "type": "null" }, { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" }] } - } - }, - "targetPortfolio": { - "type": "object", - "additionalProperties": false, - "required": [ - "basis", - "targets" - ], - "properties": { - "basis": { - "enum": [ - "weights", - "quantities" - ] - }, - "targets": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "weight", - "quantity", - "reference_price" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "weight": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/signedDecimal" - } - ] - }, - "quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "reference_price": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/positiveDecimal" - } - ] - } - } - } - } - } - }, - "order": { - "type": "object", - "additionalProperties": false, - "required": [ - "order_id", - "instrument_id", - "side", - "quantity", - "order_kind", - "trigger_price", - "limit_price", - "time_in_force", - "venue_id", - "calendar_id", - "expires_at", - "origin", - "created_event_id", - "updated_event_id", - "created_sequence", - "created_at", - "eligible_after_slice_sequence", - "triggered_at", - "triggered_slice_sequence", - "filled_quantity", - "filled_notional", - "status", - "rejection_reason" - ], - "properties": { - "order_id": { - "$ref": "#/$defs/identifier" - }, - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "side": { - "enum": [ - "buy", - "sell" - ] - }, - "quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "order_kind": { - "enum": [ - "market", - "limit", - "stop", - "stop_limit" - ] - }, - "trigger_price": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/positiveDecimal" - } - ] - }, - "limit_price": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/positiveDecimal" - } - ] - }, - "time_in_force": { - "enum": [ - "gtc", - "ioc", - "fok", - "day", - "gtd" - ] - }, - "venue_id": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/identifier" - } - ] - }, - "calendar_id": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/identifier" - } - ] - }, - "expires_at": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/timestamp" - } - ] - }, - "origin": { - "enum": [ - "direct", - "target_rebalance", - "margin_liquidation", - "borrow_recall", - "instrument_halt", - "instrument_terminal" - ] - }, - "created_event_id": { - "$ref": "#/$defs/identifier" - }, - "updated_event_id": { - "$ref": "#/$defs/identifier" - }, - "created_sequence": { - "$ref": "#/$defs/sequence" - }, - "created_at": { - "$ref": "#/$defs/timestamp" - }, - "eligible_after_slice_sequence": { - "$ref": "#/$defs/nonnegativeSequence" - }, - "triggered_at": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/timestamp" - } - ] - }, - "triggered_slice_sequence": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/sequence" - } - ] - }, - "filled_quantity": { - "$ref": "#/$defs/unsignedDecimal" - }, - "filled_notional": { - "$ref": "#/$defs/unsignedDecimal" - }, - "status": { - "enum": [ - "working", - "partially_filled", - "filled", - "cancelled", - "rejected" - ] - }, - "rejection_reason": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "string", - "minLength": 1 - } - ] - } - } - }, - "orderCancelled": { - "type": "object", - "additionalProperties": false, - "required": [ - "order", - "reason" - ], - "properties": { - "order": { - "$ref": "#/$defs/order" - }, - "reason": { - "enum": [ - "strategy_requested", - "target_replaced", - "market_ioc", - "immediate_or_cancel", - "fill_or_kill", - "day_expired", - "gtd_expired", - "margin_call", - "borrow_recall" - ] - } - } - }, - "splitApplied": { - "type": "object", - "additionalProperties": false, - "required": [ - "action", - "previous_quantity", - "adjusted_quantity" - ], - "properties": { - "action": { - "$ref": "#/$defs/corporateAction" - }, - "previous_quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "adjusted_quantity": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "dividendApplied": { - "type": "object", - "additionalProperties": false, - "required": [ - "action", - "quantity", - "cash_amount" - ], - "properties": { - "action": { - "$ref": "#/$defs/corporateAction" - }, - "quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "cash_amount": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "distributionApplied": { - "type": "object", - "additionalProperties": false, - "required": ["action", "source_quantity", "destination_quantity", "fractional_quantity", "allocated_basis", "fractional_basis", "cash_in_lieu"], - "properties": { - "action": { "$ref": "#/$defs/corporateAction" }, - "source_quantity": { "$ref": "#/$defs/signedDecimal" }, - "destination_quantity": { "$ref": "#/$defs/signedDecimal" }, - "fractional_quantity": { "$ref": "#/$defs/signedDecimal" }, - "allocated_basis": { "$ref": "#/$defs/signedDecimal" }, - "fractional_basis": { "$ref": "#/$defs/signedDecimal" }, - "cash_in_lieu": { "$ref": "#/$defs/signedDecimal" } - } - }, - "lifecycleApplied": { - "type": "object", - "additionalProperties": false, - "required": ["lifecycle_event", "listing", "liquidated_quantity", "cash_amount"], - "properties": { - "lifecycle_event": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/lifecycleEvent" }, - "listing": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "symbol", "status", "provider_mappings"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "symbol": { "$ref": "#/$defs/identifier" }, - "status": { "enum": ["tradable", "halted", "expired", "delisted"] }, - "provider_mappings": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["provider", "provider_instrument_id"], - "properties": { - "provider": { "$ref": "#/$defs/identifier" }, - "provider_instrument_id": { "$ref": "#/$defs/identifier" } - } - } - } - } - }, - "liquidated_quantity": { "$ref": "#/$defs/signedDecimal" }, - "cash_amount": { "$ref": "#/$defs/signedDecimal" } - } - }, - "orderAdjusted": { - "type": "object", - "additionalProperties": false, - "required": [ - "order", - "action_id" - ], - "properties": { - "order": { - "$ref": "#/$defs/order" - }, - "action_id": { - "$ref": "#/$defs/identifier" - } - } - }, - "executionPriceSelected": { - "type": "object", - "additionalProperties": false, - "required": ["order_id", "instrument_id", "side", "reference_price", "spread_adjustment", "impact_adjustment", "final_price"], - "properties": { - "order_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "reference_price": { "$ref": "#/$defs/positiveDecimal" }, - "spread_adjustment": { "$ref": "#/$defs/unsignedDecimal" }, - "impact_adjustment": { "$ref": "#/$defs/unsignedDecimal" }, - "final_price": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "fill": { - "type": "object", - "additionalProperties": false, - "required": [ - "fill_id", - "order_id", - "instrument_id", - "quote_currency", - "side", - "quantity", - "price", - "notional", - "fee", - "executed_at", - "slice_sequence", - "fee_components" - ], - "properties": { - "fill_id": { - "$ref": "#/$defs/identifier" - }, - "order_id": { - "$ref": "#/$defs/identifier" - }, - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "quote_currency": { - "$ref": "#/$defs/identifier" - }, - "side": { - "enum": [ - "buy", - "sell" - ] - }, - "quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "price": { - "$ref": "#/$defs/positiveDecimal" - }, - "notional": { - "$ref": "#/$defs/positiveDecimal" - }, - "fee": { - "$ref": "#/$defs/signedDecimal" - }, - "fee_components": { - "type": "array", - "items": { - "$ref": "#/$defs/calculatedFeeComponent" - } - }, - "executed_at": { - "$ref": "#/$defs/timestamp" - }, - "slice_sequence": { - "$ref": "#/$defs/sequence" - } - } - }, - "calculatedFeeComponent": { - "type": "object", - "additionalProperties": false, - "required": [ - "name", - "kind", - "currency", - "amount", - "quote_amount" - ], - "properties": { - "name": { - "$ref": "#/$defs/identifier" - }, - "kind": { - "enum": [ - "fixed", - "notional_bps", - "per_unit", - "minimum_adjustment", - "maximum_adjustment" - ] - }, - "currency": { - "$ref": "#/$defs/identifier" - }, - "amount": { - "$ref": "#/$defs/signedDecimal" - }, - "quote_amount": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "feeComponentAttribution": { - "type": "object", - "additionalProperties": false, - "required": [ - "name", - "kind", - "currency", - "amount", - "quote_currency", - "quote_amount", - "base_amount" - ], - "properties": { - "name": { - "$ref": "#/$defs/identifier" - }, - "kind": { - "enum": [ - "fixed", - "notional_bps", - "per_unit", - "minimum_adjustment", - "maximum_adjustment" - ] - }, - "currency": { - "$ref": "#/$defs/identifier" - }, - "amount": { - "$ref": "#/$defs/signedDecimal" - }, - "quote_currency": { - "$ref": "#/$defs/identifier" - }, - "quote_amount": { - "$ref": "#/$defs/signedDecimal" - }, - "base_amount": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "quantityThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "unit", - "value" - ], - "properties": { - "unit": { - "const": "quantity" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "moneyThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "unit", - "value" - ], - "properties": { - "unit": { - "const": "money" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "ratioThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "unit", - "value" - ], - "properties": { - "unit": { - "const": "ratio" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "basisPointsThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "unit", - "value" - ], - "properties": { - "unit": { - "const": "basis_points" - }, - "value": { - "type": "integer", - "minimum": 1, - "maximum": 10000 - } - } - }, - "instrumentQuantityThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "unit", - "value" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "unit": { - "const": "quantity" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "instrumentMoneyThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "unit", - "value" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "unit": { - "const": "money" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "currencyMoneyThreshold": { - "type": "object", - "additionalProperties": false, - "required": ["currency", "unit", "value"], - "properties": { - "currency": { "$ref": "#/$defs/identifier" }, - "unit": { "const": "money" }, - "value": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "settlementPositionThreshold": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "unit", "value"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "unit": { "const": "quantity" }, - "value": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "instrumentBasisPointsThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "unit", - "value" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "unit": { - "const": "basis_points" - }, - "value": { - "type": "integer", - "minimum": 1, - "maximum": 10000 - } - } - }, - "instrumentShortingThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "value" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "value": { - "const": false - } - } - }, - "groupMoneyThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "group_id", - "unit", - "value" - ], - "properties": { - "group_id": { - "$ref": "#/$defs/identifier" - }, - "unit": { - "const": "money" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "groupRatioThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "group_id", - "unit", - "value" - ], - "properties": { - "group_id": { - "$ref": "#/$defs/identifier" - }, - "unit": { - "const": "ratio" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "fillClipReason": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "max_order_quantity" - }, - "threshold": { - "$ref": "#/$defs/quantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "max_long_position" - }, - "threshold": { - "$ref": "#/$defs/quantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "max_short_position" - }, - "threshold": { - "$ref": "#/$defs/quantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "max_gross_exposure" - }, - "threshold": { - "$ref": "#/$defs/moneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "max_leverage" - }, - "threshold": { - "$ref": "#/$defs/ratioThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "initial_margin" - }, - "threshold": { - "$ref": "#/$defs/basisPointsThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_max_long_position" - }, - "threshold": { - "$ref": "#/$defs/instrumentQuantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["version", "policy", "threshold"], - "properties": { - "version": { "const": "1" }, - "policy": { "const": "settlement_cash_buying_power" }, - "threshold": { "$ref": "#/$defs/currencyMoneyThreshold" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["version", "policy", "threshold"], - "properties": { - "version": { "const": "1" }, - "policy": { "const": "settlement_position_availability" }, - "threshold": { "$ref": "#/$defs/settlementPositionThreshold" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_max_short_position" - }, - "threshold": { - "$ref": "#/$defs/instrumentQuantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_max_notional_exposure" - }, - "threshold": { - "$ref": "#/$defs/instrumentMoneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_shorting_disabled" - }, - "threshold": { - "$ref": "#/$defs/instrumentShortingThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_borrow_availability" - }, - "threshold": { - "$ref": "#/$defs/instrumentQuantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_initial_margin" - }, - "threshold": { - "$ref": "#/$defs/instrumentBasisPointsThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "group_max_gross_exposure" - }, - "threshold": { - "$ref": "#/$defs/groupMoneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "group_max_long_exposure" - }, - "threshold": { - "$ref": "#/$defs/groupMoneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "group_max_short_exposure" - }, - "threshold": { - "$ref": "#/$defs/groupMoneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "group_max_absolute_net_exposure" - }, - "threshold": { - "$ref": "#/$defs/groupMoneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "group_max_concentration" - }, - "threshold": { - "$ref": "#/$defs/groupRatioThreshold" - } - } - } - ] - }, - "fillClipped": { - "type": "object", - "additionalProperties": false, - "required": [ - "reason", - "order_id", - "instrument_id", - "proposed_quantity", - "permitted_quantity", - "price" - ], - "properties": { - "reason": { - "$ref": "#/$defs/fillClipReason" - }, - "order_id": { - "$ref": "#/$defs/identifier" - }, - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "proposed_quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "permitted_quantity": { - "$ref": "#/$defs/unsignedDecimal" - }, - "price": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "borrowFee": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "quote_currency", - "short_quantity", - "reference_price", - "borrow_bps", - "period_start", - "period_end", - "fee" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "quote_currency": { - "$ref": "#/$defs/identifier" - }, - "short_quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "reference_price": { - "$ref": "#/$defs/positiveDecimal" - }, - "borrow_bps": { - "type": "integer", - "minimum": 1, - "maximum": 10000 - }, - "period_start": { - "$ref": "#/$defs/timestamp" - }, - "period_end": { - "$ref": "#/$defs/timestamp" - }, - "fee": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "borrowCharge": { - "type": "object", - "additionalProperties": false, - "required": [ - "observation", - "quote_currency", - "short_quantity", - "reference_price", - "day_count", - "compounding", - "period_start", - "period_end", - "amount" - ], - "properties": { - "observation": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/borrowObservation" - }, - "quote_currency": { - "$ref": "#/$defs/identifier" - }, - "short_quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "reference_price": { - "$ref": "#/$defs/positiveDecimal" - }, - "day_count": { - "enum": [ - "actual_365", - "actual_360" - ] - }, - "compounding": { - "enum": [ - "simple", - "daily" - ] - }, - "period_start": { - "$ref": "#/$defs/timestamp" - }, - "period_end": { - "$ref": "#/$defs/timestamp" - }, - "amount": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "borrowRecall": { - "type": "object", - "additionalProperties": false, - "required": [ - "observation", - "short_quantity", - "close_out_quantity" - ], - "properties": { - "observation": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/borrowObservation" - }, - "short_quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "close_out_quantity": { - "$ref": "#/$defs/unsignedDecimal" - } - } - }, - "cashInterest": { - "type": "object", - "additionalProperties": false, - "required": [ - "observation", - "opening_balance", - "applied_rate_bps", - "day_count", - "compounding", - "period_start", - "period_end", - "amount", - "closing_balance" - ], - "properties": { - "observation": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/cashRateObservation" - }, - "opening_balance": { - "$ref": "#/$defs/signedDecimal" - }, - "applied_rate_bps": { - "type": "integer", - "minimum": -1000000, - "maximum": 1000000 - }, - "day_count": { - "enum": [ - "actual_365", - "actual_360" - ] - }, - "compounding": { - "enum": [ - "simple", - "daily" - ] - }, - "period_start": { - "$ref": "#/$defs/timestamp" - }, - "period_end": { - "$ref": "#/$defs/timestamp" - }, - "amount": { - "$ref": "#/$defs/signedDecimal" - }, - "closing_balance": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "cashAttribution": { - "type": "object", - "additionalProperties": false, - "required": [ - "currency", - "amount", - "fx_rate", - "base_value", - "interest", - "base_interest", - "settled_amount", - "unsettled_amount", - "base_settled_value", - "base_unsettled_value" - ], - "properties": { - "currency": { - "$ref": "#/$defs/identifier" - }, - "amount": { - "$ref": "#/$defs/signedDecimal" - }, - "fx_rate": { - "$ref": "#/$defs/positiveDecimal" - }, - "base_value": { - "$ref": "#/$defs/signedDecimal" - }, - "interest": { - "$ref": "#/$defs/signedDecimal" - }, - "base_interest": { - "$ref": "#/$defs/signedDecimal" - }, - "settled_amount": { - "$ref": "#/$defs/signedDecimal" - }, - "unsettled_amount": { - "$ref": "#/$defs/signedDecimal" - }, - "base_settled_value": { - "$ref": "#/$defs/signedDecimal" - }, - "base_unsettled_value": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "positionAttribution": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "quote_currency", - "quantity", - "settled_quantity", - "unsettled_quantity", - "mark", - "fx_rate", - "market_value", - "base_market_value", - "cost_basis", - "base_cost_basis", - "realized_pnl", - "base_realized_pnl", - "unrealized_pnl", - "base_unrealized_pnl", - "dividend_pnl", - "base_dividend_pnl", - "execution_fees", - "base_execution_fees", - "borrow_fees", - "base_borrow_fees", - "total_fees", - "base_total_fees", - "execution_fee_components" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "quote_currency": { - "$ref": "#/$defs/identifier" - }, - "quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "settled_quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "unsettled_quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "mark": { - "$ref": "#/$defs/positiveDecimal" - }, - "fx_rate": { - "$ref": "#/$defs/positiveDecimal" - }, - "market_value": { - "$ref": "#/$defs/signedDecimal" - }, - "base_market_value": { - "$ref": "#/$defs/signedDecimal" - }, - "cost_basis": { - "$ref": "#/$defs/signedDecimal" - }, - "base_cost_basis": { - "$ref": "#/$defs/signedDecimal" - }, - "realized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "base_realized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "unrealized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "base_unrealized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "dividend_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "base_dividend_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "execution_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "base_execution_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "borrow_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "base_borrow_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "total_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "base_total_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "execution_fee_components": { - "type": "array", - "items": { - "$ref": "#/$defs/feeComponentAttribution" - } - } - } - }, - "margin": { - "type": "object", - "additionalProperties": false, - "required": [ - "initial_requirement", - "maintenance_requirement", - "initial_excess", - "maintenance_excess", - "margin_call" - ], - "properties": { - "initial_requirement": { - "$ref": "#/$defs/unsignedDecimal" - }, - "maintenance_requirement": { - "$ref": "#/$defs/unsignedDecimal" - }, - "initial_excess": { - "$ref": "#/$defs/signedDecimal" - }, - "maintenance_excess": { - "$ref": "#/$defs/signedDecimal" - }, - "margin_call": { - "type": "boolean" - } - } - }, - "groupExposure": { - "type": "object", - "additionalProperties": false, - "required": [ - "group_id", - "gross_exposure", - "net_exposure", - "long_exposure", - "short_exposure", - "concentration" - ], - "properties": { - "group_id": { - "$ref": "#/$defs/identifier" - }, - "gross_exposure": { - "$ref": "#/$defs/unsignedDecimal" - }, - "net_exposure": { - "$ref": "#/$defs/signedDecimal" - }, - "long_exposure": { - "$ref": "#/$defs/unsignedDecimal" - }, - "short_exposure": { - "$ref": "#/$defs/unsignedDecimal" - }, - "concentration": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/signedDecimal" - } - ] - } - } - }, - "valuation": { - "type": "object", - "additionalProperties": false, - "required": [ - "base_currency", - "cash", - "settled_cash", - "unsettled_cash", - "net_market_value", - "long_market_value", - "short_market_value", - "gross_exposure", - "cost_basis", - "realized_pnl", - "unrealized_pnl", - "equity", - "dividend_pnl", - "execution_fees", - "borrow_fees", - "cash_interest", - "total_fees", - "cash_balances", - "positions", - "margin", - "group_exposures", - "execution_fee_components" - ], - "properties": { - "base_currency": { - "$ref": "#/$defs/identifier" - }, - "cash": { - "$ref": "#/$defs/signedDecimal" - }, - "settled_cash": { - "$ref": "#/$defs/signedDecimal" - }, - "unsettled_cash": { - "$ref": "#/$defs/signedDecimal" - }, - "net_market_value": { - "$ref": "#/$defs/signedDecimal" - }, - "long_market_value": { - "$ref": "#/$defs/unsignedDecimal" - }, - "short_market_value": { - "$ref": "#/$defs/unsignedDecimal" - }, - "gross_exposure": { - "$ref": "#/$defs/unsignedDecimal" - }, - "cost_basis": { - "$ref": "#/$defs/signedDecimal" - }, - "realized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "unrealized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "equity": { - "$ref": "#/$defs/signedDecimal" - }, - "dividend_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "execution_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "borrow_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "total_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "cash_balances": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/cashAttribution" - } - }, - "positions": { - "type": "array", - "items": { - "$ref": "#/$defs/positionAttribution" - } - }, - "margin": { - "$ref": "#/$defs/margin" - }, - "group_exposures": { - "type": "array", - "items": { - "$ref": "#/$defs/groupExposure" - } - }, - "execution_fee_components": { - "type": "array", - "items": { - "$ref": "#/$defs/feeComponentAttribution" - } - }, - "cash_interest": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "intentRejected": { - "type": "object", - "additionalProperties": false, - "required": [ - "reason" - ], - "properties": { - "reason": { - "type": "string", - "minLength": 1 - } - } - }, - "metric": { - "type": "object", - "additionalProperties": false, - "required": [ - "name", - "value" - ], - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "runCompleted": { - "type": "object", - "additionalProperties": false, - "required": [ - "scenario_sha256", - "execution_model", - "valuation", - "order_counts" - ], - "properties": { - "scenario_sha256": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, - "execution_model": { - "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1"] - }, - "valuation": { - "$ref": "#/$defs/valuation" - }, - "order_counts": { - "type": "object", - "additionalProperties": false, - "required": [ - "total", - "active", - "filled", - "rejected", - "cancelled" - ], - "properties": { - "total": { - "type": "integer", - "minimum": 0 - }, - "active": { - "type": "integer", - "minimum": 0 - }, - "filled": { - "type": "integer", - "minimum": 0 - }, - "rejected": { - "type": "integer", - "minimum": 0 - }, - "cancelled": { - "type": "integer", - "minimum": 0 - } - } - } - } - } - } -} diff --git a/contracts/v14/scenario-stream.schema.json b/contracts/v14/scenario-stream.schema.json deleted file mode 100644 index 6093644..0000000 --- a/contracts/v14/scenario-stream.schema.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v14/scenario-stream.schema.json", - "title": "Trading Engine v14 replay scenario stream record", - "description": "Market-slice records are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-record rule is enforced semantically.", - "oneOf": [ - { "$ref": "#/$defs/headerRecord" }, - { "$ref": "#/$defs/sliceRecord" }, - { "$ref": "#/$defs/endRecord" } - ], - "$defs": { - "headerRecord": { - "type": "object", - "additionalProperties": false, - "required": ["contract_version", "scenario_sequence", "record_type", "payload"], - "properties": { - "contract_version": { "const": "14" }, - "scenario_sequence": { "const": "1" }, - "record_type": { "const": "scenario_header" }, - "payload": { "$ref": "#/$defs/headerPayload" } - } - }, - "sliceRecord": { - "type": "object", - "additionalProperties": false, - "required": ["contract_version", "scenario_sequence", "record_type", "payload"], - "properties": { - "contract_version": { "const": "14" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/sequence" }, - "record_type": { "const": "market_slice" }, - "payload": { "$ref": "#/$defs/slicePayload" } - } - }, - "endRecord": { - "type": "object", - "additionalProperties": false, - "required": ["contract_version", "scenario_sequence", "record_type", "payload"], - "properties": { - "contract_version": { "const": "14" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/sequence" }, - "record_type": { "const": "scenario_end" }, - "payload": { - "type": "object", - "additionalProperties": false, - "required": ["slice_count"], - "properties": { "slice_count": { "type": "string", "pattern": "^(?:0|[1-9][0-9]*)$" } } - } - } - }, - "headerPayload": { - "type": "object", - "additionalProperties": false, - "required": ["metadata", "run_id", "base_currency", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "max_internal_events"], - "properties": { - "metadata": { "type": "object" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, - "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/initialPortfolio" }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/instrument" } }, - "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/financing" }, - "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/settlement" }, - "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 } - } - }, - "slicePayload": { - "type": "object", - "additionalProperties": false, - "required": ["market_slice", "intents"], - "properties": { - "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v14/scenario.schema.json b/contracts/v14/scenario.schema.json deleted file mode 100644 index f4117b5..0000000 --- a/contracts/v14/scenario.schema.json +++ /dev/null @@ -1,770 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json", - "title": "Trading Engine v14 replay scenario", - "description": "Strict deterministic scenario contract with explicit venue-local session policies resolved to absolute instants outside the reducer.", - "type": "object", - "additionalProperties": false, - "required": ["contract_version", "metadata", "run_id", "base_currency", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "max_internal_events", "schedule", "slices"], - "properties": { - "contract_version": { "const": "14" }, - "metadata": { "type": "object" }, - "run_id": { "$ref": "#/$defs/identifier" }, - "base_currency": { "$ref": "#/$defs/identifier" }, - "initial_portfolio": { "$ref": "#/$defs/initialPortfolio" }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "#/$defs/instrument" } - }, - "venue_calendars": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/venueCalendar" } - }, - "risk": { "$ref": "#/$defs/risk" }, - "execution": { "$ref": "#/$defs/execution" }, - "financing": { "$ref": "#/$defs/financing" }, - "settlement": { "$ref": "#/$defs/settlement" }, - "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 }, - "schedule": { "type": "array", "items": { "$ref": "#/$defs/scheduleItem" } }, - "slices": { - "description": "Slices are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-item rule is enforced semantically.", - "type": "array", - "items": { "$ref": "#/$defs/marketSlice" } - } - }, - "$defs": { - "settlement": { - "type": "object", - "additionalProperties": false, - "required": ["cash_buying_power", "position_availability", "calendars", "rules"], - "properties": { - "cash_buying_power": { "enum": ["total_cash", "settled_cash"] }, - "position_availability": { "enum": ["total_positions", "settled_positions"] }, - "calendars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementCalendar" } }, - "rules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementRule" } } - } - }, - "settlementCalendar": { - "type": "object", - "additionalProperties": false, - "required": ["calendar_id", "version", "business_dates"], - "properties": { - "calendar_id": { "$ref": "#/$defs/identifier" }, - "version": { "const": "1" }, - "business_dates": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" } } - } - }, - "settlementRule": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "calendar_id", "lag_business_days"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "calendar_id": { "$ref": "#/$defs/identifier" }, - "lag_business_days": { "type": "integer", "minimum": 0, "maximum": 30 } - } - }, - "settlementFailure": { - "type": "object", - "additionalProperties": false, - "required": ["instruction_id", "reason"], - "properties": { - "instruction_id": { "$ref": "#/$defs/identifier" }, - "reason": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } - } - }, - "financing": { - "type": "object", - "additionalProperties": false, - "required": ["day_count", "compounding", "borrow_missing_data", "cash_missing_data", "locate_policy", "recall_policy"], - "properties": { - "day_count": { "enum": ["actual_365", "actual_360"] }, - "compounding": { "enum": ["simple", "daily"] }, - "borrow_missing_data": { "enum": ["reject", "zero"] }, - "cash_missing_data": { "enum": ["reject", "zero"] }, - "locate_policy": { "enum": ["reject_order", "clip_fill"] }, - "recall_policy": { "enum": ["reject_new_shorts", "close_out"] } - } - }, - "borrowObservation": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "effective_at", "available_quantity", "annual_rate_bps", "recalled"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "effective_at": { "$ref": "#/$defs/timestamp" }, - "available_quantity": { "$ref": "#/$defs/unsignedDecimal" }, - "annual_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, - "recalled": { "type": "boolean" } - } - }, - "cashRateObservation": { - "type": "object", - "additionalProperties": false, - "required": ["currency", "effective_at", "credit_rate_bps", "debit_rate_bps"], - "properties": { - "currency": { "$ref": "#/$defs/identifier" }, - "effective_at": { "$ref": "#/$defs/timestamp" }, - "credit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, - "debit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 } - } - }, - "identifier": { - "type": "string", - "minLength": 1, - "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" - }, - "signedDecimal": { - "type": "string", - "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" - }, - "unsignedDecimal": { - "type": "string", - "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "positiveDecimal": { - "type": "string", - "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "sequence": { "type": "string", "pattern": "^[1-9][0-9]*$" }, - "timestamp": { - "type": "string", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" - }, - "cashBalance": { - "type": "object", - "additionalProperties": false, - "required": ["currency", "amount"], - "properties": { - "currency": { "$ref": "#/$defs/identifier" }, - "amount": { "$ref": "#/$defs/signedDecimal" } - } - }, - "initialPortfolio": { - "type": "object", - "additionalProperties": false, - "required": ["cash", "positions", "marks", "fx_rates"], - "properties": { - "cash": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashBalance" } }, - "positions": { "type": "array", "items": { "$ref": "#/$defs/initialPosition" } }, - "marks": { "type": "array", "items": { "$ref": "#/$defs/initialMark" } }, - "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } } - } - }, - "initialPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "cost_basis", "realized_pnl", "dividend_pnl", "execution_fees", "borrow_fees"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "quantity": { "$ref": "#/$defs/signedDecimal" }, - "cost_basis": { "$ref": "#/$defs/signedDecimal" }, - "realized_pnl": { "$ref": "#/$defs/signedDecimal" }, - "dividend_pnl": { "$ref": "#/$defs/signedDecimal" }, - "execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, - "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "initialMark": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "price"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "price": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "instrument": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "symbol", "quote_currency", "tick_size", "lot_size"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "symbol": { "$ref": "#/$defs/identifier" }, - "quote_currency": { "$ref": "#/$defs/identifier" }, - "tick_size": { "$ref": "#/$defs/positiveDecimal" }, - "lot_size": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "venueCalendar": { - "type": "object", - "additionalProperties": false, - "required": ["calendar_id", "calendar_version", "venue_id", "instrument_ids", "sessions"], - "properties": { - "calendar_id": { "$ref": "#/$defs/identifier" }, - "calendar_version": { "const": "1" }, - "venue_id": { "$ref": "#/$defs/identifier" }, - "instrument_ids": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { "$ref": "#/$defs/identifier" } - }, - "sessions": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/venueSession" } - } - } - }, - "venueSession": { - "oneOf": [ - { "$ref": "#/$defs/openVenueSession" }, - { "$ref": "#/$defs/holidayVenueSession" } - ] - }, - "openVenueSession": { - "type": "object", - "additionalProperties": false, - "required": ["session_date", "policy", "phases"], - "properties": { - "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "policy": { "enum": ["regular", "early_close"] }, - "phases": { - "type": "array", - "minItems": 1, - "maxItems": 5, - "items": { "$ref": "#/$defs/venuePhase" } - } - } - }, - "holidayVenueSession": { - "type": "object", - "additionalProperties": false, - "required": ["session_date", "policy", "phases"], - "properties": { - "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "policy": { "const": "holiday" }, - "phases": { "type": "array", "maxItems": 0 } - } - }, - "venuePhase": { - "type": "object", - "additionalProperties": false, - "required": ["phase", "opens_at", "closes_at"], - "properties": { - "phase": { "enum": ["premarket", "opening_auction", "regular", "closing_auction", "postmarket"] }, - "opens_at": { "$ref": "#/$defs/timestamp" }, - "closes_at": { "$ref": "#/$defs/timestamp" } - } - }, - "risk": { - "type": "object", - "additionalProperties": false, - "required": ["max_gross_exposure", "max_leverage", "short_borrow_bps", "instrument_policies", "groups"], - "properties": { - "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, - "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, - "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "instrument_policies": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/instrumentRiskPolicy" } - }, - "groups": { - "type": "array", - "items": { "$ref": "#/$defs/riskGroup" } - } - } - }, - "instrumentRiskPolicy": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "max_order_quantity", "max_long_position", "max_short_position", "max_notional_exposure", "initial_margin_bps", "maintenance_margin_bps", "shorting_allowed"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, - "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_notional_exposure": { "$ref": "#/$defs/positiveDecimal" }, - "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "shorting_allowed": { "type": "boolean" } - } - }, - "nullablePositiveDecimal": { - "oneOf": [ - { "type": "null" }, - { "$ref": "#/$defs/positiveDecimal" } - ] - }, - "riskGroup": { - "type": "object", - "additionalProperties": false, - "required": ["group_id", "group_version", "group_type", "instrument_ids", "limits"], - "properties": { - "group_id": { "$ref": "#/$defs/identifier" }, - "group_version": { "const": "1" }, - "group_type": { "enum": ["issuer", "sector", "currency", "country", "asset_class", "custom"] }, - "instrument_ids": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { "$ref": "#/$defs/identifier" } - }, - "limits": { "$ref": "#/$defs/riskGroupLimits" } - } - }, - "riskGroupLimits": { - "type": "object", - "additionalProperties": false, - "required": ["max_gross_exposure", "max_long_exposure", "max_short_exposure", "max_absolute_net_exposure", "max_concentration"], - "properties": { - "max_gross_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_long_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_short_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_absolute_net_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_concentration": { - "oneOf": [ - { "type": "null" }, - { "allOf": [{ "$ref": "#/$defs/positiveDecimal" }, { "pattern": "^(?:0[.][0-9]{0,5}[1-9]|1)$" }] } - ] - } - } - }, - "execution": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "enum": ["completed_bar_next_open_v1", "completed_bar_adverse_touch_v1"] }, - "configuration": { "$ref": "#/$defs/conservativeBarConfiguration" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "quote_trade_v1" }, - "configuration": { "$ref": "#/$defs/quoteTradeConfiguration" } - } - } - ] - }, - "completedBarV1Configuration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fee_schedules"], - "properties": { - "version": { "const": "2" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } - } - }, - "conservativeBarConfiguration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fee_schedules", "spread_model", "impact_model"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, - "spread_model": { "$ref": "#/$defs/fixedSpreadModel" }, - "impact_model": { "$ref": "#/$defs/linearImpactModel" } - } - }, - "quoteTradeConfiguration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fee_schedules"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } - } - }, - "fixedSpreadModel": { - "type": "object", - "additionalProperties": false, - "required": ["model", "half_spread_bps"], - "properties": { - "model": { "const": "fixed_half_spread_v1" }, - "half_spread_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "linearImpactModel": { - "type": "object", - "additionalProperties": false, - "required": ["model", "coefficient_bps", "missing_volume_policy"], - "properties": { - "model": { "const": "linear_participation_v1" }, - "coefficient_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "missing_volume_policy": { "enum": ["reject", "zero_impact"] } - } - }, - "feeSchedule": { - "type": "object", - "additionalProperties": false, - "required": ["schedule_id", "instrument_id", "settlement_currency", "minimum", "maximum", "components"], - "properties": { - "schedule_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "settlement_currency": { "$ref": "#/$defs/identifier" }, - "minimum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, - "maximum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, - "components": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeComponent" } } - } - }, - "feeComponent": { - "type": "object", - "additionalProperties": false, - "required": ["name", "currency", "kind", "value", "rounding", "applies_to"], - "properties": { - "name": { "$ref": "#/$defs/identifier" }, - "currency": { "$ref": "#/$defs/identifier" }, - "kind": { "enum": ["fixed", "notional_bps", "per_unit"] }, - "value": { "oneOf": [{ "$ref": "#/$defs/signedDecimal" }, { "type": "integer", "minimum": -10000, "maximum": 10000 }] }, - "rounding": { "enum": ["up", "down", "nearest"] }, - "applies_to": { "enum": ["any", "maker", "taker"] } - }, - "allOf": [ - { "if": { "properties": { "kind": { "const": "notional_bps" } } }, "then": { "properties": { "value": { "type": "integer" } } } }, - { "if": { "properties": { "kind": { "enum": ["fixed", "per_unit"] } } }, "then": { "properties": { "value": { "$ref": "#/$defs/signedDecimal" } } } } - ] - }, - "scheduleItem": { - "type": "object", - "additionalProperties": false, - "required": ["after_slice_sequence", "intents"], - "properties": { - "after_slice_sequence": { "$ref": "#/$defs/sequence" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "#/$defs/intent" } } - } - }, - "intent": { - "oneOf": [ - { "$ref": "#/$defs/targetWeights" }, - { "$ref": "#/$defs/targetQuantities" }, - { "$ref": "#/$defs/submitOrder" }, - { "$ref": "#/$defs/cancelOrder" }, - { "$ref": "#/$defs/metric" } - ] - }, - "targetWeights": { - "type": "object", - "additionalProperties": false, - "required": ["type", "targets"], - "properties": { - "type": { "const": "target_weights" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "weight"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "weight": { "$ref": "#/$defs/signedDecimal" } - } - } - } - } - }, - "targetQuantities": { - "type": "object", - "additionalProperties": false, - "required": ["type", "targets"], - "properties": { - "type": { "const": "target_quantities" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "quantity": { "$ref": "#/$defs/signedDecimal" } - } - } - } - } - }, - "submitOrder": { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "side", "quantity", "order_kind", "trigger_price", "limit_price", "time_in_force", "venue_id", "calendar_id", "expires_at"], - "properties": { - "type": { "const": "submit_order" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "order_kind": { "enum": ["market", "limit", "stop", "stop_limit"] }, - "trigger_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, - "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, - "time_in_force": { "enum": ["gtc", "ioc", "fok", "day", "gtd"] }, - "venue_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, - "calendar_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, - "expires_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] } - }, - "allOf": [ - { "if": { "properties": { "order_kind": { "const": "market" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "type": "null" } } } }, - { "if": { "properties": { "order_kind": { "const": "limit" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, - { "if": { "properties": { "order_kind": { "const": "stop" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "type": "null" } } } }, - { "if": { "properties": { "order_kind": { "const": "stop_limit" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, - { "if": { "properties": { "time_in_force": { "const": "day" } } }, "then": { "properties": { "venue_id": { "$ref": "#/$defs/identifier" }, "calendar_id": { "$ref": "#/$defs/identifier" }, "expires_at": { "type": "null" } } } }, - { "if": { "properties": { "time_in_force": { "const": "gtd" } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "$ref": "#/$defs/timestamp" } } } }, - { "if": { "properties": { "time_in_force": { "enum": ["gtc", "ioc", "fok"] } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "type": "null" } } } } - ] - }, - "cancelOrder": { - "type": "object", - "additionalProperties": false, - "required": ["type", "order_id"], - "properties": { - "type": { "const": "cancel_order" }, - "order_id": { "$ref": "#/$defs/identifier" } - } - }, - "metric": { - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "value"], - "properties": { - "type": { "const": "emit_metric" }, - "name": { "type": "string" }, - "value": { "type": "string" } - } - }, - "marketSlice": { - "type": "object", - "additionalProperties": false, - "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "market_events", "fx_rates", "corporate_actions", "borrow_observations", "cash_rate_observations", "settlement_failures", "lifecycle_events"], - "properties": { - "slice_sequence": { "$ref": "#/$defs/sequence" }, - "start_at": { "$ref": "#/$defs/timestamp" }, - "end_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "bars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/bar" } }, - "market_events": { "type": "array", "items": { "$ref": "#/$defs/marketEvent" } }, - "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } }, - "corporate_actions": { "type": "array", "items": { "$ref": "#/$defs/corporateAction" } }, - "borrow_observations": { "type": "array", "items": { "$ref": "#/$defs/borrowObservation" } }, - "cash_rate_observations": { "type": "array", "items": { "$ref": "#/$defs/cashRateObservation" } }, - "settlement_failures": { "type": "array", "items": { "$ref": "#/$defs/settlementFailure" } }, - "lifecycle_events": { "type": "array", "items": { "$ref": "#/$defs/lifecycleEvent" } } - } - }, - "bar": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "open", "high", "low", "close", "volume"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "open": { "$ref": "#/$defs/positiveDecimal" }, - "high": { "$ref": "#/$defs/positiveDecimal" }, - "low": { "$ref": "#/$defs/positiveDecimal" }, - "close": { "$ref": "#/$defs/positiveDecimal" }, - "volume": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] } - } - }, - "marketEvent": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "bid_price", "bid_quantity", "ask_price", "ask_quantity"], - "properties": { - "type": { "const": "quote" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "bid_price": { "$ref": "#/$defs/positiveDecimal" }, - "bid_quantity": { "$ref": "#/$defs/positiveDecimal" }, - "ask_price": { "$ref": "#/$defs/positiveDecimal" }, - "ask_quantity": { "$ref": "#/$defs/positiveDecimal" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "price", "quantity", "aggressor_side"], - "properties": { - "type": { "const": "trade" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "aggressor_side": { "enum": ["buy", "sell", "unknown"] } - } - } - ] - }, - "fxRate": { - "type": "object", - "additionalProperties": false, - "required": ["currency", "rate"], - "properties": { - "currency": { "$ref": "#/$defs/identifier" }, - "rate": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "corporateAction": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "action_id", "instrument_id", "numerator", "denominator"], - "properties": { - "type": { "const": "split" }, - "action_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "numerator": { "$ref": "#/$defs/sequence" }, - "denominator": { "$ref": "#/$defs/sequence" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "action_id", "instrument_id", "amount_per_unit"], - "properties": { - "type": { "const": "cash_dividend" }, - "action_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "amount_per_unit": { "$ref": "#/$defs/positiveDecimal" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], - "properties": { - "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, - "action_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "destination_instrument_id": { "$ref": "#/$defs/identifier" }, - "numerator": { "$ref": "#/$defs/sequence" }, - "denominator": { "$ref": "#/$defs/sequence" }, - "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fractional_policy": { "$ref": "#/$defs/fractionalPolicy" } - } - } - ] - }, - "fractionalPolicy": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["policy"], - "properties": { "policy": { "const": "reject" } } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["policy", "price", "currency"], - "properties": { - "policy": { "const": "cash_in_lieu" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "currency": { "$ref": "#/$defs/identifier" } - } - } - ] - }, - "terminalPolicy": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["policy"], - "properties": { "policy": { "const": "hold" } } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["policy", "price", "currency"], - "properties": { - "policy": { "const": "cash_out" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "currency": { "$ref": "#/$defs/identifier" } - } - } - ] - }, - "lifecycleEvent": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "reason"], - "properties": { - "type": { "const": "halt" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "reason": { "type": "string", "minLength": 1 } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id"], - "properties": { - "type": { "const": "resume" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "symbol", "provider", "provider_instrument_id"], - "properties": { - "type": { "const": "identifier_change" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "symbol": { "$ref": "#/$defs/identifier" }, - "provider": { "$ref": "#/$defs/identifier" }, - "provider_instrument_id": { "$ref": "#/$defs/identifier" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "terminal_policy"], - "properties": { - "type": { "const": "expiration" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "terminal_policy": { "$ref": "#/$defs/terminalPolicy" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "terminal_policy", "reason"], - "properties": { - "type": { "const": "delisting" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "terminal_policy": { "$ref": "#/$defs/terminalPolicy" }, - "reason": { "type": "string", "minLength": 1 } - } - } - ] - } - } -} diff --git a/contracts/v15/README.md b/contracts/v15/README.md deleted file mode 100644 index a1917b8..0000000 --- a/contracts/v15/README.md +++ /dev/null @@ -1,117 +0,0 @@ -# Trading Engine contract v15 - -This directory is the authoritative v15 process and file contract shared by Trading Engine and its -clients. Versions 14 through 3 remain readable during their client transitions. - -- `scenario.schema.json` validates batch replay inputs. -- `scenario-stream.schema.json` validates each JSON Lines scenario-stream record. -- `journal.schema.json` validates each JSON Lines audit record. -- The files under `fixtures/` form the canonical valid conformance corpus. -- `fill-clipped.scenario.json` and its journal exercise a leverage-limited partial fill. - -Version 7 requires exactly one explicit risk policy per catalog instrument. Each policy defines -order, signed-position, notional, initial-margin, maintenance-margin, and shorting limits. Versioned -risk groups have explicit membership, may overlap, and can constrain gross, long, short, absolute -net, and gross-to-equity concentration exposure. - -Runtime validation requires exact currency, position-mark, and FX coverage; known instruments; -lot-aligned quantities; tick-aligned positive marks; basis with the same sign as quantity; -nonnegative fee histories; the base FX rate equal to one; instrument, group, aggregate exposure, -leverage, and initial-margin limits. Signed cash is valid. A successful v15 run emits `initial_state` -immediately after `run_started`, followed by a reconciled initial `valuation`, before market data. - -Admission and fill clipping include working-order reservations. When multiple groups limit the same -fill, lexical group identity is the deterministic tie breaker. Valuations and strategy contexts -carry group exposure snapshots, and clipping thresholds identify the exact instrument or group. - -Every v15 scenario, stream record, and journal record carries `"contract_version": "15"`. - -Version 8 adds explicit `market`, `limit`, `stop`, and `stop_limit` orders with `gtc`, `ioc`, -`fok`, `day`, and `gtd` time-in-force policies. `day` orders identify both their venue and the -exact versioned calendar; `gtd` orders carry an absolute expiry timestamp. Older contracts retain -their frozen mapping: market orders are IOC and limit orders are GTC. - -Stops evaluate only completed OHLCV bars. A gap through the trigger records the bar start as the -trigger time; an intrabar touch records the bar end. Trigger state and slice sequence are journaled, -and an activated order cannot execute before the following slice. A stop becomes a market order; -a stop-limit becomes its configured limit order. Splits adjust both trigger and limit prices. - -IOC orders cancel any remainder after their first eligible slice. FOK orders fill only when the -full remaining quantity fits both execution capacity and risk capacity, otherwise they cancel with -no fill. DAY orders cancel after matching the slice that reaches the selected session's final -phase close. GTD orders cancel before matching any completed bar whose end reaches or passes the -expiry, avoiding ambiguous partial-bar execution. - -The v10 `execution` object uses `completed_bar_v1` configuration version `"2"`: participation basis -points plus exactly one composable fee schedule per instrument. Named fixed, notional-basis-point, -and per-unit components declare currency, rounding, and maker/taker applicability. Optional -per-fill minimums and caps use the schedule settlement currency; negative components represent -rebates. Fills and valuations retain every native, quote, and base-currency attribution. Runtime -capabilities also advertise frozen configuration version `"1"` for older scenario contracts. - -Version 10 adds a required `financing` policy and effective-time observations on every market -slice. Borrow observations provide per-instrument locate availability, signed annual rates, and -recall state. Cash observations provide separate annual credit and debit rates per currency. -Policies select Actual/365 or Actual/360 day count, simple or daily compounding, missing-data -handling, locate rejection or fill clipping, and recall rejection or deterministic close-out. - -Borrow availability is enforced when a fill would create or increase a short. Recalls cancel -active sells and may submit priority IOC covers until the position is flat. Borrow charges and cash -interest use the exact slice interval, update native ledgers deterministically, and emit dedicated -journal records. Valuations report cash interest separately and include it in aggregate realized -P&L. Version 9 and earlier retain their frozen fixed-borrow behavior and wire shapes. - -Version 12 separates trade-date economic accounting from settlement-date availability. A required -settlement policy selects total or settled cash buying power and total or settled position -availability. Versioned calendars enumerate canonical business dates, and each instrument has an -explicit business-day lag. Every fill creates a deterministic settlement instruction containing -its cash and position movements, trade date, and due date. A due instruction either settles on the -first eligible slice or records a named failure supplied by that slice. - -Valuations and strategy contexts report settled and unsettled cash and quantities without changing -economic equity. Journals include instruction-created, completed, and failed events. Scenario v10 -and strategy protocol v8 retain their frozen immediate-settlement wire behavior. - -Version 12 adds exact stock-dividend, rights, and spin-off distributions. Each distribution names -its destination instrument, exact entitlement ratio, basis allocation in basis points, and either -rejects fractional entitlements or converts them to cash at an explicit price and currency. -Stock dividends adjust persistent targets and eligible working orders; every distribution journals -delivered quantity, fractional quantity, allocated basis, fractional basis, and cash in lieu. - -Lifecycle events keep stable instrument identity separate from mutable symbol and provider -mappings. Halt and resume transitions control tradability. Expiration and delisting are terminal, -cancel active orders, clear target exposure, and require an explicit hold or cash-out policy. -Cash-out specifies its terminal price and currency. Every transition journals the source event, -resulting listing state, provider provenance, liquidated quantity, and cash attribution. - -Version 13 adds `completed_bar_next_open_v1` and `completed_bar_adverse_touch_v1` without changing -the frozen `completed_bar_v1` semantics. Next-open limits require a marketable later open; -adverse-touch limits require a one-tick trade-through before a maker fill is eligible. Both models -declare fixed half-spread and linear participation-impact catalogs, including an explicit policy -for missing bar volume. Price costs round away from the reference price to instrument ticks and -cannot violate a limit. An `execution_price_selected` audit record attributes the reference price, -spread adjustment, impact adjustment, and final executable price before each fill. - -Version 14 adds `quote_trade_v1` and causally ordered `market_events`. Quotes expose bid/ask price -and displayed size. Trades expose price, size, and buy, sell, or unknown aggressor side. Each event -records economic, availability, and receipt timestamps plus a positive ingest sequence. Replay -orders events by availability, receipt, and ingest sequence. Marketable orders consume only -displayed quote liquidity; passive orders require appropriately aggressed trade evidence, and an -unknown aggressor never fills them. Event capacity is shared deterministically across order -priority and fills retain the event's economic timestamp. Completed bars remain the valuation -boundary. The `quote-trade` batch, stream, and journal fixtures demonstrate equivalent replay. - -Version 15 adds `order_book_v1` and bounded level-two `order_book_events`. Every per-instrument -slice bundle starts with a complete snapshot and continues with contiguous absolute set, delete, -and aggressor-classified trade updates. Snapshots and updates reject crossed books, missing -deletes, sequence gaps, tick or lot misalignment, and depth beyond the configured limit; locked -books are valid. State is rebuilt from each slice snapshot, so replay never depends on hidden data -from a prior slice. - -Marketable orders walk observable opposite-side depth in price priority. Passive limit orders join -behind displayed same-price quantity and earlier engine orders. Reductions decrease quantity ahead, -adds join behind, and only appropriately aggressed trades consume the queue and fill the order. -Partial fills and cancellations therefore remain deterministic. Book liquidity is independent of -bar and quote/trade execution semantics, while completed bars remain the valuation boundary. The -`order-book` batch, stream, and journal fixtures demonstrate cancellation, queue depletion, maker -fills, bounded state, and batch/stream equivalence. diff --git a/contracts/v15/dune b/contracts/v15/dune deleted file mode 100644 index d1742ac..0000000 --- a/contracts/v15/dune +++ /dev/null @@ -1,36 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v15/journal.schema.json) - (scenario-stream.schema.json as contracts/v15/scenario-stream.schema.json) - (scenario.schema.json as contracts/v15/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v15/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v15/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl - as - contracts/v15/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v15/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v15/fixtures/fill-clipped.scenario.json) - (fixtures/quote-trade.journal.jsonl - as - contracts/v15/fixtures/quote-trade.journal.jsonl) - (fixtures/quote-trade.scenario.json - as - contracts/v15/fixtures/quote-trade.scenario.json) - (fixtures/quote-trade.scenario.jsonl - as - contracts/v15/fixtures/quote-trade.scenario.jsonl) - (fixtures/order-book.journal.jsonl - as - contracts/v15/fixtures/order-book.journal.jsonl) - (fixtures/order-book.scenario.json - as - contracts/v15/fixtures/order-book.scenario.json) - (fixtures/order-book.scenario.jsonl - as - contracts/v15/fixtures/order-book.scenario.jsonl))) diff --git a/contracts/v15/fixtures/demo.journal.jsonl b/contracts/v15/fixtures/demo.journal.jsonl deleted file mode 100644 index c14e704..0000000 --- a/contracts/v15/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,29 +0,0 @@ -{"contract_version":"15","engine_sequence":"1","event_id":"demo-event-000000000001","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"068f28c64905f5847ed3ecfac808940c3f1ba43e0d198d96f11929ba234703bc","execution_model":"completed_bar_adverse_touch_v1"}} -{"contract_version":"15","engine_sequence":"2","event_id":"demo-event-000000000002","causation_ids":["demo-event-000000000001"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"1","cost_basis":"90","realized_pnl":"5","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25"}],"marks":[{"instrument_id":"demo-equity-acme","price":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"10000","net_market_value":"100","long_market_value":"100","short_market_value":"0","gross_exposure":"100","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"10","equity":"10100","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"100","fx_rate":"1","market_value":"100","base_market_value":"100","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"10","base_unrealized_pnl":"10","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} -{"contract_version":"15","engine_sequence":"3","event_id":"demo-event-000000000003","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"10000","net_market_value":"100","long_market_value":"100","short_market_value":"0","gross_exposure":"100","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"10","equity":"10100","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"100","fx_rate":"1","market_value":"100","base_market_value":"100","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"10","base_unrealized_pnl":"10","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"15","engine_sequence":"5","event_id":"demo-event-000000000005","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"10000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-02T14:30:00.000000Z","period_end":"2026-01-02T21:00:00.000000Z","amount":"0.074201","closing_balance":"10000.074201"}} -{"contract_version":"15","engine_sequence":"6","event_id":"demo-event-000000000006","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} -{"contract_version":"15","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} -{"contract_version":"15","engine_sequence":"8","event_id":"demo-event-000000000008","causation_ids":["demo-event-000000000004","demo-event-000000000006"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"8.715","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000008","updated_event_id":"demo-event-000000000008","created_sequence":"8","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"15","engine_sequence":"9","event_id":"demo-event-000000000009","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"10000.074201","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5.074201","unrealized_pnl":"14","equity":"10104.074201","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000.074201","fx_rate":"1","base_value":"10000.074201","interest":"0.074201","base_interest":"0.074201","settled_amount":"10000.074201","unsettled_amount":"0","base_settled_value":"10000.074201","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"104","fx_rate":"1","market_value":"104","base_market_value":"104","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"14","base_unrealized_pnl":"14","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.074201","settled_cash":"10000.074201","unsettled_cash":"0","margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052.074201","maintenance_excess":"10078.074201","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"13"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"15","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"10000.074201","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-05T14:30:00.000000Z","period_end":"2026-01-05T21:00:00.000000Z","amount":"0.074201","closing_balance":"10000.148402"}} -{"contract_version":"15","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000008","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","reference_price":"103","spread_adjustment":"0.06","impact_adjustment":"0.13","final_price":"103.19"}} -{"contract_version":"15","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000012"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000001","order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"6.5","price":"103.19","notional":"670.735","fee":"0.920735","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_amount":"0.670735"}]}} -{"contract_version":"15","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000008","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"order_cancelled","payload":{"order":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"8.715","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000008","updated_event_id":"demo-event-000000000008","created_sequence":"8","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"6.5","filled_notional":"670.735","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"15","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000006","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","side":"buy","quantity":"2.215","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000015","updated_event_id":"demo-event-000000000015","created_sequence":"15","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"15","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9328.492667","net_market_value":"802.5","long_market_value":"802.5","short_market_value":"0","gross_exposure":"802.5","cost_basis":"761.655735","realized_pnl":"5.148402","unrealized_pnl":"40.844265","equity":"10130.992667","dividend_pnl":"1","execution_fees":"1.420735","borrow_fees":"0.25","total_fees":"1.670735","cash_balances":[{"currency":"USD","amount":"9328.492667","fx_rate":"1","base_value":"9328.492667","interest":"0.148402","base_interest":"0.148402","settled_amount":"9328.492667","unsettled_amount":"0","base_settled_value":"9328.492667","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7.5","mark":"107","fx_rate":"1","market_value":"802.5","base_market_value":"802.5","cost_basis":"761.655735","base_cost_basis":"761.655735","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.844265","base_unrealized_pnl":"40.844265","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.420735","base_execution_fees":"1.420735","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.670735","base_total_fees":"1.670735","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_currency":"USD","quote_amount":"0.25","base_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_currency":"USD","quote_amount":"0.670735","base_amount":"0.670735"}],"settled_quantity":"7.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_currency":"USD","quote_amount":"0.25","base_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_currency":"USD","quote_amount":"0.670735","base_amount":"0.670735"}],"cash_interest":"0.148402","settled_cash":"9328.492667","unsettled_cash":"0","margin":{"initial_requirement":"401.25","maintenance_requirement":"200.625","initial_excess":"9729.742667","maintenance_excess":"9930.367667","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"3","start_at":"2026-01-06T14:30:00.000000Z","end_at":"2026-01-06T21:00:00.000000Z","available_at":"2026-01-06T21:00:01.000000Z","received_at":"2026-01-06T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"107","high":"109","low":"104","close":"105","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"15","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"9328.492667","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-06T14:30:00.000000Z","period_end":"2026-01-06T21:00:00.000000Z","amount":"0.069218","closing_balance":"9328.561885"}} -{"contract_version":"15","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","side":"buy","reference_price":"107","spread_adjustment":"0.06","impact_adjustment":"0.01","final_price":"107.07"}} -{"contract_version":"15","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000002","order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"2.215","price":"107.07","notional":"237.16005","fee":"0.487161","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.237161","quote_amount":"0.237161"}]}} -{"contract_version":"15","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"target_portfolio_requested","payload":{"basis":"quantities","targets":[{"instrument_id":"demo-equity-acme","weight":null,"quantity":"2.5","reference_price":null}]}} -{"contract_version":"15","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000017","demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","side":"sell","quantity":"7.215","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000022","updated_event_id":"demo-event-000000000022","created_sequence":"22","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"15","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.914674","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.302946","realized_pnl":"5.21762","unrealized_pnl":"20.772054","equity":"10110.989674","dividend_pnl":"1","execution_fees":"1.907896","borrow_fees":"0.25","total_fees":"2.157896","cash_balances":[{"currency":"USD","amount":"9090.914674","fx_rate":"1","base_value":"9090.914674","interest":"0.21762","base_interest":"0.21762","settled_amount":"9090.914674","unsettled_amount":"0","base_settled_value":"9090.914674","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"9.715","mark":"105","fx_rate":"1","market_value":"1020.075","base_market_value":"1020.075","cost_basis":"999.302946","base_cost_basis":"999.302946","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.772054","base_unrealized_pnl":"20.772054","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.907896","base_execution_fees":"1.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.157896","base_total_fees":"2.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.5","quote_currency":"USD","quote_amount":"0.5","base_amount":"0.5"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.907896","quote_currency":"USD","quote_amount":"0.907896","base_amount":"0.907896"}],"settled_quantity":"9.715","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.5","quote_currency":"USD","quote_amount":"0.5","base_amount":"0.5"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.907896","quote_currency":"USD","quote_amount":"0.907896","base_amount":"0.907896"}],"cash_interest":"0.21762","settled_cash":"9090.914674","unsettled_cash":"0","margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.952174","maintenance_excess":"9855.970924","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"24","event_id":"demo-event-000000000024","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"4","start_at":"2026-01-07T14:30:00.000000Z","end_at":"2026-01-07T21:00:00.000000Z","available_at":"2026-01-07T21:00:01.000000Z","received_at":"2026-01-07T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"105","high":"107","low":"103","close":"106","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"15","engine_sequence":"25","event_id":"demo-event-000000000025","causation_ids":["demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"9090.914674","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-07T14:30:00.000000Z","period_end":"2026-01-07T21:00:00.000000Z","amount":"0.067455","closing_balance":"9090.982129"}} -{"contract_version":"15","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000022","demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","side":"sell","reference_price":"105","spread_adjustment":"0.06","impact_adjustment":"0.02","final_price":"104.92"}} -{"contract_version":"15","engine_sequence":"27","event_id":"demo-event-000000000027","causation_ids":["demo-event-000000000026"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000003","order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"sell","quantity":"7.215","price":"104.92","notional":"756.9978","fee":"1","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.756998","quote_amount":"0.756998"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_amount":"-0.006998"}]}} -{"contract_version":"15","engine_sequence":"28","event_id":"demo-event-000000000028","causation_ids":["demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.979929","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.154644","realized_pnl":"19.134573","unrealized_pnl":"7.845356","equity":"10111.979929","dividend_pnl":"1","execution_fees":"2.907896","borrow_fees":"0.25","total_fees":"3.157896","cash_balances":[{"currency":"USD","amount":"9846.979929","fx_rate":"1","base_value":"9846.979929","interest":"0.285075","base_interest":"0.285075","settled_amount":"9846.979929","unsettled_amount":"0","base_settled_value":"9846.979929","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","mark":"106","fx_rate":"1","market_value":"265","base_market_value":"265","cost_basis":"257.154644","base_cost_basis":"257.154644","realized_pnl":"18.849498","base_realized_pnl":"18.849498","unrealized_pnl":"7.845356","base_unrealized_pnl":"7.845356","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.907896","base_execution_fees":"2.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"settled_quantity":"2.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"cash_interest":"0.285075","settled_cash":"9846.979929","unsettled_cash":"0","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.479929","maintenance_excess":"10045.729929","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"29","event_id":"demo-event-000000000029","causation_ids":["demo-event-000000000028"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"068f28c64905f5847ed3ecfac808940c3f1ba43e0d198d96f11929ba234703bc","execution_model":"completed_bar_adverse_touch_v1","valuation":{"base_currency":"USD","cash":"9846.979929","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.154644","realized_pnl":"19.134573","unrealized_pnl":"7.845356","equity":"10111.979929","dividend_pnl":"1","execution_fees":"2.907896","borrow_fees":"0.25","total_fees":"3.157896","cash_balances":[{"currency":"USD","amount":"9846.979929","fx_rate":"1","base_value":"9846.979929","interest":"0.285075","base_interest":"0.285075","settled_amount":"9846.979929","unsettled_amount":"0","base_settled_value":"9846.979929","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","mark":"106","fx_rate":"1","market_value":"265","base_market_value":"265","cost_basis":"257.154644","base_cost_basis":"257.154644","realized_pnl":"18.849498","base_realized_pnl":"18.849498","unrealized_pnl":"7.845356","base_unrealized_pnl":"7.845356","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.907896","base_execution_fees":"2.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"settled_quantity":"2.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"cash_interest":"0.285075","settled_cash":"9846.979929","unsettled_cash":"0","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.479929","maintenance_excess":"10045.729929","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v15/fixtures/demo.scenario.json b/contracts/v15/fixtures/demo.scenario.json deleted file mode 100644 index b2f17e1..0000000 --- a/contracts/v15/fixtures/demo.scenario.json +++ /dev/null @@ -1,465 +0,0 @@ -{ - "contract_version": "15", - "metadata": { - "producer": "trading-engine-demo", - "purpose": "deterministic conformance fixture" - }, - "run_id": "demo", - "base_currency": "USD", - "initial_portfolio": { - "cash": [ - { - "currency": "USD", - "amount": "10000" - } - ], - "positions": [ - { - "instrument_id": "demo-equity-acme", - "quantity": "1", - "cost_basis": "90", - "realized_pnl": "5", - "dividend_pnl": "1", - "execution_fees": "0.5", - "borrow_fees": "0.25" - } - ], - "marks": [ - { - "instrument_id": "demo-equity-acme", - "price": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ] - }, - "instruments": [ - { - "instrument_id": "demo-equity-acme", - "symbol": "ACME", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "0.001" - } - ], - "venue_calendars": [ - { - "calendar_id": "demo-xnas-2026", - "calendar_version": "1", - "venue_id": "XNAS", - "instrument_ids": [ - "demo-equity-acme" - ], - "sessions": [ - { - "session_date": "2026-01-01", - "policy": "holiday", - "phases": [] - }, - { - "session_date": "2026-01-02", - "policy": "regular", - "phases": [ - { - "phase": "premarket", - "opens_at": "2026-01-02T09:00:00Z", - "closes_at": "2026-01-02T14:25:00Z" - }, - { - "phase": "opening_auction", - "opens_at": "2026-01-02T14:25:00Z", - "closes_at": "2026-01-02T14:30:00Z" - }, - { - "phase": "regular", - "opens_at": "2026-01-02T14:30:00Z", - "closes_at": "2026-01-02T20:55:00Z" - }, - { - "phase": "closing_auction", - "opens_at": "2026-01-02T20:55:00Z", - "closes_at": "2026-01-02T21:00:00Z" - }, - { - "phase": "postmarket", - "opens_at": "2026-01-02T21:00:00Z", - "closes_at": "2026-01-03T01:00:00Z" - } - ] - }, - { - "session_date": "2026-01-05", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-05T14:30:00Z", - "closes_at": "2026-01-05T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-06", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-06T14:30:00Z", - "closes_at": "2026-01-06T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-07", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-07T14:30:00Z", - "closes_at": "2026-01-07T21:00:00Z" - } - ] - }, - { - "session_date": "2026-01-08", - "policy": "early_close", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-01-08T14:30:00Z", - "closes_at": "2026-01-08T18:00:00Z" - } - ] - } - ] - } - ], - "risk": { - "max_gross_exposure": "1000000", - "max_leverage": "2", - "short_borrow_bps": 100, - "instrument_policies": [ - { - "instrument_id": "demo-equity-acme", - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_notional_exposure": "1000000", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "shorting_allowed": true - } - ], - "groups": [] - }, - "execution": { - "model": "completed_bar_adverse_touch_v1", - "configuration": { - "version": "1", - "participation_bps": 5000, - "fee_schedules": [ - { - "schedule_id": "demo-acme-fees-v1", - "instrument_id": "demo-equity-acme", - "settlement_currency": "USD", - "minimum": "0.3", - "maximum": "1", - "components": [ - { - "name": "broker", - "currency": "USD", - "kind": "fixed", - "value": "0.25", - "rounding": "up", - "applies_to": "any" - }, - { - "name": "exchange", - "currency": "USD", - "kind": "notional_bps", - "value": 10, - "rounding": "up", - "applies_to": "taker" - }, - { - "name": "maker_rebate", - "currency": "USD", - "kind": "notional_bps", - "value": -2, - "rounding": "nearest", - "applies_to": "maker" - } - ] - } - ], - "spread_model": { - "model": "fixed_half_spread_v1", - "half_spread_bps": 5 - }, - "impact_model": { - "model": "linear_participation_v1", - "coefficient_bps": 25, - "missing_volume_policy": "reject" - } - } - }, - "max_internal_events": 1000, - "schedule": [ - { - "after_slice_sequence": "1", - "intents": [ - { - "type": "target_weights", - "targets": [ - { - "instrument_id": "demo-equity-acme", - "weight": "0.1" - } - ] - }, - { - "type": "emit_metric", - "name": "desired_weight", - "value": "0.1" - } - ] - }, - { - "after_slice_sequence": "3", - "intents": [ - { - "type": "target_quantities", - "targets": [ - { - "instrument_id": "demo-equity-acme", - "quantity": "2.5" - } - ] - } - ] - } - ], - "slices": [ - { - "slice_sequence": "1", - "start_at": "2026-01-02T14:30:00Z", - "end_at": "2026-01-02T21:00:00Z", - "available_at": "2026-01-02T21:00:01Z", - "received_at": "2026-01-02T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "100", - "high": "105", - "low": "99", - "close": "104", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-02T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-02T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - }, - { - "slice_sequence": "2", - "start_at": "2026-01-05T14:30:00Z", - "end_at": "2026-01-05T21:00:00Z", - "available_at": "2026-01-05T21:00:01Z", - "received_at": "2026-01-05T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "103", - "high": "108", - "low": "102", - "close": "107", - "volume": "13" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-05T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-05T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - }, - { - "slice_sequence": "3", - "start_at": "2026-01-06T14:30:00Z", - "end_at": "2026-01-06T21:00:00Z", - "available_at": "2026-01-06T21:00:01Z", - "received_at": "2026-01-06T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "107", - "high": "109", - "low": "104", - "close": "105", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-06T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-06T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - }, - { - "slice_sequence": "4", - "start_at": "2026-01-07T14:30:00Z", - "end_at": "2026-01-07T21:00:00Z", - "available_at": "2026-01-07T21:00:01Z", - "received_at": "2026-01-07T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "105", - "high": "107", - "low": "103", - "close": "106", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-07T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-07T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - }, - "settlement": { - "cash_buying_power": "total_cash", - "position_availability": "total_positions", - "calendars": [ - { - "calendar_id": "default-settlement", - "version": "1", - "business_dates": [ - "2026-01-02", - "2026-01-05", - "2026-01-06", - "2026-01-07", - "2026-01-08", - "2026-01-09", - "2026-02-02", - "2026-02-03", - "2026-02-04", - "2026-02-05" - ] - } - ], - "rules": [ - { - "instrument_id": "demo-equity-acme", - "calendar_id": "default-settlement", - "lag_business_days": 1 - } - ] - } -} diff --git a/contracts/v15/fixtures/demo.scenario.jsonl b/contracts/v15/fixtures/demo.scenario.jsonl deleted file mode 100644 index 251ded8..0000000 --- a/contracts/v15/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"run_id":"demo","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"1","cost_basis":"90","realized_pnl":"5","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25"}],"marks":[{"instrument_id":"demo-equity-acme","price":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"0.001"}],"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00Z","closes_at":"2026-01-02T14:25:00Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00Z","closes_at":"2026-01-02T14:30:00Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00Z","closes_at":"2026-01-02T20:55:00Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00Z","closes_at":"2026-01-02T21:00:00Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00Z","closes_at":"2026-01-03T01:00:00Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00Z","closes_at":"2026-01-05T21:00:00Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00Z","closes_at":"2026-01-06T21:00:00Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00Z","closes_at":"2026-01-07T21:00:00Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00Z","closes_at":"2026-01-08T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_adverse_touch_v1","configuration":{"version":"1","participation_bps":5000,"fee_schedules":[{"schedule_id":"demo-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":"0.3","maximum":"1","components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"taker"},{"name":"maker_rebate","currency":"USD","kind":"notional_bps","value":-2,"rounding":"nearest","applies_to":"maker"}]}],"spread_model":{"model":"fixed_half_spread_v1","half_spread_bps":5},"impact_model":{"model":"linear_participation_v1","coefficient_bps":25,"missing_volume_policy":"reject"}}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]}}} -{"contract_version":"15","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","weight":"0.1"}],"type":"target_weights"},{"name":"desired_weight","type":"emit_metric","value":"0.1"}],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"13"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"15","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","quantity":"2.5"}],"type":"target_quantities"}],"market_slice":{"available_at":"2026-01-06T21:00:01Z","bars":[{"close":"105","high":"109","instrument_id":"demo-equity-acme","low":"104","open":"107","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-06T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-06T21:00:02Z","slice_sequence":"3","start_at":"2026-01-06T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-07T21:00:01Z","bars":[{"close":"106","high":"107","instrument_id":"demo-equity-acme","low":"103","open":"105","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-07T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-07T21:00:02Z","slice_sequence":"4","start_at":"2026-01-07T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"15","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v15/fixtures/fill-clipped.journal.jsonl b/contracts/v15/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index 133c789..0000000 --- a/contracts/v15/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"contract_version":"15","engine_sequence":"1","event_id":"fill-clipped-event-000000000001","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"02138360b4f18f05c851efd2c16fecc1adb49ae668f6f7e72c3d903bea561002","execution_model":"completed_bar_v1"}} -{"contract_version":"15","engine_sequence":"2","event_id":"fill-clipped-event-000000000002","causation_ids":["fill-clipped-event-000000000001"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"550"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"550","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"550","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550","fx_rate":"1","base_value":"550","interest":"0","base_interest":"0","settled_amount":"550","unsettled_amount":"0","base_settled_value":"550","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"550","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} -{"contract_version":"15","engine_sequence":"3","event_id":"fill-clipped-event-000000000003","causation_ids":["fill-clipped-event-000000000002"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"550","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550","fx_rate":"1","base_value":"550","interest":"0","base_interest":"0","settled_amount":"550","unsettled_amount":"0","base_settled_value":"550","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"550","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"15","engine_sequence":"5","event_id":"fill-clipped-event-000000000005","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"550","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.004081","closing_balance":"550.004081"}} -{"contract_version":"15","engine_sequence":"6","event_id":"fill-clipped-event-000000000006","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000006","updated_event_id":"fill-clipped-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"15","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550.004081","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.004081","unrealized_pnl":"0","equity":"550.004081","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.004081","fx_rate":"1","base_value":"550.004081","interest":"0.004081","base_interest":"0.004081","settled_amount":"550.004081","unsettled_amount":"0","base_settled_value":"550.004081","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.004081","settled_cash":"550.004081","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.004081","maintenance_excess":"550.004081","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"15","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"550.004081","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.004081","closing_balance":"550.008162"}} -{"contract_version":"15","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000006","fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_clipped","payload":{"reason":{"version":"1","policy":"max_leverage","threshold":{"unit":"ratio","value":"1"}},"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","proposed_quantity":"10","permitted_quantity":"0","price":"100"}} -{"contract_version":"15","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000006","fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"order_cancelled","payload":{"order":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000006","updated_event_id":"fill-clipped-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"15","engine_sequence":"12","event_id":"fill-clipped-event-000000000012","causation_ids":["fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550.008162","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.008162","unrealized_pnl":"0","equity":"550.008162","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.008162","fx_rate":"1","base_value":"550.008162","interest":"0.008162","base_interest":"0.008162","settled_amount":"550.008162","unsettled_amount":"0","base_settled_value":"550.008162","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"100","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"13","event_id":"fill-clipped-event-000000000013","causation_ids":["fill-clipped-event-000000000012"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"02138360b4f18f05c851efd2c16fecc1adb49ae668f6f7e72c3d903bea561002","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"550.008162","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.008162","unrealized_pnl":"0","equity":"550.008162","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.008162","fx_rate":"1","base_value":"550.008162","interest":"0.008162","base_interest":"0.008162","settled_amount":"550.008162","unsettled_amount":"0","base_settled_value":"550.008162","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"100","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v15/fixtures/order-book.journal.jsonl b/contracts/v15/fixtures/order-book.journal.jsonl deleted file mode 100644 index 7c9fded..0000000 --- a/contracts/v15/fixtures/order-book.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"contract_version":"15","engine_sequence":"1","event_id":"order-book-event-000000000001","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"f330f2a12a85e8b24bd3bc1bbb7fdf9a08654ed0d7dd8f0a3c712e031ef80128","execution_model":"order_book_v1"}} -{"contract_version":"15","engine_sequence":"2","event_id":"order-book-event-000000000002","causation_ids":["order-book-event-000000000001"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} -{"contract_version":"15","engine_sequence":"3","event_id":"order-book-event-000000000003","causation_ids":["order-book-event-000000000002"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"4","event_id":"order-book-event-000000000004","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00.000000Z","available_at":"2026-02-02T14:31:01.000000Z","received_at":"2026-02-02T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}} -{"contract_version":"15","engine_sequence":"5","event_id":"order-book-event-000000000005","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} -{"contract_version":"15","engine_sequence":"6","event_id":"order-book-event-000000000006","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"order-book-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"order-book-event-000000000006","updated_event_id":"order-book-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"15","engine_sequence":"7","event_id":"order-book-event-000000000007","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"8","event_id":"order-book-event-000000000008","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00.000000Z","available_at":"2026-02-03T14:35:01.000000Z","received_at":"2026-02-03T14:35:02.000000Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00.000000Z","available_at":"2026-02-03T14:36:01.000000Z","received_at":"2026-02-03T14:36:02.000000Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}} -{"contract_version":"15","engine_sequence":"9","event_id":"order-book-event-000000000009","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} -{"contract_version":"15","engine_sequence":"10","event_id":"order-book-event-000000000010","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000001","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"100","notional":"400","fee":"10","executed_at":"2026-02-03T14:35:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} -{"contract_version":"15","engine_sequence":"11","event_id":"order-book-event-000000000011","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000002","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:36:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} -{"contract_version":"15","engine_sequence":"12","event_id":"order-book-event-000000000012","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"980.02968","fx_rate":"1","base_value":"980.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"980.02968","unsettled_amount":"0","base_settled_value":"980.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"980.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"13","event_id":"order-book-event-000000000013","causation_ids":["order-book-event-000000000012"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"f330f2a12a85e8b24bd3bc1bbb7fdf9a08654ed0d7dd8f0a3c712e031ef80128","execution_model":"order_book_v1","valuation":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"980.02968","fx_rate":"1","base_value":"980.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"980.02968","unsettled_amount":"0","base_settled_value":"980.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"980.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v15/fixtures/order-book.scenario.json b/contracts/v15/fixtures/order-book.scenario.json deleted file mode 100644 index 7e27ab1..0000000 --- a/contracts/v15/fixtures/order-book.scenario.json +++ /dev/null @@ -1,378 +0,0 @@ -{ - "contract_version": "15", - "metadata": { - "producer": "trading-engine", - "purpose": "bounded order-book replay conformance fixture" - }, - "run_id": "order-book", - "base_currency": "USD", - "initial_portfolio": { - "cash": [ - { - "currency": "USD", - "amount": "2000" - } - ], - "positions": [], - "marks": [], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ] - }, - "instruments": [ - { - "instrument_id": "clip-equity", - "symbol": "CLIP", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "venue_calendars": [ - { - "calendar_id": "clip-xnas-2026", - "calendar_version": "1", - "venue_id": "XNAS", - "instrument_ids": [ - "clip-equity" - ], - "sessions": [ - { - "session_date": "2026-02-02", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-02T14:30:00Z", - "closes_at": "2026-02-02T21:00:00Z" - } - ] - }, - { - "session_date": "2026-02-03", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-03T14:30:00Z", - "closes_at": "2026-02-03T21:00:00Z" - } - ] - }, - { - "session_date": "2026-02-04", - "policy": "early_close", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-04T14:30:00Z", - "closes_at": "2026-02-04T18:00:00Z" - } - ] - } - ] - } - ], - "risk": { - "max_gross_exposure": "1000000000", - "max_leverage": "1", - "short_borrow_bps": 100, - "instrument_policies": [ - { - "instrument_id": "clip-equity", - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_notional_exposure": "1000000000", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "shorting_allowed": true - } - ], - "groups": [] - }, - "execution": { - "model": "order_book_v1", - "configuration": { - "version": "1", - "participation_bps": 10000, - "fee_schedules": [ - { - "schedule_id": "clip-fees-v1", - "instrument_id": "clip-equity", - "settlement_currency": "USD", - "minimum": null, - "maximum": null, - "components": [ - { - "name": "broker", - "currency": "USD", - "kind": "fixed", - "value": "10", - "rounding": "up", - "applies_to": "any" - } - ] - } - ], - "max_depth_levels": 10 - } - }, - "max_internal_events": 1000, - "schedule": [ - { - "after_slice_sequence": "1", - "intents": [ - { - "type": "submit_order", - "instrument_id": "clip-equity", - "side": "buy", - "quantity": "10", - "order_kind": "limit", - "trigger_price": null, - "limit_price": "100", - "time_in_force": "gtc", - "venue_id": null, - "calendar_id": null, - "expires_at": null - } - ] - } - ], - "slices": [ - { - "slice_sequence": "1", - "start_at": "2026-02-02T14:30:00Z", - "end_at": "2026-02-02T21:00:00Z", - "available_at": "2026-02-02T21:00:01Z", - "received_at": "2026-02-02T21:00:02Z", - "bars": [ - { - "instrument_id": "clip-equity", - "open": "50", - "high": "50", - "low": "50", - "close": "50", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "clip-equity", - "effective_at": "2026-02-02T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-02-02T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [ - { - "type": "snapshot", - "instrument_id": "clip-equity", - "event_at": "2026-02-02T14:31:00Z", - "available_at": "2026-02-02T14:31:01Z", - "received_at": "2026-02-02T14:31:02Z", - "ingest_sequence": "1", - "book_sequence": "1", - "bids": [ - { - "price": "49", - "quantity": "20" - } - ], - "asks": [ - { - "price": "51", - "quantity": "20" - } - ] - } - ] - }, - { - "slice_sequence": "2", - "start_at": "2026-02-03T14:30:00Z", - "end_at": "2026-02-03T21:00:00Z", - "available_at": "2026-02-03T21:00:01Z", - "received_at": "2026-02-03T21:00:02Z", - "bars": [ - { - "instrument_id": "clip-equity", - "open": "100", - "high": "100", - "low": "100", - "close": "100", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "clip-equity", - "effective_at": "2026-02-03T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-02-03T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [ - { - "type": "snapshot", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:31:00Z", - "available_at": "2026-02-03T14:31:01Z", - "received_at": "2026-02-03T14:31:02Z", - "ingest_sequence": "1", - "book_sequence": "1", - "bids": [ - { - "price": "100", - "quantity": "5" - } - ], - "asks": [ - { - "price": "101", - "quantity": "20" - } - ] - }, - { - "type": "set", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:32:00Z", - "available_at": "2026-02-03T14:32:01Z", - "received_at": "2026-02-03T14:32:02Z", - "ingest_sequence": "2", - "book_sequence": "2", - "side": "bid", - "price": "100", - "quantity": "3" - }, - { - "type": "trade", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:33:00Z", - "available_at": "2026-02-03T14:33:01Z", - "received_at": "2026-02-03T14:33:02Z", - "ingest_sequence": "3", - "book_sequence": "3", - "price": "100", - "quantity": "3", - "aggressor_side": "sell" - }, - { - "type": "set", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:34:00Z", - "available_at": "2026-02-03T14:34:01Z", - "received_at": "2026-02-03T14:34:02Z", - "ingest_sequence": "4", - "book_sequence": "4", - "side": "bid", - "price": "100", - "quantity": "10" - }, - { - "type": "trade", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:35:00Z", - "available_at": "2026-02-03T14:35:01Z", - "received_at": "2026-02-03T14:35:02Z", - "ingest_sequence": "5", - "book_sequence": "5", - "price": "100", - "quantity": "4", - "aggressor_side": "sell" - }, - { - "type": "trade", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:36:00Z", - "available_at": "2026-02-03T14:36:01Z", - "received_at": "2026-02-03T14:36:02Z", - "ingest_sequence": "6", - "book_sequence": "6", - "price": "100", - "quantity": "6", - "aggressor_side": "sell" - } - ] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - }, - "settlement": { - "cash_buying_power": "total_cash", - "position_availability": "total_positions", - "calendars": [ - { - "calendar_id": "default-settlement", - "version": "1", - "business_dates": [ - "2026-01-02", - "2026-01-05", - "2026-01-06", - "2026-01-07", - "2026-01-08", - "2026-01-09", - "2026-02-02", - "2026-02-03", - "2026-02-04", - "2026-02-05" - ] - } - ], - "rules": [ - { - "instrument_id": "clip-equity", - "calendar_id": "default-settlement", - "lag_business_days": 1 - } - ] - } -} diff --git a/contracts/v15/fixtures/order-book.scenario.jsonl b/contracts/v15/fixtures/order-book.scenario.jsonl deleted file mode 100644 index ef9c90e..0000000 --- a/contracts/v15/fixtures/order-book.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded order-book replay conformance fixture"},"run_id":"order-book","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"order_book_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}],"max_depth_levels":10}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} -{"contract_version":"15","scenario_sequence":"2","record_type":"market_slice","payload":{"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}],"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00Z","available_at":"2026-02-02T14:31:01Z","received_at":"2026-02-02T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}}} -{"contract_version":"15","scenario_sequence":"3","record_type":"market_slice","payload":{"intents":[],"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00Z","available_at":"2026-02-03T14:35:01Z","received_at":"2026-02-03T14:35:02Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00Z","available_at":"2026-02-03T14:36:01Z","received_at":"2026-02-03T14:36:02Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}}} -{"contract_version":"15","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v15/fixtures/quote-trade.journal.jsonl b/contracts/v15/fixtures/quote-trade.journal.jsonl deleted file mode 100644 index 20bc174..0000000 --- a/contracts/v15/fixtures/quote-trade.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"contract_version":"15","engine_sequence":"1","event_id":"quote-trade-event-000000000001","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"b062a14acf0722c6d77e0e81985e12bf3a2a150dd7e493e1a811a393032c8d7f","execution_model":"quote_trade_v1"}} -{"contract_version":"15","engine_sequence":"2","event_id":"quote-trade-event-000000000002","causation_ids":["quote-trade-event-000000000001"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} -{"contract_version":"15","engine_sequence":"3","event_id":"quote-trade-event-000000000003","causation_ids":["quote-trade-event-000000000002"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"4","event_id":"quote-trade-event-000000000004","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"15","engine_sequence":"5","event_id":"quote-trade-event-000000000005","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} -{"contract_version":"15","engine_sequence":"6","event_id":"quote-trade-event-000000000006","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"quote-trade-event-000000000006","updated_event_id":"quote-trade-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"15","engine_sequence":"7","event_id":"quote-trade-event-000000000007","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"8","event_id":"quote-trade-event-000000000008","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]}} -{"contract_version":"15","engine_sequence":"9","event_id":"quote-trade-event-000000000009","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} -{"contract_version":"15","engine_sequence":"10","event_id":"quote-trade-event-000000000010","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000001","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"99","notional":"396","fee":"10","executed_at":"2026-02-03T14:33:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} -{"contract_version":"15","engine_sequence":"11","event_id":"quote-trade-event-000000000011","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000002","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:34:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} -{"contract_version":"15","engine_sequence":"12","event_id":"quote-trade-event-000000000012","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]}} -{"contract_version":"15","engine_sequence":"13","event_id":"quote-trade-event-000000000013","causation_ids":["quote-trade-event-000000000012"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"b062a14acf0722c6d77e0e81985e12bf3a2a150dd7e493e1a811a393032c8d7f","execution_model":"quote_trade_v1","valuation":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v15/fixtures/quote-trade.scenario.jsonl b/contracts/v15/fixtures/quote-trade.scenario.jsonl deleted file mode 100644 index 6c2b50d..0000000 --- a/contracts/v15/fixtures/quote-trade.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded quote and trade replay fixture"},"run_id":"quote-trade","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"quote_trade_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} -{"contract_version":"15","scenario_sequence":"2","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]},"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}]}} -{"contract_version":"15","scenario_sequence":"3","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]},"intents":[]}} -{"contract_version":"15","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v15/journal.schema.json b/contracts/v15/journal.schema.json deleted file mode 100644 index d9360c0..0000000 --- a/contracts/v15/journal.schema.json +++ /dev/null @@ -1,2427 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json", - "title": "Trading Engine v15 audit journal record", - "type": "object", - "additionalProperties": false, - "required": [ - "contract_version", - "engine_sequence", - "event_id", - "causation_ids", - "run_id", - "recorded_at", - "event_type", - "payload" - ], - "properties": { - "contract_version": { - "const": "15" - }, - "engine_sequence": { - "$ref": "#/$defs/sequence" - }, - "event_id": { - "$ref": "#/$defs/identifier" - }, - "causation_ids": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/$defs/identifier" - } - }, - "run_id": { - "$ref": "#/$defs/identifier" - }, - "recorded_at": { - "$ref": "#/$defs/timestamp" - }, - "event_type": { - "enum": [ - "run_started", - "initial_state", - "market_slice_received", - "target_portfolio_requested", - "order_accepted", - "order_rejected", - "order_triggered", - "order_cancelled", - "split_applied", - "cash_dividend_applied", - "distribution_applied", - "lifecycle_applied", - "order_adjusted", - "execution_price_selected", - "fill_applied", - "settlement_instruction_created", - "settlement_completed", - "settlement_failed", - "fill_clipped", - "borrow_fee_applied", - "borrow_charge_applied", - "borrow_recall_received", - "cash_interest_applied", - "margin_call", - "margin_restored", - "intent_rejected", - "metric_emitted", - "valuation", - "run_completed" - ] - }, - "payload": { - "type": "object" - } - }, - "allOf": [ - { - "if": { "properties": { "event_type": { "const": "execution_price_selected" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/executionPriceSelected" } } } - }, - { - "if": { - "properties": { - "event_type": { - "const": "run_started" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/runStarted" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "initial_state" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/initialState" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "market_slice_received" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/marketSlice" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "target_portfolio_requested" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/targetPortfolio" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "enum": [ - "order_accepted", - "order_rejected", - "order_triggered" - ] - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/order" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "order_cancelled" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/orderCancelled" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "split_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/splitApplied" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "cash_dividend_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/dividendApplied" - } - } - } - }, - { - "if": { "properties": { "event_type": { "const": "distribution_applied" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/distributionApplied" } } } - }, - { - "if": { "properties": { "event_type": { "const": "lifecycle_applied" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/lifecycleApplied" } } } - }, - { - "if": { - "properties": { - "event_type": { - "const": "order_adjusted" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/orderAdjusted" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "fill_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/fill" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "enum": [ - "settlement_instruction_created", - "settlement_completed", - "settlement_failed" - ] - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/settlementInstruction" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "fill_clipped" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/fillClipped" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "borrow_fee_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/borrowFee" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "borrow_charge_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/borrowCharge" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "borrow_recall_received" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/borrowRecall" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "cash_interest_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/cashInterest" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "enum": [ - "margin_call", - "margin_restored", - "valuation" - ] - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/valuation" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "intent_rejected" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/intentRejected" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "metric_emitted" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/metric" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "run_completed" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/runCompleted" - } - } - } - } - ], - "$defs": { - "identifier": { - "type": "string", - "minLength": 1, - "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" - }, - "signedDecimal": { - "type": "string", - "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" - }, - "unsignedDecimal": { - "type": "string", - "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "positiveDecimal": { - "type": "string", - "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "sequence": { - "type": "string", - "pattern": "^[1-9][0-9]*$" - }, - "nonnegativeSequence": { - "type": "string", - "pattern": "^(?:0|[1-9][0-9]*)$" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" - }, - "runStarted": { - "type": "object", - "additionalProperties": false, - "required": [ - "scenario_sha256", - "execution_model" - ], - "properties": { - "scenario_sha256": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, - "execution_model": { - "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] - } - } - }, - "initialState": { - "type": "object", - "additionalProperties": false, - "required": [ - "portfolio", - "valuation" - ], - "properties": { - "portfolio": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/initialPortfolio" - }, - "valuation": { - "$ref": "#/$defs/valuation" - } - } - }, - "bar": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "open", - "high", - "low", - "close", - "volume" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "open": { - "$ref": "#/$defs/positiveDecimal" - }, - "high": { - "$ref": "#/$defs/positiveDecimal" - }, - "low": { - "$ref": "#/$defs/positiveDecimal" - }, - "close": { - "$ref": "#/$defs/positiveDecimal" - }, - "volume": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/unsignedDecimal" - } - ] - } - } - }, - "fxRate": { - "type": "object", - "additionalProperties": false, - "required": [ - "currency", - "rate" - ], - "properties": { - "currency": { - "$ref": "#/$defs/identifier" - }, - "rate": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "corporateAction": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": [ - "type", - "action_id", - "instrument_id", - "numerator", - "denominator" - ], - "properties": { - "type": { - "const": "split" - }, - "action_id": { - "$ref": "#/$defs/identifier" - }, - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "numerator": { - "$ref": "#/$defs/sequence" - }, - "denominator": { - "$ref": "#/$defs/sequence" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "type", - "action_id", - "instrument_id", - "amount_per_unit" - ], - "properties": { - "type": { - "const": "cash_dividend" - }, - "action_id": { - "$ref": "#/$defs/identifier" - }, - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "amount_per_unit": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], - "properties": { - "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, - "action_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "destination_instrument_id": { "$ref": "#/$defs/identifier" }, - "numerator": { "$ref": "#/$defs/sequence" }, - "denominator": { "$ref": "#/$defs/sequence" }, - "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fractional_policy": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/fractionalPolicy" } - } - } - ] - }, - "marketSlice": { - "type": "object", - "additionalProperties": false, - "required": [ - "slice_sequence", - "start_at", - "end_at", - "available_at", - "received_at", - "bars", - "fx_rates", - "corporate_actions", - "borrow_observations", - "cash_rate_observations", - "settlement_failures", - "lifecycle_events", - "market_events", - "order_book_events" - ], - "properties": { - "slice_sequence": { - "$ref": "#/$defs/sequence" - }, - "start_at": { - "$ref": "#/$defs/timestamp" - }, - "end_at": { - "$ref": "#/$defs/timestamp" - }, - "available_at": { - "$ref": "#/$defs/timestamp" - }, - "received_at": { - "$ref": "#/$defs/timestamp" - }, - "bars": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/bar" - } - }, - "fx_rates": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/fxRate" - } - }, - "corporate_actions": { - "type": "array", - "items": { - "$ref": "#/$defs/corporateAction" - } - }, - "borrow_observations": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/borrowObservation" - } - }, - "cash_rate_observations": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/cashRateObservation" - } - }, - "settlement_failures": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/settlementFailure" - } - }, - "lifecycle_events": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/lifecycleEvent" - } - }, - "market_events": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/marketEvent" - } - }, - "order_book_events": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/orderBookEvent" - } - } - } - }, - "settlementInstruction": { - "type": "object", - "additionalProperties": false, - "required": ["instruction_id", "fill_id", "instrument_id", "currency", "cash_movement", "position_movement", "trade_date", "due_date", "status", "settled_at", "failed_at", "failure_reason"], - "properties": { - "instruction_id": { "$ref": "#/$defs/identifier" }, - "fill_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "currency": { "$ref": "#/$defs/identifier" }, - "cash_movement": { "$ref": "#/$defs/signedDecimal" }, - "position_movement": { "$ref": "#/$defs/signedDecimal" }, - "trade_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "due_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "status": { "enum": ["pending", "settled", "failed"] }, - "settled_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, - "failed_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, - "failure_reason": { "oneOf": [{ "type": "null" }, { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" }] } - } - }, - "targetPortfolio": { - "type": "object", - "additionalProperties": false, - "required": [ - "basis", - "targets" - ], - "properties": { - "basis": { - "enum": [ - "weights", - "quantities" - ] - }, - "targets": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "weight", - "quantity", - "reference_price" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "weight": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/signedDecimal" - } - ] - }, - "quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "reference_price": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/positiveDecimal" - } - ] - } - } - } - } - } - }, - "order": { - "type": "object", - "additionalProperties": false, - "required": [ - "order_id", - "instrument_id", - "side", - "quantity", - "order_kind", - "trigger_price", - "limit_price", - "time_in_force", - "venue_id", - "calendar_id", - "expires_at", - "origin", - "created_event_id", - "updated_event_id", - "created_sequence", - "created_at", - "eligible_after_slice_sequence", - "triggered_at", - "triggered_slice_sequence", - "filled_quantity", - "filled_notional", - "status", - "rejection_reason" - ], - "properties": { - "order_id": { - "$ref": "#/$defs/identifier" - }, - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "side": { - "enum": [ - "buy", - "sell" - ] - }, - "quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "order_kind": { - "enum": [ - "market", - "limit", - "stop", - "stop_limit" - ] - }, - "trigger_price": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/positiveDecimal" - } - ] - }, - "limit_price": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/positiveDecimal" - } - ] - }, - "time_in_force": { - "enum": [ - "gtc", - "ioc", - "fok", - "day", - "gtd" - ] - }, - "venue_id": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/identifier" - } - ] - }, - "calendar_id": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/identifier" - } - ] - }, - "expires_at": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/timestamp" - } - ] - }, - "origin": { - "enum": [ - "direct", - "target_rebalance", - "margin_liquidation", - "borrow_recall", - "instrument_halt", - "instrument_terminal" - ] - }, - "created_event_id": { - "$ref": "#/$defs/identifier" - }, - "updated_event_id": { - "$ref": "#/$defs/identifier" - }, - "created_sequence": { - "$ref": "#/$defs/sequence" - }, - "created_at": { - "$ref": "#/$defs/timestamp" - }, - "eligible_after_slice_sequence": { - "$ref": "#/$defs/nonnegativeSequence" - }, - "triggered_at": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/timestamp" - } - ] - }, - "triggered_slice_sequence": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/sequence" - } - ] - }, - "filled_quantity": { - "$ref": "#/$defs/unsignedDecimal" - }, - "filled_notional": { - "$ref": "#/$defs/unsignedDecimal" - }, - "status": { - "enum": [ - "working", - "partially_filled", - "filled", - "cancelled", - "rejected" - ] - }, - "rejection_reason": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "string", - "minLength": 1 - } - ] - } - } - }, - "orderCancelled": { - "type": "object", - "additionalProperties": false, - "required": [ - "order", - "reason" - ], - "properties": { - "order": { - "$ref": "#/$defs/order" - }, - "reason": { - "enum": [ - "strategy_requested", - "target_replaced", - "market_ioc", - "immediate_or_cancel", - "fill_or_kill", - "day_expired", - "gtd_expired", - "margin_call", - "borrow_recall" - ] - } - } - }, - "splitApplied": { - "type": "object", - "additionalProperties": false, - "required": [ - "action", - "previous_quantity", - "adjusted_quantity" - ], - "properties": { - "action": { - "$ref": "#/$defs/corporateAction" - }, - "previous_quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "adjusted_quantity": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "dividendApplied": { - "type": "object", - "additionalProperties": false, - "required": [ - "action", - "quantity", - "cash_amount" - ], - "properties": { - "action": { - "$ref": "#/$defs/corporateAction" - }, - "quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "cash_amount": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "distributionApplied": { - "type": "object", - "additionalProperties": false, - "required": ["action", "source_quantity", "destination_quantity", "fractional_quantity", "allocated_basis", "fractional_basis", "cash_in_lieu"], - "properties": { - "action": { "$ref": "#/$defs/corporateAction" }, - "source_quantity": { "$ref": "#/$defs/signedDecimal" }, - "destination_quantity": { "$ref": "#/$defs/signedDecimal" }, - "fractional_quantity": { "$ref": "#/$defs/signedDecimal" }, - "allocated_basis": { "$ref": "#/$defs/signedDecimal" }, - "fractional_basis": { "$ref": "#/$defs/signedDecimal" }, - "cash_in_lieu": { "$ref": "#/$defs/signedDecimal" } - } - }, - "lifecycleApplied": { - "type": "object", - "additionalProperties": false, - "required": ["lifecycle_event", "listing", "liquidated_quantity", "cash_amount"], - "properties": { - "lifecycle_event": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/lifecycleEvent" }, - "listing": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "symbol", "status", "provider_mappings"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "symbol": { "$ref": "#/$defs/identifier" }, - "status": { "enum": ["tradable", "halted", "expired", "delisted"] }, - "provider_mappings": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["provider", "provider_instrument_id"], - "properties": { - "provider": { "$ref": "#/$defs/identifier" }, - "provider_instrument_id": { "$ref": "#/$defs/identifier" } - } - } - } - } - }, - "liquidated_quantity": { "$ref": "#/$defs/signedDecimal" }, - "cash_amount": { "$ref": "#/$defs/signedDecimal" } - } - }, - "orderAdjusted": { - "type": "object", - "additionalProperties": false, - "required": [ - "order", - "action_id" - ], - "properties": { - "order": { - "$ref": "#/$defs/order" - }, - "action_id": { - "$ref": "#/$defs/identifier" - } - } - }, - "executionPriceSelected": { - "type": "object", - "additionalProperties": false, - "required": ["order_id", "instrument_id", "side", "reference_price", "spread_adjustment", "impact_adjustment", "final_price"], - "properties": { - "order_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "reference_price": { "$ref": "#/$defs/positiveDecimal" }, - "spread_adjustment": { "$ref": "#/$defs/unsignedDecimal" }, - "impact_adjustment": { "$ref": "#/$defs/unsignedDecimal" }, - "final_price": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "fill": { - "type": "object", - "additionalProperties": false, - "required": [ - "fill_id", - "order_id", - "instrument_id", - "quote_currency", - "side", - "quantity", - "price", - "notional", - "fee", - "executed_at", - "slice_sequence", - "fee_components" - ], - "properties": { - "fill_id": { - "$ref": "#/$defs/identifier" - }, - "order_id": { - "$ref": "#/$defs/identifier" - }, - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "quote_currency": { - "$ref": "#/$defs/identifier" - }, - "side": { - "enum": [ - "buy", - "sell" - ] - }, - "quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "price": { - "$ref": "#/$defs/positiveDecimal" - }, - "notional": { - "$ref": "#/$defs/positiveDecimal" - }, - "fee": { - "$ref": "#/$defs/signedDecimal" - }, - "fee_components": { - "type": "array", - "items": { - "$ref": "#/$defs/calculatedFeeComponent" - } - }, - "executed_at": { - "$ref": "#/$defs/timestamp" - }, - "slice_sequence": { - "$ref": "#/$defs/sequence" - } - } - }, - "calculatedFeeComponent": { - "type": "object", - "additionalProperties": false, - "required": [ - "name", - "kind", - "currency", - "amount", - "quote_amount" - ], - "properties": { - "name": { - "$ref": "#/$defs/identifier" - }, - "kind": { - "enum": [ - "fixed", - "notional_bps", - "per_unit", - "minimum_adjustment", - "maximum_adjustment" - ] - }, - "currency": { - "$ref": "#/$defs/identifier" - }, - "amount": { - "$ref": "#/$defs/signedDecimal" - }, - "quote_amount": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "feeComponentAttribution": { - "type": "object", - "additionalProperties": false, - "required": [ - "name", - "kind", - "currency", - "amount", - "quote_currency", - "quote_amount", - "base_amount" - ], - "properties": { - "name": { - "$ref": "#/$defs/identifier" - }, - "kind": { - "enum": [ - "fixed", - "notional_bps", - "per_unit", - "minimum_adjustment", - "maximum_adjustment" - ] - }, - "currency": { - "$ref": "#/$defs/identifier" - }, - "amount": { - "$ref": "#/$defs/signedDecimal" - }, - "quote_currency": { - "$ref": "#/$defs/identifier" - }, - "quote_amount": { - "$ref": "#/$defs/signedDecimal" - }, - "base_amount": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "quantityThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "unit", - "value" - ], - "properties": { - "unit": { - "const": "quantity" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "moneyThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "unit", - "value" - ], - "properties": { - "unit": { - "const": "money" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "ratioThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "unit", - "value" - ], - "properties": { - "unit": { - "const": "ratio" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "basisPointsThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "unit", - "value" - ], - "properties": { - "unit": { - "const": "basis_points" - }, - "value": { - "type": "integer", - "minimum": 1, - "maximum": 10000 - } - } - }, - "instrumentQuantityThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "unit", - "value" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "unit": { - "const": "quantity" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "instrumentMoneyThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "unit", - "value" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "unit": { - "const": "money" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "currencyMoneyThreshold": { - "type": "object", - "additionalProperties": false, - "required": ["currency", "unit", "value"], - "properties": { - "currency": { "$ref": "#/$defs/identifier" }, - "unit": { "const": "money" }, - "value": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "settlementPositionThreshold": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "unit", "value"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "unit": { "const": "quantity" }, - "value": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "instrumentBasisPointsThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "unit", - "value" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "unit": { - "const": "basis_points" - }, - "value": { - "type": "integer", - "minimum": 1, - "maximum": 10000 - } - } - }, - "instrumentShortingThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "value" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "value": { - "const": false - } - } - }, - "groupMoneyThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "group_id", - "unit", - "value" - ], - "properties": { - "group_id": { - "$ref": "#/$defs/identifier" - }, - "unit": { - "const": "money" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "groupRatioThreshold": { - "type": "object", - "additionalProperties": false, - "required": [ - "group_id", - "unit", - "value" - ], - "properties": { - "group_id": { - "$ref": "#/$defs/identifier" - }, - "unit": { - "const": "ratio" - }, - "value": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "fillClipReason": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "max_order_quantity" - }, - "threshold": { - "$ref": "#/$defs/quantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "max_long_position" - }, - "threshold": { - "$ref": "#/$defs/quantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "max_short_position" - }, - "threshold": { - "$ref": "#/$defs/quantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "max_gross_exposure" - }, - "threshold": { - "$ref": "#/$defs/moneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "max_leverage" - }, - "threshold": { - "$ref": "#/$defs/ratioThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "initial_margin" - }, - "threshold": { - "$ref": "#/$defs/basisPointsThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_max_long_position" - }, - "threshold": { - "$ref": "#/$defs/instrumentQuantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["version", "policy", "threshold"], - "properties": { - "version": { "const": "1" }, - "policy": { "const": "settlement_cash_buying_power" }, - "threshold": { "$ref": "#/$defs/currencyMoneyThreshold" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["version", "policy", "threshold"], - "properties": { - "version": { "const": "1" }, - "policy": { "const": "settlement_position_availability" }, - "threshold": { "$ref": "#/$defs/settlementPositionThreshold" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_max_short_position" - }, - "threshold": { - "$ref": "#/$defs/instrumentQuantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_max_notional_exposure" - }, - "threshold": { - "$ref": "#/$defs/instrumentMoneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_shorting_disabled" - }, - "threshold": { - "$ref": "#/$defs/instrumentShortingThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_borrow_availability" - }, - "threshold": { - "$ref": "#/$defs/instrumentQuantityThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "instrument_initial_margin" - }, - "threshold": { - "$ref": "#/$defs/instrumentBasisPointsThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "group_max_gross_exposure" - }, - "threshold": { - "$ref": "#/$defs/groupMoneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "group_max_long_exposure" - }, - "threshold": { - "$ref": "#/$defs/groupMoneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "group_max_short_exposure" - }, - "threshold": { - "$ref": "#/$defs/groupMoneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "group_max_absolute_net_exposure" - }, - "threshold": { - "$ref": "#/$defs/groupMoneyThreshold" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "policy", - "threshold" - ], - "properties": { - "version": { - "const": "1" - }, - "policy": { - "const": "group_max_concentration" - }, - "threshold": { - "$ref": "#/$defs/groupRatioThreshold" - } - } - } - ] - }, - "fillClipped": { - "type": "object", - "additionalProperties": false, - "required": [ - "reason", - "order_id", - "instrument_id", - "proposed_quantity", - "permitted_quantity", - "price" - ], - "properties": { - "reason": { - "$ref": "#/$defs/fillClipReason" - }, - "order_id": { - "$ref": "#/$defs/identifier" - }, - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "proposed_quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "permitted_quantity": { - "$ref": "#/$defs/unsignedDecimal" - }, - "price": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "borrowFee": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "quote_currency", - "short_quantity", - "reference_price", - "borrow_bps", - "period_start", - "period_end", - "fee" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "quote_currency": { - "$ref": "#/$defs/identifier" - }, - "short_quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "reference_price": { - "$ref": "#/$defs/positiveDecimal" - }, - "borrow_bps": { - "type": "integer", - "minimum": 1, - "maximum": 10000 - }, - "period_start": { - "$ref": "#/$defs/timestamp" - }, - "period_end": { - "$ref": "#/$defs/timestamp" - }, - "fee": { - "$ref": "#/$defs/positiveDecimal" - } - } - }, - "borrowCharge": { - "type": "object", - "additionalProperties": false, - "required": [ - "observation", - "quote_currency", - "short_quantity", - "reference_price", - "day_count", - "compounding", - "period_start", - "period_end", - "amount" - ], - "properties": { - "observation": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/borrowObservation" - }, - "quote_currency": { - "$ref": "#/$defs/identifier" - }, - "short_quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "reference_price": { - "$ref": "#/$defs/positiveDecimal" - }, - "day_count": { - "enum": [ - "actual_365", - "actual_360" - ] - }, - "compounding": { - "enum": [ - "simple", - "daily" - ] - }, - "period_start": { - "$ref": "#/$defs/timestamp" - }, - "period_end": { - "$ref": "#/$defs/timestamp" - }, - "amount": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "borrowRecall": { - "type": "object", - "additionalProperties": false, - "required": [ - "observation", - "short_quantity", - "close_out_quantity" - ], - "properties": { - "observation": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/borrowObservation" - }, - "short_quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "close_out_quantity": { - "$ref": "#/$defs/unsignedDecimal" - } - } - }, - "cashInterest": { - "type": "object", - "additionalProperties": false, - "required": [ - "observation", - "opening_balance", - "applied_rate_bps", - "day_count", - "compounding", - "period_start", - "period_end", - "amount", - "closing_balance" - ], - "properties": { - "observation": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/cashRateObservation" - }, - "opening_balance": { - "$ref": "#/$defs/signedDecimal" - }, - "applied_rate_bps": { - "type": "integer", - "minimum": -1000000, - "maximum": 1000000 - }, - "day_count": { - "enum": [ - "actual_365", - "actual_360" - ] - }, - "compounding": { - "enum": [ - "simple", - "daily" - ] - }, - "period_start": { - "$ref": "#/$defs/timestamp" - }, - "period_end": { - "$ref": "#/$defs/timestamp" - }, - "amount": { - "$ref": "#/$defs/signedDecimal" - }, - "closing_balance": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "cashAttribution": { - "type": "object", - "additionalProperties": false, - "required": [ - "currency", - "amount", - "fx_rate", - "base_value", - "interest", - "base_interest", - "settled_amount", - "unsettled_amount", - "base_settled_value", - "base_unsettled_value" - ], - "properties": { - "currency": { - "$ref": "#/$defs/identifier" - }, - "amount": { - "$ref": "#/$defs/signedDecimal" - }, - "fx_rate": { - "$ref": "#/$defs/positiveDecimal" - }, - "base_value": { - "$ref": "#/$defs/signedDecimal" - }, - "interest": { - "$ref": "#/$defs/signedDecimal" - }, - "base_interest": { - "$ref": "#/$defs/signedDecimal" - }, - "settled_amount": { - "$ref": "#/$defs/signedDecimal" - }, - "unsettled_amount": { - "$ref": "#/$defs/signedDecimal" - }, - "base_settled_value": { - "$ref": "#/$defs/signedDecimal" - }, - "base_unsettled_value": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "positionAttribution": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "quote_currency", - "quantity", - "settled_quantity", - "unsettled_quantity", - "mark", - "fx_rate", - "market_value", - "base_market_value", - "cost_basis", - "base_cost_basis", - "realized_pnl", - "base_realized_pnl", - "unrealized_pnl", - "base_unrealized_pnl", - "dividend_pnl", - "base_dividend_pnl", - "execution_fees", - "base_execution_fees", - "borrow_fees", - "base_borrow_fees", - "total_fees", - "base_total_fees", - "execution_fee_components" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "quote_currency": { - "$ref": "#/$defs/identifier" - }, - "quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "settled_quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "unsettled_quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "mark": { - "$ref": "#/$defs/positiveDecimal" - }, - "fx_rate": { - "$ref": "#/$defs/positiveDecimal" - }, - "market_value": { - "$ref": "#/$defs/signedDecimal" - }, - "base_market_value": { - "$ref": "#/$defs/signedDecimal" - }, - "cost_basis": { - "$ref": "#/$defs/signedDecimal" - }, - "base_cost_basis": { - "$ref": "#/$defs/signedDecimal" - }, - "realized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "base_realized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "unrealized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "base_unrealized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "dividend_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "base_dividend_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "execution_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "base_execution_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "borrow_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "base_borrow_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "total_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "base_total_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "execution_fee_components": { - "type": "array", - "items": { - "$ref": "#/$defs/feeComponentAttribution" - } - } - } - }, - "margin": { - "type": "object", - "additionalProperties": false, - "required": [ - "initial_requirement", - "maintenance_requirement", - "initial_excess", - "maintenance_excess", - "margin_call" - ], - "properties": { - "initial_requirement": { - "$ref": "#/$defs/unsignedDecimal" - }, - "maintenance_requirement": { - "$ref": "#/$defs/unsignedDecimal" - }, - "initial_excess": { - "$ref": "#/$defs/signedDecimal" - }, - "maintenance_excess": { - "$ref": "#/$defs/signedDecimal" - }, - "margin_call": { - "type": "boolean" - } - } - }, - "groupExposure": { - "type": "object", - "additionalProperties": false, - "required": [ - "group_id", - "gross_exposure", - "net_exposure", - "long_exposure", - "short_exposure", - "concentration" - ], - "properties": { - "group_id": { - "$ref": "#/$defs/identifier" - }, - "gross_exposure": { - "$ref": "#/$defs/unsignedDecimal" - }, - "net_exposure": { - "$ref": "#/$defs/signedDecimal" - }, - "long_exposure": { - "$ref": "#/$defs/unsignedDecimal" - }, - "short_exposure": { - "$ref": "#/$defs/unsignedDecimal" - }, - "concentration": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/signedDecimal" - } - ] - } - } - }, - "valuation": { - "type": "object", - "additionalProperties": false, - "required": [ - "base_currency", - "cash", - "settled_cash", - "unsettled_cash", - "net_market_value", - "long_market_value", - "short_market_value", - "gross_exposure", - "cost_basis", - "realized_pnl", - "unrealized_pnl", - "equity", - "dividend_pnl", - "execution_fees", - "borrow_fees", - "cash_interest", - "total_fees", - "cash_balances", - "positions", - "margin", - "group_exposures", - "execution_fee_components" - ], - "properties": { - "base_currency": { - "$ref": "#/$defs/identifier" - }, - "cash": { - "$ref": "#/$defs/signedDecimal" - }, - "settled_cash": { - "$ref": "#/$defs/signedDecimal" - }, - "unsettled_cash": { - "$ref": "#/$defs/signedDecimal" - }, - "net_market_value": { - "$ref": "#/$defs/signedDecimal" - }, - "long_market_value": { - "$ref": "#/$defs/unsignedDecimal" - }, - "short_market_value": { - "$ref": "#/$defs/unsignedDecimal" - }, - "gross_exposure": { - "$ref": "#/$defs/unsignedDecimal" - }, - "cost_basis": { - "$ref": "#/$defs/signedDecimal" - }, - "realized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "unrealized_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "equity": { - "$ref": "#/$defs/signedDecimal" - }, - "dividend_pnl": { - "$ref": "#/$defs/signedDecimal" - }, - "execution_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "borrow_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "total_fees": { - "$ref": "#/$defs/signedDecimal" - }, - "cash_balances": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/cashAttribution" - } - }, - "positions": { - "type": "array", - "items": { - "$ref": "#/$defs/positionAttribution" - } - }, - "margin": { - "$ref": "#/$defs/margin" - }, - "group_exposures": { - "type": "array", - "items": { - "$ref": "#/$defs/groupExposure" - } - }, - "execution_fee_components": { - "type": "array", - "items": { - "$ref": "#/$defs/feeComponentAttribution" - } - }, - "cash_interest": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "intentRejected": { - "type": "object", - "additionalProperties": false, - "required": [ - "reason" - ], - "properties": { - "reason": { - "type": "string", - "minLength": 1 - } - } - }, - "metric": { - "type": "object", - "additionalProperties": false, - "required": [ - "name", - "value" - ], - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "runCompleted": { - "type": "object", - "additionalProperties": false, - "required": [ - "scenario_sha256", - "execution_model", - "valuation", - "order_counts" - ], - "properties": { - "scenario_sha256": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, - "execution_model": { - "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] - }, - "valuation": { - "$ref": "#/$defs/valuation" - }, - "order_counts": { - "type": "object", - "additionalProperties": false, - "required": [ - "total", - "active", - "filled", - "rejected", - "cancelled" - ], - "properties": { - "total": { - "type": "integer", - "minimum": 0 - }, - "active": { - "type": "integer", - "minimum": 0 - }, - "filled": { - "type": "integer", - "minimum": 0 - }, - "rejected": { - "type": "integer", - "minimum": 0 - }, - "cancelled": { - "type": "integer", - "minimum": 0 - } - } - } - } - } - } -} diff --git a/contracts/v15/scenario-stream.schema.json b/contracts/v15/scenario-stream.schema.json deleted file mode 100644 index 5af7c61..0000000 --- a/contracts/v15/scenario-stream.schema.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v15/scenario-stream.schema.json", - "title": "Trading Engine v15 replay scenario stream record", - "description": "Market-slice records are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-record rule is enforced semantically.", - "oneOf": [ - { "$ref": "#/$defs/headerRecord" }, - { "$ref": "#/$defs/sliceRecord" }, - { "$ref": "#/$defs/endRecord" } - ], - "$defs": { - "headerRecord": { - "type": "object", - "additionalProperties": false, - "required": ["contract_version", "scenario_sequence", "record_type", "payload"], - "properties": { - "contract_version": { "const": "15" }, - "scenario_sequence": { "const": "1" }, - "record_type": { "const": "scenario_header" }, - "payload": { "$ref": "#/$defs/headerPayload" } - } - }, - "sliceRecord": { - "type": "object", - "additionalProperties": false, - "required": ["contract_version", "scenario_sequence", "record_type", "payload"], - "properties": { - "contract_version": { "const": "15" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/sequence" }, - "record_type": { "const": "market_slice" }, - "payload": { "$ref": "#/$defs/slicePayload" } - } - }, - "endRecord": { - "type": "object", - "additionalProperties": false, - "required": ["contract_version", "scenario_sequence", "record_type", "payload"], - "properties": { - "contract_version": { "const": "15" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/sequence" }, - "record_type": { "const": "scenario_end" }, - "payload": { - "type": "object", - "additionalProperties": false, - "required": ["slice_count"], - "properties": { "slice_count": { "type": "string", "pattern": "^(?:0|[1-9][0-9]*)$" } } - } - } - }, - "headerPayload": { - "type": "object", - "additionalProperties": false, - "required": ["metadata", "run_id", "base_currency", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "max_internal_events"], - "properties": { - "metadata": { "type": "object" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, - "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/initialPortfolio" }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/instrument" } }, - "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/financing" }, - "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/settlement" }, - "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 } - } - }, - "slicePayload": { - "type": "object", - "additionalProperties": false, - "required": ["market_slice", "intents"], - "properties": { - "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v15/scenario.schema.json b/contracts/v15/scenario.schema.json deleted file mode 100644 index ffab82b..0000000 --- a/contracts/v15/scenario.schema.json +++ /dev/null @@ -1,870 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json", - "title": "Trading Engine v15 replay scenario", - "description": "Strict deterministic scenario contract with explicit venue-local session policies resolved to absolute instants outside the reducer.", - "type": "object", - "additionalProperties": false, - "required": ["contract_version", "metadata", "run_id", "base_currency", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "max_internal_events", "schedule", "slices"], - "properties": { - "contract_version": { "const": "15" }, - "metadata": { "type": "object" }, - "run_id": { "$ref": "#/$defs/identifier" }, - "base_currency": { "$ref": "#/$defs/identifier" }, - "initial_portfolio": { "$ref": "#/$defs/initialPortfolio" }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "#/$defs/instrument" } - }, - "venue_calendars": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/venueCalendar" } - }, - "risk": { "$ref": "#/$defs/risk" }, - "execution": { "$ref": "#/$defs/execution" }, - "financing": { "$ref": "#/$defs/financing" }, - "settlement": { "$ref": "#/$defs/settlement" }, - "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 }, - "schedule": { "type": "array", "items": { "$ref": "#/$defs/scheduleItem" } }, - "slices": { - "description": "Slices are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-item rule is enforced semantically.", - "type": "array", - "items": { "$ref": "#/$defs/marketSlice" } - } - }, - "$defs": { - "settlement": { - "type": "object", - "additionalProperties": false, - "required": ["cash_buying_power", "position_availability", "calendars", "rules"], - "properties": { - "cash_buying_power": { "enum": ["total_cash", "settled_cash"] }, - "position_availability": { "enum": ["total_positions", "settled_positions"] }, - "calendars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementCalendar" } }, - "rules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementRule" } } - } - }, - "settlementCalendar": { - "type": "object", - "additionalProperties": false, - "required": ["calendar_id", "version", "business_dates"], - "properties": { - "calendar_id": { "$ref": "#/$defs/identifier" }, - "version": { "const": "1" }, - "business_dates": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" } } - } - }, - "settlementRule": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "calendar_id", "lag_business_days"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "calendar_id": { "$ref": "#/$defs/identifier" }, - "lag_business_days": { "type": "integer", "minimum": 0, "maximum": 30 } - } - }, - "settlementFailure": { - "type": "object", - "additionalProperties": false, - "required": ["instruction_id", "reason"], - "properties": { - "instruction_id": { "$ref": "#/$defs/identifier" }, - "reason": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } - } - }, - "financing": { - "type": "object", - "additionalProperties": false, - "required": ["day_count", "compounding", "borrow_missing_data", "cash_missing_data", "locate_policy", "recall_policy"], - "properties": { - "day_count": { "enum": ["actual_365", "actual_360"] }, - "compounding": { "enum": ["simple", "daily"] }, - "borrow_missing_data": { "enum": ["reject", "zero"] }, - "cash_missing_data": { "enum": ["reject", "zero"] }, - "locate_policy": { "enum": ["reject_order", "clip_fill"] }, - "recall_policy": { "enum": ["reject_new_shorts", "close_out"] } - } - }, - "borrowObservation": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "effective_at", "available_quantity", "annual_rate_bps", "recalled"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "effective_at": { "$ref": "#/$defs/timestamp" }, - "available_quantity": { "$ref": "#/$defs/unsignedDecimal" }, - "annual_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, - "recalled": { "type": "boolean" } - } - }, - "cashRateObservation": { - "type": "object", - "additionalProperties": false, - "required": ["currency", "effective_at", "credit_rate_bps", "debit_rate_bps"], - "properties": { - "currency": { "$ref": "#/$defs/identifier" }, - "effective_at": { "$ref": "#/$defs/timestamp" }, - "credit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, - "debit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 } - } - }, - "identifier": { - "type": "string", - "minLength": 1, - "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" - }, - "signedDecimal": { - "type": "string", - "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" - }, - "unsignedDecimal": { - "type": "string", - "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "positiveDecimal": { - "type": "string", - "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "sequence": { "type": "string", "pattern": "^[1-9][0-9]*$" }, - "timestamp": { - "type": "string", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" - }, - "cashBalance": { - "type": "object", - "additionalProperties": false, - "required": ["currency", "amount"], - "properties": { - "currency": { "$ref": "#/$defs/identifier" }, - "amount": { "$ref": "#/$defs/signedDecimal" } - } - }, - "initialPortfolio": { - "type": "object", - "additionalProperties": false, - "required": ["cash", "positions", "marks", "fx_rates"], - "properties": { - "cash": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashBalance" } }, - "positions": { "type": "array", "items": { "$ref": "#/$defs/initialPosition" } }, - "marks": { "type": "array", "items": { "$ref": "#/$defs/initialMark" } }, - "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } } - } - }, - "initialPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "cost_basis", "realized_pnl", "dividend_pnl", "execution_fees", "borrow_fees"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "quantity": { "$ref": "#/$defs/signedDecimal" }, - "cost_basis": { "$ref": "#/$defs/signedDecimal" }, - "realized_pnl": { "$ref": "#/$defs/signedDecimal" }, - "dividend_pnl": { "$ref": "#/$defs/signedDecimal" }, - "execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, - "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "initialMark": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "price"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "price": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "instrument": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "symbol", "quote_currency", "tick_size", "lot_size"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "symbol": { "$ref": "#/$defs/identifier" }, - "quote_currency": { "$ref": "#/$defs/identifier" }, - "tick_size": { "$ref": "#/$defs/positiveDecimal" }, - "lot_size": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "venueCalendar": { - "type": "object", - "additionalProperties": false, - "required": ["calendar_id", "calendar_version", "venue_id", "instrument_ids", "sessions"], - "properties": { - "calendar_id": { "$ref": "#/$defs/identifier" }, - "calendar_version": { "const": "1" }, - "venue_id": { "$ref": "#/$defs/identifier" }, - "instrument_ids": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { "$ref": "#/$defs/identifier" } - }, - "sessions": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/venueSession" } - } - } - }, - "venueSession": { - "oneOf": [ - { "$ref": "#/$defs/openVenueSession" }, - { "$ref": "#/$defs/holidayVenueSession" } - ] - }, - "openVenueSession": { - "type": "object", - "additionalProperties": false, - "required": ["session_date", "policy", "phases"], - "properties": { - "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "policy": { "enum": ["regular", "early_close"] }, - "phases": { - "type": "array", - "minItems": 1, - "maxItems": 5, - "items": { "$ref": "#/$defs/venuePhase" } - } - } - }, - "holidayVenueSession": { - "type": "object", - "additionalProperties": false, - "required": ["session_date", "policy", "phases"], - "properties": { - "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "policy": { "const": "holiday" }, - "phases": { "type": "array", "maxItems": 0 } - } - }, - "venuePhase": { - "type": "object", - "additionalProperties": false, - "required": ["phase", "opens_at", "closes_at"], - "properties": { - "phase": { "enum": ["premarket", "opening_auction", "regular", "closing_auction", "postmarket"] }, - "opens_at": { "$ref": "#/$defs/timestamp" }, - "closes_at": { "$ref": "#/$defs/timestamp" } - } - }, - "risk": { - "type": "object", - "additionalProperties": false, - "required": ["max_gross_exposure", "max_leverage", "short_borrow_bps", "instrument_policies", "groups"], - "properties": { - "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, - "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, - "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "instrument_policies": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/instrumentRiskPolicy" } - }, - "groups": { - "type": "array", - "items": { "$ref": "#/$defs/riskGroup" } - } - } - }, - "instrumentRiskPolicy": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "max_order_quantity", "max_long_position", "max_short_position", "max_notional_exposure", "initial_margin_bps", "maintenance_margin_bps", "shorting_allowed"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, - "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_notional_exposure": { "$ref": "#/$defs/positiveDecimal" }, - "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "shorting_allowed": { "type": "boolean" } - } - }, - "nullablePositiveDecimal": { - "oneOf": [ - { "type": "null" }, - { "$ref": "#/$defs/positiveDecimal" } - ] - }, - "riskGroup": { - "type": "object", - "additionalProperties": false, - "required": ["group_id", "group_version", "group_type", "instrument_ids", "limits"], - "properties": { - "group_id": { "$ref": "#/$defs/identifier" }, - "group_version": { "const": "1" }, - "group_type": { "enum": ["issuer", "sector", "currency", "country", "asset_class", "custom"] }, - "instrument_ids": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { "$ref": "#/$defs/identifier" } - }, - "limits": { "$ref": "#/$defs/riskGroupLimits" } - } - }, - "riskGroupLimits": { - "type": "object", - "additionalProperties": false, - "required": ["max_gross_exposure", "max_long_exposure", "max_short_exposure", "max_absolute_net_exposure", "max_concentration"], - "properties": { - "max_gross_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_long_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_short_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_absolute_net_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_concentration": { - "oneOf": [ - { "type": "null" }, - { "allOf": [{ "$ref": "#/$defs/positiveDecimal" }, { "pattern": "^(?:0[.][0-9]{0,5}[1-9]|1)$" }] } - ] - } - } - }, - "execution": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "enum": ["completed_bar_next_open_v1", "completed_bar_adverse_touch_v1"] }, - "configuration": { "$ref": "#/$defs/conservativeBarConfiguration" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "quote_trade_v1" }, - "configuration": { "$ref": "#/$defs/quoteTradeConfiguration" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "order_book_v1" }, - "configuration": { "$ref": "#/$defs/orderBookConfiguration" } - } - } - ] - }, - "completedBarV1Configuration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fee_schedules"], - "properties": { - "version": { "const": "2" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } - } - }, - "conservativeBarConfiguration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fee_schedules", "spread_model", "impact_model"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, - "spread_model": { "$ref": "#/$defs/fixedSpreadModel" }, - "impact_model": { "$ref": "#/$defs/linearImpactModel" } - } - }, - "quoteTradeConfiguration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fee_schedules"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } - } - }, - "orderBookConfiguration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fee_schedules", "max_depth_levels"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, - "max_depth_levels": { "type": "integer", "minimum": 1, "maximum": 1024 } - } - }, - "fixedSpreadModel": { - "type": "object", - "additionalProperties": false, - "required": ["model", "half_spread_bps"], - "properties": { - "model": { "const": "fixed_half_spread_v1" }, - "half_spread_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "linearImpactModel": { - "type": "object", - "additionalProperties": false, - "required": ["model", "coefficient_bps", "missing_volume_policy"], - "properties": { - "model": { "const": "linear_participation_v1" }, - "coefficient_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "missing_volume_policy": { "enum": ["reject", "zero_impact"] } - } - }, - "feeSchedule": { - "type": "object", - "additionalProperties": false, - "required": ["schedule_id", "instrument_id", "settlement_currency", "minimum", "maximum", "components"], - "properties": { - "schedule_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "settlement_currency": { "$ref": "#/$defs/identifier" }, - "minimum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, - "maximum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, - "components": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeComponent" } } - } - }, - "feeComponent": { - "type": "object", - "additionalProperties": false, - "required": ["name", "currency", "kind", "value", "rounding", "applies_to"], - "properties": { - "name": { "$ref": "#/$defs/identifier" }, - "currency": { "$ref": "#/$defs/identifier" }, - "kind": { "enum": ["fixed", "notional_bps", "per_unit"] }, - "value": { "oneOf": [{ "$ref": "#/$defs/signedDecimal" }, { "type": "integer", "minimum": -10000, "maximum": 10000 }] }, - "rounding": { "enum": ["up", "down", "nearest"] }, - "applies_to": { "enum": ["any", "maker", "taker"] } - }, - "allOf": [ - { "if": { "properties": { "kind": { "const": "notional_bps" } } }, "then": { "properties": { "value": { "type": "integer" } } } }, - { "if": { "properties": { "kind": { "enum": ["fixed", "per_unit"] } } }, "then": { "properties": { "value": { "$ref": "#/$defs/signedDecimal" } } } } - ] - }, - "scheduleItem": { - "type": "object", - "additionalProperties": false, - "required": ["after_slice_sequence", "intents"], - "properties": { - "after_slice_sequence": { "$ref": "#/$defs/sequence" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "#/$defs/intent" } } - } - }, - "intent": { - "oneOf": [ - { "$ref": "#/$defs/targetWeights" }, - { "$ref": "#/$defs/targetQuantities" }, - { "$ref": "#/$defs/submitOrder" }, - { "$ref": "#/$defs/cancelOrder" }, - { "$ref": "#/$defs/metric" } - ] - }, - "targetWeights": { - "type": "object", - "additionalProperties": false, - "required": ["type", "targets"], - "properties": { - "type": { "const": "target_weights" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "weight"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "weight": { "$ref": "#/$defs/signedDecimal" } - } - } - } - } - }, - "targetQuantities": { - "type": "object", - "additionalProperties": false, - "required": ["type", "targets"], - "properties": { - "type": { "const": "target_quantities" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "quantity": { "$ref": "#/$defs/signedDecimal" } - } - } - } - } - }, - "submitOrder": { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "side", "quantity", "order_kind", "trigger_price", "limit_price", "time_in_force", "venue_id", "calendar_id", "expires_at"], - "properties": { - "type": { "const": "submit_order" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "order_kind": { "enum": ["market", "limit", "stop", "stop_limit"] }, - "trigger_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, - "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, - "time_in_force": { "enum": ["gtc", "ioc", "fok", "day", "gtd"] }, - "venue_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, - "calendar_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, - "expires_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] } - }, - "allOf": [ - { "if": { "properties": { "order_kind": { "const": "market" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "type": "null" } } } }, - { "if": { "properties": { "order_kind": { "const": "limit" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, - { "if": { "properties": { "order_kind": { "const": "stop" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "type": "null" } } } }, - { "if": { "properties": { "order_kind": { "const": "stop_limit" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, - { "if": { "properties": { "time_in_force": { "const": "day" } } }, "then": { "properties": { "venue_id": { "$ref": "#/$defs/identifier" }, "calendar_id": { "$ref": "#/$defs/identifier" }, "expires_at": { "type": "null" } } } }, - { "if": { "properties": { "time_in_force": { "const": "gtd" } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "$ref": "#/$defs/timestamp" } } } }, - { "if": { "properties": { "time_in_force": { "enum": ["gtc", "ioc", "fok"] } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "type": "null" } } } } - ] - }, - "cancelOrder": { - "type": "object", - "additionalProperties": false, - "required": ["type", "order_id"], - "properties": { - "type": { "const": "cancel_order" }, - "order_id": { "$ref": "#/$defs/identifier" } - } - }, - "metric": { - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "value"], - "properties": { - "type": { "const": "emit_metric" }, - "name": { "type": "string" }, - "value": { "type": "string" } - } - }, - "marketSlice": { - "type": "object", - "additionalProperties": false, - "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "market_events", "order_book_events", "fx_rates", "corporate_actions", "borrow_observations", "cash_rate_observations", "settlement_failures", "lifecycle_events"], - "properties": { - "slice_sequence": { "$ref": "#/$defs/sequence" }, - "start_at": { "$ref": "#/$defs/timestamp" }, - "end_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "bars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/bar" } }, - "market_events": { "type": "array", "items": { "$ref": "#/$defs/marketEvent" } }, - "order_book_events": { "type": "array", "items": { "$ref": "#/$defs/orderBookEvent" } }, - "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } }, - "corporate_actions": { "type": "array", "items": { "$ref": "#/$defs/corporateAction" } }, - "borrow_observations": { "type": "array", "items": { "$ref": "#/$defs/borrowObservation" } }, - "cash_rate_observations": { "type": "array", "items": { "$ref": "#/$defs/cashRateObservation" } }, - "settlement_failures": { "type": "array", "items": { "$ref": "#/$defs/settlementFailure" } }, - "lifecycle_events": { "type": "array", "items": { "$ref": "#/$defs/lifecycleEvent" } } - } - }, - "bar": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "open", "high", "low", "close", "volume"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "open": { "$ref": "#/$defs/positiveDecimal" }, - "high": { "$ref": "#/$defs/positiveDecimal" }, - "low": { "$ref": "#/$defs/positiveDecimal" }, - "close": { "$ref": "#/$defs/positiveDecimal" }, - "volume": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] } - } - }, - "marketEvent": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "bid_price", "bid_quantity", "ask_price", "ask_quantity"], - "properties": { - "type": { "const": "quote" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "bid_price": { "$ref": "#/$defs/positiveDecimal" }, - "bid_quantity": { "$ref": "#/$defs/positiveDecimal" }, - "ask_price": { "$ref": "#/$defs/positiveDecimal" }, - "ask_quantity": { "$ref": "#/$defs/positiveDecimal" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "price", "quantity", "aggressor_side"], - "properties": { - "type": { "const": "trade" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "aggressor_side": { "enum": ["buy", "sell", "unknown"] } - } - } - ] - }, - "orderBookLevel": { - "type": "object", - "additionalProperties": false, - "required": ["price", "quantity"], - "properties": { - "price": { "$ref": "#/$defs/positiveDecimal" }, - "quantity": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "orderBookEvent": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "bids", "asks"], - "properties": { - "type": { "const": "snapshot" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "book_sequence": { "$ref": "#/$defs/sequence" }, - "bids": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } }, - "asks": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price", "quantity"], - "properties": { - "type": { "const": "set" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "book_sequence": { "$ref": "#/$defs/sequence" }, - "side": { "enum": ["bid", "ask"] }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "quantity": { "$ref": "#/$defs/positiveDecimal" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price"], - "properties": { - "type": { "const": "delete" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "book_sequence": { "$ref": "#/$defs/sequence" }, - "side": { "enum": ["bid", "ask"] }, - "price": { "$ref": "#/$defs/positiveDecimal" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "price", "quantity", "aggressor_side"], - "properties": { - "type": { "const": "trade" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "book_sequence": { "$ref": "#/$defs/sequence" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "aggressor_side": { "enum": ["buy", "sell", "unknown"] } - } - } - ] - }, - "fxRate": { - "type": "object", - "additionalProperties": false, - "required": ["currency", "rate"], - "properties": { - "currency": { "$ref": "#/$defs/identifier" }, - "rate": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "corporateAction": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "action_id", "instrument_id", "numerator", "denominator"], - "properties": { - "type": { "const": "split" }, - "action_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "numerator": { "$ref": "#/$defs/sequence" }, - "denominator": { "$ref": "#/$defs/sequence" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "action_id", "instrument_id", "amount_per_unit"], - "properties": { - "type": { "const": "cash_dividend" }, - "action_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "amount_per_unit": { "$ref": "#/$defs/positiveDecimal" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], - "properties": { - "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, - "action_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "destination_instrument_id": { "$ref": "#/$defs/identifier" }, - "numerator": { "$ref": "#/$defs/sequence" }, - "denominator": { "$ref": "#/$defs/sequence" }, - "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fractional_policy": { "$ref": "#/$defs/fractionalPolicy" } - } - } - ] - }, - "fractionalPolicy": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["policy"], - "properties": { "policy": { "const": "reject" } } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["policy", "price", "currency"], - "properties": { - "policy": { "const": "cash_in_lieu" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "currency": { "$ref": "#/$defs/identifier" } - } - } - ] - }, - "terminalPolicy": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["policy"], - "properties": { "policy": { "const": "hold" } } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["policy", "price", "currency"], - "properties": { - "policy": { "const": "cash_out" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "currency": { "$ref": "#/$defs/identifier" } - } - } - ] - }, - "lifecycleEvent": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "reason"], - "properties": { - "type": { "const": "halt" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "reason": { "type": "string", "minLength": 1 } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id"], - "properties": { - "type": { "const": "resume" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "symbol", "provider", "provider_instrument_id"], - "properties": { - "type": { "const": "identifier_change" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "symbol": { "$ref": "#/$defs/identifier" }, - "provider": { "$ref": "#/$defs/identifier" }, - "provider_instrument_id": { "$ref": "#/$defs/identifier" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "terminal_policy"], - "properties": { - "type": { "const": "expiration" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "terminal_policy": { "$ref": "#/$defs/terminalPolicy" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "terminal_policy", "reason"], - "properties": { - "type": { "const": "delisting" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "terminal_policy": { "$ref": "#/$defs/terminalPolicy" }, - "reason": { "type": "string", "minLength": 1 } - } - } - ] - } - } -} diff --git a/contracts/v16/README.md b/contracts/v16/README.md deleted file mode 100644 index 03ac9da..0000000 --- a/contracts/v16/README.md +++ /dev/null @@ -1,122 +0,0 @@ -# Trading Engine contract v16 - -This directory is the authoritative v16 process and file contract shared by Trading Engine and its -clients. Versions 14 through 3 remain readable during their client transitions. - -- `scenario.schema.json` validates batch replay inputs. -- `scenario-stream.schema.json` validates each JSON Lines scenario-stream record. -- `journal.schema.json` validates each JSON Lines audit record. -- The files under `fixtures/` form the canonical valid conformance corpus. -- `fill-clipped.scenario.json` and its journal exercise a leverage-limited partial fill. - -Version 7 requires exactly one explicit risk policy per catalog instrument. Each policy defines -order, signed-position, notional, initial-margin, maintenance-margin, and shorting limits. Versioned -risk groups have explicit membership, may overlap, and can constrain gross, long, short, absolute -net, and gross-to-equity concentration exposure. - -Runtime validation requires exact currency, position-mark, and FX coverage; known instruments; -lot-aligned quantities; tick-aligned positive marks; basis with the same sign as quantity; -nonnegative fee histories; the base FX rate equal to one; instrument, group, aggregate exposure, -leverage, and initial-margin limits. Signed cash is valid. A successful v16 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 v16 scenario, stream record, and journal record carries `"contract_version": "16"`. - -Version 8 adds explicit `market`, `limit`, `stop`, and `stop_limit` orders with `gtc`, `ioc`, -`fok`, `day`, and `gtd` time-in-force policies. `day` orders identify both their venue and the -exact versioned calendar; `gtd` orders carry an absolute expiry timestamp. Older contracts retain -their frozen mapping: market orders are IOC and limit orders are GTC. - -Stops evaluate only completed OHLCV bars. A gap through the trigger records the bar start as the -trigger time; an intrabar touch records the bar end. Trigger state and slice sequence are journaled, -and an activated order cannot execute before the following slice. A stop becomes a market order; -a stop-limit becomes its configured limit order. Splits adjust both trigger and limit prices. - -IOC orders cancel any remainder after their first eligible slice. FOK orders fill only when the -full remaining quantity fits both execution capacity and risk capacity, otherwise they cancel with -no fill. DAY orders cancel after matching the slice that reaches the selected session's final -phase close. GTD orders cancel before matching any completed bar whose end reaches or passes the -expiry, avoiding ambiguous partial-bar execution. - -The v10 `execution` object uses `completed_bar_v1` configuration version `"2"`: participation basis -points plus exactly one composable fee schedule per instrument. Named fixed, notional-basis-point, -and per-unit components declare currency, rounding, and maker/taker applicability. Optional -per-fill minimums and caps use the schedule settlement currency; negative components represent -rebates. Fills and valuations retain every native, quote, and base-currency attribution. Runtime -capabilities also advertise frozen configuration version `"1"` for older scenario contracts. - -Version 10 adds a required `financing` policy and effective-time observations on every market -slice. Borrow observations provide per-instrument locate availability, signed annual rates, and -recall state. Cash observations provide separate annual credit and debit rates per currency. -Policies select Actual/365 or Actual/360 day count, simple or daily compounding, missing-data -handling, locate rejection or fill clipping, and recall rejection or deterministic close-out. - -Borrow availability is enforced when a fill would create or increase a short. Recalls cancel -active sells and may submit priority IOC covers until the position is flat. Borrow charges and cash -interest use the exact slice interval, update native ledgers deterministically, and emit dedicated -journal records. Valuations report cash interest separately and include it in aggregate realized -P&L. Version 9 and earlier retain their frozen fixed-borrow behavior and wire shapes. - -Version 12 separates trade-date economic accounting from settlement-date availability. A required -settlement policy selects total or settled cash buying power and total or settled position -availability. Versioned calendars enumerate canonical business dates, and each instrument has an -explicit business-day lag. Every fill creates a deterministic settlement instruction containing -its cash and position movements, trade date, and due date. A due instruction either settles on the -first eligible slice or records a named failure supplied by that slice. - -Valuations and strategy contexts report settled and unsettled cash and quantities without changing -economic equity. Journals include instruction-created, completed, and failed events. Scenario v10 -and strategy protocol v8 retain their frozen immediate-settlement wire behavior. - -Version 12 adds exact stock-dividend, rights, and spin-off distributions. Each distribution names -its destination instrument, exact entitlement ratio, basis allocation in basis points, and either -rejects fractional entitlements or converts them to cash at an explicit price and currency. -Stock dividends adjust persistent targets and eligible working orders; every distribution journals -delivered quantity, fractional quantity, allocated basis, fractional basis, and cash in lieu. - -Lifecycle events keep stable instrument identity separate from mutable symbol and provider -mappings. Halt and resume transitions control tradability. Expiration and delisting are terminal, -cancel active orders, clear target exposure, and require an explicit hold or cash-out policy. -Cash-out specifies its terminal price and currency. Every transition journals the source event, -resulting listing state, provider provenance, liquidated quantity, and cash attribution. - -Version 14 adds `completed_bar_next_open_v1` and `completed_bar_adverse_touch_v1` without changing -the frozen `completed_bar_v1` semantics. Next-open limits require a marketable later open; -adverse-touch limits require a one-tick trade-through before a maker fill is eligible. Both models -declare fixed half-spread and linear participation-impact catalogs, including an explicit policy -for missing bar volume. Price costs round away from the reference price to instrument ticks and -cannot violate a limit. An `execution_price_selected` audit record attributes the reference price, -spread adjustment, impact adjustment, and final executable price before each fill. - -Version 14 adds `quote_trade_v1` and causally ordered `market_events`. Quotes expose bid/ask price -and displayed size. Trades expose price, size, and buy, sell, or unknown aggressor side. Each event -records economic, availability, and receipt timestamps plus a positive ingest sequence. Replay -orders events by availability, receipt, and ingest sequence. Marketable orders consume only -displayed quote liquidity; passive orders require appropriately aggressed trade evidence, and an -unknown aggressor never fills them. Event capacity is shared deterministically across order -priority and fills retain the event's economic timestamp. Completed bars remain the valuation -boundary. The `quote-trade` batch, stream, and journal fixtures demonstrate equivalent replay. - -Version 15 added `order_book_v1` and bounded level-two `order_book_events`. Every per-instrument -slice bundle starts with a complete snapshot and continues with contiguous absolute set, delete, -and aggressor-classified trade updates. Snapshots and updates reject crossed books, missing -deletes, sequence gaps, tick or lot misalignment, and depth beyond the configured limit; locked -books are valid. State is rebuilt from each slice snapshot, so replay never depends on hidden data -from a prior slice. - -Marketable orders walk observable opposite-side depth in price priority. Passive limit orders join -behind displayed same-price quantity and earlier engine orders. Reductions decrease quantity ahead, -adds join behind, and only appropriately aggressed trades consume the queue and fill the order. -Partial fills and cancellations therefore remain deterministic. Book liquidity is independent of -bar and quote/trade execution semantics, while completed bars remain the valuation boundary. The -`order-book` batch, stream, and journal fixtures demonstrate cancellation, queue depletion, maker -fills, bounded state, and batch/stream equivalence. - -Version 16 replaces string-only metrics with typed observations. Numeric values use canonical -decimal strings; string and boolean values retain their JSON types. Optional units, a closed -aggregation enum, and up to 16 unique dimensions are bounded at ingestion. Dimension keys are -sorted before journal encoding for deterministic downstream reconciliation. diff --git a/contracts/v16/dune b/contracts/v16/dune deleted file mode 100644 index 0e7e927..0000000 --- a/contracts/v16/dune +++ /dev/null @@ -1,36 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v16/journal.schema.json) - (scenario-stream.schema.json as contracts/v16/scenario-stream.schema.json) - (scenario.schema.json as contracts/v16/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v16/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v16/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl - as - contracts/v16/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v16/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v16/fixtures/fill-clipped.scenario.json) - (fixtures/quote-trade.journal.jsonl - as - contracts/v16/fixtures/quote-trade.journal.jsonl) - (fixtures/quote-trade.scenario.json - as - contracts/v16/fixtures/quote-trade.scenario.json) - (fixtures/quote-trade.scenario.jsonl - as - contracts/v16/fixtures/quote-trade.scenario.jsonl) - (fixtures/order-book.journal.jsonl - as - contracts/v16/fixtures/order-book.journal.jsonl) - (fixtures/order-book.scenario.json - as - contracts/v16/fixtures/order-book.scenario.json) - (fixtures/order-book.scenario.jsonl - as - contracts/v16/fixtures/order-book.scenario.jsonl))) diff --git a/contracts/v16/fixtures/demo.journal.jsonl b/contracts/v16/fixtures/demo.journal.jsonl deleted file mode 100644 index 2590d37..0000000 --- a/contracts/v16/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,29 +0,0 @@ -{"contract_version":"16","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":"a56b9b38f18d93e2953f90c8b052026d174e91c01a9465f8070a22040820a78d","execution_model":"completed_bar_adverse_touch_v1"}} -{"contract_version":"16","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":"16","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":"16","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"16","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":"16","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":"16","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":{"type":"numeric","value":"0.1"},"unit":"ratio","dimensions":{"instrument":"demo-equity-acme","source":"strategy"},"aggregation":"last"}} -{"contract_version":"16","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":"16","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":"16","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"13"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"16","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":"16","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":"16","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":"16","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":"16","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":"16","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":"16","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"3","start_at":"2026-01-06T14:30:00.000000Z","end_at":"2026-01-06T21:00:00.000000Z","available_at":"2026-01-06T21:00:01.000000Z","received_at":"2026-01-06T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"107","high":"109","low":"104","close":"105","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"16","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":"16","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":"16","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":"16","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":"16","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":"16","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":"16","engine_sequence":"24","event_id":"demo-event-000000000024","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"4","start_at":"2026-01-07T14:30:00.000000Z","end_at":"2026-01-07T21:00:00.000000Z","available_at":"2026-01-07T21:00:01.000000Z","received_at":"2026-01-07T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"105","high":"107","low":"103","close":"106","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"16","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":"16","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":"16","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":"16","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":"16","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":"a56b9b38f18d93e2953f90c8b052026d174e91c01a9465f8070a22040820a78d","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/v16/fixtures/demo.scenario.json b/contracts/v16/fixtures/demo.scenario.json deleted file mode 100644 index b8b8fb3..0000000 --- a/contracts/v16/fixtures/demo.scenario.json +++ /dev/null @@ -1,468 +0,0 @@ -{ - "contract_version": "16", - "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": { "type": "numeric", "value": "0.1" }, - "unit": "ratio", - "dimensions": { "source": "strategy", "instrument": "demo-equity-acme" }, - "aggregation": "last" - } - ] - }, - { - "after_slice_sequence": "3", - "intents": [ - { - "type": "target_quantities", - "targets": [ - { - "instrument_id": "demo-equity-acme", - "quantity": "2.5" - } - ] - } - ] - } - ], - "slices": [ - { - "slice_sequence": "1", - "start_at": "2026-01-02T14:30:00Z", - "end_at": "2026-01-02T21:00:00Z", - "available_at": "2026-01-02T21:00:01Z", - "received_at": "2026-01-02T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "100", - "high": "105", - "low": "99", - "close": "104", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-02T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-02T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - }, - { - "slice_sequence": "2", - "start_at": "2026-01-05T14:30:00Z", - "end_at": "2026-01-05T21:00:00Z", - "available_at": "2026-01-05T21:00:01Z", - "received_at": "2026-01-05T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "103", - "high": "108", - "low": "102", - "close": "107", - "volume": "13" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-05T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-05T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - }, - { - "slice_sequence": "3", - "start_at": "2026-01-06T14:30:00Z", - "end_at": "2026-01-06T21:00:00Z", - "available_at": "2026-01-06T21:00:01Z", - "received_at": "2026-01-06T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "107", - "high": "109", - "low": "104", - "close": "105", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-06T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-06T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - }, - { - "slice_sequence": "4", - "start_at": "2026-01-07T14:30:00Z", - "end_at": "2026-01-07T21:00:00Z", - "available_at": "2026-01-07T21:00:01Z", - "received_at": "2026-01-07T21:00:02Z", - "bars": [ - { - "instrument_id": "demo-equity-acme", - "open": "105", - "high": "107", - "low": "103", - "close": "106", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "demo-equity-acme", - "effective_at": "2026-01-07T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-01-07T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - }, - "settlement": { - "cash_buying_power": "total_cash", - "position_availability": "total_positions", - "calendars": [ - { - "calendar_id": "default-settlement", - "version": "1", - "business_dates": [ - "2026-01-02", - "2026-01-05", - "2026-01-06", - "2026-01-07", - "2026-01-08", - "2026-01-09", - "2026-02-02", - "2026-02-03", - "2026-02-04", - "2026-02-05" - ] - } - ], - "rules": [ - { - "instrument_id": "demo-equity-acme", - "calendar_id": "default-settlement", - "lag_business_days": 1 - } - ] - } -} diff --git a/contracts/v16/fixtures/demo.scenario.jsonl b/contracts/v16/fixtures/demo.scenario.jsonl deleted file mode 100644 index 28012c0..0000000 --- a/contracts/v16/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"16","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":"16","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","weight":"0.1"}],"type":"target_weights"},{"aggregation":"last","dimensions":{"instrument":"demo-equity-acme","source":"strategy"},"name":"desired_weight","type":"emit_metric","unit":"ratio","value":{"type":"numeric","value":"0.1"}}],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"16","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"13"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"16","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","quantity":"2.5"}],"type":"target_quantities"}],"market_slice":{"available_at":"2026-01-06T21:00:01Z","bars":[{"close":"105","high":"109","instrument_id":"demo-equity-acme","low":"104","open":"107","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-06T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-06T21:00:02Z","slice_sequence":"3","start_at":"2026-01-06T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"16","payload":{"intents":[],"market_slice":{"available_at":"2026-01-07T21:00:01Z","bars":[{"close":"106","high":"107","instrument_id":"demo-equity-acme","low":"103","open":"105","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-07T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-07T21:00:02Z","slice_sequence":"4","start_at":"2026-01-07T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"16","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v16/fixtures/fill-clipped.journal.jsonl b/contracts/v16/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index 3419c5a..0000000 --- a/contracts/v16/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"contract_version":"16","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":"dd1ca1913eb2ef4e0070887065763fb75e81a1e27b4497bf2afd09546975bd07","execution_model":"completed_bar_v1"}} -{"contract_version":"16","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":"16","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":"16","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"16","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":"16","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":"16","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":"16","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"16","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":"16","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":"16","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":"16","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":"16","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":"dd1ca1913eb2ef4e0070887065763fb75e81a1e27b4497bf2afd09546975bd07","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/v16/fixtures/fill-clipped.scenario.json b/contracts/v16/fixtures/fill-clipped.scenario.json deleted file mode 100644 index eea4e6d..0000000 --- a/contracts/v16/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,273 +0,0 @@ -{ - "contract_version": "16", - "metadata": { - "producer": "trading-engine", - "purpose": "fill clipping conformance fixture" - }, - "run_id": "fill-clipped", - "base_currency": "USD", - "initial_portfolio": { - "cash": [ - { - "currency": "USD", - "amount": "550" - } - ], - "positions": [], - "marks": [], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ] - }, - "instruments": [ - { - "instrument_id": "clip-equity", - "symbol": "CLIP", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "venue_calendars": [ - { - "calendar_id": "clip-xnas-2026", - "calendar_version": "1", - "venue_id": "XNAS", - "instrument_ids": [ - "clip-equity" - ], - "sessions": [ - { - "session_date": "2026-02-02", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-02T14:30:00Z", - "closes_at": "2026-02-02T21:00:00Z" - } - ] - }, - { - "session_date": "2026-02-03", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-03T14:30:00Z", - "closes_at": "2026-02-03T21:00:00Z" - } - ] - }, - { - "session_date": "2026-02-04", - "policy": "early_close", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-04T14:30:00Z", - "closes_at": "2026-02-04T18:00:00Z" - } - ] - } - ] - } - ], - "risk": { - "max_gross_exposure": "1000000000", - "max_leverage": "1", - "short_borrow_bps": 100, - "instrument_policies": [ - { - "instrument_id": "clip-equity", - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_notional_exposure": "1000000000", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "shorting_allowed": true - } - ], - "groups": [] - }, - "execution": { - "model": "completed_bar_v1", - "configuration": { - "version": "2", - "participation_bps": 10000, - "fee_schedules": [ - { - "schedule_id": "clip-fees-v1", - "instrument_id": "clip-equity", - "settlement_currency": "USD", - "minimum": null, - "maximum": null, - "components": [ - { - "name": "broker", - "currency": "USD", - "kind": "fixed", - "value": "10", - "rounding": "up", - "applies_to": "any" - } - ] - } - ] - } - }, - "max_internal_events": 1000, - "schedule": [ - { - "after_slice_sequence": "1", - "intents": [ - { - "type": "submit_order", - "instrument_id": "clip-equity", - "side": "buy", - "quantity": "10", - "order_kind": "market", - "trigger_price": null, - "limit_price": null, - "time_in_force": "ioc", - "venue_id": null, - "calendar_id": null, - "expires_at": null - } - ] - } - ], - "slices": [ - { - "slice_sequence": "1", - "start_at": "2026-02-02T14:30:00Z", - "end_at": "2026-02-02T21:00:00Z", - "available_at": "2026-02-02T21:00:01Z", - "received_at": "2026-02-02T21:00:02Z", - "bars": [ - { - "instrument_id": "clip-equity", - "open": "50", - "high": "50", - "low": "50", - "close": "50", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "clip-equity", - "effective_at": "2026-02-02T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-02-02T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - }, - { - "slice_sequence": "2", - "start_at": "2026-02-03T14:30:00Z", - "end_at": "2026-02-03T21:00:00Z", - "available_at": "2026-02-03T21:00:01Z", - "received_at": "2026-02-03T21:00:02Z", - "bars": [ - { - "instrument_id": "clip-equity", - "open": "100", - "high": "100", - "low": "100", - "close": "100", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "clip-equity", - "effective_at": "2026-02-03T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-02-03T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - }, - "settlement": { - "cash_buying_power": "total_cash", - "position_availability": "total_positions", - "calendars": [ - { - "calendar_id": "default-settlement", - "version": "1", - "business_dates": [ - "2026-01-02", - "2026-01-05", - "2026-01-06", - "2026-01-07", - "2026-01-08", - "2026-01-09", - "2026-02-02", - "2026-02-03", - "2026-02-04", - "2026-02-05" - ] - } - ], - "rules": [ - { - "instrument_id": "clip-equity", - "calendar_id": "default-settlement", - "lag_business_days": 1 - } - ] - } -} diff --git a/contracts/v16/fixtures/order-book.journal.jsonl b/contracts/v16/fixtures/order-book.journal.jsonl deleted file mode 100644 index 684543d..0000000 --- a/contracts/v16/fixtures/order-book.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"contract_version":"16","engine_sequence":"1","event_id":"order-book-event-000000000001","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"10f1592feb856e3acc16a2eb3ebd23ff2c7641e092b88307e44efcd9f2072d74","execution_model":"order_book_v1"}} -{"contract_version":"16","engine_sequence":"2","event_id":"order-book-event-000000000002","causation_ids":["order-book-event-000000000001"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} -{"contract_version":"16","engine_sequence":"3","event_id":"order-book-event-000000000003","causation_ids":["order-book-event-000000000002"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} -{"contract_version":"16","engine_sequence":"4","event_id":"order-book-event-000000000004","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00.000000Z","available_at":"2026-02-02T14:31:01.000000Z","received_at":"2026-02-02T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}} -{"contract_version":"16","engine_sequence":"5","event_id":"order-book-event-000000000005","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} -{"contract_version":"16","engine_sequence":"6","event_id":"order-book-event-000000000006","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"order-book-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"order-book-event-000000000006","updated_event_id":"order-book-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"16","engine_sequence":"7","event_id":"order-book-event-000000000007","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} -{"contract_version":"16","engine_sequence":"8","event_id":"order-book-event-000000000008","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00.000000Z","available_at":"2026-02-03T14:35:01.000000Z","received_at":"2026-02-03T14:35:02.000000Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00.000000Z","available_at":"2026-02-03T14:36:01.000000Z","received_at":"2026-02-03T14:36:02.000000Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}} -{"contract_version":"16","engine_sequence":"9","event_id":"order-book-event-000000000009","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} -{"contract_version":"16","engine_sequence":"10","event_id":"order-book-event-000000000010","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000001","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"100","notional":"400","fee":"10","executed_at":"2026-02-03T14:35:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} -{"contract_version":"16","engine_sequence":"11","event_id":"order-book-event-000000000011","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000002","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:36:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} -{"contract_version":"16","engine_sequence":"12","event_id":"order-book-event-000000000012","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"980.02968","fx_rate":"1","base_value":"980.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"980.02968","unsettled_amount":"0","base_settled_value":"980.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"980.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]}} -{"contract_version":"16","engine_sequence":"13","event_id":"order-book-event-000000000013","causation_ids":["order-book-event-000000000012"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"10f1592feb856e3acc16a2eb3ebd23ff2c7641e092b88307e44efcd9f2072d74","execution_model":"order_book_v1","valuation":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"980.02968","fx_rate":"1","base_value":"980.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"980.02968","unsettled_amount":"0","base_settled_value":"980.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"980.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v16/fixtures/order-book.scenario.jsonl b/contracts/v16/fixtures/order-book.scenario.jsonl deleted file mode 100644 index e8aa5d3..0000000 --- a/contracts/v16/fixtures/order-book.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"16","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded order-book replay conformance fixture"},"run_id":"order-book","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"order_book_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}],"max_depth_levels":10}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} -{"contract_version":"16","scenario_sequence":"2","record_type":"market_slice","payload":{"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}],"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00Z","available_at":"2026-02-02T14:31:01Z","received_at":"2026-02-02T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}}} -{"contract_version":"16","scenario_sequence":"3","record_type":"market_slice","payload":{"intents":[],"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00Z","available_at":"2026-02-03T14:35:01Z","received_at":"2026-02-03T14:35:02Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00Z","available_at":"2026-02-03T14:36:01Z","received_at":"2026-02-03T14:36:02Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}}} -{"contract_version":"16","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v16/fixtures/quote-trade.journal.jsonl b/contracts/v16/fixtures/quote-trade.journal.jsonl deleted file mode 100644 index b6e48b2..0000000 --- a/contracts/v16/fixtures/quote-trade.journal.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"contract_version":"16","engine_sequence":"1","event_id":"quote-trade-event-000000000001","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"0e57b52243f3fc9fe37b94dc8fbde9a8b69c6c680bd5369e4d77c63060b853e8","execution_model":"quote_trade_v1"}} -{"contract_version":"16","engine_sequence":"2","event_id":"quote-trade-event-000000000002","causation_ids":["quote-trade-event-000000000001"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} -{"contract_version":"16","engine_sequence":"3","event_id":"quote-trade-event-000000000003","causation_ids":["quote-trade-event-000000000002"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} -{"contract_version":"16","engine_sequence":"4","event_id":"quote-trade-event-000000000004","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} -{"contract_version":"16","engine_sequence":"5","event_id":"quote-trade-event-000000000005","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} -{"contract_version":"16","engine_sequence":"6","event_id":"quote-trade-event-000000000006","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"quote-trade-event-000000000006","updated_event_id":"quote-trade-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"16","engine_sequence":"7","event_id":"quote-trade-event-000000000007","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} -{"contract_version":"16","engine_sequence":"8","event_id":"quote-trade-event-000000000008","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]}} -{"contract_version":"16","engine_sequence":"9","event_id":"quote-trade-event-000000000009","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} -{"contract_version":"16","engine_sequence":"10","event_id":"quote-trade-event-000000000010","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000001","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"99","notional":"396","fee":"10","executed_at":"2026-02-03T14:33:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} -{"contract_version":"16","engine_sequence":"11","event_id":"quote-trade-event-000000000011","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000002","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:34:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} -{"contract_version":"16","engine_sequence":"12","event_id":"quote-trade-event-000000000012","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]}} -{"contract_version":"16","engine_sequence":"13","event_id":"quote-trade-event-000000000013","causation_ids":["quote-trade-event-000000000012"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"0e57b52243f3fc9fe37b94dc8fbde9a8b69c6c680bd5369e4d77c63060b853e8","execution_model":"quote_trade_v1","valuation":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v16/fixtures/quote-trade.scenario.json b/contracts/v16/fixtures/quote-trade.scenario.json deleted file mode 100644 index baf05f8..0000000 --- a/contracts/v16/fixtures/quote-trade.scenario.json +++ /dev/null @@ -1,319 +0,0 @@ -{ - "contract_version": "16", - "metadata": { - "producer": "trading-engine", - "purpose": "bounded quote and trade replay fixture" - }, - "run_id": "quote-trade", - "base_currency": "USD", - "initial_portfolio": { - "cash": [ - { - "currency": "USD", - "amount": "2000" - } - ], - "positions": [], - "marks": [], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ] - }, - "instruments": [ - { - "instrument_id": "clip-equity", - "symbol": "CLIP", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "venue_calendars": [ - { - "calendar_id": "clip-xnas-2026", - "calendar_version": "1", - "venue_id": "XNAS", - "instrument_ids": [ - "clip-equity" - ], - "sessions": [ - { - "session_date": "2026-02-02", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-02T14:30:00Z", - "closes_at": "2026-02-02T21:00:00Z" - } - ] - }, - { - "session_date": "2026-02-03", - "policy": "regular", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-03T14:30:00Z", - "closes_at": "2026-02-03T21:00:00Z" - } - ] - }, - { - "session_date": "2026-02-04", - "policy": "early_close", - "phases": [ - { - "phase": "regular", - "opens_at": "2026-02-04T14:30:00Z", - "closes_at": "2026-02-04T18:00:00Z" - } - ] - } - ] - } - ], - "risk": { - "max_gross_exposure": "1000000000", - "max_leverage": "1", - "short_borrow_bps": 100, - "instrument_policies": [ - { - "instrument_id": "clip-equity", - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_notional_exposure": "1000000000", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "shorting_allowed": true - } - ], - "groups": [] - }, - "execution": { - "model": "quote_trade_v1", - "configuration": { - "version": "1", - "participation_bps": 10000, - "fee_schedules": [ - { - "schedule_id": "clip-fees-v1", - "instrument_id": "clip-equity", - "settlement_currency": "USD", - "minimum": null, - "maximum": null, - "components": [ - { - "name": "broker", - "currency": "USD", - "kind": "fixed", - "value": "10", - "rounding": "up", - "applies_to": "any" - } - ] - } - ] - } - }, - "max_internal_events": 1000, - "schedule": [ - { - "after_slice_sequence": "1", - "intents": [ - { - "type": "submit_order", - "instrument_id": "clip-equity", - "side": "buy", - "quantity": "10", - "order_kind": "limit", - "trigger_price": null, - "limit_price": "100", - "time_in_force": "gtc", - "venue_id": null, - "calendar_id": null, - "expires_at": null - } - ] - } - ], - "slices": [ - { - "slice_sequence": "1", - "start_at": "2026-02-02T14:30:00Z", - "end_at": "2026-02-02T21:00:00Z", - "available_at": "2026-02-02T21:00:01Z", - "received_at": "2026-02-02T21:00:02Z", - "bars": [ - { - "instrument_id": "clip-equity", - "open": "50", - "high": "50", - "low": "50", - "close": "50", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "clip-equity", - "effective_at": "2026-02-02T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-02-02T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [], - "order_book_events": [] - }, - { - "slice_sequence": "2", - "start_at": "2026-02-03T14:30:00Z", - "end_at": "2026-02-03T21:00:00Z", - "available_at": "2026-02-03T21:00:01Z", - "received_at": "2026-02-03T21:00:02Z", - "bars": [ - { - "instrument_id": "clip-equity", - "open": "100", - "high": "100", - "low": "100", - "close": "100", - "volume": "100" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [], - "borrow_observations": [ - { - "instrument_id": "clip-equity", - "effective_at": "2026-02-03T14:30:00Z", - "available_quantity": "1000", - "annual_rate_bps": 100, - "recalled": false - } - ], - "cash_rate_observations": [ - { - "currency": "USD", - "effective_at": "2026-02-03T14:30:00Z", - "credit_rate_bps": 100, - "debit_rate_bps": 200 - } - ], - "settlement_failures": [], - "lifecycle_events": [], - "market_events": [ - { - "type": "quote", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:31:00Z", - "available_at": "2026-02-03T14:31:01Z", - "received_at": "2026-02-03T14:31:02Z", - "ingest_sequence": "1", - "bid_price": "99", - "bid_quantity": "20", - "ask_price": "101", - "ask_quantity": "20" - }, - { - "type": "trade", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:32:00Z", - "available_at": "2026-02-03T14:32:01Z", - "received_at": "2026-02-03T14:32:02Z", - "ingest_sequence": "2", - "price": "100", - "quantity": "5", - "aggressor_side": "unknown" - }, - { - "type": "trade", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:33:00Z", - "available_at": "2026-02-03T14:33:01Z", - "received_at": "2026-02-03T14:33:02Z", - "ingest_sequence": "3", - "price": "99", - "quantity": "4", - "aggressor_side": "sell" - }, - { - "type": "trade", - "instrument_id": "clip-equity", - "event_at": "2026-02-03T14:34:00Z", - "available_at": "2026-02-03T14:34:01Z", - "received_at": "2026-02-03T14:34:02Z", - "ingest_sequence": "4", - "price": "100", - "quantity": "10", - "aggressor_side": "sell" - } - ], - "order_book_events": [] - } - ], - "financing": { - "day_count": "actual_365", - "compounding": "simple", - "borrow_missing_data": "reject", - "cash_missing_data": "reject", - "locate_policy": "clip_fill", - "recall_policy": "close_out" - }, - "settlement": { - "cash_buying_power": "total_cash", - "position_availability": "total_positions", - "calendars": [ - { - "calendar_id": "default-settlement", - "version": "1", - "business_dates": [ - "2026-01-02", - "2026-01-05", - "2026-01-06", - "2026-01-07", - "2026-01-08", - "2026-01-09", - "2026-02-02", - "2026-02-03", - "2026-02-04", - "2026-02-05" - ] - } - ], - "rules": [ - { - "instrument_id": "clip-equity", - "calendar_id": "default-settlement", - "lag_business_days": 1 - } - ] - } -} diff --git a/contracts/v16/fixtures/quote-trade.scenario.jsonl b/contracts/v16/fixtures/quote-trade.scenario.jsonl deleted file mode 100644 index 842d0e7..0000000 --- a/contracts/v16/fixtures/quote-trade.scenario.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"contract_version":"16","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded quote and trade replay fixture"},"run_id":"quote-trade","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"quote_trade_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} -{"contract_version":"16","scenario_sequence":"2","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]},"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}]}} -{"contract_version":"16","scenario_sequence":"3","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]},"intents":[]}} -{"contract_version":"16","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v16/journal.schema.json b/contracts/v16/journal.schema.json deleted file mode 100644 index 2315fa1..0000000 --- a/contracts/v16/journal.schema.json +++ /dev/null @@ -1,2441 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v16/journal.schema.json", - "title": "Trading Engine v16 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": "16" - }, - "engine_sequence": { - "$ref": "#/$defs/sequence" - }, - "event_id": { - "$ref": "#/$defs/identifier" - }, - "causation_ids": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/$defs/identifier" - } - }, - "run_id": { - "$ref": "#/$defs/identifier" - }, - "recorded_at": { - "$ref": "#/$defs/timestamp" - }, - "event_type": { - "enum": [ - "run_started", - "initial_state", - "market_slice_received", - "target_portfolio_requested", - "order_accepted", - "order_rejected", - "order_triggered", - "order_cancelled", - "split_applied", - "cash_dividend_applied", - "distribution_applied", - "lifecycle_applied", - "order_adjusted", - "execution_price_selected", - "fill_applied", - "settlement_instruction_created", - "settlement_completed", - "settlement_failed", - "fill_clipped", - "borrow_fee_applied", - "borrow_charge_applied", - "borrow_recall_received", - "cash_interest_applied", - "margin_call", - "margin_restored", - "intent_rejected", - "metric_emitted", - "valuation", - "run_completed" - ] - }, - "payload": { - "type": "object" - } - }, - "allOf": [ - { - "if": { "properties": { "event_type": { "const": "execution_price_selected" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/executionPriceSelected" } } } - }, - { - "if": { - "properties": { - "event_type": { - "const": "run_started" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/runStarted" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "initial_state" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/initialState" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "market_slice_received" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/marketSlice" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "target_portfolio_requested" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/targetPortfolio" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "enum": [ - "order_accepted", - "order_rejected", - "order_triggered" - ] - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/order" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "order_cancelled" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/orderCancelled" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "split_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/splitApplied" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "cash_dividend_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/dividendApplied" - } - } - } - }, - { - "if": { "properties": { "event_type": { "const": "distribution_applied" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/distributionApplied" } } } - }, - { - "if": { "properties": { "event_type": { "const": "lifecycle_applied" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/lifecycleApplied" } } } - }, - { - "if": { - "properties": { - "event_type": { - "const": "order_adjusted" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/orderAdjusted" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "fill_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/fill" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "enum": [ - "settlement_instruction_created", - "settlement_completed", - "settlement_failed" - ] - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/settlementInstruction" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "fill_clipped" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/fillClipped" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "borrow_fee_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/borrowFee" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "borrow_charge_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/borrowCharge" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "borrow_recall_received" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/borrowRecall" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "cash_interest_applied" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/cashInterest" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "enum": [ - "margin_call", - "margin_restored", - "valuation" - ] - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/valuation" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "intent_rejected" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/intentRejected" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "metric_emitted" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/metric" - } - } - } - }, - { - "if": { - "properties": { - "event_type": { - "const": "run_completed" - } - } - }, - "then": { - "properties": { - "payload": { - "$ref": "#/$defs/runCompleted" - } - } - } - } - ], - "$defs": { - "identifier": { - "type": "string", - "minLength": 1, - "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" - }, - "signedDecimal": { - "type": "string", - "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" - }, - "unsignedDecimal": { - "type": "string", - "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "positiveDecimal": { - "type": "string", - "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "sequence": { - "type": "string", - "pattern": "^[1-9][0-9]*$" - }, - "nonnegativeSequence": { - "type": "string", - "pattern": "^(?:0|[1-9][0-9]*)$" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" - }, - "runStarted": { - "type": "object", - "additionalProperties": false, - "required": [ - "scenario_sha256", - "execution_model" - ], - "properties": { - "scenario_sha256": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, - "execution_model": { - "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] - } - } - }, - "initialState": { - "type": "object", - "additionalProperties": false, - "required": [ - "portfolio", - "valuation" - ], - "properties": { - "portfolio": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/scenario.schema.json#/$defs/fractionalPolicy" } - } - } - ] - }, - "marketSlice": { - "type": "object", - "additionalProperties": false, - "required": [ - "slice_sequence", - "start_at", - "end_at", - "available_at", - "received_at", - "bars", - "fx_rates", - "corporate_actions", - "borrow_observations", - "cash_rate_observations", - "settlement_failures", - "lifecycle_events", - "market_events", - "order_book_events" - ], - "properties": { - "slice_sequence": { - "$ref": "#/$defs/sequence" - }, - "start_at": { - "$ref": "#/$defs/timestamp" - }, - "end_at": { - "$ref": "#/$defs/timestamp" - }, - "available_at": { - "$ref": "#/$defs/timestamp" - }, - "received_at": { - "$ref": "#/$defs/timestamp" - }, - "bars": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/bar" - } - }, - "fx_rates": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/fxRate" - } - }, - "corporate_actions": { - "type": "array", - "items": { - "$ref": "#/$defs/corporateAction" - } - }, - "borrow_observations": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/borrowObservation" - } - }, - "cash_rate_observations": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/cashRateObservation" - } - }, - "settlement_failures": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/settlementFailure" - } - }, - "lifecycle_events": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/lifecycleEvent" - } - }, - "market_events": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/marketEvent" - } - }, - "order_book_events": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/orderBookEvent" - } - } - } - }, - "settlementInstruction": { - "type": "object", - "additionalProperties": false, - "required": ["instruction_id", "fill_id", "instrument_id", "currency", "cash_movement", "position_movement", "trade_date", "due_date", "status", "settled_at", "failed_at", "failure_reason"], - "properties": { - "instruction_id": { "$ref": "#/$defs/identifier" }, - "fill_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "currency": { "$ref": "#/$defs/identifier" }, - "cash_movement": { "$ref": "#/$defs/signedDecimal" }, - "position_movement": { "$ref": "#/$defs/signedDecimal" }, - "trade_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "due_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "status": { "enum": ["pending", "settled", "failed"] }, - "settled_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, - "failed_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, - "failure_reason": { "oneOf": [{ "type": "null" }, { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" }] } - } - }, - "targetPortfolio": { - "type": "object", - "additionalProperties": false, - "required": [ - "basis", - "targets" - ], - "properties": { - "basis": { - "enum": [ - "weights", - "quantities" - ] - }, - "targets": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "weight", - "quantity", - "reference_price" - ], - "properties": { - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "weight": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/signedDecimal" - } - ] - }, - "quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "reference_price": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/positiveDecimal" - } - ] - } - } - } - } - } - }, - "order": { - "type": "object", - "additionalProperties": false, - "required": [ - "order_id", - "instrument_id", - "side", - "quantity", - "order_kind", - "trigger_price", - "limit_price", - "time_in_force", - "venue_id", - "calendar_id", - "expires_at", - "origin", - "created_event_id", - "updated_event_id", - "created_sequence", - "created_at", - "eligible_after_slice_sequence", - "triggered_at", - "triggered_slice_sequence", - "filled_quantity", - "filled_notional", - "status", - "rejection_reason" - ], - "properties": { - "order_id": { - "$ref": "#/$defs/identifier" - }, - "instrument_id": { - "$ref": "#/$defs/identifier" - }, - "side": { - "enum": [ - "buy", - "sell" - ] - }, - "quantity": { - "$ref": "#/$defs/positiveDecimal" - }, - "order_kind": { - "enum": [ - "market", - "limit", - "stop", - "stop_limit" - ] - }, - "trigger_price": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/positiveDecimal" - } - ] - }, - "limit_price": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/positiveDecimal" - } - ] - }, - "time_in_force": { - "enum": [ - "gtc", - "ioc", - "fok", - "day", - "gtd" - ] - }, - "venue_id": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/identifier" - } - ] - }, - "calendar_id": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/identifier" - } - ] - }, - "expires_at": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/timestamp" - } - ] - }, - "origin": { - "enum": [ - "direct", - "target_rebalance", - "margin_liquidation", - "borrow_recall", - "instrument_halt", - "instrument_terminal" - ] - }, - "created_event_id": { - "$ref": "#/$defs/identifier" - }, - "updated_event_id": { - "$ref": "#/$defs/identifier" - }, - "created_sequence": { - "$ref": "#/$defs/sequence" - }, - "created_at": { - "$ref": "#/$defs/timestamp" - }, - "eligible_after_slice_sequence": { - "$ref": "#/$defs/nonnegativeSequence" - }, - "triggered_at": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/timestamp" - } - ] - }, - "triggered_slice_sequence": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/sequence" - } - ] - }, - "filled_quantity": { - "$ref": "#/$defs/unsignedDecimal" - }, - "filled_notional": { - "$ref": "#/$defs/unsignedDecimal" - }, - "status": { - "enum": [ - "working", - "partially_filled", - "filled", - "cancelled", - "rejected" - ] - }, - "rejection_reason": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "string", - "minLength": 1 - } - ] - } - } - }, - "orderCancelled": { - "type": "object", - "additionalProperties": false, - "required": [ - "order", - "reason" - ], - "properties": { - "order": { - "$ref": "#/$defs/order" - }, - "reason": { - "enum": [ - "strategy_requested", - "target_replaced", - "market_ioc", - "immediate_or_cancel", - "fill_or_kill", - "day_expired", - "gtd_expired", - "margin_call", - "borrow_recall" - ] - } - } - }, - "splitApplied": { - "type": "object", - "additionalProperties": false, - "required": [ - "action", - "previous_quantity", - "adjusted_quantity" - ], - "properties": { - "action": { - "$ref": "#/$defs/corporateAction" - }, - "previous_quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "adjusted_quantity": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "dividendApplied": { - "type": "object", - "additionalProperties": false, - "required": [ - "action", - "quantity", - "cash_amount" - ], - "properties": { - "action": { - "$ref": "#/$defs/corporateAction" - }, - "quantity": { - "$ref": "#/$defs/signedDecimal" - }, - "cash_amount": { - "$ref": "#/$defs/signedDecimal" - } - } - }, - "distributionApplied": { - "type": "object", - "additionalProperties": false, - "required": ["action", "source_quantity", "destination_quantity", "fractional_quantity", "allocated_basis", "fractional_basis", "cash_in_lieu"], - "properties": { - "action": { "$ref": "#/$defs/corporateAction" }, - "source_quantity": { "$ref": "#/$defs/signedDecimal" }, - "destination_quantity": { "$ref": "#/$defs/signedDecimal" }, - "fractional_quantity": { "$ref": "#/$defs/signedDecimal" }, - "allocated_basis": { "$ref": "#/$defs/signedDecimal" }, - "fractional_basis": { "$ref": "#/$defs/signedDecimal" }, - "cash_in_lieu": { "$ref": "#/$defs/signedDecimal" } - } - }, - "lifecycleApplied": { - "type": "object", - "additionalProperties": false, - "required": ["lifecycle_event", "listing", "liquidated_quantity", "cash_amount"], - "properties": { - "lifecycle_event": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/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/v16/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/v16/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", "minLength": 1, "maxLength": 128, "pattern": "^\\S(?:.*\\S)?$" }, - "value": { "$ref": "#/$defs/metricValue" }, - "unit": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, - "dimensions": { - "type": "object", - "maxProperties": 16, - "propertyNames": { "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, - "additionalProperties": { "type": "string", "maxLength": 128 } - }, - "aggregation": { "enum": ["last", "sum", "minimum", "maximum", "mean"] } - }, - "allOf": [ - { "if": { "properties": { "value": { "properties": { "type": { "enum": ["string", "boolean"] } } } } }, "then": { "properties": { "aggregation": { "enum": ["last"] } } } } - ] - }, - "metricValue": { - "oneOf": [ - { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "numeric" }, "value": { "type": "string", "pattern": "^(0|[1-9][0-9]*|-[1-9][0-9]*)(\\.[0-9]*[1-9])?$|^-0\\.[0-9]*[1-9]$" } } }, - { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "string" }, "value": { "type": "string", "maxLength": 1024 } } }, - { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "boolean" }, "value": { "type": "boolean" } } } - ] - }, - "runCompleted": { - "type": "object", - "additionalProperties": false, - "required": [ - "scenario_sha256", - "execution_model", - "valuation", - "order_counts" - ], - "properties": { - "scenario_sha256": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, - "execution_model": { - "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] - }, - "valuation": { - "$ref": "#/$defs/valuation" - }, - "order_counts": { - "type": "object", - "additionalProperties": false, - "required": [ - "total", - "active", - "filled", - "rejected", - "cancelled" - ], - "properties": { - "total": { - "type": "integer", - "minimum": 0 - }, - "active": { - "type": "integer", - "minimum": 0 - }, - "filled": { - "type": "integer", - "minimum": 0 - }, - "rejected": { - "type": "integer", - "minimum": 0 - }, - "cancelled": { - "type": "integer", - "minimum": 0 - } - } - } - } - } - } -} diff --git a/contracts/v16/scenario-stream.schema.json b/contracts/v16/scenario-stream.schema.json deleted file mode 100644 index e3b1694..0000000 --- a/contracts/v16/scenario-stream.schema.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v16/scenario-stream.schema.json", - "title": "Trading Engine v16 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": "16" }, - "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": "16" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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": "16" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/identifier" }, - "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/initialPortfolio" }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/instrument" } }, - "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/execution" }, - "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/financing" }, - "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/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/v16/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v16/scenario.schema.json b/contracts/v16/scenario.schema.json deleted file mode 100644 index 1af81e1..0000000 --- a/contracts/v16/scenario.schema.json +++ /dev/null @@ -1,888 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v16/scenario.schema.json", - "title": "Trading Engine v16 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": "16" }, - "metadata": { "type": "object" }, - "run_id": { "$ref": "#/$defs/identifier" }, - "base_currency": { "$ref": "#/$defs/identifier" }, - "initial_portfolio": { "$ref": "#/$defs/initialPortfolio" }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "#/$defs/instrument" } - }, - "venue_calendars": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/venueCalendar" } - }, - "risk": { "$ref": "#/$defs/risk" }, - "execution": { "$ref": "#/$defs/execution" }, - "financing": { "$ref": "#/$defs/financing" }, - "settlement": { "$ref": "#/$defs/settlement" }, - "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 }, - "schedule": { "type": "array", "items": { "$ref": "#/$defs/scheduleItem" } }, - "slices": { - "description": "Slices are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-item rule is enforced semantically.", - "type": "array", - "items": { "$ref": "#/$defs/marketSlice" } - } - }, - "$defs": { - "settlement": { - "type": "object", - "additionalProperties": false, - "required": ["cash_buying_power", "position_availability", "calendars", "rules"], - "properties": { - "cash_buying_power": { "enum": ["total_cash", "settled_cash"] }, - "position_availability": { "enum": ["total_positions", "settled_positions"] }, - "calendars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementCalendar" } }, - "rules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementRule" } } - } - }, - "settlementCalendar": { - "type": "object", - "additionalProperties": false, - "required": ["calendar_id", "version", "business_dates"], - "properties": { - "calendar_id": { "$ref": "#/$defs/identifier" }, - "version": { "const": "1" }, - "business_dates": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" } } - } - }, - "settlementRule": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "calendar_id", "lag_business_days"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "calendar_id": { "$ref": "#/$defs/identifier" }, - "lag_business_days": { "type": "integer", "minimum": 0, "maximum": 30 } - } - }, - "settlementFailure": { - "type": "object", - "additionalProperties": false, - "required": ["instruction_id", "reason"], - "properties": { - "instruction_id": { "$ref": "#/$defs/identifier" }, - "reason": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } - } - }, - "financing": { - "type": "object", - "additionalProperties": false, - "required": ["day_count", "compounding", "borrow_missing_data", "cash_missing_data", "locate_policy", "recall_policy"], - "properties": { - "day_count": { "enum": ["actual_365", "actual_360"] }, - "compounding": { "enum": ["simple", "daily"] }, - "borrow_missing_data": { "enum": ["reject", "zero"] }, - "cash_missing_data": { "enum": ["reject", "zero"] }, - "locate_policy": { "enum": ["reject_order", "clip_fill"] }, - "recall_policy": { "enum": ["reject_new_shorts", "close_out"] } - } - }, - "borrowObservation": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "effective_at", "available_quantity", "annual_rate_bps", "recalled"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "effective_at": { "$ref": "#/$defs/timestamp" }, - "available_quantity": { "$ref": "#/$defs/unsignedDecimal" }, - "annual_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, - "recalled": { "type": "boolean" } - } - }, - "cashRateObservation": { - "type": "object", - "additionalProperties": false, - "required": ["currency", "effective_at", "credit_rate_bps", "debit_rate_bps"], - "properties": { - "currency": { "$ref": "#/$defs/identifier" }, - "effective_at": { "$ref": "#/$defs/timestamp" }, - "credit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, - "debit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 } - } - }, - "identifier": { - "type": "string", - "minLength": 1, - "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" - }, - "signedDecimal": { - "type": "string", - "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" - }, - "unsignedDecimal": { - "type": "string", - "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "positiveDecimal": { - "type": "string", - "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" - }, - "sequence": { "type": "string", "pattern": "^[1-9][0-9]*$" }, - "timestamp": { - "type": "string", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" - }, - "cashBalance": { - "type": "object", - "additionalProperties": false, - "required": ["currency", "amount"], - "properties": { - "currency": { "$ref": "#/$defs/identifier" }, - "amount": { "$ref": "#/$defs/signedDecimal" } - } - }, - "initialPortfolio": { - "type": "object", - "additionalProperties": false, - "required": ["cash", "positions", "marks", "fx_rates"], - "properties": { - "cash": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashBalance" } }, - "positions": { "type": "array", "items": { "$ref": "#/$defs/initialPosition" } }, - "marks": { "type": "array", "items": { "$ref": "#/$defs/initialMark" } }, - "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } } - } - }, - "initialPosition": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity", "cost_basis", "realized_pnl", "dividend_pnl", "execution_fees", "borrow_fees"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "quantity": { "$ref": "#/$defs/signedDecimal" }, - "cost_basis": { "$ref": "#/$defs/signedDecimal" }, - "realized_pnl": { "$ref": "#/$defs/signedDecimal" }, - "dividend_pnl": { "$ref": "#/$defs/signedDecimal" }, - "execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, - "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "initialMark": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "price"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "price": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "instrument": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "symbol", "quote_currency", "tick_size", "lot_size"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "symbol": { "$ref": "#/$defs/identifier" }, - "quote_currency": { "$ref": "#/$defs/identifier" }, - "tick_size": { "$ref": "#/$defs/positiveDecimal" }, - "lot_size": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "venueCalendar": { - "type": "object", - "additionalProperties": false, - "required": ["calendar_id", "calendar_version", "venue_id", "instrument_ids", "sessions"], - "properties": { - "calendar_id": { "$ref": "#/$defs/identifier" }, - "calendar_version": { "const": "1" }, - "venue_id": { "$ref": "#/$defs/identifier" }, - "instrument_ids": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { "$ref": "#/$defs/identifier" } - }, - "sessions": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/venueSession" } - } - } - }, - "venueSession": { - "oneOf": [ - { "$ref": "#/$defs/openVenueSession" }, - { "$ref": "#/$defs/holidayVenueSession" } - ] - }, - "openVenueSession": { - "type": "object", - "additionalProperties": false, - "required": ["session_date", "policy", "phases"], - "properties": { - "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "policy": { "enum": ["regular", "early_close"] }, - "phases": { - "type": "array", - "minItems": 1, - "maxItems": 5, - "items": { "$ref": "#/$defs/venuePhase" } - } - } - }, - "holidayVenueSession": { - "type": "object", - "additionalProperties": false, - "required": ["session_date", "policy", "phases"], - "properties": { - "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, - "policy": { "const": "holiday" }, - "phases": { "type": "array", "maxItems": 0 } - } - }, - "venuePhase": { - "type": "object", - "additionalProperties": false, - "required": ["phase", "opens_at", "closes_at"], - "properties": { - "phase": { "enum": ["premarket", "opening_auction", "regular", "closing_auction", "postmarket"] }, - "opens_at": { "$ref": "#/$defs/timestamp" }, - "closes_at": { "$ref": "#/$defs/timestamp" } - } - }, - "risk": { - "type": "object", - "additionalProperties": false, - "required": ["max_gross_exposure", "max_leverage", "short_borrow_bps", "instrument_policies", "groups"], - "properties": { - "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, - "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, - "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "instrument_policies": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/instrumentRiskPolicy" } - }, - "groups": { - "type": "array", - "items": { "$ref": "#/$defs/riskGroup" } - } - } - }, - "instrumentRiskPolicy": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "max_order_quantity", "max_long_position", "max_short_position", "max_notional_exposure", "initial_margin_bps", "maintenance_margin_bps", "shorting_allowed"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, - "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_notional_exposure": { "$ref": "#/$defs/positiveDecimal" }, - "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "shorting_allowed": { "type": "boolean" } - } - }, - "nullablePositiveDecimal": { - "oneOf": [ - { "type": "null" }, - { "$ref": "#/$defs/positiveDecimal" } - ] - }, - "riskGroup": { - "type": "object", - "additionalProperties": false, - "required": ["group_id", "group_version", "group_type", "instrument_ids", "limits"], - "properties": { - "group_id": { "$ref": "#/$defs/identifier" }, - "group_version": { "const": "1" }, - "group_type": { "enum": ["issuer", "sector", "currency", "country", "asset_class", "custom"] }, - "instrument_ids": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { "$ref": "#/$defs/identifier" } - }, - "limits": { "$ref": "#/$defs/riskGroupLimits" } - } - }, - "riskGroupLimits": { - "type": "object", - "additionalProperties": false, - "required": ["max_gross_exposure", "max_long_exposure", "max_short_exposure", "max_absolute_net_exposure", "max_concentration"], - "properties": { - "max_gross_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_long_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_short_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_absolute_net_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, - "max_concentration": { - "oneOf": [ - { "type": "null" }, - { "allOf": [{ "$ref": "#/$defs/positiveDecimal" }, { "pattern": "^(?:0[.][0-9]{0,5}[1-9]|1)$" }] } - ] - } - } - }, - "execution": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "enum": ["completed_bar_next_open_v1", "completed_bar_adverse_touch_v1"] }, - "configuration": { "$ref": "#/$defs/conservativeBarConfiguration" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "quote_trade_v1" }, - "configuration": { "$ref": "#/$defs/quoteTradeConfiguration" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "order_book_v1" }, - "configuration": { "$ref": "#/$defs/orderBookConfiguration" } - } - } - ] - }, - "completedBarV1Configuration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fee_schedules"], - "properties": { - "version": { "const": "2" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } - } - }, - "conservativeBarConfiguration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fee_schedules", "spread_model", "impact_model"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, - "spread_model": { "$ref": "#/$defs/fixedSpreadModel" }, - "impact_model": { "$ref": "#/$defs/linearImpactModel" } - } - }, - "quoteTradeConfiguration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fee_schedules"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } - } - }, - "orderBookConfiguration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fee_schedules", "max_depth_levels"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, - "max_depth_levels": { "type": "integer", "minimum": 1, "maximum": 1024 } - } - }, - "fixedSpreadModel": { - "type": "object", - "additionalProperties": false, - "required": ["model", "half_spread_bps"], - "properties": { - "model": { "const": "fixed_half_spread_v1" }, - "half_spread_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "linearImpactModel": { - "type": "object", - "additionalProperties": false, - "required": ["model", "coefficient_bps", "missing_volume_policy"], - "properties": { - "model": { "const": "linear_participation_v1" }, - "coefficient_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "missing_volume_policy": { "enum": ["reject", "zero_impact"] } - } - }, - "feeSchedule": { - "type": "object", - "additionalProperties": false, - "required": ["schedule_id", "instrument_id", "settlement_currency", "minimum", "maximum", "components"], - "properties": { - "schedule_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "settlement_currency": { "$ref": "#/$defs/identifier" }, - "minimum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, - "maximum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, - "components": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeComponent" } } - } - }, - "feeComponent": { - "type": "object", - "additionalProperties": false, - "required": ["name", "currency", "kind", "value", "rounding", "applies_to"], - "properties": { - "name": { "$ref": "#/$defs/identifier" }, - "currency": { "$ref": "#/$defs/identifier" }, - "kind": { "enum": ["fixed", "notional_bps", "per_unit"] }, - "value": { "oneOf": [{ "$ref": "#/$defs/signedDecimal" }, { "type": "integer", "minimum": -10000, "maximum": 10000 }] }, - "rounding": { "enum": ["up", "down", "nearest"] }, - "applies_to": { "enum": ["any", "maker", "taker"] } - }, - "allOf": [ - { "if": { "properties": { "kind": { "const": "notional_bps" } } }, "then": { "properties": { "value": { "type": "integer" } } } }, - { "if": { "properties": { "kind": { "enum": ["fixed", "per_unit"] } } }, "then": { "properties": { "value": { "$ref": "#/$defs/signedDecimal" } } } } - ] - }, - "scheduleItem": { - "type": "object", - "additionalProperties": false, - "required": ["after_slice_sequence", "intents"], - "properties": { - "after_slice_sequence": { "$ref": "#/$defs/sequence" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "#/$defs/intent" } } - } - }, - "intent": { - "oneOf": [ - { "$ref": "#/$defs/targetWeights" }, - { "$ref": "#/$defs/targetQuantities" }, - { "$ref": "#/$defs/submitOrder" }, - { "$ref": "#/$defs/cancelOrder" }, - { "$ref": "#/$defs/metric" } - ] - }, - "targetWeights": { - "type": "object", - "additionalProperties": false, - "required": ["type", "targets"], - "properties": { - "type": { "const": "target_weights" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "weight"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "weight": { "$ref": "#/$defs/signedDecimal" } - } - } - } - } - }, - "targetQuantities": { - "type": "object", - "additionalProperties": false, - "required": ["type", "targets"], - "properties": { - "type": { "const": "target_quantities" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "quantity": { "$ref": "#/$defs/signedDecimal" } - } - } - } - } - }, - "submitOrder": { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "side", "quantity", "order_kind", "trigger_price", "limit_price", "time_in_force", "venue_id", "calendar_id", "expires_at"], - "properties": { - "type": { "const": "submit_order" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "order_kind": { "enum": ["market", "limit", "stop", "stop_limit"] }, - "trigger_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, - "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, - "time_in_force": { "enum": ["gtc", "ioc", "fok", "day", "gtd"] }, - "venue_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, - "calendar_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, - "expires_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] } - }, - "allOf": [ - { "if": { "properties": { "order_kind": { "const": "market" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "type": "null" } } } }, - { "if": { "properties": { "order_kind": { "const": "limit" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, - { "if": { "properties": { "order_kind": { "const": "stop" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "type": "null" } } } }, - { "if": { "properties": { "order_kind": { "const": "stop_limit" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, - { "if": { "properties": { "time_in_force": { "const": "day" } } }, "then": { "properties": { "venue_id": { "$ref": "#/$defs/identifier" }, "calendar_id": { "$ref": "#/$defs/identifier" }, "expires_at": { "type": "null" } } } }, - { "if": { "properties": { "time_in_force": { "const": "gtd" } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "$ref": "#/$defs/timestamp" } } } }, - { "if": { "properties": { "time_in_force": { "enum": ["gtc", "ioc", "fok"] } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "type": "null" } } } } - ] - }, - "cancelOrder": { - "type": "object", - "additionalProperties": false, - "required": ["type", "order_id"], - "properties": { - "type": { "const": "cancel_order" }, - "order_id": { "$ref": "#/$defs/identifier" } - } - }, - "metric": { - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "value"], - "properties": { - "type": { "const": "emit_metric" }, - "name": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^\\S(?:.*\\S)?$" }, - "value": { "$ref": "#/$defs/metricValue" }, - "unit": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, - "dimensions": { - "type": "object", - "maxProperties": 16, - "propertyNames": { "minLength": 1, "maxLength": 64, "pattern": "^\\S(?:.*\\S)?$" }, - "additionalProperties": { "type": "string", "maxLength": 128 } - }, - "aggregation": { "enum": ["last", "sum", "minimum", "maximum", "mean"] } - }, - "allOf": [ - { "if": { "properties": { "value": { "properties": { "type": { "enum": ["string", "boolean"] } } } } }, "then": { "properties": { "aggregation": { "enum": ["last"] } } } } - ] - }, - "metricValue": { - "oneOf": [ - { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "numeric" }, "value": { "type": "string", "pattern": "^(0|[1-9][0-9]*|-[1-9][0-9]*)(\\.[0-9]*[1-9])?$|^-0\\.[0-9]*[1-9]$" } } }, - { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "string" }, "value": { "type": "string", "maxLength": 1024 } } }, - { "type": "object", "additionalProperties": false, "required": ["type", "value"], "properties": { "type": { "const": "boolean" }, "value": { "type": "boolean" } } } - ] - }, - "marketSlice": { - "type": "object", - "additionalProperties": false, - "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "market_events", "order_book_events", "fx_rates", "corporate_actions", "borrow_observations", "cash_rate_observations", "settlement_failures", "lifecycle_events"], - "properties": { - "slice_sequence": { "$ref": "#/$defs/sequence" }, - "start_at": { "$ref": "#/$defs/timestamp" }, - "end_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "bars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/bar" } }, - "market_events": { "type": "array", "items": { "$ref": "#/$defs/marketEvent" } }, - "order_book_events": { "type": "array", "items": { "$ref": "#/$defs/orderBookEvent" } }, - "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } }, - "corporate_actions": { "type": "array", "items": { "$ref": "#/$defs/corporateAction" } }, - "borrow_observations": { "type": "array", "items": { "$ref": "#/$defs/borrowObservation" } }, - "cash_rate_observations": { "type": "array", "items": { "$ref": "#/$defs/cashRateObservation" } }, - "settlement_failures": { "type": "array", "items": { "$ref": "#/$defs/settlementFailure" } }, - "lifecycle_events": { "type": "array", "items": { "$ref": "#/$defs/lifecycleEvent" } } - } - }, - "bar": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "open", "high", "low", "close", "volume"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "open": { "$ref": "#/$defs/positiveDecimal" }, - "high": { "$ref": "#/$defs/positiveDecimal" }, - "low": { "$ref": "#/$defs/positiveDecimal" }, - "close": { "$ref": "#/$defs/positiveDecimal" }, - "volume": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] } - } - }, - "marketEvent": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "bid_price", "bid_quantity", "ask_price", "ask_quantity"], - "properties": { - "type": { "const": "quote" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "bid_price": { "$ref": "#/$defs/positiveDecimal" }, - "bid_quantity": { "$ref": "#/$defs/positiveDecimal" }, - "ask_price": { "$ref": "#/$defs/positiveDecimal" }, - "ask_quantity": { "$ref": "#/$defs/positiveDecimal" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "price", "quantity", "aggressor_side"], - "properties": { - "type": { "const": "trade" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "aggressor_side": { "enum": ["buy", "sell", "unknown"] } - } - } - ] - }, - "orderBookLevel": { - "type": "object", - "additionalProperties": false, - "required": ["price", "quantity"], - "properties": { - "price": { "$ref": "#/$defs/positiveDecimal" }, - "quantity": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "orderBookEvent": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "bids", "asks"], - "properties": { - "type": { "const": "snapshot" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "book_sequence": { "$ref": "#/$defs/sequence" }, - "bids": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } }, - "asks": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price", "quantity"], - "properties": { - "type": { "const": "set" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "book_sequence": { "$ref": "#/$defs/sequence" }, - "side": { "enum": ["bid", "ask"] }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "quantity": { "$ref": "#/$defs/positiveDecimal" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price"], - "properties": { - "type": { "const": "delete" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "book_sequence": { "$ref": "#/$defs/sequence" }, - "side": { "enum": ["bid", "ask"] }, - "price": { "$ref": "#/$defs/positiveDecimal" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "price", "quantity", "aggressor_side"], - "properties": { - "type": { "const": "trade" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "event_at": { "$ref": "#/$defs/timestamp" }, - "available_at": { "$ref": "#/$defs/timestamp" }, - "received_at": { "$ref": "#/$defs/timestamp" }, - "ingest_sequence": { "$ref": "#/$defs/sequence" }, - "book_sequence": { "$ref": "#/$defs/sequence" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "aggressor_side": { "enum": ["buy", "sell", "unknown"] } - } - } - ] - }, - "fxRate": { - "type": "object", - "additionalProperties": false, - "required": ["currency", "rate"], - "properties": { - "currency": { "$ref": "#/$defs/identifier" }, - "rate": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "corporateAction": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "action_id", "instrument_id", "numerator", "denominator"], - "properties": { - "type": { "const": "split" }, - "action_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "numerator": { "$ref": "#/$defs/sequence" }, - "denominator": { "$ref": "#/$defs/sequence" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "action_id", "instrument_id", "amount_per_unit"], - "properties": { - "type": { "const": "cash_dividend" }, - "action_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "amount_per_unit": { "$ref": "#/$defs/positiveDecimal" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], - "properties": { - "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, - "action_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "destination_instrument_id": { "$ref": "#/$defs/identifier" }, - "numerator": { "$ref": "#/$defs/sequence" }, - "denominator": { "$ref": "#/$defs/sequence" }, - "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fractional_policy": { "$ref": "#/$defs/fractionalPolicy" } - } - } - ] - }, - "fractionalPolicy": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["policy"], - "properties": { "policy": { "const": "reject" } } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["policy", "price", "currency"], - "properties": { - "policy": { "const": "cash_in_lieu" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "currency": { "$ref": "#/$defs/identifier" } - } - } - ] - }, - "terminalPolicy": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["policy"], - "properties": { "policy": { "const": "hold" } } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["policy", "price", "currency"], - "properties": { - "policy": { "const": "cash_out" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "currency": { "$ref": "#/$defs/identifier" } - } - } - ] - }, - "lifecycleEvent": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "reason"], - "properties": { - "type": { "const": "halt" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "reason": { "type": "string", "minLength": 1 } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id"], - "properties": { - "type": { "const": "resume" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "symbol", "provider", "provider_instrument_id"], - "properties": { - "type": { "const": "identifier_change" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "symbol": { "$ref": "#/$defs/identifier" }, - "provider": { "$ref": "#/$defs/identifier" }, - "provider_instrument_id": { "$ref": "#/$defs/identifier" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "terminal_policy"], - "properties": { - "type": { "const": "expiration" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "terminal_policy": { "$ref": "#/$defs/terminalPolicy" } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["type", "event_id", "instrument_id", "terminal_policy", "reason"], - "properties": { - "type": { "const": "delisting" }, - "event_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "terminal_policy": { "$ref": "#/$defs/terminalPolicy" }, - "reason": { "type": "string", "minLength": 1 } - } - } - ] - } - } -} diff --git a/contracts/v2/README.md b/contracts/v2/README.md deleted file mode 100644 index 06dd6f3..0000000 --- a/contracts/v2/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Trading Engine contract v2 - -This frozen directory preserves the historical v2 process and file contract. The current runtime -advertises v4 and v3 only; these artifacts remain available for provenance and schema-only -compatibility testing by older consumers. - -- `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. -- `fixtures/demo.scenario.json`, `fixtures/demo.scenario.jsonl`, and - `fixtures/demo.journal.jsonl` form the canonical valid conformance corpus. - -Every batch scenario, scenario-stream record, and journal record carries -`"contract_version": "2"`. Consumers must reject missing or unsupported versions before -interpreting the rest of a document. - -Version 2 adds an explicit scenario execution-model selection. Every audit record also carries a -deterministic `event_id` and an ordered list of prior `causation_ids`. Valuations contain -per-instrument position attribution that reconciles exactly to their aggregate market value, cost -basis, realized and unrealized P&L, and fees. diff --git a/contracts/v2/dune b/contracts/v2/dune deleted file mode 100644 index 3ec9298..0000000 --- a/contracts/v2/dune +++ /dev/null @@ -1,10 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v2/journal.schema.json) - (scenario-stream.schema.json as contracts/v2/scenario-stream.schema.json) - (scenario.schema.json as contracts/v2/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v2/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v2/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl as contracts/v2/fixtures/demo.scenario.jsonl))) diff --git a/contracts/v2/fixtures/demo.journal.jsonl b/contracts/v2/fixtures/demo.journal.jsonl deleted file mode 100644 index a6232a7..0000000 --- a/contracts/v2/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,20 +0,0 @@ -{"contract_version":"2","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":"21834e964dd6daab292e6285924384970b3341f7166ead1d38f8edb284541e44","execution_model":"completed_bar_v1"}} -{"contract_version":"2","engine_sequence":"2","event_id":"demo-event-000000000002","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"}]}} -{"contract_version":"2","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9","reference_price":"104"}]}} -{"contract_version":"2","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} -{"contract_version":"2","engine_sequence":"5","event_id":"demo-event-000000000005","causation_ids":["demo-event-000000000002","demo-event-000000000003"],"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":"9","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"2","engine_sequence":"6","event_id":"demo-event-000000000006","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"cash":"10000","market_value":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"10000","total_fees":"0","positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","market_value":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","total_fees":"0"}]}} -{"contract_version":"2","engine_sequence":"7","event_id":"demo-event-000000000007","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":"12"}]}} -{"contract_version":"2","engine_sequence":"8","event_id":"demo-event-000000000008","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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","side":"buy","quantity":"6","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} -{"contract_version":"2","engine_sequence":"9","event_id":"demo-event-000000000009","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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":"9","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"2","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000003","demo-event-000000000007"],"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":"3","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000010","created_sequence":"10","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"2","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"cash":"9381.132","market_value":"642","cost_basis":"618.868","realized_pnl":"0","unrealized_pnl":"23.132","equity":"10023.132","total_fees":"0.868","positions":[{"instrument_id":"demo-equity-acme","quantity":"6","mark":"107","market_value":"642","cost_basis":"618.868","realized_pnl":"0","unrealized_pnl":"23.132","total_fees":"0.868"}]}} -{"contract_version":"2","engine_sequence":"12","event_id":"demo-event-000000000012","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"}]}} -{"contract_version":"2","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000010","demo-event-000000000012"],"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","side":"buy","quantity":"3","price":"107","notional":"321","fee":"0.571","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} -{"contract_version":"2","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000012"],"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","reference_price":null}]}} -{"contract_version":"2","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000015","created_sequence":"15","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"2","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000012"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"cash":"9059.561","market_value":"945","cost_basis":"940.439","realized_pnl":"0","unrealized_pnl":"4.561","equity":"10004.561","total_fees":"1.439","positions":[{"instrument_id":"demo-equity-acme","quantity":"9","mark":"105","market_value":"945","cost_basis":"940.439","realized_pnl":"0","unrealized_pnl":"4.561","total_fees":"1.439"}]}} -{"contract_version":"2","engine_sequence":"17","event_id":"demo-event-000000000017","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"}]}} -{"contract_version":"2","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"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","side":"sell","quantity":"7","price":"105","notional":"735","fee":"0.985","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} -{"contract_version":"2","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"cash":"9793.576","market_value":"212","cost_basis":"208.986445","realized_pnl":"2.562445","unrealized_pnl":"3.013555","equity":"10005.576","total_fees":"2.424","positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"106","market_value":"212","cost_basis":"208.986445","realized_pnl":"2.562445","unrealized_pnl":"3.013555","total_fees":"2.424"}]}} -{"contract_version":"2","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"21834e964dd6daab292e6285924384970b3341f7166ead1d38f8edb284541e44","execution_model":"completed_bar_v1","valuation":{"cash":"9793.576","market_value":"212","cost_basis":"208.986445","realized_pnl":"2.562445","unrealized_pnl":"3.013555","equity":"10005.576","total_fees":"2.424","positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"106","market_value":"212","cost_basis":"208.986445","realized_pnl":"2.562445","unrealized_pnl":"3.013555","total_fees":"2.424"}]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v2/fixtures/demo.scenario.json b/contracts/v2/fixtures/demo.scenario.json deleted file mode 100644 index 898011a..0000000 --- a/contracts/v2/fixtures/demo.scenario.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "contract_version": "2", - "metadata": { - "producer": "trading-engine-demo", - "purpose": "deterministic conformance fixture" - }, - "run_id": "demo", - "base_currency": "USD", - "initial_cash": "10000", - "instruments": [ - { - "instrument_id": "demo-equity-acme", - "symbol": "ACME", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "risk": { - "max_order_quantity": "1000", - "max_position": "1000" - }, - "execution": { - "model": "completed_bar_v1", - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 - }, - "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" - } - ] - } - ] - } - ], - "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" - } - ] - }, - { - "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": "12" - } - ] - }, - { - "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" - } - ] - }, - { - "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" - } - ] - } - ] -} diff --git a/contracts/v2/fixtures/demo.scenario.jsonl b/contracts/v2/fixtures/demo.scenario.jsonl deleted file mode 100644 index 399b062..0000000 --- a/contracts/v2/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"2","payload":{"base_currency":"USD","execution":{"fee_bps":10,"fixed_fee":"0.25","model":"completed_bar_v1","participation_bps":5000},"initial_cash":"10000","instruments":[{"instrument_id":"demo-equity-acme","lot_size":"1","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"max_internal_events":1000,"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"risk":{"max_order_quantity":"1000","max_position":"1000"},"run_id":"demo"},"record_type":"scenario_header","scenario_sequence":"1"} -{"contract_version":"2","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"}],"end_at":"2026-01-02T21:00:00Z","received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"2","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":"12"}],"end_at":"2026-01-05T21:00:00Z","received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"2","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","quantity":"2"}],"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"}],"end_at":"2026-01-06T21:00:00Z","received_at":"2026-01-06T21:00:02Z","slice_sequence":"3","start_at":"2026-01-06T14:30:00Z"}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"2","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"}],"end_at":"2026-01-07T21:00:00Z","received_at":"2026-01-07T21:00:02Z","slice_sequence":"4","start_at":"2026-01-07T14:30:00Z"}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"2","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v2/journal.schema.json b/contracts/v2/journal.schema.json deleted file mode 100644 index b2f9327..0000000 --- a/contracts/v2/journal.schema.json +++ /dev/null @@ -1,431 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v2/journal.schema.json", - "title": "Trading Engine v2 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": "2" }, - "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", - "market_slice_received", - "target_portfolio_requested", - "order_accepted", - "order_rejected", - "order_cancelled", - "fill_applied", - "cash_limited", - "intent_rejected", - "metric_emitted", - "valuation", - "run_completed" - ] - }, - "payload": { "type": "object" } - }, - "allOf": [ - { - "if": { "properties": { "event_type": { "const": "run_started" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/runStarted" } } } - }, - { - "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": { "const": "order_accepted" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/order" } } } - }, - { - "if": { "properties": { "event_type": { "const": "order_rejected" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/order" } } } - }, - { - "if": { "properties": { "event_type": { "const": "order_cancelled" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/orderCancelled" } } } - }, - { - "if": { "properties": { "event_type": { "const": "fill_applied" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/fill" } } } - }, - { - "if": { "properties": { "event_type": { "const": "cash_limited" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/cashLimited" } } } - }, - { - "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": "valuation" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/valuation" } } } - }, - { - "if": { "properties": { "event_type": { "const": "run_completed" } } }, - "then": { "properties": { "payload": { "$ref": "#/$defs/runCompleted" } } } - } - ], - "$defs": { - "identifier": { - "type": "string", - "minLength": 1, - "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" - }, - "unsignedDecimal": { - "type": "string", - "pattern": "^(?:0|[1-9][0-9]*)(?:[.][0-9]{0,5}[1-9])?$" - }, - "signedDecimal": { - "type": "string", - "pattern": "^(?:0(?:[.][0-9]{0,5}[1-9])?|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?|-(?: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])?)$" - }, - "weight": { - "type": "string", - "pattern": "^(?:0(?:[.][0-9]{0,5}[1-9])?|1)$" - }, - "quantity": { - "type": "string", - "pattern": "^(?:0|[1-9][0-9]*)$" - }, - "positiveQuantity": { "type": "string", "pattern": "^[1-9][0-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})$" - }, - "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "executionModel": { "const": "completed_bar_v1" }, - "runStarted": { - "type": "object", - "additionalProperties": false, - "required": ["scenario_sha256", "execution_model"], - "properties": { - "scenario_sha256": { "$ref": "#/$defs/sha256" }, - "execution_model": { "$ref": "#/$defs/executionModel" } - } - }, - "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": [ - { "$ref": "#/$defs/quantity" }, - { "type": "null" } - ] - } - } - }, - "marketSlice": { - "type": "object", - "additionalProperties": false, - "required": [ - "slice_sequence", - "start_at", - "end_at", - "available_at", - "received_at", - "bars" - ], - "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" } - } - } - }, - "requestedWeightTarget": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "weight", "quantity", "reference_price"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "weight": { "$ref": "#/$defs/weight" }, - "quantity": { "$ref": "#/$defs/quantity" }, - "reference_price": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "requestedQuantityTarget": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "weight", "quantity", "reference_price"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "weight": { "type": "null" }, - "quantity": { "$ref": "#/$defs/quantity" }, - "reference_price": { "type": "null" } - } - }, - "targetPortfolio": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": ["basis", "targets"], - "properties": { - "basis": { "const": "weights" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/requestedWeightTarget" } - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": ["basis", "targets"], - "properties": { - "basis": { "const": "quantities" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/requestedQuantityTarget" } - } - } - } - ] - }, - "order": { - "type": "object", - "additionalProperties": false, - "required": [ - "order_id", - "instrument_id", - "side", - "quantity", - "order_kind", - "limit_price", - "origin", - "created_event_id", - "created_sequence", - "created_at", - "eligible_after_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/positiveQuantity" }, - "order_kind": { "enum": ["market", "limit"] }, - "limit_price": { - "oneOf": [ - { "$ref": "#/$defs/positiveDecimal" }, - { "type": "null" } - ] - }, - "origin": { "enum": ["direct", "target_rebalance"] }, - "created_event_id": { "$ref": "#/$defs/identifier" }, - "created_sequence": { "$ref": "#/$defs/sequence" }, - "created_at": { "$ref": "#/$defs/timestamp" }, - "eligible_after_slice_sequence": { "$ref": "#/$defs/sequence" }, - "filled_quantity": { "$ref": "#/$defs/quantity" }, - "filled_notional": { "$ref": "#/$defs/unsignedDecimal" }, - "status": { - "enum": ["working", "partially_filled", "filled", "cancelled", "rejected"] - }, - "rejection_reason": { - "oneOf": [{ "type": "string" }, { "type": "null" }] - } - } - }, - "orderCancelled": { - "type": "object", - "additionalProperties": false, - "required": ["order", "reason"], - "properties": { - "order": { "$ref": "#/$defs/order" }, - "reason": { "enum": ["strategy_requested", "target_replaced", "market_ioc"] } - } - }, - "fill": { - "type": "object", - "additionalProperties": false, - "required": [ - "fill_id", - "order_id", - "instrument_id", - "side", - "quantity", - "price", - "notional", - "fee", - "executed_at", - "slice_sequence" - ], - "properties": { - "fill_id": { "$ref": "#/$defs/identifier" }, - "order_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveQuantity" }, - "price": { "$ref": "#/$defs/positiveDecimal" }, - "notional": { "$ref": "#/$defs/unsignedDecimal" }, - "fee": { "$ref": "#/$defs/unsignedDecimal" }, - "executed_at": { "$ref": "#/$defs/timestamp" }, - "slice_sequence": { "$ref": "#/$defs/sequence" } - } - }, - "cashLimited": { - "type": "object", - "additionalProperties": false, - "required": [ - "order_id", - "instrument_id", - "requested_quantity", - "affordable_quantity", - "price" - ], - "properties": { - "order_id": { "$ref": "#/$defs/identifier" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "requested_quantity": { "$ref": "#/$defs/positiveQuantity" }, - "affordable_quantity": { "$ref": "#/$defs/quantity" }, - "price": { "$ref": "#/$defs/positiveDecimal" } - } - }, - "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", "minLength": 1 }, - "value": { "type": "string" } - } - }, - "positionAttribution": { - "type": "object", - "additionalProperties": false, - "required": [ - "instrument_id", - "quantity", - "mark", - "market_value", - "cost_basis", - "realized_pnl", - "unrealized_pnl", - "total_fees" - ], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "quantity": { "$ref": "#/$defs/quantity" }, - "mark": { "$ref": "#/$defs/positiveDecimal" }, - "market_value": { "$ref": "#/$defs/unsignedDecimal" }, - "cost_basis": { "$ref": "#/$defs/unsignedDecimal" }, - "realized_pnl": { "$ref": "#/$defs/signedDecimal" }, - "unrealized_pnl": { "$ref": "#/$defs/signedDecimal" }, - "total_fees": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "valuation": { - "type": "object", - "additionalProperties": false, - "required": [ - "cash", - "market_value", - "cost_basis", - "realized_pnl", - "unrealized_pnl", - "equity", - "total_fees", - "positions" - ], - "properties": { - "cash": { "$ref": "#/$defs/unsignedDecimal" }, - "market_value": { "$ref": "#/$defs/unsignedDecimal" }, - "cost_basis": { "$ref": "#/$defs/unsignedDecimal" }, - "realized_pnl": { "$ref": "#/$defs/signedDecimal" }, - "unrealized_pnl": { "$ref": "#/$defs/signedDecimal" }, - "equity": { "$ref": "#/$defs/unsignedDecimal" }, - "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, - "positions": { - "type": "array", - "items": { "$ref": "#/$defs/positionAttribution" } - } - } - }, - "orderCounts": { - "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 } - } - }, - "runCompleted": { - "type": "object", - "additionalProperties": false, - "required": [ - "scenario_sha256", - "execution_model", - "valuation", - "order_counts" - ], - "properties": { - "scenario_sha256": { "$ref": "#/$defs/sha256" }, - "execution_model": { "$ref": "#/$defs/executionModel" }, - "valuation": { "$ref": "#/$defs/valuation" }, - "order_counts": { "$ref": "#/$defs/orderCounts" } - } - } - } -} diff --git a/contracts/v2/scenario-stream.schema.json b/contracts/v2/scenario-stream.schema.json deleted file mode 100644 index 6f63da0..0000000 --- a/contracts/v2/scenario-stream.schema.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v2/scenario-stream.schema.json", - "title": "Trading Engine v2 replay scenario stream record", - "description": "The structural contract for one v2 record in a bounded-memory JSON Lines replay scenario. The engine additionally checks record order, terminal counts, model support, catalog coverage, market ordering, causality, risk, tick, lot, time, and OHLC invariants.", - "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": "2" }, - "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": "2" }, - "scenario_sequence": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v2/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": "2" }, - "scenario_sequence": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v2/scenario.schema.json#/$defs/sequence" - }, - "record_type": { "const": "scenario_end" }, - "payload": { "$ref": "#/$defs/endPayload" } - } - }, - "headerPayload": { - "type": "object", - "additionalProperties": false, - "required": [ - "metadata", - "run_id", - "base_currency", - "initial_cash", - "instruments", - "risk", - "execution", - "max_internal_events" - ], - "properties": { - "metadata": { "type": "object" }, - "run_id": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v2/scenario.schema.json#/$defs/identifier" - }, - "base_currency": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v2/scenario.schema.json#/$defs/identifier" - }, - "initial_cash": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v2/scenario.schema.json#/$defs/unsignedDecimal" - }, - "instruments": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v2/scenario.schema.json#/$defs/instrument" - } - }, - "risk": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v2/scenario.schema.json#/$defs/risk" - }, - "execution": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v2/scenario.schema.json#/$defs/execution" - }, - "max_internal_events": { "type": "integer", "minimum": 1 } - } - }, - "slicePayload": { - "type": "object", - "additionalProperties": false, - "required": ["market_slice", "intents"], - "properties": { - "market_slice": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v2/scenario.schema.json#/$defs/marketSlice" - }, - "intents": { - "type": "array", - "items": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v2/scenario.schema.json#/$defs/intent" - } - } - } - }, - "endPayload": { - "type": "object", - "additionalProperties": false, - "required": ["slice_count"], - "properties": { - "slice_count": { - "$ref": "https://github.com/fallblu/trading-engine/contracts/v2/scenario.schema.json#/$defs/quantity" - } - } - } - } -} diff --git a/contracts/v2/scenario.schema.json b/contracts/v2/scenario.schema.json deleted file mode 100644 index 0322b8c..0000000 --- a/contracts/v2/scenario.schema.json +++ /dev/null @@ -1,316 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v2/scenario.schema.json", - "title": "Trading Engine v2 replay scenario", - "description": "The strict structural input contract. The engine additionally checks catalog coverage, ordering, causality, risk, tick, lot, time, and OHLC invariants.", - "type": "object", - "additionalProperties": false, - "required": [ - "contract_version", - "metadata", - "run_id", - "base_currency", - "initial_cash", - "instruments", - "risk", - "execution", - "max_internal_events", - "schedule", - "slices" - ], - "properties": { - "contract_version": { "const": "2" }, - "metadata": { "type": "object" }, - "run_id": { "$ref": "#/$defs/identifier" }, - "base_currency": { "$ref": "#/$defs/identifier" }, - "initial_cash": { "$ref": "#/$defs/unsignedDecimal" }, - "instruments": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/instrument" } - }, - "risk": { "$ref": "#/$defs/risk" }, - "execution": { "$ref": "#/$defs/execution" }, - "max_internal_events": { "type": "integer", "minimum": 1 }, - "schedule": { - "type": "array", - "items": { "$ref": "#/$defs/scheduleItem" } - }, - "slices": { - "type": "array", - "items": { "$ref": "#/$defs/marketSlice" } - } - }, - "$defs": { - "identifier": { - "type": "string", - "minLength": 1, - "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" - }, - "unsignedDecimal": { - "type": "string", - "pattern": "^(?:0|[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])?)$" - }, - "weight": { - "type": "string", - "pattern": "^(?:0(?:[.][0-9]{0,5}[1-9])?|1)$" - }, - "quantity": { - "type": "string", - "pattern": "^(?:0|[1-9][0-9]*)$" - }, - "positiveQuantity": { - "type": "string", - "pattern": "^[1-9][0-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})$" - }, - "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/positiveQuantity" } - } - }, - "risk": { - "type": "object", - "additionalProperties": false, - "required": ["max_order_quantity", "max_position"], - "properties": { - "max_order_quantity": { "$ref": "#/$defs/positiveQuantity" }, - "max_position": { "$ref": "#/$defs/positiveQuantity" } - } - }, - "execution": { - "type": "object", - "additionalProperties": false, - "required": ["model", "participation_bps", "fixed_fee", "fee_bps"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "participation_bps": { - "type": "integer", - "minimum": 0, - "maximum": 10000 - }, - "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, - "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "scheduleItem": { - "type": "object", - "additionalProperties": false, - "required": ["after_slice_sequence", "intents"], - "properties": { - "after_slice_sequence": { "$ref": "#/$defs/sequence" }, - "intents": { - "type": "array", - "items": { "$ref": "#/$defs/intent" } - } - } - }, - "intent": { - "oneOf": [ - { "$ref": "#/$defs/targetWeightsIntent" }, - { "$ref": "#/$defs/targetQuantitiesIntent" }, - { "$ref": "#/$defs/marketOrderIntent" }, - { "$ref": "#/$defs/limitOrderIntent" }, - { "$ref": "#/$defs/cancelOrderIntent" }, - { "$ref": "#/$defs/metricIntent" } - ] - }, - "targetWeightsIntent": { - "type": "object", - "additionalProperties": false, - "required": ["type", "targets"], - "properties": { - "type": { "const": "target_weights" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/weightTarget" } - } - } - }, - "weightTarget": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "weight"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "weight": { "$ref": "#/$defs/weight" } - } - }, - "targetQuantitiesIntent": { - "type": "object", - "additionalProperties": false, - "required": ["type", "targets"], - "properties": { - "type": { "const": "target_quantities" }, - "targets": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/quantityTarget" } - } - } - }, - "quantityTarget": { - "type": "object", - "additionalProperties": false, - "required": ["instrument_id", "quantity"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, - "quantity": { "$ref": "#/$defs/quantity" } - } - }, - "orderFields": { - "type": "object", - "required": [ - "type", - "instrument_id", - "side", - "quantity", - "order_kind", - "limit_price" - ], - "properties": { - "type": { "const": "submit_order" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveQuantity" } - } - }, - "marketOrderIntent": { - "allOf": [ - { "$ref": "#/$defs/orderFields" }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "type", - "instrument_id", - "side", - "quantity", - "order_kind", - "limit_price" - ], - "properties": { - "type": true, - "instrument_id": true, - "side": true, - "quantity": true, - "order_kind": { "const": "market" }, - "limit_price": { "type": "null" } - } - } - ] - }, - "limitOrderIntent": { - "allOf": [ - { "$ref": "#/$defs/orderFields" }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "type", - "instrument_id", - "side", - "quantity", - "order_kind", - "limit_price" - ], - "properties": { - "type": true, - "instrument_id": true, - "side": true, - "quantity": true, - "order_kind": { "const": "limit" }, - "limit_price": { "$ref": "#/$defs/positiveDecimal" } - } - } - ] - }, - "cancelOrderIntent": { - "type": "object", - "additionalProperties": false, - "required": ["type", "order_id"], - "properties": { - "type": { "const": "cancel_order" }, - "order_id": { "$ref": "#/$defs/identifier" } - } - }, - "metricIntent": { - "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" - ], - "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" } - } - } - }, - "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": [ - { "$ref": "#/$defs/quantity" }, - { "type": "null" } - ] - } - } - } - } -} diff --git a/contracts/v3/README.md b/contracts/v3/README.md deleted file mode 100644 index f667908..0000000 --- a/contracts/v3/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Trading Engine contract v3 - -This directory is the authoritative v3 process and file contract shared by Trading Engine and -its clients. Version 2 remains frozen under `contracts/v2`. - -- `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. - -Version 3 adds exact six-decimal quantities, signed targets and positions, explicit per-currency -cash and FX marks, splits and cash dividends, borrow costs, exposure and margin policy, and causal -margin-call/liquidation events. Every v3 scenario, stream record, and journal record carries -`"contract_version": "3"`; consumers reject missing or unsupported versions before interpreting -the remainder of a document. diff --git a/contracts/v3/dune b/contracts/v3/dune deleted file mode 100644 index 55cb489..0000000 --- a/contracts/v3/dune +++ /dev/null @@ -1,10 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v3/journal.schema.json) - (scenario-stream.schema.json as contracts/v3/scenario-stream.schema.json) - (scenario.schema.json as contracts/v3/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v3/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v3/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl as contracts/v3/fixtures/demo.scenario.jsonl))) diff --git a/contracts/v3/fixtures/demo.journal.jsonl b/contracts/v3/fixtures/demo.journal.jsonl deleted file mode 100644 index 475885a..0000000 --- a/contracts/v3/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,20 +0,0 @@ -{"contract_version":"3","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":"3e19fa66bc6425bb8ed7a89b338080a831dd39ea778c3c7f9e8ce1d3370fbee0","execution_model":"completed_bar_v1"}} -{"contract_version":"3","engine_sequence":"2","event_id":"demo-event-000000000002","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":[]}} -{"contract_version":"3","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.615","reference_price":"104"}]}} -{"contract_version":"3","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} -{"contract_version":"3","engine_sequence":"5","event_id":"demo-event-000000000005","causation_ids":["demo-event-000000000002","demo-event-000000000003"],"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":"9.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","updated_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"3","engine_sequence":"6","event_id":"demo-event-000000000006","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":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"10000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"0","mark":"104","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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"10000","maintenance_excess":"10000","margin_call":false}}} -{"contract_version":"3","engine_sequence":"7","event_id":"demo-event-000000000007","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} -{"contract_version":"3","engine_sequence":"8","event_id":"demo-event-000000000008","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} -{"contract_version":"3","engine_sequence":"9","event_id":"demo-event-000000000009","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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":"9.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","updated_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"3","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000003","demo-event-000000000007"],"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":"3.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000010","updated_event_id":"demo-event-000000000010","created_sequence":"10","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"3","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"642","long_market_value":"642","short_market_value":"0","gross_exposure":"642","cost_basis":"618.868","realized_pnl":"0","unrealized_pnl":"23.132","equity":"10023.132","dividend_pnl":"0","execution_fees":"0.868","borrow_fees":"0","total_fees":"0.868","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"6","mark":"107","fx_rate":"1","market_value":"642","base_market_value":"642","cost_basis":"618.868","base_cost_basis":"618.868","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"23.132","base_unrealized_pnl":"23.132","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0.868","base_execution_fees":"0.868","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0.868","base_total_fees":"0.868"}],"margin":{"initial_requirement":"321","maintenance_requirement":"160.5","initial_excess":"9702.132","maintenance_excess":"9862.632","margin_call":false}}} -{"contract_version":"3","engine_sequence":"12","event_id":"demo-event-000000000012","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":[]}} -{"contract_version":"3","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000010","demo-event-000000000012"],"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":"3.615","price":"107","notional":"386.805","fee":"0.636805","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} -{"contract_version":"3","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000012"],"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":"3","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.115","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000015","updated_event_id":"demo-event-000000000015","created_sequence":"15","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"3","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000012"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"8993.690195","net_market_value":"1009.575","long_market_value":"1009.575","short_market_value":"0","gross_exposure":"1009.575","cost_basis":"1006.309805","realized_pnl":"0","unrealized_pnl":"3.265195","equity":"10003.265195","dividend_pnl":"0","execution_fees":"1.504805","borrow_fees":"0","total_fees":"1.504805","cash_balances":[{"currency":"USD","amount":"8993.690195","fx_rate":"1","base_value":"8993.690195"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"9.615","mark":"105","fx_rate":"1","market_value":"1009.575","base_market_value":"1009.575","cost_basis":"1006.309805","base_cost_basis":"1006.309805","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"3.265195","base_unrealized_pnl":"3.265195","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"1.504805","base_execution_fees":"1.504805","borrow_fees":"0","base_borrow_fees":"0","total_fees":"1.504805","base_total_fees":"1.504805"}],"margin":{"initial_requirement":"504.7875","maintenance_requirement":"252.39375","initial_excess":"9498.477695","maintenance_excess":"9750.871445","margin_call":false}}} -{"contract_version":"3","engine_sequence":"17","event_id":"demo-event-000000000017","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":[]}} -{"contract_version":"3","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"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.115","price":"105","notional":"747.075","fee":"0.997075","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} -{"contract_version":"3","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}}} -{"contract_version":"3","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"3e19fa66bc6425bb8ed7a89b338080a831dd39ea778c3c7f9e8ce1d3370fbee0","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v3/fixtures/demo.scenario.json b/contracts/v3/fixtures/demo.scenario.json deleted file mode 100644 index d6ac1fa..0000000 --- a/contracts/v3/fixtures/demo.scenario.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "contract_version": "3", - "metadata": { - "producer": "trading-engine-demo", - "purpose": "deterministic conformance fixture" - }, - "run_id": "demo", - "base_currency": "USD", - "initial_cash": [ - { "currency": "USD", "amount": "10000" } - ], - "instruments": [ - { - "instrument_id": "demo-equity-acme", - "symbol": "ACME", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "0.001" - } - ], - "risk": { - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_gross_exposure": "1000000", - "max_leverage": "2", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "short_borrow_bps": 100 - }, - "execution": { - "model": "completed_bar_v1", - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 - }, - "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": [] - }, - { - "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": "12" } - ], - "fx_rates": [{ "currency": "USD", "rate": "1" }], - "corporate_actions": [] - }, - { - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v3/fixtures/demo.scenario.jsonl b/contracts/v3/fixtures/demo.scenario.jsonl deleted file mode 100644 index 09afea0..0000000 --- a/contracts/v3/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"3","payload":{"base_currency":"USD","execution":{"fee_bps":10,"fixed_fee":"0.25","model":"completed_bar_v1","participation_bps":5000},"initial_cash":[{"amount":"10000","currency":"USD"}],"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"0.001","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"max_internal_events":1000,"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"risk":{"initial_margin_bps":5000,"maintenance_margin_bps":2500,"max_gross_exposure":"1000000","max_leverage":"2","max_long_position":"1000","max_order_quantity":"1000","max_short_position":"1000","short_borrow_bps":100},"run_id":"demo"},"record_type":"scenario_header","scenario_sequence":"1"} -{"contract_version":"3","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"3","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"3","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"}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"3","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"}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"3","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v3/journal.schema.json b/contracts/v3/journal.schema.json deleted file mode 100644 index f6027d8..0000000 --- a/contracts/v3/journal.schema.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v3/journal.schema.json", - "title": "Trading Engine v3 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": "3" }, - "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", "market_slice_received", "target_portfolio_requested", "order_accepted", "order_rejected", "order_cancelled", "split_applied", "cash_dividend_applied", "order_adjusted", "fill_applied", "margin_limited", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] - }, - "payload": { "type": "object" } - }, - "allOf": [ - { "if": { "properties": { "event_type": { "const": "run_started" } } }, "then": { "properties": { "payload": { "$ref": "#/$defs/runStarted" } } } }, - { "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"] } } }, "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": "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": { "const": "margin_limited" } } }, "then": { "properties": { "payload": { "$ref": "#/$defs/marginLimited" } } } }, - { "if": { "properties": { "event_type": { "const": "borrow_fee_applied" } } }, "then": { "properties": { "payload": { "$ref": "#/$defs/borrowFee" } } } }, - { "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": { "const": "completed_bar_v1" } } - }, - "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" } } } - ] - }, - "marketSlice": { - "type": "object", "additionalProperties": false, - "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], - "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" } } - } - }, - "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", "limit_price", "origin", "created_event_id", "updated_event_id", "created_sequence", "created_at", "eligible_after_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"] }, "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, "origin": { "enum": ["direct", "target_rebalance", "margin_liquidation"] }, "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" }, "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", "margin_call"] } } - }, - "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" } } - }, - "orderAdjusted": { - "type": "object", "additionalProperties": false, "required": ["order", "action_id"], - "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } - }, - "fill": { - "type": "object", "additionalProperties": false, - "required": ["fill_id", "order_id", "instrument_id", "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", "slice_sequence"], - "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/unsignedDecimal" }, "executed_at": { "$ref": "#/$defs/timestamp" }, "slice_sequence": { "$ref": "#/$defs/sequence" } } - }, - "marginLimited": { - "type": "object", "additionalProperties": false, "required": ["order_id", "instrument_id", "requested_quantity", "permitted_quantity", "price"], - "properties": { "order_id": { "$ref": "#/$defs/identifier" }, "instrument_id": { "$ref": "#/$defs/identifier" }, "requested_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" } } - }, - "cashAttribution": { - "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], - "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } - }, - "positionAttribution": { - "type": "object", "additionalProperties": false, - "required": ["instrument_id", "quote_currency", "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"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, "quote_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "base_execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_total_fees": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "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" } } - }, - "valuation": { - "type": "object", "additionalProperties": false, - "required": ["base_currency", "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", "total_fees", "cash_balances", "positions", "margin"], - "properties": { - "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "cash_balances": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashAttribution" } }, "positions": { "type": "array", "items": { "$ref": "#/$defs/positionAttribution" } }, "margin": { "$ref": "#/$defs/margin" } - } - }, - "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": { "const": "completed_bar_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/v3/scenario-stream.schema.json b/contracts/v3/scenario-stream.schema.json deleted file mode 100644 index bd10528..0000000 --- a/contracts/v3/scenario-stream.schema.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v3/scenario-stream.schema.json", - "title": "Trading Engine v3 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": "3" }, - "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": "3" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/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": "3" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/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_cash", "instruments", "risk", "execution", "max_internal_events"], - "properties": { - "metadata": { "type": "object" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/cashBalance" } }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/instrument" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/execution" }, - "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/v3/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v3/scenario.schema.json b/contracts/v3/scenario.schema.json deleted file mode 100644 index 8d90cfe..0000000 --- a/contracts/v3/scenario.schema.json +++ /dev/null @@ -1,263 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v3/scenario.schema.json", - "title": "Trading Engine v3 replay scenario", - "description": "Strict deterministic scenario contract for fractional quantities, explicit FX, corporate actions, signed positions, and margin risk.", - "type": "object", - "additionalProperties": false, - "required": ["contract_version", "metadata", "run_id", "base_currency", "initial_cash", "instruments", "risk", "execution", "max_internal_events", "schedule", "slices"], - "properties": { - "contract_version": { "const": "3" }, - "metadata": { "type": "object" }, - "run_id": { "$ref": "#/$defs/identifier" }, - "base_currency": { "$ref": "#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/cashBalance" } - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "#/$defs/instrument" } - }, - "risk": { "$ref": "#/$defs/risk" }, - "execution": { "$ref": "#/$defs/execution" }, - "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": { - "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/unsignedDecimal" } - } - }, - "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" } - } - }, - "risk": { - "type": "object", - "additionalProperties": false, - "required": ["max_order_quantity", "max_long_position", "max_short_position", "max_gross_exposure", "max_leverage", "initial_margin_bps", "maintenance_margin_bps", "short_borrow_bps"], - "properties": { - "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, - "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, - "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, - "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "execution": { - "type": "object", - "additionalProperties": false, - "required": ["model", "participation_bps", "fixed_fee", "fee_bps"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, - "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "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", "limit_price"], - "properties": { - "type": { "const": "submit_order" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "order_kind": { "enum": ["market", "limit"] }, - "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] } - } - }, - "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"], - "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" } } - } - }, - "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" } - } - } - ] - } - } -} diff --git a/contracts/v4/README.md b/contracts/v4/README.md deleted file mode 100644 index b86761d..0000000 --- a/contracts/v4/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Trading Engine contract v4 - -This directory is the authoritative v4 process and file contract shared by Trading Engine and -its clients. Version 3 remains available under `contracts/v3` during the client transition. - -- `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 4 replaces the ambiguous `margin_limited` journal event with `fill_clipped`. Its versioned, -exhaustive reason identifies the limiting policy and a typed threshold alongside the proposed and -permitted quantities. Every v4 scenario, stream record, and journal record carries -`"contract_version": "4"`; consumers reject missing or unsupported versions before interpreting -the remainder of a document. The scenario shape is otherwise unchanged from v3. - -`fill_clipped.payload.reason.version` is `"1"`. Its exhaustive policy and threshold pairs are: - -| Policy | Threshold unit | Threshold value | -| --- | --- | --- | -| `max_order_quantity` | `quantity` | Configured maximum order quantity | -| `max_long_position` | `quantity` | Configured maximum long position | -| `max_short_position` | `quantity` | Configured maximum short position magnitude | -| `max_gross_exposure` | `money` | Configured maximum gross exposure | -| `max_leverage` | `ratio` | Configured maximum leverage | -| `initial_margin` | `basis_points` | Configured initial-margin basis points | - -The event also records `order_id`, `instrument_id`, `proposed_quantity`, `permitted_quantity`, and -the proposed fill `price`. A consumer must reject unknown reason versions, policies, threshold -units, and policy/unit combinations. diff --git a/contracts/v4/dune b/contracts/v4/dune deleted file mode 100644 index 6cd8a55..0000000 --- a/contracts/v4/dune +++ /dev/null @@ -1,16 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v4/journal.schema.json) - (scenario-stream.schema.json as contracts/v4/scenario-stream.schema.json) - (scenario.schema.json as contracts/v4/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v4/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v4/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl as contracts/v4/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v4/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v4/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v4/fixtures/demo.journal.jsonl b/contracts/v4/fixtures/demo.journal.jsonl deleted file mode 100644 index 503d7c5..0000000 --- a/contracts/v4/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,20 +0,0 @@ -{"contract_version":"4","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":"991890e8c1cc839a0c321a6d30b2ba20a8b588d4135310f43a548fbc929e9fcf","execution_model":"completed_bar_v1"}} -{"contract_version":"4","engine_sequence":"2","event_id":"demo-event-000000000002","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":[]}} -{"contract_version":"4","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.615","reference_price":"104"}]}} -{"contract_version":"4","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} -{"contract_version":"4","engine_sequence":"5","event_id":"demo-event-000000000005","causation_ids":["demo-event-000000000002","demo-event-000000000003"],"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":"9.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","updated_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"4","engine_sequence":"6","event_id":"demo-event-000000000006","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":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"10000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"0","mark":"104","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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"10000","maintenance_excess":"10000","margin_call":false}}} -{"contract_version":"4","engine_sequence":"7","event_id":"demo-event-000000000007","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} -{"contract_version":"4","engine_sequence":"8","event_id":"demo-event-000000000008","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} -{"contract_version":"4","engine_sequence":"9","event_id":"demo-event-000000000009","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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":"9.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","updated_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"4","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000003","demo-event-000000000007"],"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":"3.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000010","updated_event_id":"demo-event-000000000010","created_sequence":"10","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"4","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"642","long_market_value":"642","short_market_value":"0","gross_exposure":"642","cost_basis":"618.868","realized_pnl":"0","unrealized_pnl":"23.132","equity":"10023.132","dividend_pnl":"0","execution_fees":"0.868","borrow_fees":"0","total_fees":"0.868","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"6","mark":"107","fx_rate":"1","market_value":"642","base_market_value":"642","cost_basis":"618.868","base_cost_basis":"618.868","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"23.132","base_unrealized_pnl":"23.132","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0.868","base_execution_fees":"0.868","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0.868","base_total_fees":"0.868"}],"margin":{"initial_requirement":"321","maintenance_requirement":"160.5","initial_excess":"9702.132","maintenance_excess":"9862.632","margin_call":false}}} -{"contract_version":"4","engine_sequence":"12","event_id":"demo-event-000000000012","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":[]}} -{"contract_version":"4","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000010","demo-event-000000000012"],"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":"3.615","price":"107","notional":"386.805","fee":"0.636805","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} -{"contract_version":"4","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000012"],"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":"4","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.115","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000015","updated_event_id":"demo-event-000000000015","created_sequence":"15","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"4","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000012"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"8993.690195","net_market_value":"1009.575","long_market_value":"1009.575","short_market_value":"0","gross_exposure":"1009.575","cost_basis":"1006.309805","realized_pnl":"0","unrealized_pnl":"3.265195","equity":"10003.265195","dividend_pnl":"0","execution_fees":"1.504805","borrow_fees":"0","total_fees":"1.504805","cash_balances":[{"currency":"USD","amount":"8993.690195","fx_rate":"1","base_value":"8993.690195"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"9.615","mark":"105","fx_rate":"1","market_value":"1009.575","base_market_value":"1009.575","cost_basis":"1006.309805","base_cost_basis":"1006.309805","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"3.265195","base_unrealized_pnl":"3.265195","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"1.504805","base_execution_fees":"1.504805","borrow_fees":"0","base_borrow_fees":"0","total_fees":"1.504805","base_total_fees":"1.504805"}],"margin":{"initial_requirement":"504.7875","maintenance_requirement":"252.39375","initial_excess":"9498.477695","maintenance_excess":"9750.871445","margin_call":false}}} -{"contract_version":"4","engine_sequence":"17","event_id":"demo-event-000000000017","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":[]}} -{"contract_version":"4","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"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.115","price":"105","notional":"747.075","fee":"0.997075","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} -{"contract_version":"4","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}}} -{"contract_version":"4","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"991890e8c1cc839a0c321a6d30b2ba20a8b588d4135310f43a548fbc929e9fcf","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v4/fixtures/demo.scenario.json b/contracts/v4/fixtures/demo.scenario.json deleted file mode 100644 index c5174ec..0000000 --- a/contracts/v4/fixtures/demo.scenario.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "contract_version": "4", - "metadata": { - "producer": "trading-engine-demo", - "purpose": "deterministic conformance fixture" - }, - "run_id": "demo", - "base_currency": "USD", - "initial_cash": [ - { "currency": "USD", "amount": "10000" } - ], - "instruments": [ - { - "instrument_id": "demo-equity-acme", - "symbol": "ACME", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "0.001" - } - ], - "risk": { - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_gross_exposure": "1000000", - "max_leverage": "2", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "short_borrow_bps": 100 - }, - "execution": { - "model": "completed_bar_v1", - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 - }, - "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": [] - }, - { - "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": "12" } - ], - "fx_rates": [{ "currency": "USD", "rate": "1" }], - "corporate_actions": [] - }, - { - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v4/fixtures/demo.scenario.jsonl b/contracts/v4/fixtures/demo.scenario.jsonl deleted file mode 100644 index 829d797..0000000 --- a/contracts/v4/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"4","payload":{"base_currency":"USD","execution":{"fee_bps":10,"fixed_fee":"0.25","model":"completed_bar_v1","participation_bps":5000},"initial_cash":[{"amount":"10000","currency":"USD"}],"instruments":[{"instrument_id":"demo-equity-acme","lot_size":"0.001","quote_currency":"USD","symbol":"ACME","tick_size":"0.01"}],"max_internal_events":1000,"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"risk":{"initial_margin_bps":5000,"maintenance_margin_bps":2500,"max_gross_exposure":"1000000","max_leverage":"2","max_long_position":"1000","max_order_quantity":"1000","max_short_position":"1000","short_borrow_bps":100},"run_id":"demo"},"record_type":"scenario_header","scenario_sequence":"1"} -{"contract_version":"4","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"4","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"4","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"}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"4","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"}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"4","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v4/fixtures/fill-clipped.journal.jsonl b/contracts/v4/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index 67d9a50..0000000 --- a/contracts/v4/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,10 +0,0 @@ -{"contract_version":"4","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":"6ae16145d56f1ff2b7594c04917fd026a18e8fea9beb688a8aaf4a84cc7dca2c","execution_model":"completed_bar_v1"}} -{"contract_version":"4","engine_sequence":"2","event_id":"fill-clipped-event-000000000002","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":[]}} -{"contract_version":"4","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":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000003","updated_event_id":"fill-clipped-event-000000000003","created_sequence":"3","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"4","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false}}} -{"contract_version":"4","engine_sequence":"5","event_id":"fill-clipped-event-000000000005","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":[]}} -{"contract_version":"4","engine_sequence":"6","event_id":"fill-clipped-event-000000000006","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"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":"5","price":"100"}} -{"contract_version":"4","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"fill-clipped-fill-000000000001","order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"5","price":"100","notional":"500","fee":"10","executed_at":"2026-02-03T14:30:00.000000Z","slice_sequence":"2"}} -{"contract_version":"4","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"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","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000003","updated_event_id":"fill-clipped-event-000000000003","created_sequence":"3","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"5","filled_notional":"500","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"4","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}}} -{"contract_version":"4","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000009"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"6ae16145d56f1ff2b7594c04917fd026a18e8fea9beb688a8aaf4a84cc7dca2c","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v4/fixtures/fill-clipped.scenario.json b/contracts/v4/fixtures/fill-clipped.scenario.json deleted file mode 100644 index c575c46..0000000 --- a/contracts/v4/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "contract_version": "4", - "metadata": { - "producer": "trading-engine", - "purpose": "fill clipping conformance fixture" - }, - "run_id": "fill-clipped", - "base_currency": "USD", - "initial_cash": [ - { "currency": "USD", "amount": "550" } - ], - "instruments": [ - { - "instrument_id": "clip-equity", - "symbol": "CLIP", - "quote_currency": "USD", - "tick_size": "0.01", - "lot_size": "1" - } - ], - "risk": { - "max_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_gross_exposure": "1000000000", - "max_leverage": "1", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "short_borrow_bps": 100 - }, - "execution": { - "model": "completed_bar_v1", - "participation_bps": 10000, - "fixed_fee": "10", - "fee_bps": 0 - }, - "max_internal_events": 1000, - "schedule": [ - { - "after_slice_sequence": "1", - "intents": [ - { - "type": "submit_order", - "instrument_id": "clip-equity", - "side": "buy", - "quantity": "10", - "order_kind": "market", - "limit_price": 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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v4/journal.schema.json b/contracts/v4/journal.schema.json deleted file mode 100644 index c580f03..0000000 --- a/contracts/v4/journal.schema.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v4/journal.schema.json", - "title": "Trading Engine v4 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": "4" }, - "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", "market_slice_received", "target_portfolio_requested", "order_accepted", "order_rejected", "order_cancelled", "split_applied", "cash_dividend_applied", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] - }, - "payload": { "type": "object" } - }, - "allOf": [ - { "if": { "properties": { "event_type": { "const": "run_started" } } }, "then": { "properties": { "payload": { "$ref": "#/$defs/runStarted" } } } }, - { "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"] } } }, "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } - }, - "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" } } } - ] - }, - "marketSlice": { - "type": "object", "additionalProperties": false, - "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], - "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" } } - } - }, - "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", "limit_price", "origin", "created_event_id", "updated_event_id", "created_sequence", "created_at", "eligible_after_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"] }, "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, "origin": { "enum": ["direct", "target_rebalance", "margin_liquidation"] }, "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" }, "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", "margin_call"] } } - }, - "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" } } - }, - "orderAdjusted": { - "type": "object", "additionalProperties": false, "required": ["order", "action_id"], - "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } - }, - "fill": { - "type": "object", "additionalProperties": false, - "required": ["fill_id", "order_id", "instrument_id", "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", "slice_sequence"], - "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/unsignedDecimal" }, "executed_at": { "$ref": "#/$defs/timestamp" }, "slice_sequence": { "$ref": "#/$defs/sequence" } } - }, - "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 } } - }, - "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" } } } - ] - }, - "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" } } - }, - "cashAttribution": { - "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], - "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } - }, - "positionAttribution": { - "type": "object", "additionalProperties": false, - "required": ["instrument_id", "quote_currency", "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"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, "quote_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "base_execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_total_fees": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "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" } } - }, - "valuation": { - "type": "object", "additionalProperties": false, - "required": ["base_currency", "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", "total_fees", "cash_balances", "positions", "margin"], - "properties": { - "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "cash_balances": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashAttribution" } }, "positions": { "type": "array", "items": { "$ref": "#/$defs/positionAttribution" } }, "margin": { "$ref": "#/$defs/margin" } - } - }, - "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": { "const": "completed_bar_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/v4/scenario-stream.schema.json b/contracts/v4/scenario-stream.schema.json deleted file mode 100644 index c1da40a..0000000 --- a/contracts/v4/scenario-stream.schema.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v4/scenario-stream.schema.json", - "title": "Trading Engine v4 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": "4" }, - "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": "4" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/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": "4" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/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_cash", "instruments", "risk", "execution", "max_internal_events"], - "properties": { - "metadata": { "type": "object" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/cashBalance" } }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/instrument" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/execution" }, - "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/v4/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v4/scenario.schema.json b/contracts/v4/scenario.schema.json deleted file mode 100644 index d3aea43..0000000 --- a/contracts/v4/scenario.schema.json +++ /dev/null @@ -1,263 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v4/scenario.schema.json", - "title": "Trading Engine v4 replay scenario", - "description": "Strict deterministic scenario contract for fractional quantities, explicit FX, corporate actions, signed positions, and margin risk.", - "type": "object", - "additionalProperties": false, - "required": ["contract_version", "metadata", "run_id", "base_currency", "initial_cash", "instruments", "risk", "execution", "max_internal_events", "schedule", "slices"], - "properties": { - "contract_version": { "const": "4" }, - "metadata": { "type": "object" }, - "run_id": { "$ref": "#/$defs/identifier" }, - "base_currency": { "$ref": "#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/cashBalance" } - }, - "instruments": { - "type": "array", - "minItems": 1, - "maxItems": 4096, - "items": { "$ref": "#/$defs/instrument" } - }, - "risk": { "$ref": "#/$defs/risk" }, - "execution": { "$ref": "#/$defs/execution" }, - "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": { - "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/unsignedDecimal" } - } - }, - "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" } - } - }, - "risk": { - "type": "object", - "additionalProperties": false, - "required": ["max_order_quantity", "max_long_position", "max_short_position", "max_gross_exposure", "max_leverage", "initial_margin_bps", "maintenance_margin_bps", "short_borrow_bps"], - "properties": { - "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, - "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, - "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, - "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "execution": { - "type": "object", - "additionalProperties": false, - "required": ["model", "participation_bps", "fixed_fee", "fee_bps"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, - "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "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", "limit_price"], - "properties": { - "type": { "const": "submit_order" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "order_kind": { "enum": ["market", "limit"] }, - "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] } - } - }, - "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"], - "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" } } - } - }, - "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" } - } - } - ] - } - } -} diff --git a/contracts/v5/README.md b/contracts/v5/README.md deleted file mode 100644 index 5495e2e..0000000 --- a/contracts/v5/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Trading Engine contract v5 - -This directory is the authoritative v5 process and file contract shared by Trading Engine and its -clients. Versions 4 and 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 5 adds explicit immutable venue-calendar snapshots. Each calendar has stable venue and -calendar identities, a calendar contract version, explicit instrument membership, and ordered -date policies. A date is either a holiday or an open session named as regular or early-close. -Open sessions contain absolute timestamp intervals for configured premarket, opening-auction, -regular, closing-auction, and postmarket phases. - -Calendar producers resolve local civil time, time-zone database versions, daylight-saving rules, -and clock changes before creating a scenario. The reducer receives only absolute instants. Missing -date policies are errors and must never be inferred from weekdays or adjacent sessions. Runtime -validation also rejects duplicate calendar identities, overlapping instrument membership, missing -instrument coverage, unordered or overlapping phases, holidays with phases, and open sessions -without a regular phase. - -Every v5 scenario, stream record, and journal record carries `"contract_version": "5"`. - -The v5 `execution` object also namespaces strict configuration beneath the stable model name. -`completed_bar_v1` configuration version `"1"` requires participation basis points, fixed fee, -and fee basis points. Runtime capabilities describe its required fields, supported market and limit -orders, completed-OHLCV data requirement, and numeric limits. diff --git a/contracts/v5/dune b/contracts/v5/dune deleted file mode 100644 index 6e47d5e..0000000 --- a/contracts/v5/dune +++ /dev/null @@ -1,16 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v5/journal.schema.json) - (scenario-stream.schema.json as contracts/v5/scenario-stream.schema.json) - (scenario.schema.json as contracts/v5/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v5/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v5/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl as contracts/v5/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v5/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v5/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v5/fixtures/demo.journal.jsonl b/contracts/v5/fixtures/demo.journal.jsonl deleted file mode 100644 index 7900887..0000000 --- a/contracts/v5/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,20 +0,0 @@ -{"contract_version":"5","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":"b800732e40c20c06605c6a1352d3482a3f41fc7ae4b07594860a1c3f153a655c","execution_model":"completed_bar_v1"}} -{"contract_version":"5","engine_sequence":"2","event_id":"demo-event-000000000002","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":[]}} -{"contract_version":"5","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.615","reference_price":"104"}]}} -{"contract_version":"5","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} -{"contract_version":"5","engine_sequence":"5","event_id":"demo-event-000000000005","causation_ids":["demo-event-000000000002","demo-event-000000000003"],"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":"9.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","updated_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"5","engine_sequence":"6","event_id":"demo-event-000000000006","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":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"10000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"0","mark":"104","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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"10000","maintenance_excess":"10000","margin_call":false}}} -{"contract_version":"5","engine_sequence":"7","event_id":"demo-event-000000000007","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} -{"contract_version":"5","engine_sequence":"8","event_id":"demo-event-000000000008","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} -{"contract_version":"5","engine_sequence":"9","event_id":"demo-event-000000000009","causation_ids":["demo-event-000000000005","demo-event-000000000007"],"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":"9.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000005","updated_event_id":"demo-event-000000000005","created_sequence":"5","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"5","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000003","demo-event-000000000007"],"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":"3.615","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000010","updated_event_id":"demo-event-000000000010","created_sequence":"10","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"5","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"642","long_market_value":"642","short_market_value":"0","gross_exposure":"642","cost_basis":"618.868","realized_pnl":"0","unrealized_pnl":"23.132","equity":"10023.132","dividend_pnl":"0","execution_fees":"0.868","borrow_fees":"0","total_fees":"0.868","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"6","mark":"107","fx_rate":"1","market_value":"642","base_market_value":"642","cost_basis":"618.868","base_cost_basis":"618.868","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"23.132","base_unrealized_pnl":"23.132","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0.868","base_execution_fees":"0.868","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0.868","base_total_fees":"0.868"}],"margin":{"initial_requirement":"321","maintenance_requirement":"160.5","initial_excess":"9702.132","maintenance_excess":"9862.632","margin_call":false}}} -{"contract_version":"5","engine_sequence":"12","event_id":"demo-event-000000000012","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":[]}} -{"contract_version":"5","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000010","demo-event-000000000012"],"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":"3.615","price":"107","notional":"386.805","fee":"0.636805","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} -{"contract_version":"5","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000012"],"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":"5","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.115","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000015","updated_event_id":"demo-event-000000000015","created_sequence":"15","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"5","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000012"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"8993.690195","net_market_value":"1009.575","long_market_value":"1009.575","short_market_value":"0","gross_exposure":"1009.575","cost_basis":"1006.309805","realized_pnl":"0","unrealized_pnl":"3.265195","equity":"10003.265195","dividend_pnl":"0","execution_fees":"1.504805","borrow_fees":"0","total_fees":"1.504805","cash_balances":[{"currency":"USD","amount":"8993.690195","fx_rate":"1","base_value":"8993.690195"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"9.615","mark":"105","fx_rate":"1","market_value":"1009.575","base_market_value":"1009.575","cost_basis":"1006.309805","base_cost_basis":"1006.309805","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"3.265195","base_unrealized_pnl":"3.265195","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"1.504805","base_execution_fees":"1.504805","borrow_fees":"0","base_borrow_fees":"0","total_fees":"1.504805","base_total_fees":"1.504805"}],"margin":{"initial_requirement":"504.7875","maintenance_requirement":"252.39375","initial_excess":"9498.477695","maintenance_excess":"9750.871445","margin_call":false}}} -{"contract_version":"5","engine_sequence":"17","event_id":"demo-event-000000000017","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":[]}} -{"contract_version":"5","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"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.115","price":"105","notional":"747.075","fee":"0.997075","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} -{"contract_version":"5","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}}} -{"contract_version":"5","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"b800732e40c20c06605c6a1352d3482a3f41fc7ae4b07594860a1c3f153a655c","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9739.76812","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"261.651016","realized_pnl":"1.419136","unrealized_pnl":"3.348984","equity":"10004.76812","dividend_pnl":"0","execution_fees":"2.50188","borrow_fees":"0","total_fees":"2.50188","cash_balances":[{"currency":"USD","amount":"9739.76812","fx_rate":"1","base_value":"9739.76812"}],"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":"261.651016","base_cost_basis":"261.651016","realized_pnl":"1.419136","base_realized_pnl":"1.419136","unrealized_pnl":"3.348984","base_unrealized_pnl":"3.348984","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"2.50188","base_execution_fees":"2.50188","borrow_fees":"0","base_borrow_fees":"0","total_fees":"2.50188","base_total_fees":"2.50188"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9872.26812","maintenance_excess":"9938.51812","margin_call":false}},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v5/fixtures/demo.scenario.json b/contracts/v5/fixtures/demo.scenario.json deleted file mode 100644 index 1ba8e32..0000000 --- a/contracts/v5/fixtures/demo.scenario.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "contract_version": "5", - "metadata": { - "producer": "trading-engine-demo", - "purpose": "deterministic conformance fixture" - }, - "run_id": "demo", - "base_currency": "USD", - "initial_cash": [ - { "currency": "USD", "amount": "10000" } - ], - "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_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_gross_exposure": "1000000", - "max_leverage": "2", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "short_borrow_bps": 100 - }, - "execution": { - "model": "completed_bar_v1", - "configuration": { - "version": "1", - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 - } - }, - "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": [] - }, - { - "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": "12" } - ], - "fx_rates": [{ "currency": "USD", "rate": "1" }], - "corporate_actions": [] - }, - { - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v5/fixtures/demo.scenario.jsonl b/contracts/v5/fixtures/demo.scenario.jsonl deleted file mode 100644 index 7a9733a..0000000 --- a/contracts/v5/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"5","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"run_id":"demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"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_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_gross_exposure":"1000000","max_leverage":"2","initial_margin_bps":5000,"maintenance_margin_bps":2500,"short_borrow_bps":100},"execution":{"model":"completed_bar_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000}} -{"contract_version":"5","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"5","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"5","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"}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"5","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"}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"5","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v5/fixtures/fill-clipped.journal.jsonl b/contracts/v5/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index 3734bbb..0000000 --- a/contracts/v5/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,10 +0,0 @@ -{"contract_version":"5","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":"3a66be243ea4ae30e5554c7e0dbdc091bd7dd7c5003f4faf726f04b0fbdaeed6","execution_model":"completed_bar_v1"}} -{"contract_version":"5","engine_sequence":"2","event_id":"fill-clipped-event-000000000002","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":[]}} -{"contract_version":"5","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":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000003","updated_event_id":"fill-clipped-event-000000000003","created_sequence":"3","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"5","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false}}} -{"contract_version":"5","engine_sequence":"5","event_id":"fill-clipped-event-000000000005","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":[]}} -{"contract_version":"5","engine_sequence":"6","event_id":"fill-clipped-event-000000000006","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"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":"5","price":"100"}} -{"contract_version":"5","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"fill-clipped-fill-000000000001","order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"5","price":"100","notional":"500","fee":"10","executed_at":"2026-02-03T14:30:00.000000Z","slice_sequence":"2"}} -{"contract_version":"5","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000003","fill-clipped-event-000000000005"],"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","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000003","updated_event_id":"fill-clipped-event-000000000003","created_sequence":"3","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"5","filled_notional":"500","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"5","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}}} -{"contract_version":"5","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000009"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"3a66be243ea4ae30e5554c7e0dbdc091bd7dd7c5003f4faf726f04b0fbdaeed6","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v5/fixtures/fill-clipped.scenario.json b/contracts/v5/fixtures/fill-clipped.scenario.json deleted file mode 100644 index b213a8b..0000000 --- a/contracts/v5/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "contract_version": "5", - "metadata": { - "producer": "trading-engine", - "purpose": "fill clipping conformance fixture" - }, - "run_id": "fill-clipped", - "base_currency": "USD", - "initial_cash": [ - { "currency": "USD", "amount": "550" } - ], - "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_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_gross_exposure": "1000000000", - "max_leverage": "1", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "short_borrow_bps": 100 - }, - "execution": { - "model": "completed_bar_v1", - "configuration": { - "version": "1", - "participation_bps": 10000, - "fixed_fee": "10", - "fee_bps": 0 - } - }, - "max_internal_events": 1000, - "schedule": [ - { - "after_slice_sequence": "1", - "intents": [ - { - "type": "submit_order", - "instrument_id": "clip-equity", - "side": "buy", - "quantity": "10", - "order_kind": "market", - "limit_price": 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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v5/journal.schema.json b/contracts/v5/journal.schema.json deleted file mode 100644 index ca161ad..0000000 --- a/contracts/v5/journal.schema.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v5/journal.schema.json", - "title": "Trading Engine v5 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": "5" }, - "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", "market_slice_received", "target_portfolio_requested", "order_accepted", "order_rejected", "order_cancelled", "split_applied", "cash_dividend_applied", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] - }, - "payload": { "type": "object" } - }, - "allOf": [ - { "if": { "properties": { "event_type": { "const": "run_started" } } }, "then": { "properties": { "payload": { "$ref": "#/$defs/runStarted" } } } }, - { "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"] } } }, "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } - }, - "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" } } } - ] - }, - "marketSlice": { - "type": "object", "additionalProperties": false, - "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], - "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" } } - } - }, - "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", "limit_price", "origin", "created_event_id", "updated_event_id", "created_sequence", "created_at", "eligible_after_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"] }, "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, "origin": { "enum": ["direct", "target_rebalance", "margin_liquidation"] }, "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" }, "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", "margin_call"] } } - }, - "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" } } - }, - "orderAdjusted": { - "type": "object", "additionalProperties": false, "required": ["order", "action_id"], - "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } - }, - "fill": { - "type": "object", "additionalProperties": false, - "required": ["fill_id", "order_id", "instrument_id", "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", "slice_sequence"], - "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/unsignedDecimal" }, "executed_at": { "$ref": "#/$defs/timestamp" }, "slice_sequence": { "$ref": "#/$defs/sequence" } } - }, - "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 } } - }, - "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" } } } - ] - }, - "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" } } - }, - "cashAttribution": { - "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], - "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } - }, - "positionAttribution": { - "type": "object", "additionalProperties": false, - "required": ["instrument_id", "quote_currency", "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"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, "quote_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "base_execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_total_fees": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "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" } } - }, - "valuation": { - "type": "object", "additionalProperties": false, - "required": ["base_currency", "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", "total_fees", "cash_balances", "positions", "margin"], - "properties": { - "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "cash_balances": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashAttribution" } }, "positions": { "type": "array", "items": { "$ref": "#/$defs/positionAttribution" } }, "margin": { "$ref": "#/$defs/margin" } - } - }, - "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": { "const": "completed_bar_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/v5/scenario-stream.schema.json b/contracts/v5/scenario-stream.schema.json deleted file mode 100644 index 87d151a..0000000 --- a/contracts/v5/scenario-stream.schema.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v5/scenario-stream.schema.json", - "title": "Trading Engine v5 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": "5" }, - "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": "5" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/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": "5" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/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_cash", "instruments", "venue_calendars", "risk", "execution", "max_internal_events"], - "properties": { - "metadata": { "type": "object" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/identifier" }, - "initial_cash": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/cashBalance" } }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/instrument" } }, - "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/execution" }, - "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/v5/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v5/scenario.schema.json b/contracts/v5/scenario.schema.json deleted file mode 100644 index f9696da..0000000 --- a/contracts/v5/scenario.schema.json +++ /dev/null @@ -1,339 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v5/scenario.schema.json", - "title": "Trading Engine v5 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_cash", "instruments", "venue_calendars", "risk", "execution", "max_internal_events", "schedule", "slices"], - "properties": { - "contract_version": { "const": "5" }, - "metadata": { "type": "object" }, - "run_id": { "$ref": "#/$defs/identifier" }, - "base_currency": { "$ref": "#/$defs/identifier" }, - "initial_cash": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/cashBalance" } - }, - "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" }, - "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": { - "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/unsignedDecimal" } - } - }, - "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_order_quantity", "max_long_position", "max_short_position", "max_gross_exposure", "max_leverage", "initial_margin_bps", "maintenance_margin_bps", "short_borrow_bps"], - "properties": { - "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, - "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, - "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, - "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "execution": { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } - } - }, - "completedBarV1Configuration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fixed_fee", "fee_bps"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, - "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "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", "limit_price"], - "properties": { - "type": { "const": "submit_order" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "order_kind": { "enum": ["market", "limit"] }, - "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] } - } - }, - "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"], - "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" } } - } - }, - "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" } - } - } - ] - } - } -} diff --git a/contracts/v6/README.md b/contracts/v6/README.md deleted file mode 100644 index ff1bd28..0000000 --- a/contracts/v6/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Trading Engine contract v6 - -This directory is the authoritative v6 process and file contract shared by Trading Engine and its -clients. Versions 5, 4, and 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 6 replaces cash-only initialization with an explicit portfolio snapshot. It carries signed -cash, signed positions, native cost basis, realized and dividend P&L histories, execution and -borrow fee histories, position marks, and currency-to-base FX marks. Historical attribution is -point-in-time state and is not applied to cash again. - -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; position limits; aggregate exposure and -leverage limits; and initial margin. Signed cash is valid. A successful v6 run emits `initial_state` -immediately after `run_started`, followed by a reconciled initial `valuation`, before market data. - -Version 6 retains the immutable venue-calendar and model-owned execution-configuration envelopes -introduced by v5. - -Every v6 scenario, stream record, and journal record carries `"contract_version": "6"`. - -The v6 `execution` object namespaces strict configuration beneath the stable model name. -`completed_bar_v1` configuration version `"1"` requires participation basis points, fixed fee, -and fee basis points. Runtime capabilities describe its required fields, supported market and limit -orders, completed-OHLCV data requirement, and numeric limits. diff --git a/contracts/v6/dune b/contracts/v6/dune deleted file mode 100644 index 1bf8fd9..0000000 --- a/contracts/v6/dune +++ /dev/null @@ -1,16 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v6/journal.schema.json) - (scenario-stream.schema.json as contracts/v6/scenario-stream.schema.json) - (scenario.schema.json as contracts/v6/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v6/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v6/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl as contracts/v6/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v6/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v6/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v6/fixtures/demo.journal.jsonl b/contracts/v6/fixtures/demo.journal.jsonl deleted file mode 100644 index c9cca94..0000000 --- a/contracts/v6/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,22 +0,0 @@ -{"contract_version":"6","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":"c98534261412acebf4b49d87619dc7c951538581c2a7dd05f315eb64d761cec2","execution_model":"completed_bar_v1"}} -{"contract_version":"6","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false}}}} -{"contract_version":"6","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false}}} -{"contract_version":"6","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":[]}} -{"contract_version":"6","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} -{"contract_version":"6","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":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} -{"contract_version":"6","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004","demo-event-000000000005"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000007","updated_event_id":"demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"6","engine_sequence":"8","event_id":"demo-event-000000000008","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","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"14","equity":"10104","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"}],"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"}],"margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052","maintenance_excess":"10078","margin_call":false}}} -{"contract_version":"6","engine_sequence":"9","event_id":"demo-event-000000000009","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} -{"contract_version":"6","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} -{"contract_version":"6","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000007","updated_event_id":"demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"6","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000005","demo-event-000000000009"],"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.715","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000012","updated_event_id":"demo-event-000000000012","created_sequence":"12","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"6","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000009"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5","unrealized_pnl":"40.132","equity":"10130.132","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618"}],"margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.632","maintenance_excess":"9942.882","margin_call":false}}} -{"contract_version":"6","engine_sequence":"14","event_id":"demo-event-000000000014","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":[]}} -{"contract_version":"6","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.715","price":"107","notional":"290.505","fee":"0.540505","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} -{"contract_version":"6","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000014"],"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":"6","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":["demo-event-000000000014","demo-event-000000000016"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000017","updated_event_id":"demo-event-000000000017","created_sequence":"17","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"6","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000014"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.086495","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5","unrealized_pnl":"20.161495","equity":"10110.161495","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.086495","fx_rate":"1","base_value":"9090.086495"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505"}],"margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.123995","maintenance_excess":"9855.142745","margin_call":false}}} -{"contract_version":"6","engine_sequence":"19","event_id":"demo-event-000000000019","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":[]}} -{"contract_version":"6","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000017","demo-event-000000000019"],"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":"105","notional":"757.575","fee":"1.007575","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} -{"contract_version":"6","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false}}} -{"contract_version":"6","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"c98534261412acebf4b49d87619dc7c951538581c2a7dd05f315eb64d761cec2","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false}},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v6/fixtures/demo.scenario.json b/contracts/v6/fixtures/demo.scenario.json deleted file mode 100644 index 5b4303c..0000000 --- a/contracts/v6/fixtures/demo.scenario.json +++ /dev/null @@ -1,294 +0,0 @@ -{ - "contract_version": "6", - "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_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_gross_exposure": "1000000", - "max_leverage": "2", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "short_borrow_bps": 100 - }, - "execution": { - "model": "completed_bar_v1", - "configuration": { - "version": "1", - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 - } - }, - "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": [] - }, - { - "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": "12" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [] - }, - { - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v6/fixtures/demo.scenario.jsonl b/contracts/v6/fixtures/demo.scenario.jsonl deleted file mode 100644 index 712bef4..0000000 --- a/contracts/v6/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"6","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_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_gross_exposure":"1000000","max_leverage":"2","initial_margin_bps":5000,"maintenance_margin_bps":2500,"short_borrow_bps":100},"execution":{"model":"completed_bar_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000}} -{"contract_version":"6","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"6","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"6","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"}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"6","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"}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"6","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v6/fixtures/fill-clipped.journal.jsonl b/contracts/v6/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index 14bd262..0000000 --- a/contracts/v6/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,12 +0,0 @@ -{"contract_version":"6","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":"0ba5499f954080438421e7bf439a0258214392ab6baab7d69015d8c622d5c04c","execution_model":"completed_bar_v1"}} -{"contract_version":"6","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false}}}} -{"contract_version":"6","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false}}} -{"contract_version":"6","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":[]}} -{"contract_version":"6","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":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"6","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":"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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false}}} -{"contract_version":"6","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","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":[]}} -{"contract_version":"6","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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":"5","price":"100"}} -{"contract_version":"6","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"fill-clipped-fill-000000000001","order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"5","price":"100","notional":"500","fee":"10","executed_at":"2026-02-03T14:30:00.000000Z","slice_sequence":"2"}} -{"contract_version":"6","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"5","filled_notional":"500","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"6","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000007"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}}} -{"contract_version":"6","engine_sequence":"12","event_id":"fill-clipped-event-000000000012","causation_ids":["fill-clipped-event-000000000011"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"0ba5499f954080438421e7bf439a0258214392ab6baab7d69015d8c622d5c04c","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"40","net_market_value":"500","long_market_value":"500","short_market_value":"0","gross_exposure":"500","cost_basis":"510","realized_pnl":"0","unrealized_pnl":"-10","equity":"540","dividend_pnl":"0","execution_fees":"10","borrow_fees":"0","total_fees":"10","cash_balances":[{"currency":"USD","amount":"40","fx_rate":"1","base_value":"40"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"5","mark":"100","fx_rate":"1","market_value":"500","base_market_value":"500","cost_basis":"510","base_cost_basis":"510","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-10","base_unrealized_pnl":"-10","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"10","base_execution_fees":"10","borrow_fees":"0","base_borrow_fees":"0","total_fees":"10","base_total_fees":"10"}],"margin":{"initial_requirement":"250","maintenance_requirement":"125","initial_excess":"290","maintenance_excess":"415","margin_call":false}},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v6/fixtures/fill-clipped.scenario.json b/contracts/v6/fixtures/fill-clipped.scenario.json deleted file mode 100644 index b72cefa..0000000 --- a/contracts/v6/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,164 +0,0 @@ -{ - "contract_version": "6", - "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_order_quantity": "1000", - "max_long_position": "1000", - "max_short_position": "1000", - "max_gross_exposure": "1000000000", - "max_leverage": "1", - "initial_margin_bps": 5000, - "maintenance_margin_bps": 2500, - "short_borrow_bps": 100 - }, - "execution": { - "model": "completed_bar_v1", - "configuration": { - "version": "1", - "participation_bps": 10000, - "fixed_fee": "10", - "fee_bps": 0 - } - }, - "max_internal_events": 1000, - "schedule": [ - { - "after_slice_sequence": "1", - "intents": [ - { - "type": "submit_order", - "instrument_id": "clip-equity", - "side": "buy", - "quantity": "10", - "order_kind": "market", - "limit_price": 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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v6/journal.schema.json b/contracts/v6/journal.schema.json deleted file mode 100644 index 0203411..0000000 --- a/contracts/v6/journal.schema.json +++ /dev/null @@ -1,186 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v6/journal.schema.json", - "title": "Trading Engine v6 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": "6" }, - "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_cancelled", "split_applied", "cash_dividend_applied", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] - }, - "payload": { "type": "object" } - }, - "allOf": [ - { "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"] } } }, "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } - }, - "initialState": { - "type": "object", "additionalProperties": false, - "required": ["portfolio", "valuation"], - "properties": { - "portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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" } } } - ] - }, - "marketSlice": { - "type": "object", "additionalProperties": false, - "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], - "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" } } - } - }, - "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", "limit_price", "origin", "created_event_id", "updated_event_id", "created_sequence", "created_at", "eligible_after_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"] }, "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, "origin": { "enum": ["direct", "target_rebalance", "margin_liquidation"] }, "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" }, "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", "margin_call"] } } - }, - "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" } } - }, - "orderAdjusted": { - "type": "object", "additionalProperties": false, "required": ["order", "action_id"], - "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } - }, - "fill": { - "type": "object", "additionalProperties": false, - "required": ["fill_id", "order_id", "instrument_id", "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", "slice_sequence"], - "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/unsignedDecimal" }, "executed_at": { "$ref": "#/$defs/timestamp" }, "slice_sequence": { "$ref": "#/$defs/sequence" } } - }, - "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 } } - }, - "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" } } } - ] - }, - "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" } } - }, - "cashAttribution": { - "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], - "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } - }, - "positionAttribution": { - "type": "object", "additionalProperties": false, - "required": ["instrument_id", "quote_currency", "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"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, "quote_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "base_execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_total_fees": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "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" } } - }, - "valuation": { - "type": "object", "additionalProperties": false, - "required": ["base_currency", "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", "total_fees", "cash_balances", "positions", "margin"], - "properties": { - "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "cash_balances": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashAttribution" } }, "positions": { "type": "array", "items": { "$ref": "#/$defs/positionAttribution" } }, "margin": { "$ref": "#/$defs/margin" } - } - }, - "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": { "const": "completed_bar_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/v6/scenario-stream.schema.json b/contracts/v6/scenario-stream.schema.json deleted file mode 100644 index f70b604..0000000 --- a/contracts/v6/scenario-stream.schema.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v6/scenario-stream.schema.json", - "title": "Trading Engine v6 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": "6" }, - "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": "6" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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": "6" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/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", "max_internal_events"], - "properties": { - "metadata": { "type": "object" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/identifier" }, - "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/initialPortfolio" }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/instrument" } }, - "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/execution" }, - "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/v6/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v6/scenario.schema.json b/contracts/v6/scenario.schema.json deleted file mode 100644 index d5e75ea..0000000 --- a/contracts/v6/scenario.schema.json +++ /dev/null @@ -1,369 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v6/scenario.schema.json", - "title": "Trading Engine v6 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", "max_internal_events", "schedule", "slices"], - "properties": { - "contract_version": { "const": "6" }, - "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" }, - "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": { - "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_order_quantity", "max_long_position", "max_short_position", "max_gross_exposure", "max_leverage", "initial_margin_bps", "maintenance_margin_bps", "short_borrow_bps"], - "properties": { - "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, - "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, - "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, - "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, - "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, - "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "execution": { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } - } - }, - "completedBarV1Configuration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fixed_fee", "fee_bps"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, - "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "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", "limit_price"], - "properties": { - "type": { "const": "submit_order" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "order_kind": { "enum": ["market", "limit"] }, - "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] } - } - }, - "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"], - "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" } } - } - }, - "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" } - } - } - ] - } - } -} diff --git a/contracts/v7/README.md b/contracts/v7/README.md deleted file mode 100644 index 1ec393d..0000000 --- a/contracts/v7/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Trading Engine contract v7 - -This directory is the authoritative v7 process and file contract shared by Trading Engine and its -clients. Versions 6, 5, 4, and 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 v7 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 v7 scenario, stream record, and journal record carries `"contract_version": "7"`. - -The v7 `execution` object retains the versioned configuration introduced by v5. -`completed_bar_v1` configuration version `"1"` requires participation basis points, fixed fee, -and fee basis points. Runtime capabilities describe its required fields, supported market and limit -orders, completed-OHLCV data requirement, and numeric limits. diff --git a/contracts/v7/dune b/contracts/v7/dune deleted file mode 100644 index 4497ccd..0000000 --- a/contracts/v7/dune +++ /dev/null @@ -1,16 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v7/journal.schema.json) - (scenario-stream.schema.json as contracts/v7/scenario-stream.schema.json) - (scenario.schema.json as contracts/v7/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v7/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v7/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl as contracts/v7/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v7/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v7/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v7/fixtures/demo.journal.jsonl b/contracts/v7/fixtures/demo.journal.jsonl deleted file mode 100644 index acf860e..0000000 --- a/contracts/v7/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,22 +0,0 @@ -{"contract_version":"7","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":"d1991fa67140bff80fcbeb9b04b211d8c9cf4f41d4fba39dcec66d5ef3e5fab9","execution_model":"completed_bar_v1"}} -{"contract_version":"7","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} -{"contract_version":"7","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} -{"contract_version":"7","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":[]}} -{"contract_version":"7","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} -{"contract_version":"7","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":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} -{"contract_version":"7","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004","demo-event-000000000005"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000007","updated_event_id":"demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"7","engine_sequence":"8","event_id":"demo-event-000000000008","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","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"14","equity":"10104","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"}],"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"}],"margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052","maintenance_excess":"10078","margin_call":false},"group_exposures":[]}} -{"contract_version":"7","engine_sequence":"9","event_id":"demo-event-000000000009","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} -{"contract_version":"7","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} -{"contract_version":"7","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000007","updated_event_id":"demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"7","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000005","demo-event-000000000009"],"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.715","order_kind":"market","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000012","updated_event_id":"demo-event-000000000012","created_sequence":"12","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"7","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000009"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5","unrealized_pnl":"40.132","equity":"10130.132","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618"}],"margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.632","maintenance_excess":"9942.882","margin_call":false},"group_exposures":[]}} -{"contract_version":"7","engine_sequence":"14","event_id":"demo-event-000000000014","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":[]}} -{"contract_version":"7","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.715","price":"107","notional":"290.505","fee":"0.540505","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} -{"contract_version":"7","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000014"],"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":"7","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":["demo-event-000000000014","demo-event-000000000016"],"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","limit_price":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000017","updated_event_id":"demo-event-000000000017","created_sequence":"17","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"7","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000014"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.086495","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5","unrealized_pnl":"20.161495","equity":"10110.161495","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.086495","fx_rate":"1","base_value":"9090.086495"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505"}],"margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.123995","maintenance_excess":"9855.142745","margin_call":false},"group_exposures":[]}} -{"contract_version":"7","engine_sequence":"19","event_id":"demo-event-000000000019","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":[]}} -{"contract_version":"7","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000017","demo-event-000000000019"],"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":"105","notional":"757.575","fee":"1.007575","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} -{"contract_version":"7","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false},"group_exposures":[]}} -{"contract_version":"7","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"d1991fa67140bff80fcbeb9b04b211d8c9cf4f41d4fba39dcec66d5ef3e5fab9","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v7/fixtures/demo.scenario.json b/contracts/v7/fixtures/demo.scenario.json deleted file mode 100644 index 7ad99d3..0000000 --- a/contracts/v7/fixtures/demo.scenario.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "contract_version": "7", - "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_v1", - "configuration": { - "version": "1", - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 - } - }, - "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": [] - }, - { - "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": "12" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [] - }, - { - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v7/fixtures/demo.scenario.jsonl b/contracts/v7/fixtures/demo.scenario.jsonl deleted file mode 100644 index 6bd7435..0000000 --- a/contracts/v7/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"7","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_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000}} -{"contract_version":"7","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"7","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"7","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"}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"7","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"}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"7","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v7/fixtures/fill-clipped.journal.jsonl b/contracts/v7/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index 678ea43..0000000 --- a/contracts/v7/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,11 +0,0 @@ -{"contract_version":"7","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":"61f1319c6667400bec59562d106b580a7607e97fa3f1d837018ac7c4f38cc6bb","execution_model":"completed_bar_v1"}} -{"contract_version":"7","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} -{"contract_version":"7","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"7","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":[]}} -{"contract_version":"7","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":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} -{"contract_version":"7","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":"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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"7","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","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":[]}} -{"contract_version":"7","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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":"7","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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","limit_price":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","filled_quantity":"0","filled_notional":"0","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"7","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000007"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21: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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"7","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000010"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"61f1319c6667400bec59562d106b580a7607e97fa3f1d837018ac7c4f38cc6bb","execution_model":"completed_bar_v1","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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v7/fixtures/fill-clipped.scenario.json b/contracts/v7/fixtures/fill-clipped.scenario.json deleted file mode 100644 index 159d2e4..0000000 --- a/contracts/v7/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,172 +0,0 @@ -{ - "contract_version": "7", - "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": "1", - "participation_bps": 10000, - "fixed_fee": "10", - "fee_bps": 0 - } - }, - "max_internal_events": 1000, - "schedule": [ - { - "after_slice_sequence": "1", - "intents": [ - { - "type": "submit_order", - "instrument_id": "clip-equity", - "side": "buy", - "quantity": "10", - "order_kind": "market", - "limit_price": 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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v7/journal.schema.json b/contracts/v7/journal.schema.json deleted file mode 100644 index 808db61..0000000 --- a/contracts/v7/journal.schema.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v7/journal.schema.json", - "title": "Trading Engine v6 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": "7" }, - "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_cancelled", "split_applied", "cash_dividend_applied", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] - }, - "payload": { "type": "object" } - }, - "allOf": [ - { "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"] } } }, "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } - }, - "initialState": { - "type": "object", "additionalProperties": false, - "required": ["portfolio", "valuation"], - "properties": { - "portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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" } } } - ] - }, - "marketSlice": { - "type": "object", "additionalProperties": false, - "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], - "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" } } - } - }, - "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", "limit_price", "origin", "created_event_id", "updated_event_id", "created_sequence", "created_at", "eligible_after_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"] }, "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, "origin": { "enum": ["direct", "target_rebalance", "margin_liquidation"] }, "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" }, "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", "margin_call"] } } - }, - "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" } } - }, - "orderAdjusted": { - "type": "object", "additionalProperties": false, "required": ["order", "action_id"], - "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } - }, - "fill": { - "type": "object", "additionalProperties": false, - "required": ["fill_id", "order_id", "instrument_id", "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", "slice_sequence"], - "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/unsignedDecimal" }, "executed_at": { "$ref": "#/$defs/timestamp" }, "slice_sequence": { "$ref": "#/$defs/sequence" } } - }, - "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" } } - }, - "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": "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_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" } } - }, - "cashAttribution": { - "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], - "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } - }, - "positionAttribution": { - "type": "object", "additionalProperties": false, - "required": ["instrument_id", "quote_currency", "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"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, "quote_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "base_execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_total_fees": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "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", "net_market_value", "long_market_value", "short_market_value", "gross_exposure", "cost_basis", "realized_pnl", "unrealized_pnl", "equity", "dividend_pnl", "execution_fees", "borrow_fees", "total_fees", "cash_balances", "positions", "margin", "group_exposures"], - "properties": { - "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "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" } } - } - }, - "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": { "const": "completed_bar_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/v7/scenario-stream.schema.json b/contracts/v7/scenario-stream.schema.json deleted file mode 100644 index 90afc77..0000000 --- a/contracts/v7/scenario-stream.schema.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v7/scenario-stream.schema.json", - "title": "Trading Engine v6 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": "7" }, - "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": "7" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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": "7" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/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", "max_internal_events"], - "properties": { - "metadata": { "type": "object" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/identifier" }, - "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/initialPortfolio" }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/instrument" } }, - "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/execution" }, - "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/v7/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v7/scenario.schema.json b/contracts/v7/scenario.schema.json deleted file mode 100644 index 8dbb6ae..0000000 --- a/contracts/v7/scenario.schema.json +++ /dev/null @@ -1,428 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v7/scenario.schema.json", - "title": "Trading Engine v6 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", "max_internal_events", "schedule", "slices"], - "properties": { - "contract_version": { "const": "7" }, - "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" }, - "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": { - "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": { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } - } - }, - "completedBarV1Configuration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fixed_fee", "fee_bps"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, - "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "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", "limit_price"], - "properties": { - "type": { "const": "submit_order" }, - "instrument_id": { "$ref": "#/$defs/identifier" }, - "side": { "enum": ["buy", "sell"] }, - "quantity": { "$ref": "#/$defs/positiveDecimal" }, - "order_kind": { "enum": ["market", "limit"] }, - "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] } - } - }, - "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"], - "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" } } - } - }, - "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" } - } - } - ] - } - } -} diff --git a/contracts/v8/README.md b/contracts/v8/README.md deleted file mode 100644 index b459d5a..0000000 --- a/contracts/v8/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Trading Engine contract v8 - -This directory is the authoritative v8 process and file contract shared by Trading Engine and its -clients. Versions 7, 6, 5, 4, and 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 8 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 v8 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 v8 scenario, stream record, and journal record carries `"contract_version": "8"`. - -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 v8 `execution` object retains the versioned configuration introduced by v5. -`completed_bar_v1` configuration version `"1"` requires participation basis points, fixed fee, -and fee basis points. Runtime capabilities describe its required fields, supported market and limit -orders (including stop and stop-limit), completed-OHLCV data requirement, and numeric limits. diff --git a/contracts/v8/dune b/contracts/v8/dune deleted file mode 100644 index 2f462da..0000000 --- a/contracts/v8/dune +++ /dev/null @@ -1,16 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v8/journal.schema.json) - (scenario-stream.schema.json as contracts/v8/scenario-stream.schema.json) - (scenario.schema.json as contracts/v8/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v8/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v8/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl as contracts/v8/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v8/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v8/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v8/fixtures/demo.journal.jsonl b/contracts/v8/fixtures/demo.journal.jsonl deleted file mode 100644 index 77fb100..0000000 --- a/contracts/v8/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,22 +0,0 @@ -{"contract_version":"8","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":"85f7c99e0666159579c79256b3d0dc5f9c328e1275b79465fe1d4c93883e68f1","execution_model":"completed_bar_v1"}} -{"contract_version":"8","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} -{"contract_version":"8","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"}],"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"}],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} -{"contract_version":"8","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":[]}} -{"contract_version":"8","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} -{"contract_version":"8","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":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} -{"contract_version":"8","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004","demo-event-000000000005"],"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-000000000007","updated_event_id":"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}} -{"contract_version":"8","engine_sequence":"8","event_id":"demo-event-000000000008","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","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"14","equity":"10104","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"}],"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"}],"margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052","maintenance_excess":"10078","margin_call":false},"group_exposures":[]}} -{"contract_version":"8","engine_sequence":"9","event_id":"demo-event-000000000009","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} -{"contract_version":"8","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","price":"103","notional":"618","fee":"0.868","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2"}} -{"contract_version":"8","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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-000000000007","updated_event_id":"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":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"immediate_or_cancel"}} -{"contract_version":"8","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000005","demo-event-000000000009"],"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.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-000000000012","updated_event_id":"demo-event-000000000012","created_sequence":"12","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":"8","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000009"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5","unrealized_pnl":"40.132","equity":"10130.132","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618"}],"margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.632","maintenance_excess":"9942.882","margin_call":false},"group_exposures":[]}} -{"contract_version":"8","engine_sequence":"14","event_id":"demo-event-000000000014","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":[]}} -{"contract_version":"8","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.715","price":"107","notional":"290.505","fee":"0.540505","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3"}} -{"contract_version":"8","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000014"],"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":"8","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":["demo-event-000000000014","demo-event-000000000016"],"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-000000000017","updated_event_id":"demo-event-000000000017","created_sequence":"17","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":"8","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000014"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.086495","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5","unrealized_pnl":"20.161495","equity":"10110.161495","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.086495","fx_rate":"1","base_value":"9090.086495"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505"}],"margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.123995","maintenance_excess":"9855.142745","margin_call":false},"group_exposures":[]}} -{"contract_version":"8","engine_sequence":"19","event_id":"demo-event-000000000019","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":[]}} -{"contract_version":"8","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000017","demo-event-000000000019"],"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":"105","notional":"757.575","fee":"1.007575","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4"}} -{"contract_version":"8","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false},"group_exposures":[]}} -{"contract_version":"8","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"85f7c99e0666159579c79256b3d0dc5f9c328e1275b79465fe1d4c93883e68f1","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.65392","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.965682","unrealized_pnl":"7.688238","equity":"10111.65392","dividend_pnl":"1","execution_fees":"2.91608","borrow_fees":"0.25","total_fees":"3.16608","cash_balances":[{"currency":"USD","amount":"9846.65392","fx_rate":"1","base_value":"9846.65392"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.965682","base_realized_pnl":"18.965682","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.91608","base_execution_fees":"2.91608","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.16608","base_total_fees":"3.16608"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.15392","maintenance_excess":"10045.40392","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v8/fixtures/demo.scenario.json b/contracts/v8/fixtures/demo.scenario.json deleted file mode 100644 index ba9ab60..0000000 --- a/contracts/v8/fixtures/demo.scenario.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "contract_version": "8", - "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_v1", - "configuration": { - "version": "1", - "participation_bps": 5000, - "fixed_fee": "0.25", - "fee_bps": 10 - } - }, - "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": [] - }, - { - "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": "12" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [] - }, - { - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v8/fixtures/demo.scenario.jsonl b/contracts/v8/fixtures/demo.scenario.jsonl deleted file mode 100644 index 3b4f365..0000000 --- a/contracts/v8/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"8","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_v1","configuration":{"version":"1","participation_bps":5000,"fixed_fee":"0.25","fee_bps":10}},"max_internal_events":1000}} -{"contract_version":"8","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"8","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"8","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"}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"8","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"}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"8","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v8/fixtures/fill-clipped.journal.jsonl b/contracts/v8/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index 26542b7..0000000 --- a/contracts/v8/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,11 +0,0 @@ -{"contract_version":"8","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":"1935c1744a959181894f6610c539e6d3d27ebce56d5737c8286f3e1b4417cb21","execution_model":"completed_bar_v1"}} -{"contract_version":"8","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} -{"contract_version":"8","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"}],"positions":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"8","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":[]}} -{"contract_version":"8","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":"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-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","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":"8","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":"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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"8","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","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":[]}} -{"contract_version":"8","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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":"8","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","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":"immediate_or_cancel"}} -{"contract_version":"8","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000007"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21: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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"8","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000010"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"1935c1744a959181894f6610c539e6d3d27ebce56d5737c8286f3e1b4417cb21","execution_model":"completed_bar_v1","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"}],"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"}],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v8/fixtures/fill-clipped.scenario.json b/contracts/v8/fixtures/fill-clipped.scenario.json deleted file mode 100644 index 8f66605..0000000 --- a/contracts/v8/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "contract_version": "8", - "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": "1", - "participation_bps": 10000, - "fixed_fee": "10", - "fee_bps": 0 - } - }, - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v8/journal.schema.json b/contracts/v8/journal.schema.json deleted file mode 100644 index d7e3107..0000000 --- a/contracts/v8/journal.schema.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v8/journal.schema.json", - "title": "Trading Engine v6 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": "8" }, - "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", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] - }, - "payload": { "type": "object" } - }, - "allOf": [ - { "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } - }, - "initialState": { - "type": "object", "additionalProperties": false, - "required": ["portfolio", "valuation"], - "properties": { - "portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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" } } } - ] - }, - "marketSlice": { - "type": "object", "additionalProperties": false, - "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], - "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" } } - } - }, - "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"] }, "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"] } } - }, - "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" } } - }, - "orderAdjusted": { - "type": "object", "additionalProperties": false, "required": ["order", "action_id"], - "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } - }, - "fill": { - "type": "object", "additionalProperties": false, - "required": ["fill_id", "order_id", "instrument_id", "quote_currency", "side", "quantity", "price", "notional", "fee", "executed_at", "slice_sequence"], - "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/unsignedDecimal" }, "executed_at": { "$ref": "#/$defs/timestamp" }, "slice_sequence": { "$ref": "#/$defs/sequence" } } - }, - "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" } } - }, - "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": "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_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" } } - }, - "cashAttribution": { - "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], - "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } - }, - "positionAttribution": { - "type": "object", "additionalProperties": false, - "required": ["instrument_id", "quote_currency", "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"], - "properties": { - "instrument_id": { "$ref": "#/$defs/identifier" }, "quote_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "base_execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "base_total_fees": { "$ref": "#/$defs/unsignedDecimal" } - } - }, - "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", "net_market_value", "long_market_value", "short_market_value", "gross_exposure", "cost_basis", "realized_pnl", "unrealized_pnl", "equity", "dividend_pnl", "execution_fees", "borrow_fees", "total_fees", "cash_balances", "positions", "margin", "group_exposures"], - "properties": { - "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "total_fees": { "$ref": "#/$defs/unsignedDecimal" }, "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" } } - } - }, - "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": { "const": "completed_bar_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/v8/scenario-stream.schema.json b/contracts/v8/scenario-stream.schema.json deleted file mode 100644 index 7798820..0000000 --- a/contracts/v8/scenario-stream.schema.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v8/scenario-stream.schema.json", - "title": "Trading Engine v6 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": "8" }, - "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": "8" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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": "8" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/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", "max_internal_events"], - "properties": { - "metadata": { "type": "object" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/identifier" }, - "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/initialPortfolio" }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/instrument" } }, - "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/execution" }, - "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/v8/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json#/$defs/intent" } } - } - } - } -} diff --git a/contracts/v8/scenario.schema.json b/contracts/v8/scenario.schema.json deleted file mode 100644 index de87de4..0000000 --- a/contracts/v8/scenario.schema.json +++ /dev/null @@ -1,442 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v8/scenario.schema.json", - "title": "Trading Engine v6 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", "max_internal_events", "schedule", "slices"], - "properties": { - "contract_version": { "const": "8" }, - "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" }, - "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": { - "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": { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } - } - }, - "completedBarV1Configuration": { - "type": "object", - "additionalProperties": false, - "required": ["version", "participation_bps", "fixed_fee", "fee_bps"], - "properties": { - "version": { "const": "1" }, - "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, - "fixed_fee": { "$ref": "#/$defs/unsignedDecimal" }, - "fee_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } - } - }, - "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"], - "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" } } - } - }, - "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" } - } - } - ] - } - } -} diff --git a/contracts/v9/README.md b/contracts/v9/README.md deleted file mode 100644 index f9ad441..0000000 --- a/contracts/v9/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Trading Engine contract v9 - -This directory is the authoritative v9 process and file contract shared by Trading Engine and its -clients. Versions 8, 7, 6, 5, 4, and 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 v9 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 v9 scenario, stream record, and journal record carries `"contract_version": "9"`. - -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 v9 `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. diff --git a/contracts/v9/dune b/contracts/v9/dune deleted file mode 100644 index 4396587..0000000 --- a/contracts/v9/dune +++ /dev/null @@ -1,16 +0,0 @@ -(install - (section share) - (package trading_engine) - (files - (journal.schema.json as contracts/v9/journal.schema.json) - (scenario-stream.schema.json as contracts/v9/scenario-stream.schema.json) - (scenario.schema.json as contracts/v9/scenario.schema.json) - (fixtures/demo.journal.jsonl as contracts/v9/fixtures/demo.journal.jsonl) - (fixtures/demo.scenario.json as contracts/v9/fixtures/demo.scenario.json) - (fixtures/demo.scenario.jsonl as contracts/v9/fixtures/demo.scenario.jsonl) - (fixtures/fill-clipped.journal.jsonl - as - contracts/v9/fixtures/fill-clipped.journal.jsonl) - (fixtures/fill-clipped.scenario.json - as - contracts/v9/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v9/fixtures/demo.journal.jsonl b/contracts/v9/fixtures/demo.journal.jsonl deleted file mode 100644 index 71efc14..0000000 --- a/contracts/v9/fixtures/demo.journal.jsonl +++ /dev/null @@ -1,22 +0,0 @@ -{"contract_version":"9","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":"8a65aeaaa3c548704b926ecc99c0795bc51fc8279677b5c37848e8919e0fea1d","execution_model":"completed_bar_v1"}} -{"contract_version":"9","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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} -{"contract_version":"9","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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} -{"contract_version":"9","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":[]}} -{"contract_version":"9","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":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} -{"contract_version":"9","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":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} -{"contract_version":"9","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004","demo-event-000000000005"],"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-000000000007","updated_event_id":"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}} -{"contract_version":"9","engine_sequence":"8","event_id":"demo-event-000000000008","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","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"14","equity":"10104","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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052","maintenance_excess":"10078","margin_call":false},"group_exposures":[]}} -{"contract_version":"9","engine_sequence":"9","event_id":"demo-event-000000000009","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":"12"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[]}} -{"contract_version":"9","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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","price":"103","notional":"618","fee":"0.868","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.618","quote_amount":"0.618"}]}} -{"contract_version":"9","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000007","demo-event-000000000009"],"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-000000000007","updated_event_id":"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":"6","filled_notional":"618","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} -{"contract_version":"9","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000005","demo-event-000000000009"],"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.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-000000000012","updated_event_id":"demo-event-000000000012","created_sequence":"12","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":"9","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000009"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9381.132","net_market_value":"749","long_market_value":"749","short_market_value":"0","gross_exposure":"749","cost_basis":"708.868","realized_pnl":"5","unrealized_pnl":"40.132","equity":"10130.132","dividend_pnl":"1","execution_fees":"1.368","borrow_fees":"0.25","total_fees":"1.618","cash_balances":[{"currency":"USD","amount":"9381.132","fx_rate":"1","base_value":"9381.132"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7","mark":"107","fx_rate":"1","market_value":"749","base_market_value":"749","cost_basis":"708.868","base_cost_basis":"708.868","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.132","base_unrealized_pnl":"40.132","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.368","base_execution_fees":"1.368","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.618","base_total_fees":"1.618","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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}]}],"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.618","quote_currency":"USD","quote_amount":"0.618","base_amount":"0.618"}],"margin":{"initial_requirement":"374.5","maintenance_requirement":"187.25","initial_excess":"9755.632","maintenance_excess":"9942.882","margin_call":false},"group_exposures":[]}} -{"contract_version":"9","engine_sequence":"14","event_id":"demo-event-000000000014","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":[]}} -{"contract_version":"9","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000012","demo-event-000000000014"],"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.715","price":"107","notional":"290.505","fee":"0.540505","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.290505","quote_amount":"0.290505"}]}} -{"contract_version":"9","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000014"],"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":"9","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":["demo-event-000000000014","demo-event-000000000016"],"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-000000000017","updated_event_id":"demo-event-000000000017","created_sequence":"17","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":"9","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000014"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.086495","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.913505","realized_pnl":"5","unrealized_pnl":"20.161495","equity":"10110.161495","dividend_pnl":"1","execution_fees":"1.908505","borrow_fees":"0.25","total_fees":"2.158505","cash_balances":[{"currency":"USD","amount":"9090.086495","fx_rate":"1","base_value":"9090.086495"}],"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.913505","base_cost_basis":"999.913505","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.161495","base_unrealized_pnl":"20.161495","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.908505","base_execution_fees":"1.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.158505","base_total_fees":"2.158505","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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}]}],"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.908505","quote_currency":"USD","quote_amount":"0.908505","base_amount":"0.908505"}],"margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.123995","maintenance_excess":"9855.142745","margin_call":false},"group_exposures":[]}} -{"contract_version":"9","engine_sequence":"19","event_id":"demo-event-000000000019","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":[]}} -{"contract_version":"9","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000017","demo-event-000000000019"],"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":"105","notional":"757.575","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.757575","quote_amount":"0.757575"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_amount":"-0.007575"}]}} -{"contract_version":"9","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.661495","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.973257","unrealized_pnl":"7.688238","equity":"10111.661495","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.661495","fx_rate":"1","base_value":"9846.661495"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}]}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.161495","maintenance_excess":"10045.411495","margin_call":false},"group_exposures":[]}} -{"contract_version":"9","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"8a65aeaaa3c548704b926ecc99c0795bc51fc8279677b5c37848e8919e0fea1d","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"9846.661495","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.311762","realized_pnl":"18.973257","unrealized_pnl":"7.688238","equity":"10111.661495","dividend_pnl":"1","execution_fees":"2.908505","borrow_fees":"0.25","total_fees":"3.158505","cash_balances":[{"currency":"USD","amount":"9846.661495","fx_rate":"1","base_value":"9846.661495"}],"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.311762","base_cost_basis":"257.311762","realized_pnl":"18.973257","base_realized_pnl":"18.973257","unrealized_pnl":"7.688238","base_unrealized_pnl":"7.688238","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.908505","base_execution_fees":"2.908505","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.158505","base_total_fees":"3.158505","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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}]}],"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.66608","quote_currency":"USD","quote_amount":"1.66608","base_amount":"1.66608"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.007575","quote_currency":"USD","quote_amount":"-0.007575","base_amount":"-0.007575"}],"margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.161495","maintenance_excess":"10045.411495","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v9/fixtures/demo.scenario.json b/contracts/v9/fixtures/demo.scenario.json deleted file mode 100644 index f4e41af..0000000 --- a/contracts/v9/fixtures/demo.scenario.json +++ /dev/null @@ -1,314 +0,0 @@ -{ - "contract_version": "9", - "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_v1", - "configuration": { - "version": "2", - "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" } - ] - } - ] - } - }, - "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": [] - }, - { - "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": "12" - } - ], - "fx_rates": [ - { - "currency": "USD", - "rate": "1" - } - ], - "corporate_actions": [] - }, - { - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v9/fixtures/demo.scenario.jsonl b/contracts/v9/fixtures/demo.scenario.jsonl deleted file mode 100644 index d32bd25..0000000 --- a/contracts/v9/fixtures/demo.scenario.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"contract_version":"9","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_v1","configuration":{"version":"2","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"}]}]}},"max_internal_events":1000}} -{"contract_version":"9","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"}},"record_type":"market_slice","scenario_sequence":"2"} -{"contract_version":"9","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":"12"}],"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"}},"record_type":"market_slice","scenario_sequence":"3"} -{"contract_version":"9","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"}},"record_type":"market_slice","scenario_sequence":"4"} -{"contract_version":"9","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"}},"record_type":"market_slice","scenario_sequence":"5"} -{"contract_version":"9","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v9/fixtures/fill-clipped.journal.jsonl b/contracts/v9/fixtures/fill-clipped.journal.jsonl deleted file mode 100644 index 272c5b9..0000000 --- a/contracts/v9/fixtures/fill-clipped.journal.jsonl +++ /dev/null @@ -1,11 +0,0 @@ -{"contract_version":"9","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":"d2b5effb91be9d9725df41aa6a908b761931c4631aff5cbd9fad7d3f14a3c3cb","execution_model":"completed_bar_v1"}} -{"contract_version":"9","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"}],"positions":[],"execution_fee_components":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} -{"contract_version":"9","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"}],"positions":[],"execution_fee_components":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"9","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":[]}} -{"contract_version":"9","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":"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-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","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":"9","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":"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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"9","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","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":[]}} -{"contract_version":"9","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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":"9","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000005","fill-clipped-event-000000000007"],"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-000000000005","updated_event_id":"fill-clipped-event-000000000005","created_sequence":"5","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":"9","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000007"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21: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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} -{"contract_version":"9","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000010"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"d2b5effb91be9d9725df41aa6a908b761931c4631aff5cbd9fad7d3f14a3c3cb","execution_model":"completed_bar_v1","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"}],"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":[]}],"execution_fee_components":[],"margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v9/fixtures/fill-clipped.scenario.json b/contracts/v9/fixtures/fill-clipped.scenario.json deleted file mode 100644 index d14bf55..0000000 --- a/contracts/v9/fixtures/fill-clipped.scenario.json +++ /dev/null @@ -1,187 +0,0 @@ -{ - "contract_version": "9", - "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": [] - }, - { - "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": [] - } - ] -} diff --git a/contracts/v9/journal.schema.json b/contracts/v9/journal.schema.json deleted file mode 100644 index 1e6baa6..0000000 --- a/contracts/v9/journal.schema.json +++ /dev/null @@ -1,248 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v9/journal.schema.json", - "title": "Trading Engine v6 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": "9" }, - "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", "order_adjusted", "fill_applied", "fill_clipped", "borrow_fee_applied", "margin_call", "margin_restored", "intent_rejected", "metric_emitted", "valuation", "run_completed"] - }, - "payload": { "type": "object" } - }, - "allOf": [ - { "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": "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": { "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": { "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": { "const": "completed_bar_v1" } } - }, - "initialState": { - "type": "object", "additionalProperties": false, - "required": ["portfolio", "valuation"], - "properties": { - "portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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" } } } - ] - }, - "marketSlice": { - "type": "object", "additionalProperties": false, - "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "fx_rates", "corporate_actions"], - "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" } } - } - }, - "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"] }, "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"] } } - }, - "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" } } - }, - "orderAdjusted": { - "type": "object", "additionalProperties": false, "required": ["order", "action_id"], - "properties": { "order": { "$ref": "#/$defs/order" }, "action_id": { "$ref": "#/$defs/identifier" } } - }, - "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" } } - }, - "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": "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_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" } } - }, - "cashAttribution": { - "type": "object", "additionalProperties": false, "required": ["currency", "amount", "fx_rate", "base_value"], - "properties": { "currency": { "$ref": "#/$defs/identifier" }, "amount": { "$ref": "#/$defs/signedDecimal" }, "fx_rate": { "$ref": "#/$defs/positiveDecimal" }, "base_value": { "$ref": "#/$defs/signedDecimal" } } - }, - "positionAttribution": { - "type": "object", "additionalProperties": false, - "required": ["instrument_id", "quote_currency", "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" }, "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/unsignedDecimal" }, "base_borrow_fees": { "$ref": "#/$defs/unsignedDecimal" }, "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", "net_market_value", "long_market_value", "short_market_value", "gross_exposure", "cost_basis", "realized_pnl", "unrealized_pnl", "equity", "dividend_pnl", "execution_fees", "borrow_fees", "total_fees", "cash_balances", "positions", "margin", "group_exposures", "execution_fee_components"], - "properties": { - "base_currency": { "$ref": "#/$defs/identifier" }, "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/unsignedDecimal" }, "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" } } - } - }, - "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": { "const": "completed_bar_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/v9/scenario-stream.schema.json b/contracts/v9/scenario-stream.schema.json deleted file mode 100644 index a18a7ea..0000000 --- a/contracts/v9/scenario-stream.schema.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v9/scenario-stream.schema.json", - "title": "Trading Engine v6 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": "9" }, - "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": "9" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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": "9" }, - "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/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", "max_internal_events"], - "properties": { - "metadata": { "type": "object" }, - "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/identifier" }, - "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/identifier" }, - "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/initialPortfolio" }, - "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/instrument" } }, - "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/venueCalendar" } }, - "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/risk" }, - "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/execution" }, - "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/v9/scenario.schema.json#/$defs/marketSlice" }, - "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json#/$defs/intent" } } - } - } - } -} - diff --git a/contracts/v9/scenario.schema.json b/contracts/v9/scenario.schema.json deleted file mode 100644 index e90b4d8..0000000 --- a/contracts/v9/scenario.schema.json +++ /dev/null @@ -1,471 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/fallblu/trading-engine/contracts/v9/scenario.schema.json", - "title": "Trading Engine v6 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", "max_internal_events", "schedule", "slices"], - "properties": { - "contract_version": { "const": "9" }, - "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" }, - "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": { - "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": { - "type": "object", - "additionalProperties": false, - "required": ["model", "configuration"], - "properties": { - "model": { "const": "completed_bar_v1" }, - "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } - } - }, - "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" } } - } - }, - "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"], - "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" } } - } - }, - "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" } - } - } - ] - } - } -} diff --git a/docs/api-reference.md b/docs/api-reference.md index d7b9192..090341d 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/v16/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v1/README.md) remain authoritative for process boundaries. diff --git a/docs/architecture.md b/docs/architecture.md index 8f6357f..afb946d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -124,7 +124,7 @@ Running the same scenario bytes produces byte-identical audit lines. `Engine.Interactive` stops at each strategy request and exposes the immutable context and event. Its `resume` transition accepts typed intents and continues the same pure reducer. The scripted runner invokes an in-process callback at that boundary. The external runner serializes it through -protocol v4. Reducer state never contains a process, clock, pipe, timeout, or file handle. +protocol v1. Reducer state never contains a process, clock, pipe, timeout, or file handle. Each strategy callback carries an account valuation built at that reducer boundary. All callbacks for a slice use its receipt time, completed bars, and FX vector. A callback response is reduced diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index dbe9e62..cd6e70a 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -1,35 +1,21 @@ # Continuous integration -CI tests a small, explicit environment matrix instead of an accidental Cartesian product. The -public package bounds in `trading_engine.opam` define supported dependencies. The repository lock -defines the reproducible development baseline. +CI tests the supported OCaml and dependency range without creating a Cartesian product. -| Cell | Operating system | OCaml | Dependencies | Gate | Status | -| --- | --- | --- | --- | --- | --- | -| `check` | Ubuntu latest | 5.5.0 | Exact lock | Full `make check` | Required | -| `lowest-ubuntu` | Ubuntu latest | 5.5.0 | Oldest solver-valid versions inside declared bounds | Full dependency-band check | Required | -| `highest-ubuntu` | Ubuntu latest | 5.5.0 | Newest solver-valid versions inside declared bounds | Full dependency-band check | Required | -| `highest-macos` | macOS 15 | 5.5.0 | Newest solver-valid versions inside declared bounds | Build and exact journal comparison | Informational | +| Cell | Platform | Dependencies | Gate | +| --- | --- | --- | --- | +| `check` | Ubuntu | Repository lock | Full `make check` | +| `lowest-ubuntu` | Ubuntu | Oldest declared versions | Dependency-band check | +| `highest-ubuntu` | Ubuntu | Newest declared versions | Dependency-band check | +| `highest-macos` | macOS | Newest declared versions | Build and journal comparison | -The lower and upper cells resolve against the current opam repository. They deliberately test the -range declared by the package rather than pretending to be reproducible locks. A failure in either -required Ubuntu cell means the declared support bounds or the implementation must change. The -macOS cell is an early portability signal while Ubuntu remains the supported build platform. +Required Ubuntu jobs validate contract v1 schemas and fixtures, OCaml tests, protocol fuzzing, +deterministic journals, metadata, documentation, and benchmark smoke workloads. Coverage runs once +against the locked environment. -Every runtime cell replays the frozen v3 demo, v5 demo, and v5 risk-limited fill scenarios under -`TZ=UTC` and the C locale. It compares the resulting journal files byte for byte with their -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 v16 batch, stream, journal, and -strategy-v12 fixtures, including quote/trade causality and the reconciled first valuation. +The required Persistra job uses a full pinned commit. A manually dispatched moving-head job is +informational and may fail without changing the supported revision pair. -Coverage runs once in the exact locked Ubuntu environment. The required Persistra job also runs -once against its full pinned commit; it is not repeated across dependency or operating-system -cells. The manually dispatched Persistra moving-head job remains informational. - -Feature-branch pushes do not start CI; the pull-request event owns that validation and avoids a -duplicate check set. Pull requests cancel superseded commits. Push validation runs only on -`develop` and tags, where it is never cancelled, so durable integration evidence is not discarded. -The concurrency key combines the source repository and source branch without colliding with a fork -or another branch. +Pull requests own feature-branch validation and cancel superseded runs. Push validation runs on +`develop` and release tags without cancellation so integration and publication evidence is +retained. diff --git a/docs/documentation-platform.md b/docs/documentation-platform.md index 787fc7a..87c152e 100644 --- a/docs/documentation-platform.md +++ b/docs/documentation-platform.md @@ -13,8 +13,8 @@ live in `requirements/docs.lock`; the exact odoc version lives in `trading_engin The build stages the repository Markdown and entire `contracts/` tree under `_build`, adds the odoc HTML tree, and then runs `mkdocs build --strict`. Staging publishes contract README files, schemas, -and fixtures directly from their source locations. Frozen v1 and v2 pages therefore keep stable -versioned URLs and cannot diverge from the repository copies. +and fixtures directly from their source locations, so the v1 pages cannot diverge from the +repository copies. `make docs-check` performs the deterministic offline source check. `make docs-build` bootstraps the locked documentation tools, builds odoc, runs strict MkDocs, checks every generated local link, and diff --git a/docs/execution-model.md b/docs/execution-model.md index 157fcaf..745c039 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,283 +1,74 @@ # Execution model -The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v16 advertises `completed_bar_v1`, `completed_bar_next_open_v1`, -`completed_bar_adverse_touch_v1`, `quote_trade_v1`, and `order_book_v1`; embedders can inject another module through -the typed engine configuration without introducing runtime shared-library loading. The selected -name is repeated in both terminal audit records. +The scenario selects one compiled model by `execution.model`. Each model uses strict configuration +version `"1"`, declares its required fields through `--capabilities`, and shares the same order, +risk, fee, settlement, accounting, and audit pipeline. -Each compiled model owns a strict configuration contract. The v16 envelope separates selection from -model-specific parameters: +## Available models -```json -{ - "execution": { - "model": "completed_bar_v1", - "configuration": { - "version": "2", - "participation_bps": 5000, - "fee_schedules": [ - { - "schedule_id": "acme-fees-v1", - "instrument_id": "acme", - "settlement_currency": "USD", - "minimum": "0.3", - "maximum": "5", - "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" } - ] - } - ] - } - } -} -``` - -The model and configuration version are validated before replay. Unknown models, unsupported -model/version pairs, missing fields, and fields from another model are rejected. Contracts v3 and -v4 retain their frozen flat execution object; v8 and earlier configured envelopes remain frozen. - -`--capabilities` preserves the `execution_models` name list and publishes one deterministic -descriptor per model under `execution_model_contracts`: supported scenario and configuration -versions, required fields, order types, market-data requirements, and numeric limits. Clients can -therefore reject incompatible scenarios without guessing from a shared execution object. - -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. - -The quote/trade model also uses configuration version `"1"`, with `participation_bps` and the same -fee-schedule catalog. It consumes each slice's events in `(available_at, received_at, -ingest_sequence)` order. Market orders and marketable limits consume only the displayed quote size -on their side. Passive buys consume only sell-aggressor trades at or below their limit; passive -sells consume only buy-aggressor trades at or above it. An `unknown` aggressor never supplies a -passive fill. Each event has independent, lot-rounded capacity, and its `event_at` is the fill's -economic timestamp. Completed bars remain required solely for synchronized valuation. +| Model | Market evidence | +| --- | --- | +| `completed_bar_v1` | Next eligible open and optimistic intrabar limit touch | +| `completed_bar_next_open_v1` | Later marketable opens with explicit spread and impact | +| `completed_bar_adverse_touch_v1` | Opens or one-tick adverse trade-through with costs | +| `quote_trade_v1` | Displayed quotes and aggressor-classified trades | +| `order_book_v1` | Bounded level-two snapshots and contiguous updates | -The order-book model uses configuration version `"1"`, adding `max_depth_levels` from 1 through -1,024. Each instrument's slice-local bundle begins with a full bid/ask snapshot and uses contiguous -absolute set, delete, and trade updates. Crossed states, gaps, missing deletes, and states beyond -the depth limit fail replay; a locked best bid and ask is accepted. Each later slice starts from a -fresh snapshot, so no unbounded or hidden book state survives a slice boundary. +Completed bars remain mandatory for synchronized valuation. Conservative models require fixed +half-spread and linear participation-impact policies. Quote/trade and order-book models use causal +availability, receipt, and ingest ordering and never infer hidden liquidity. -Marketable orders walk observable opposite-side levels in price order. Passive limits start behind -the displayed quantity at their price and behind earlier engine orders. Reductions decrease queue -ahead, additions join behind, and an aggressor-qualified trade consumes queue ahead before filling -the order. This model has its own liquidity state and does not reuse completed-bar or quote/trade -fill semantics. Bars remain mandatory only for valuation. +## Eligibility and order lifetime -## Eligibility - -An order records the slice after which it is eligible. The matcher requires: +An order becomes eligible only when both conditions hold: ```text eligible_after_slice_sequence < current slice_sequence created_at <= current slice start_at ``` -This prevents an order emitted from a completed slice from filling inside that slice or at an open -that predates the order. The parser also requires scheduled order-changing intents to arrive by -the next slice start. - -## Portfolio targets - -`target_weights` and `target_quantities` contain one target for every configured instrument. A -weight request: - -1. Values the current account at all synchronized slice closes. -2. Multiplies that equity by each exact weight. -3. Divides by the corresponding closing price. -4. Rounds down to the instrument lot. - -Weights and quantities are signed. Gross absolute weight must stay within `max_leverage`, quantity -targets must align with their lots, and every desired quantity must stay within the configured long -or short position limit. A target that changes sign is reached causally: flatten first, then open -the opposite side on a later attempt. - -The computed desired quantities persist. After each slice, the engine compares them with actual -positions and submits at most one market order per instrument. Each order is capped at -`max_order_quantity` and rounded down to a lot, so large targets advance in bounded chunks. A -market remainder is IOC, but the desired target is retried after a later slice until reached or -superseded. +Market orders attempt the next eligible evidence and cancel any IOC remainder. Limit orders use +their configured time in force. Stop orders activate when their trigger is observed. FOK requires +the full quantity to pass liquidity and risk checks before any fill is applied. -## Working-order risk +Persistent portfolio targets are reconciled in lot-aligned, maximum-order-sized attempts until the +target is reached or replaced. A sign change flattens before opening the opposite side. -Pre-trade risk reserves each active order's unfilled quantity by side. For every instrument, it -values both the position after all reserved buys and the position after all reserved sells, then -uses the larger absolute endpoint for portfolio exposure. Opposing orders therefore cannot hide -risk by netting before either execution path is known. +## Capacity, priority, and callbacks -A new order is rejected while an active opposite-side order exists for the same instrument. This -self-cross rule applies to direct and target-generated orders. Same-side orders may coexist, and -their remaining quantities share the applicable long or short position limit. Risk-reducing orders -remain permitted when an actual position is already outside a limit, but one order may not cross -that position through zero. Fill-time position, exposure, leverage, and margin checks remain the -final defense against price and account changes after acceptance. +Completed-bar capacity is volume multiplied by `participation_bps`, rounded down to the instrument +lot. Quote and book models use only displayed or causally consumed liquidity. Liquidation orders +run first; within an origin class sells precede buys, followed by FIFO creation order. -## Market and limit prices - -A market order executes at the open of its first eligible slice. - -For a buy limit `L`: - -1. If `open <= L`, fill at `open`. -2. Otherwise, if `low <= L`, fill at `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 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: - -```text -raw capacity = floor(volume × participation_bps / 10,000) -capacity = raw capacity rounded down to the instrument lot size -``` +After each fill, the reducer applies the strategy callback response before examining the next +eligible order. A cancellation can therefore remove a later same-slice order. Newly submitted +orders wait for another slice. -Eligible liquidation orders are ordered before all other orders across the slice. Within the -liquidation and ordinary origin classes, sells precede buys; orders within a side then use -ascending creation sequence and order ID. Each instrument has its own shared capacity, so the -higher-priority order consumes that instrument's capacity first. +## Risk and fees -## Callback boundaries +Admission reserves every active order's remaining quantity. Fill-time checks use the actual price +and search for the largest permitted lot-aligned quantity under instrument position/notional, +portfolio gross exposure/leverage, margin, locate, and maximum-order limits. Exposure-reducing +fills remain available. A clipped proposal emits a typed `fill_clipped` reason. -The matcher fixes the eligible-order sequence at the slice boundary and advances it with an -immutable cursor. After each fill, the engine pauses matching and applies the strategy response -before examining the next order. The cursor then reads that order from the current OMS, skips it -if an earlier response made it terminal, and preserves any capacity that was not consumed. Orders -submitted by a callback are not part of the cursor and remain ineligible until a later slice. +Each instrument has exactly one fee schedule. Components may be fixed, notional basis points, or +per-unit; use explicit currency, rounding, and maker/taker applicability; and may include schedule +minimums, maximums, or rebates. FX conversion and every adjustment are retained in attribution. -## Corporate actions and borrow +## Financing, settlement, and lifecycle -Corporate actions are ordered by action ID and applied before matching. A split scales the signed -position, persistent quantity target, and each active order by its exact numerator/denominator -ratio. It inversely scales limit prices and preserves total position basis. If the adjusted order -cannot satisfy the configured lot or tick, the slice fails instead of silently rounding. Each -changed order emits `order_adjusted` with causal links to both the original order and split. -Unit-based risk limits do not scale with a split. Adjusted positions and persistent targets are -grandfathered: fills may reduce an out-of-limit absolute position but may not increase it, and -reconciliation orders remain bounded by the configured maximum order quantity. An adjusted active -order may exceed that maximum, but no individual fill may do so. +Effective-time observations drive short availability, borrow charges, recalls, and per-currency +credit or debit interest. Settlement instructions use explicit business calendars and lags, with +configured settled or total cash and position availability. -A cash dividend multiplies the pre-match signed position by its per-unit amount. It credits a long -or debits a short in the instrument's quote-currency ledger and records realized dividend P&L. -Stock dividends, rights, and spin-offs deliver a lot-aligned exact-ratio entitlement. Their payload -allocates basis explicitly and either rejects fractions or converts them at a declared -quote-currency price. Stock dividends also scale persistent targets and eligible working orders. - -Lifecycle events follow corporate actions and precede matching. Identifier changes preserve the -stable instrument ID while updating the symbol and named provider mapping. Halts and terminal -events cancel active orders. Expiration and delisting use an explicit hold or cash-out policy and -cannot be resumed. -Contract v10 replaces the fixed legacy rate with effective-time borrow observations. Each -observation names an instrument, available quantity, annual rate in basis points, and recall state. -Observations become active no later than the slice start and remain active until superseded. A new -short either clips to the available locate or is rejected according to `locate_policy`; existing -short quantity consumes availability. A recall rejects further shorts and, under `close_out`, -cancels active sells and submits a priority IOC buy until the short is flat. `reject_new_shorts` -retains the position but prevents it from increasing. - -Before matching, each open short accrues its observed quote-currency charge from the slice open -mark and exact `start_at`/`end_at` duration. Missing observations follow `borrow_missing_data`: -`reject` fails the slice and `zero` applies no charge while still preventing an unlocated new short. -Signed rates support rebates. Charges use the scenario's explicit `actual_365` or `actual_360` -day-count and `simple` or `daily` compounding policy. - -Cash financing uses effective-time observations per currency with separate annual credit and debit -rates. Positive balances receive the credit rate; negative balances receive the debit rate. The -same explicit interval, day-count, compounding, and deterministic micro-unit rounding rules apply. -`cash_missing_data` either rejects a nonzero balance without an observation or treats its rate as -zero. Interest updates the native cash ledger and is reported separately and within aggregate -realized P&L; debit interest can therefore produce or deepen negative equity. - -## Risk-limited fills and fees - -Contract v10 selects exactly one fee schedule per instrument. A schedule composes named `fixed`, -`notional_bps`, and `per_unit` components. Each component declares its currency, `up`, `down`, or -`nearest` rounding, and `any`, `maker`, or `taker` applicability. A limit filled at its intrabar -touch is maker liquidity; market orders and limits marketable at the open are takers. - -Component amounts are calculated in their declared currencies. Slice FX rates convert quote -notional into the component currency and each result back into the fill's quote currency. The -schedule then applies its optional minimum and maximum in the settlement currency. Any difference -is retained as a named `minimum_adjustment` or `maximum_adjustment`, so the component list always -sums exactly to the signed aggregate fill fee. Negative components are rebates. - -Liquidation proposals are processed first, followed by sells and then buys within each origin -class. For each proposal, the engine searches for the largest lot-aligned quantity whose signed -post-fill position is within the long/short cap and whose fill quantity is no greater than the -maximum order quantity. -When absolute exposure increases, the projected account must also satisfy maximum gross exposure, -maximum leverage, and initial margin. Reductions in absolute exposure are permitted without a new -initial-margin test. A clipped proposal emits `fill_clipped`; a zero permitted quantity produces -no fill. The event records reason taxonomy version `1`, the limiting policy, its typed threshold, -and both the proposed and permitted quantities. Only the applied quantity consumes shared slice -capacity. Candidate arithmetic, accounting, mark, and FX failures abort replay instead of being -misreported as policy clipping. - -This bounded-fill policy preserves split-adjusted GTC limit orders: an oversized remainder may -fill over multiple slices. Market orders remain IOC, so they fill at most one bounded quantity and -cancel any remainder after their eligible slice. - -The complete schedule, including minimum and maximum, is evaluated independently for every partial -fill, so fragmentation can change total cost. Contract v8 configuration v1 retains its frozen -`fixed_fee + ceil(notional × fee_bps / 10,000)` rule. - -## Exact values - -Prices, weights, quantities, FX rates, and money use six decimal places stored in checked `int64` -values. Signed quantities are used for positions and targets; submitted orders and fills retain a -positive quantity plus a side. Scenario strings use the canonical shortest representation: `1`, -`1.25`, `-0.5`, and `0.000001` are valid; `01`, `1.0`, excess precision, and negative zero are not. - -Orders align with lot size. Limit prices and executable OHLC values align with tick size. +Corporate actions run before matching. Splits adjust positions, targets, and working orders; +distributions allocate basis and fractional treatment explicitly. Lifecycle events preserve stable +instrument identity across symbol changes and deterministically cancel or cash out terminal assets. ## Accounting and valuation -For a buy that opens or increases a long with notional `N` and fee `F`: - -```text -cash -= N + F -quantity += fill quantity -cost basis += N + F -``` - -For a sell that reduces a long: - -```text -cash += N - F -removed basis = proportional average cost -realized P&L += N - F - removed basis -``` - -Opening a short credits `N - F` to cash and records its cost basis as the negative net proceeds. -Covering a short debits `N + F`; realized P&L is the removed negative basis minus that cover cost. -One fill may reduce a position to zero but may not cross through zero. Closing a position removes -its exact remaining basis. A partial close uses proportional average basis and leaves the exact -remainder open. - -The account maintains a signed cash ledger for every scenario currency. Each slice supplies a -complete currency-to-base FX vector, with base rate one. Valuation converts native cash, market -value, basis, P&L, and fees into the base reporting currency using the current marks: +The engine uses signed average-cost accounting in native quote currencies and converts every cash, +position, basis, P&L, and fee attribution to the scenario base currency. The core identities are: ```text net market value = sum(base FX × mark × signed quantity) @@ -286,18 +77,5 @@ unrealized P&L = net market value - remaining base cost basis equity = base cash + net market value ``` -Each valuation also emits one deterministic attribution row per marked instrument or retained -account position. A nonzero position requires a mark. A flat retained position does not; when its -mark is omitted, the row uses the canonical mark one because every mark produces zero market value -for zero quantity. Row market value, basis, realized P&L, dividend P&L, execution fees, and borrow -fees is present in both native and base values and sums exactly to the corresponding account totals. -A separate row attributes each currency ledger. Closed instruments retain cumulative realized P&L -and fees with zero quantity and basis. - -The valuation includes initial and maintenance requirements and excesses. After strategy and -target processing, negative maintenance excess triggers one `margin_call`, cancels all active -orders, clears the persistent target, and submits deterministic `margin_liquidation` market orders -in instrument-ID order. Strategies receive the resulting cancellation and liquidation-order -updates before the slice valuation. Each attempt is capped by `max_order_quantity` and lot aligned. -The engine continues on later slices until every position is flat, then emits `margin_restored` -when the maintenance condition is no longer breached. +Valuations include initial and maintenance margin. A maintenance breach cancels working orders, +clears targets, and creates bounded liquidation orders until positions are flat. diff --git a/docs/persistra.md b/docs/persistra.md index 8ebacb5..88070e9 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -1,130 +1,46 @@ # Persistra integration -Persistra and Trading Engine remain separate projects behind a strict process and file boundary. +Persistra and Trading Engine communicate only through versioned files and processes. -Persistra owns provider data, normalized observations, revisions, point-in-time research, -portfolio construction, manifests, analysis, and visualization. Trading Engine owns causal event -sequencing, target sizing, risk, order and fill state, execution simulation, exact accounting, and -execution audit artifacts. +Persistra owns provider ingestion, normalized point-in-time observations, research, portfolio +construction, and run manifests. Trading Engine owns causal sequencing, risk, orders, simulated +execution, financing, settlement, accounting, and audit journals. The engine never reads +Persistra's internal database. ## Handoff -The JSON scenario carries: +Persistra produces scenario contract v1 batch or stream input and should: -- Required `contract_version` identifying the scenario and journal protocol -- Required producer metadata preserved as JSON but ignored by execution -- Required compiled execution-model selection -- One explicit executable-instrument catalog -- Signed position, exposure, leverage, margin, borrow, participation, and fee policies -- Strictly increasing synchronized market slices with complete FX marks and corporate actions -- Effective-time borrow availability, recall, and per-currency credit/debit rate observations -- Explicit signed initial cash and positions with accounting history, marks, and FX state -- Optional scheduled full-portfolio signed weight or fractional quantity targets -- Optional direct orders, cancellations, and metrics +1. Preserve stable executable-instrument identities and provider provenance. +2. Supply complete tick, lot, currency, calendar, risk, execution, financing, and settlement + configuration. +3. Build ordered synchronized slices with explicit availability and receipt times. +4. Preserve target weights instead of pre-sizing them outside the engine. +5. Validate schemas and run `--validate-only` before accepting a replay. +6. Require v1 in the engine's advertised scenario and journal capabilities. +7. Retain the exact scenario hash and require one terminal `run_completed` record. +8. Reconcile journal cash, positions, exposure, P&L, fees, margin, and causal references. -Persistra should: +External strategies use [strategy protocol v1](../contracts/strategy/v1/README.md). Persistra must +retain the scenario, transcript, journal, executable identity, input hashes, and run manifest as one +bound artifact set. -1. Query normalized raw executable bars through its public store API. -2. Preserve provider-scoped instrument IDs or apply an explicit catalog mapping. -3. Supply tick, lot, currency, availability, and receipt policies. -4. Group one bar per instrument into each synchronized slice. -5. Preserve original portfolio weights in `target_weights` instead of pre-sizing them. -6. Populate `metadata` with dataset, policy, and build provenance. -7. Read `--capabilities` and require support for the scenario, journal, and optional strategy - protocol versions. -8. Validate the scenario through the JSON Schema and `--validate-only`. -9. Run the CLI as a separate process and import its audit journal. -10. Require the same contract version and deterministic run-scoped event-ID derivation on every - journal record. -11. Reject duplicate, unknown, forward, cross-run, or noncanonical causal references. -12. Verify the same scenario SHA-256 and selected execution model in `run_started`, - `run_completed`, and the retained manifest. -13. Reconcile every native/base position row and currency cash row to the aggregate account, - exposure, fee, and margin values. -14. Reconcile split adjustments, dividends, borrow and cash financing, recalls, risk-limited fills, - and margin liquidation against scenario and runtime state. -15. For external replay, require an empty schedule, launch an explicit strategy argument vector, - hash every declared strategy input, and validate the complete bidirectional transcript. -16. Reconcile external transcript intents to their journal outcomes. -17. Require the terminal completion record before accepting a replay. +## Compatibility gate -Do not let the engine read Persistra's internal DuckDB tables. Their schema and connection -lifecycle belong to Persistra. +Compatibility means the v1 wire contracts pass against an explicit pair of repository commits. +The required `persistra-compatibility` CI job pins the complete Persistra revision in +`.github/workflows/ci.yml`; Persistra owns the reciprocal Trading Engine pin. Neither repository +silently follows a moving branch for its required gate. -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 v16 while retaining v15 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. +Advance a pin only after both exact checkouts pass their native and cross-repository suites. When +the shared boundary changes, update schemas, fixtures, documentation, and pins together. The +optional moving-head canary is informational and does not change the supported baseline. -External strategies use the separate -[strategy protocol v14](../contracts/strategy/v14/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, -scenario hash, and journal hash. Strategy standard output remains protocol-only; logs and -diagnostics use standard error. +## Time and data rules -Persistra must answer each callback before the engine continues matching. Every callback for a -slice uses the slice receipt time and complete bar and FX snapshot. A later callback therefore -includes accepted intents returned from an earlier callback at that same replay clock. +Use raw prices for execution. Adjusted data may feed research features, but splits and +distributions must be explicit engine events. Provider as-of and Persistra retrieval times remain +provenance; `available_at` and `received_at` define replay causality. -## Compatibility guarantees - -Compatibility is defined by versioned wire contracts and an explicitly tested pair of repository -revisions. A branch name, package version, or successful build in only one repository is not a -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 v16 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 v13; a host must complete its exact initialization, event, - shutdown, timeout, and rejection lifecycle. -- **Persistra:** The required integration gate uses a full Persistra commit and its v3 scenario, - journal, and strategy integration tests. Passing that gate claims compatibility only for the - recorded revision pair and advertised versions. - -The required `persistra-compatibility` job pins the full Persistra commit stored as -`PERSISTRA_COMPAT_REVISION` in `.github/workflows/ci.yml`. It asserts the resolved checkout and -writes the SHA to the log and job summary. It never follows a repository variable or moving branch. -Persistra owns the reciprocal required pin to a reviewed Trading Engine commit. - -To advance either baseline, the repository changing its pin selects a green full commit from the -other repository, builds both exact checkouts, runs the cross-repository integration suite, and -updates the one workflow SHA in a reviewed pull request. When a contract or host/runtime behavior -changes, both repositories update their fixtures, documentation, and pins in dependency order. -Neither repository silently advances the other's required baseline. - -Maintainers can manually dispatch CI with `persistra_latest_head` enabled to test Persistra -`develop`. The `persistra-latest-head` job is nonrequired and allowed to fail, so it provides an -early signal without changing the reproducible baseline or blocking an unrelated engine change. - -## Time mapping - -- Intraday UTC timestamps map to slice event times. -- Daily labels require an explicit venue-calendar delivery policy. -- Provider as-of time remains source provenance. -- Persistra retrieval time remains acquisition provenance, not replay availability. -- `available_at` states when a strategy may use the complete synchronized slice. -- `received_at` states when the engine run observes it. -- A scheduled order-changing intent must arrive no later than the next slice start. - -Use raw prices for execution. Adjusted values can feed features, but splits and dividends require -explicit engine events before adjusted histories can support share-and-cash accounting. - -## Larger artifacts - -JSON is suitable for small and moderate scenarios. Use the versioned JSON Lines scenario stream -for larger histories. It carries one static header, one slice with its causally adjacent intents -per record, and a required terminal count. The engine validates and replays it with bounded input -and audit memory. Persistra should retain and hash that immutable stream beside the journal and -run manifest. The stream remains a file boundary; it does not couple the engine to Persistra's -database tables. +Use the JSON Lines scenario stream for larger histories. It remains an immutable file boundary, +not a database coupling. diff --git a/docs/scenario.md b/docs/scenario.md index a15d7a3..951e6fb 100644 --- a/docs/scenario.md +++ b/docs/scenario.md @@ -1,340 +1,73 @@ -# Scenario contract +# Scenario and journal -A replay scenario uses either one strict JSON object or a strict JSON Lines stream. Exact prices, -weights, quantities, money, and sequences are canonical JSON strings. Counts and basis points are -JSON integers. Unknown, missing, duplicate, noncanonical, and non-finite values fail parsing. - -Use [the v16 demo](../contracts/v16/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v16/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. +Replay input is either one strict JSON scenario or an equivalent JSON Lines stream. The +[v1 schema](../contracts/v1/scenario.schema.json) and +[canonical fixture](../contracts/v1/fixtures/demo.scenario.json) define the batch form. ```sh trading-engine --input scenario.json --validate-only trading-engine --input scenario.jsonl --input-format jsonl --validate-only ``` -Use `--input - --input-format jsonl` to read a stream from standard input. The CLI spools at most -1 GiB to a private temporary file so the same bytes can be hashed, validated, replayed, and hashed -again. Batch JSON cannot use standard input. - -## JSON Lines stream - -Use the stream for histories that should not be materialized inside the engine. The first record -is `scenario_header` and carries the static top-level fields. Each `market_slice` record carries -one complete slice and the intents evaluated after that slice. The final `scenario_end` record -declares the number of slices. It is required even for an empty stream, so a truncated valid -prefix cannot be mistaken for a complete scenario. - -Every record has exactly `contract_version`, `scenario_sequence`, `record_type`, and `payload`. -The contract version is repeated, and `scenario_sequence` is contiguous from one. Intents are -adjacent to their decision slice rather than stored in a future-looking global schedule. Before -replay, the reader checks each intent-bearing slice against the next slice's start time while -retaining only those two records. - -The batch object and stream header share one domain-construction path and the same static semantic -checks. Stream items reuse the batch slice and intent validators directly; no synthetic batch -scenario is constructed. - -The [stream record JSON Schema](../contracts/v16/scenario-stream.schema.json) validates each line, -and [the v16 stream fixture](../contracts/v16/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. - -## Top-level fields - -| Field | Meaning | -|---|---| -| `contract_version` | Required string identifying this file contract; v16 is `"16"` | -| `metadata` | Required arbitrary JSON object preserved for provenance and ignored by execution | -| `run_id` | Stable identity used in generated IDs | -| `base_currency` | Reporting currency used for aggregate risk and valuation | -| `initial_portfolio` | Signed cash and positions with accounting history, marks, and FX state | -| `instruments` | Approved executable-instrument catalog, at most 4,096 entries | -| `venue_calendars` | Immutable venue/session policies covering every configured instrument | -| `risk` | Signed position, exposure, leverage, margin, and borrow policy | -| `execution` | Capacity and fee configuration | -| `financing` | Borrow/cash day-count, compounding, locate, recall, and missing-data policies | -| `settlement` | Business-date calendars, per-instrument lags, and cash/position availability policies | -| `max_internal_events` | Positive reducer feedback cap, at most 100,000 | -| `schedule` | Intents emitted after named slices, at most 4,096 per batch | -| `slices` | Complete synchronized market observations | - -Metadata may contain nested JSON values. Duplicate object keys and non-finite numbers are rejected -at any depth. Metadata is retained on `Scenario.t` but never affects execution. - -An external strategy replay requires `schedule: []`. The JSON Lines form likewise requires every -slice record's `intents` array to be empty. This keeps one authoritative decision source: either -the scenario contract or the separate strategy protocol, never both. - -Each JSON Lines record is limited to 1 MiB, excluding its line feed. The reader accepts a final -record without a line feed and drains an oversized record without retaining bytes above the limit. - -Use `--journal -` to write a journal to standard output. The engine first creates and verifies a -complete temporary journal, then copies only journal bytes to the pipe. The final `run_completed` -record and a zero process exit status signal completeness. Success summaries move to standard error, -and diagnostics always use standard error, so protocol, journal, and summary bytes never share one -stream. Pipes do not provide exclusive no-replace publication, atomic linking, retained partial -artifacts, directory synchronization, or restart durability. They cannot be combined with -`--durable-artifacts`. - -## Instruments, risk, and execution - -Each instrument contains `instrument_id`, `symbol`, `quote_currency`, `tick_size`, and `lot_size`. -Identifiers and labels are nonempty and contain no whitespace or control characters. Tick and lot -sizes are positive exact values with at most six decimal places. Quote currencies may differ from -`base_currency`; `initial_portfolio.cash` contains every distinct quote currency plus the base -currency exactly once. - -## Initial portfolio - -The v6 `initial_portfolio` contains `cash`, `positions`, `marks`, and `fx_rates`. Cash is signed and -has exact scenario-currency coverage. Each nonzero signed position names a catalog instrument and -records signed `quantity` and `cost_basis`, signed `realized_pnl` and `dividend_pnl`, and -nonnegative `execution_fees` and `borrow_fees`. Basis has the same sign as quantity. These P&L and -fee values are point-in-time histories; importing them does not apply them to cash again. - -Position quantities align to instrument lots and respect long and short limits. Marks cover the -position set exactly, are positive, and align to instrument ticks. FX rates cover every scenario -currency exactly and the base rate is one. Before replay, the engine constructs the account, -reconciles its valuation, and enforces gross exposure, leverage, and initial margin. It accepts -negative cash when the complete marked account remains valid under the configured risk policy. - -## Venue calendars - -Contract v7 requires every instrument to belong to exactly one explicit venue calendar. A calendar -is identified by `venue_id`, `calendar_id`, and `calendar_version`; version 1 is the only supported -calendar payload. Its `sessions` are unique and ordered by `session_date`, and each date declares -one policy: `regular`, `early_close`, or `holiday`. Holidays have no phases. Open sessions must -contain a `regular` phase and may also contain `premarket`, `opening_auction`, `closing_auction`, -and `postmarket` phases in market order. Phase intervals cannot overlap. - -All phase boundaries are absolute RFC 3339 timestamps. Scenario producers, not the reducer, resolve -venue-local civil times, time-zone database versions, daylight-saving changes, and clock effects. -Calendar lookup rejects a date without an explicit policy; it never infers weekends, holidays, or -hours from adjacent entries. This makes future DAY expiry, auction eligibility, settlement, and -daily-bar publication policies depend on versioned input rather than ambient system state. - -Risk contains portfolio-wide positive `max_gross_exposure` and `max_leverage`, annualized -`short_borrow_bps`, exactly one `instrument_policies` entry per catalog instrument, and an explicit -`groups` array. Instrument policies define order, long, short, notional, initial-margin, -maintenance-margin, and shorting limits. Groups carry versioned identities and explicit membership; -they may overlap and can constrain gross, long, short, absolute net, and gross-to-equity -concentration exposure. Admission and fill clipping include working-order reservations. Every -applicable group is enforced, with group identity providing deterministic tie ordering. - -Contract v10 execution contains a stable `model` and a model-owned `configuration`. For -`completed_bar_v1`, configuration version `"2"` contains: - -- `version`, the strict model-configuration contract version -- `participation_bps`, from 0 through 10,000 -- `fee_schedules`, exactly one schedule per instrument. Each schedule has a stable ID, instrument, - settlement currency, nullable minimum and maximum, and one or more named components. - -Each component declares `currency`, `kind` (`fixed`, `notional_bps`, or `per_unit`), a signed -`value`, `rounding` (`up`, `down`, or `nearest`), and `applies_to` (`any`, `maker`, or `taker`). -Signed values permit rebates. Minimums and maximums are nonnegative and apply per fill after the -component values are converted into the settlement currency. - -The engine advertises each model's scenario and configuration versions, required fields, supported -order types, data requirements, and limits through `--capabilities.execution_model_contracts`. The -v8 and earlier contracts retain completed-bar configuration version `"1"`; v3 and v4 preserve -their flat execution object unchanged. - -Contract v13 introduced `completed_bar_next_open_v1` and -`completed_bar_adverse_touch_v1`, each with strict configuration version `"1"`. They retain -`participation_bps` and `fee_schedules`, and additionally require: - -- `spread_model`: `fixed_half_spread_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: - -```json -{ - "after_slice_sequence": "1", - "intents": [ - { - "type": "target_weights", - "targets": [ - { "instrument_id": "asset-a", "weight": "0.6" }, - { "instrument_id": "asset-b", "weight": "0.3" } - ] - } - ] -} -``` - -Supported intents are: - -- `target_weights` with a `targets` array of `instrument_id` and `weight` -- `target_quantities` with a `targets` array of `instrument_id` and `quantity` -- `submit_order` with instrument, side, quantity, kind, and nullable limit price -- `cancel_order` with a deterministic `order_id` -- `emit_metric` with a bounded string `name` and typed `value`. The value object declares - `numeric` (a canonical decimal string), `string`, or `boolean`. Optional `unit`, `aggregation` - (`last`, `sum`, `minimum`, `maximum`, or `mean`), and up to 16 string dimensions carry - reconciliation metadata. Dimension keys are unique and journal encoding sorts them - lexicographically. Contracts through v15 retain the legacy string-only shape. - -Both target forms contain every configured instrument exactly once. Weights and quantities are -signed. Gross absolute weight must not exceed `max_leverage`; quantity targets align to their -instrument lots and stay within the long and short position limits. A rebalance that crosses from -long to short, or short to long, first flattens the existing position and continues toward the -target on a later attempt. - -A market submission uses `"order_kind": "market"` and `"limit_price": null`. A limit submission -uses `"order_kind": "limit"` and a canonical positive price. Static order size, lot, and tick -checks run during parsing; position and outstanding-order checks run in the reducer. - -## Market slices - -Each slice has common timing, one bar per configured instrument, a complete set of currency-to-base -FX marks, zero or more corporate actions, and effective-time borrow and cash-rate observations: - -Timestamps use `YYYY-MM-DD[Tt]HH:MM:SS`, optional one-to-six fractional-second digits, and either -`Z`/`z` or a colonized numeric offset such as `-05:00`. Seconds range from `00` through `59`. -Audit timestamps use the same boundary. - -```json -{ - "slice_sequence": "1", - "start_at": "2026-01-02T14:30:00Z", - "end_at": "2026-01-02T21:00:00Z", - "available_at": "2026-01-02T21:00:01Z", - "received_at": "2026-01-02T21:00:02Z", - "bars": [ - { - "instrument_id": "asset-a", - "open": "100", - "high": "105", - "low": "99", - "close": "104", - "volume": "100" - } - ], - "fx_rates": [ - { "currency": "USD", "rate": "1" } - ], - "corporate_actions": [], - "borrow_observations": [ - { "instrument_id": "asset-a", "effective_at": "2026-01-02T14:30:00Z", "available_quantity": "1000", "annual_rate_bps": 100, "recalled": false } - ], - "cash_rate_observations": [ - { "currency": "USD", "effective_at": "2026-01-02T14:30:00Z", "credit_rate_bps": 100, "debit_rate_bps": 200 } - ], - "settlement_failures": [] -} -``` +## Scenario structure -Use `null` volume when unavailable; it means unlimited simulation capacity, not zero. Sequences -are positive and strictly increasing. Slices do not overlap: each start is at or after the prior -end, so equal boundaries are valid. Receipt time never moves backward. Start precedes end, -availability does not precede end, and receipt does not precede availability. OHLC values satisfy -their usual range relationships. Volume may be fractional but must align to the instrument lot. -Each slice supplies exactly one positive FX rate for every scenario currency, and the -base-currency rate is exactly one. +Every scenario carries `"contract_version": "1"` and these top-level fields: -Financing observations are unique per instrument or currency within a slice, effective no later -than the slice start, and strictly advance the effective time for their key across slices. A recall -has zero available quantity. The latest observation remains active until replaced. The top-level -`financing` object selects `actual_365` or `actual_360`, `simple` or `daily`, `reject` or `zero` -missing-data handling, `reject_order` or `clip_fill` locate behavior, and -`reject_new_shorts` or `close_out` recall behavior. +| Field | Purpose | +| --- | --- | +| `metadata` | Producer and dataset provenance; ignored by execution | +| `run_id` | Stable namespace for generated identities | +| `base_currency` | Aggregate reporting currency | +| `initial_portfolio` | Cash, positions, marks, FX, basis, P&L, and fee state | +| `instruments` | Executable catalog with tick, lot, and quote currency | +| `venue_calendars` | Explicit sessions covering every instrument | +| `risk` | Instrument policies, groups, gross exposure, leverage, and borrow limits | +| `execution` | Compiled model and strict v1 model configuration | +| `financing` | Borrow and cash-rate policies | +| `settlement` | Calendars, lags, and cash/position availability | +| `max_internal_events` | Reducer feedback limit | +| `schedule` | Intents emitted after specified slices | +| `slices` | Synchronized market and lifecycle observations | -The v12 `settlement` object selects `total_cash` or `settled_cash` buying power and -`total_positions` or `settled_positions` availability. Its immutable calendars contain ordered -canonical business dates, and each instrument has exactly one calendar and a lag from zero through -30 business days. A fill updates economic accounting immediately and creates a deterministic -instruction. Pending cash and quantity appear as unsettled attribution until the first slice on or -after the due date. A due instruction named in that slice's `settlement_failures` becomes failed -instead, retains its unsettled balances, and records the supplied reason. +Decimals are canonical strings with at most six fractional digits. Counts and basis points are +JSON integers. Unknown fields, duplicate keys, non-finite values, missing catalog coverage, +misaligned ticks or lots, invalid timestamps, and noncausal schedules are rejected. -Supported corporate actions are exact-ratio `split`, per-unit `cash_dividend`, `stock_dividend`, -`rights`, and `spin_off` records. Distribution records name a destination instrument, entitlement -ratio, basis allocation in basis points, and a fractional policy. `reject` fails on a non-lot -entitlement; `cash_in_lieu` requires an explicit destination-quote-currency price and journals the -delivered quantity, fractional quantity, allocated basis, fractional basis, and cash amount. Action -IDs are unique across the scenario. Actions are applied in canonical ID order before borrow fees -and matching. A split rescales the position, persistent target, and active orders while preserving -basis; it does not rescale unit-based risk limits. Split-adjusted positions and targets are -grandfathered under the existing reduce-only position policy. Split-adjusted orders remain active, -but each fill is bounded by `max_order_quantity`; GTC limit remainders may fill on later slices, -while market IOC remainders are cancelled. A dividend changes the quote-currency cash ledger and -realized dividend P&L, crediting a long and debiting a short. +Each slice has explicit start, end, availability, and receipt instants; complete bars and FX marks; +and optional corporate actions, financing observations, settlement failures, lifecycle events, +quotes, trades, and order-book events. Slices are strictly ordered and cannot overlap. -Version 12 slices also carry `lifecycle_events`. Stable `instrument_id` never changes. An -`identifier_change` updates the current symbol and one named provider mapping with provenance; -`halt` and `resume` control whether new exposure is accepted. `expiration` and `delisting` are -terminal and require either `hold` or an explicit quote-currency `cash_out` price. Halts and -terminal events cancel active orders. Terminal events set persistent target exposure to zero and -cash-out clears the position with exact realized-P&L attribution. +## Stream form -Version 14 slices add `market_events`. A quote records bid/ask prices and displayed quantities; a -trade records price, quantity, and `buy`, `sell`, or `unknown` aggressor side. Every event also -records `event_at`, `available_at`, `received_at`, and a positive `ingest_sequence`. Events are -strictly ordered by availability, receipt, and ingest sequence; economic time cannot follow -availability, and no event may escape its containing slice's time or observability boundary. -Prices and quantities align to the instrument tick and lot. The -[`quote-trade` fixture](../contracts/v16/fixtures/quote-trade.scenario.json) demonstrates passive -fills and has an equivalent bounded JSON Lines replay. +The [stream schema](../contracts/v1/scenario-stream.schema.json) defines three record types: -Version 15 slices add `order_book_events`. Every configured instrument supplies a fresh full -snapshot followed by contiguous absolute `set`, `delete`, and aggressor-classified `trade` updates. -Snapshots contain price-ordered unique bid and ask levels. Crossed states are invalid, while locked -books are accepted. Runtime validation enforces the configured `max_depth_levels`, known levels on -delete, sequence continuity, slice observability, and tick/lot alignment. Marketable orders walk -the visible book; passive limits queue behind displayed same-price depth, with reductions moving -them forward and additions joining behind. The -[`order-book` fixture](../contracts/v16/fixtures/order-book.scenario.json) demonstrates bounded -queue replay and has equivalent JSON Lines and journal artifacts. +1. `scenario_header` contains the static configuration. +2. Each `market_slice` contains one slice and its causally adjacent intents. +3. `scenario_end` declares the final slice count. -For causal next-open execution, an order-changing schedule entry's anchor `received_at` is no later -than the next slice `start_at`. +Every record repeats contract v1 and has a contiguous sequence. The engine validates the complete +stream before creating a journal, then replays it without retaining prior slices or audit events. +Standard input is spooled to a bounded private file so the exact bytes can be validated, hashed, +replayed, and verified again. -## Audit journal +## Strategy intents -The [journal JSON Schema](../contracts/v16/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 -so a journal remains self-describing when it is streamed or split. +Scheduled and external strategies may submit full-catalog target weights or quantities, direct +orders, cancellations, and typed metrics. An intent produced after slice `n` cannot execute inside +that slice. External strategy scenarios use an empty schedule so there is only one decision source. -The first record is `run_started` with `scenario_sha256` and the selected execution model. In v6, -`initial_state` then records the imported portfolio and reconciled valuation, followed by an -initial `valuation`; both precede the first market slice. The CLI hashes the exact batch document -or stream bytes it parses. -`market_slice_received` contains the complete normalized slice. Portfolio requests record their -basis, original weight when applicable, computed quantity, and sizing reference price. Orders use -`eligible_after_slice_sequence`; fills use `slice_sequence`. `fill_clipped` records the proposed -fill and the greatest lot-aligned permitted quantity. Its reason taxonomy version `1` names one of -`max_order_quantity`, `max_long_position`, `max_short_position`, `max_gross_exposure`, -`max_leverage`, `initial_margin`, or `instrument_borrow_availability` and carries a quantity, money, -ratio, or basis-points threshold. -Each order snapshot retains both creation and latest-update event IDs. +## Journal -The journal also records split/dividend/distribution application, lifecycle transitions, -action-driven order adjustments, observed -borrow charges, recalls and close-outs, cash-interest entries, margin calls, liquidation-origin -orders, and restoration. Every valuation contains complete -per-currency cash attribution, signed per-instrument native and base-currency attribution, long, -short, net, and gross exposure, execution and borrow fees, and its initial/maintenance margin -snapshot. Those rows reconcile exactly to the aggregate valuation. +The [journal schema](../contracts/v1/journal.schema.json) defines each append-only record. Every +record carries contract v1, a deterministic event ID, ordered prior causation IDs, the run ID, +recording time, event type, and strict payload. -A successful replay ends with exactly one `run_completed` record containing the same scenario -hash, reconciled valuation, and mutually exclusive order-status counts. A journal without that -terminal record is incomplete. The requested journal path appears only after exclusive successful -finalization; a failed run retains the `.partial` artifact. +A journal begins with `run_started`, `initial_state`, and an initial valuation. It then records the +normalized slices and all strategy, risk, order, execution, financing, settlement, lifecycle, +accounting, and valuation outcomes. Each completed slice emits one closing valuation. Successful +runs end with one `run_completed` record containing the scenario hash, final valuation, and order +counts. -`--durable-artifacts` synchronizes staged contents before publication and containing-directory -metadata after publication and partial cleanup. The filesystem must support hard links plus file -and directory synchronization. Unsupported durability operations fail with `artifact.io` and do -not silently fall back to buffered publication. +The requested path is published only after the terminal record is closed successfully. Failures +retain the `.partial` artifact. `--durable-artifacts` additionally synchronizes file and directory +metadata on supported filesystems. diff --git a/lib/audit.ml b/lib/audit.ml index 6145ce3..36a578d 100644 --- a/lib/audit.ml +++ b/lib/audit.ml @@ -73,13 +73,6 @@ type event = | Settlement_instruction_created of Settlement.instruction | Settlement_completed of Settlement.instruction | Settlement_failed of Settlement.instruction - | Margin_limited of { - order_id : Id.Order.t; - instrument_id : Id.Instrument.t; - requested_quantity : Scalar.Quantity.t; - permitted_quantity : Scalar.Quantity.t; - price : Scalar.Price.t; - } | Fill_clipped of { order_id : Id.Order.t; instrument_id : Id.Instrument.t; @@ -88,16 +81,6 @@ type event = price : Scalar.Price.t; limit : Risk.fill_limit; } - | Borrow_fee_applied of { - instrument_id : Id.Instrument.t; - quote_currency : string; - short_quantity : Scalar.Quantity.t; - reference_price : Scalar.Price.t; - borrow_bps : int; - period_start : Ptime.t; - period_end : Ptime.t; - fee : Scalar.Money.t; - } | Borrow_charge_applied of { observation : Financing.borrow_observation; quote_currency : string; @@ -199,9 +182,7 @@ let event_name = function | Settlement_instruction_created _ -> "settlement_instruction_created" | Settlement_completed _ -> "settlement_completed" | Settlement_failed _ -> "settlement_failed" - | Margin_limited _ -> "margin_limited" | Fill_clipped _ -> "fill_clipped" - | Borrow_fee_applied _ -> "borrow_fee_applied" | Borrow_charge_applied _ -> "borrow_charge_applied" | Borrow_recall_received _ -> "borrow_recall_received" | Cash_interest_applied _ -> "cash_interest_applied" diff --git a/lib/audit.mli b/lib/audit.mli index 07708df..20ba914 100644 --- a/lib/audit.mli +++ b/lib/audit.mli @@ -75,13 +75,6 @@ type event = | Settlement_instruction_created of Settlement.instruction | Settlement_completed of Settlement.instruction | Settlement_failed of Settlement.instruction - | Margin_limited of { - order_id : Id.Order.t; - instrument_id : Id.Instrument.t; - requested_quantity : Scalar.Quantity.t; - permitted_quantity : Scalar.Quantity.t; - price : Scalar.Price.t; - } | Fill_clipped of { order_id : Id.Order.t; instrument_id : Id.Instrument.t; @@ -90,16 +83,6 @@ type event = price : Scalar.Price.t; limit : Risk.fill_limit; } - | Borrow_fee_applied of { - instrument_id : Id.Instrument.t; - quote_currency : string; - short_quantity : Scalar.Quantity.t; - reference_price : Scalar.Price.t; - borrow_bps : int; - period_start : Ptime.t; - period_end : Ptime.t; - fee : Scalar.Money.t; - } | Borrow_charge_applied of { observation : Financing.borrow_observation; quote_currency : string; diff --git a/lib/codec.ml b/lib/codec.ml index fb5f935..c399627 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -440,7 +440,7 @@ let order_book_event_to_yojson event = string (Market_event.aggressor_side_to_string aggressor_side) ); ]) -let versioned_market_slice_to_yojson ~contract_version market_slice = +let market_slice_to_yojson market_slice = `Assoc [ ("slice_sequence", int64 market_slice.Market_slice.slice_sequence); @@ -454,108 +454,33 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = `List (List.map corporate_action_to_yojson market_slice.corporate_actions) ); + ( "borrow_observations", + `List + (List.map borrow_observation_to_yojson + market_slice.Market_slice.borrow_observations) ); + ( "cash_rate_observations", + `List + (List.map cash_rate_observation_to_yojson + market_slice.Market_slice.cash_rate_observations) ); + ( "settlement_failures", + `List + (List.map settlement_failure_to_yojson + market_slice.Market_slice.settlement_failures) ); + ( "lifecycle_events", + `List + (List.map lifecycle_event_to_yojson + market_slice.Market_slice.lifecycle_events) ); + ( "market_events", + `List + (List.map market_event_to_yojson + market_slice.Market_slice.market_events) ); + ( "order_book_events", + `List + (List.map order_book_event_to_yojson + market_slice.Market_slice.order_book_events) ); ] - |> function - | `Assoc fields - when List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] - -> - let settlement = - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then - [ - ( "settlement_failures", - `List - (List.map settlement_failure_to_yojson - market_slice.Market_slice.settlement_failures) ); - ] - else [] - in - let lifecycle = - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12" ] then - [ - ( "lifecycle_events", - `List - (List.map lifecycle_event_to_yojson - market_slice.Market_slice.lifecycle_events) ); - ] - else [] - in - let market_events = - if List.mem contract_version [ "16"; "15"; "14" ] then - [ - ( "market_events", - `List - (List.map market_event_to_yojson - market_slice.Market_slice.market_events) ); - ] - else [] - in - let order_book_events = - if List.mem contract_version [ "16"; "15" ] then - [ - ( "order_book_events", - `List - (List.map order_book_event_to_yojson - market_slice.Market_slice.order_book_events) ); - ] - else [] - in - `Assoc - (fields - @ [ - ( "borrow_observations", - `List - (List.map borrow_observation_to_yojson - market_slice.Market_slice.borrow_observations) ); - ( "cash_rate_observations", - `List - (List.map cash_rate_observation_to_yojson - market_slice.Market_slice.cash_rate_observations) ); - ] - @ settlement @ lifecycle @ market_events @ order_book_events) - | json -> json - -let market_slice_to_yojson market_slice = - versioned_market_slice_to_yojson ~contract_version:"9" market_slice - -let market_slice_to_yojson_v10 market_slice = - versioned_market_slice_to_yojson ~contract_version:"10" market_slice - -let market_slice_to_yojson_v11 market_slice = - versioned_market_slice_to_yojson ~contract_version:"11" 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 market_slice_to_yojson_v14 market_slice = - versioned_market_slice_to_yojson ~contract_version:"14" market_slice - -let market_slice_to_yojson_v15 market_slice = - versioned_market_slice_to_yojson ~contract_version:"15" market_slice - -let market_slice_to_yojson_v16 market_slice = - versioned_market_slice_to_yojson ~contract_version:"16" market_slice let request_fields request = - let kind, limit_price = - match request.Order.kind with - | Order.Market -> ("market", `Null) - | Order.Limit value -> ("limit", price value) - | Order.Stop value -> ("stop", price value) - | Order.Stop_limit { limit_price; _ } -> ("stop_limit", price limit_price) - in - [ - ("instrument_id", instrument_id request.instrument_id); - ("side", string (Order.side_to_string request.side)); - ("quantity", quantity request.quantity); - ("order_kind", string kind); - ("limit_price", limit_price); - ("origin", string (Order.origin_to_string request.origin)); - ] - -let request_fields_v8 request = let kind, trigger_price, limit_price = match request.Order.kind with | Order.Market -> ("market", `Null, `Null) @@ -591,27 +516,6 @@ let request_fields_v8 request = ] let order_to_yojson order = - let rejection_reason = - match order.Order.status with - | Order.Rejected reason -> string reason - | _ -> `Null - in - `Assoc - ((("order_id", order_id order.id) :: request_fields order.request) - @ [ - ("created_event_id", string (Id.Event.to_string order.created_event_id)); - ("updated_event_id", string (Id.Event.to_string order.updated_event_id)); - ("created_sequence", int64 order.created_sequence); - ("created_at", timestamp order.created_at); - ( "eligible_after_slice_sequence", - int64 order.eligible_after_slice_sequence ); - ("filled_quantity", quantity order.filled_quantity); - ("filled_notional", money order.filled_notional); - ("status", string (Order.status_to_string order.status)); - ("rejection_reason", rejection_reason); - ]) - -let order_to_yojson_v8 order = let rejection_reason = match order.Order.status with | Order.Rejected reason -> string reason @@ -624,7 +528,7 @@ let order_to_yojson_v8 order = | Some Order.Dormant | None -> (`Null, `Null) in `Assoc - ((("order_id", order_id order.id) :: request_fields_v8 order.request) + ((("order_id", order_id order.id) :: request_fields order.request) @ [ ("created_event_id", string (Id.Event.to_string order.created_event_id)); ("updated_event_id", string (Id.Event.to_string order.updated_event_id)); @@ -640,12 +544,15 @@ let order_to_yojson_v8 order = ("rejection_reason", rejection_reason); ]) -let versioned_order_to_yojson ~contract_version order = - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] - then order_to_yojson_v8 order - else order_to_yojson order +let calculated_fee_component_to_yojson component = + `Assoc + [ + ("name", string component.Fee_schedule.name); + ("kind", string component.kind); + ("currency", string component.currency); + ("amount", money component.amount); + ("quote_amount", money component.quote_amount); + ] let fill_to_yojson fill = `Assoc @@ -659,33 +566,13 @@ let fill_to_yojson fill = ("price", price fill.price); ("notional", money fill.notional); ("fee", money fill.fee); + ( "fee_components", + `List (List.map calculated_fee_component_to_yojson fill.fee_components) + ); ("executed_at", timestamp fill.executed_at); ("slice_sequence", int64 fill.slice_sequence); ] -let calculated_fee_component_to_yojson component = - `Assoc - [ - ("name", string component.Fee_schedule.name); - ("kind", string component.kind); - ("currency", string component.currency); - ("amount", money component.amount); - ("quote_amount", money component.quote_amount); - ] - -let fill_to_yojson_v9 fill = - match fill_to_yojson fill with - | `Assoc fields -> - `Assoc - (fields - @ [ - ( "fee_components", - `List - (List.map calculated_fee_component_to_yojson - fill.Fill.fee_components) ); - ]) - | _ -> assert false - let initial_position_to_yojson (position : Initial_portfolio.position) = `Assoc [ @@ -726,12 +613,26 @@ let initial_portfolio_to_yojson (portfolio : Initial_portfolio.t) = ("fx_rates", `List fx_rates); ] +let execution_fee_component_attribution_to_yojson component = + `Assoc + [ + ("name", string component.Account.name); + ("kind", string component.kind); + ("currency", string component.currency); + ("amount", money component.amount); + ("quote_currency", string component.quote_currency); + ("quote_amount", money component.quote_amount); + ("base_amount", money component.base_amount); + ] + let position_attribution_to_yojson position = `Assoc [ ("instrument_id", instrument_id position.Account.instrument_id); ("quote_currency", string position.quote_currency); ("quantity", quantity position.quantity); + ("settled_quantity", quantity position.settled_quantity); + ("unsettled_quantity", quantity position.unsettled_quantity); ("mark", price position.mark); ("fx_rate", price position.fx_rate); ("market_value", money position.market_value); @@ -746,82 +647,32 @@ let position_attribution_to_yojson position = ("base_dividend_pnl", money position.base_dividend_pnl); ("execution_fees", money position.execution_fees); ("base_execution_fees", money position.base_execution_fees); + ( "execution_fee_components", + `List + (List.map execution_fee_component_attribution_to_yojson + position.execution_fee_components) ); ("borrow_fees", money position.borrow_fees); ("base_borrow_fees", money position.base_borrow_fees); ("total_fees", money position.total_fees); ("base_total_fees", money position.base_total_fees); ] -let execution_fee_component_attribution_to_yojson component = - `Assoc - [ - ("name", string component.Account.name); - ("kind", string component.kind); - ("currency", string component.currency); - ("amount", money component.amount); - ("quote_currency", string component.quote_currency); - ("quote_amount", money component.quote_amount); - ("base_amount", money component.base_amount); - ] - -let position_attribution_to_yojson_v9 position = - match position_attribution_to_yojson position with - | `Assoc fields -> - `Assoc - (fields - @ [ - ( "execution_fee_components", - `List - (List.map execution_fee_component_attribution_to_yojson - position.Account.execution_fee_components) ); - ]) - | _ -> assert false - -let position_attribution_to_yojson_v11 position = - match position_attribution_to_yojson_v9 position with - | `Assoc fields -> - `Assoc - (fields - @ [ - ("settled_quantity", quantity position.Account.settled_quantity); - ("unsettled_quantity", quantity position.unsettled_quantity); - ]) - | _ -> assert false - let cash_attribution_to_yojson cash = `Assoc [ ("currency", string cash.Account.currency); ("amount", money cash.amount); + ("settled_amount", money cash.settled_amount); + ("unsettled_amount", money cash.unsettled_amount); ("fx_rate", price cash.fx_rate); ("base_value", money cash.base_value); + ("base_settled_value", money cash.base_settled_value); + ("base_unsettled_value", money cash.base_unsettled_value); + ("interest", money cash.interest); + ("base_interest", money cash.base_interest); ] -let cash_attribution_to_yojson_v10 cash = - match cash_attribution_to_yojson cash with - | `Assoc fields -> - `Assoc - (fields - @ [ - ("interest", money cash.Account.interest); - ("base_interest", money cash.base_interest); - ]) - | _ -> assert false - -let cash_attribution_to_yojson_v11 cash = - match cash_attribution_to_yojson_v10 cash with - | `Assoc fields -> - `Assoc - (fields - @ [ - ("settled_amount", money cash.Account.settled_amount); - ("unsettled_amount", money cash.unsettled_amount); - ("base_settled_value", money cash.base_settled_value); - ("base_unsettled_value", money cash.base_unsettled_value); - ]) - | _ -> assert false - -let account_valuation_to_yojson ?(contract_version = "8") valuation = +let account_valuation_to_yojson valuation = `Assoc [ ("base_currency", string valuation.Account.base_currency); @@ -836,59 +687,20 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ("equity", money valuation.equity); ("dividend_pnl", money valuation.dividend_pnl); ("execution_fees", money valuation.execution_fees); + ( "execution_fee_components", + `List + (List.map execution_fee_component_attribution_to_yojson + valuation.Account.execution_fee_components) ); ("borrow_fees", money valuation.borrow_fees); ("total_fees", money valuation.total_fees); + ("cash_interest", money valuation.cash_interest); + ("settled_cash", money valuation.settled_cash); + ("unsettled_cash", money valuation.unsettled_cash); ( "cash_balances", - `List - (List.map - (if - List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] - then cash_attribution_to_yojson_v11 - else if String.equal contract_version "10" then - cash_attribution_to_yojson_v10 - else cash_attribution_to_yojson) - valuation.cash_balances) ); + `List (List.map cash_attribution_to_yojson valuation.cash_balances) ); ( "positions", - `List - (List.map - (if - List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] - then position_attribution_to_yojson_v11 - else if - String.equal contract_version "9" - || String.equal contract_version "10" - then position_attribution_to_yojson_v9 - else position_attribution_to_yojson) - valuation.positions) ); + `List (List.map position_attribution_to_yojson valuation.positions) ); ] - |> function - | `Assoc fields - when List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9" ] -> - let financing = - if - List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] - then [ ("cash_interest", money valuation.Account.cash_interest) ] - else [] - in - let settlement = - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then - [ - ("settled_cash", money valuation.Account.settled_cash); - ("unsettled_cash", money valuation.unsettled_cash); - ] - else [] - in - `Assoc - (fields - @ [ - ( "execution_fee_components", - `List - (List.map execution_fee_component_attribution_to_yojson - valuation.Account.execution_fee_components) ); - ] - @ financing @ settlement) - | json -> json let margin_to_yojson margin = `Assoc @@ -912,27 +724,18 @@ let group_exposure_to_yojson (exposure : Risk.group_exposure) = Option.fold ~none:`Null ~some:weight exposure.concentration ); ] -let valuation_to_yojson ~contract_version valuation = - match - account_valuation_to_yojson ~contract_version valuation.Audit.account - with +let valuation_to_yojson valuation = + match account_valuation_to_yojson valuation.Audit.account with | `Assoc fields -> - let fields = fields @ [ ("margin", margin_to_yojson valuation.margin) ] in - let fields = - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] - then - fields - @ [ - ( "group_exposures", - `List - (List.map group_exposure_to_yojson - valuation.margin.Risk.group_exposures) ); - ] - else fields - in - `Assoc fields + `Assoc + (fields + @ [ + ("margin", margin_to_yojson valuation.margin); + ( "group_exposures", + `List + (List.map group_exposure_to_yojson + valuation.margin.Risk.group_exposures) ); + ]) | _ -> assert false let order_counts_to_yojson counts = @@ -1014,7 +817,7 @@ let metric_to_yojson metric = | Some aggregation -> [ ("aggregation", string (Metric.aggregation_to_string aggregation)) ]) -let payload_to_yojson ~contract_version = function +let payload_to_yojson = function | Audit.Run_started { scenario_sha256; execution_model } -> `Assoc [ @@ -1025,10 +828,10 @@ let payload_to_yojson ~contract_version = function `Assoc [ ("portfolio", initial_portfolio_to_yojson portfolio); - ("valuation", valuation_to_yojson ~contract_version valuation); + ("valuation", valuation_to_yojson valuation); ] | Audit.Market_slice_received market_slice -> - versioned_market_slice_to_yojson ~contract_version market_slice + market_slice_to_yojson market_slice | Audit.Target_portfolio_requested { basis; targets } -> `Assoc [ @@ -1036,13 +839,12 @@ let payload_to_yojson ~contract_version = function ("targets", `List (List.map requested_target_to_yojson targets)); ] | Audit.Order_accepted order | Audit.Order_rejected order -> - versioned_order_to_yojson ~contract_version order - | Audit.Order_triggered order -> - versioned_order_to_yojson ~contract_version order + order_to_yojson order + | Audit.Order_triggered order -> order_to_yojson order | Audit.Order_cancelled { order; reason } -> `Assoc [ - ("order", versioned_order_to_yojson ~contract_version order); + ("order", order_to_yojson order); ("reason", string (Audit.cancellation_reason_to_string reason)); ] | Audit.Split_applied { action; previous_quantity; adjusted_quantity } -> @@ -1082,7 +884,7 @@ let payload_to_yojson ~contract_version = function | Audit.Order_adjusted { order; action_id } -> `Assoc [ - ("order", versioned_order_to_yojson ~contract_version order); + ("order", order_to_yojson order); ("action_id", string (Id.Corporate_action.to_string action_id)); ] | Audit.Execution_price_selected @@ -1097,32 +899,11 @@ let payload_to_yojson ~contract_version = function ("impact_adjustment", money attribution.impact_adjustment); ("final_price", price attribution.final_price); ] - | Audit.Fill_applied fill -> - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9" ] - then fill_to_yojson_v9 fill - else fill_to_yojson fill + | Audit.Fill_applied fill -> fill_to_yojson fill | Audit.Settlement_instruction_created instruction | Audit.Settlement_completed instruction | Audit.Settlement_failed instruction -> settlement_instruction_to_yojson instruction - | Audit.Margin_limited - { - order_id = id; - instrument_id = instrument; - requested_quantity; - permitted_quantity; - price = fill_price; - } -> - `Assoc - [ - ("order_id", order_id id); - ("instrument_id", instrument_id instrument); - ("requested_quantity", quantity requested_quantity); - ("permitted_quantity", quantity permitted_quantity); - ("price", price fill_price); - ] | Audit.Fill_clipped { order_id = id; @@ -1148,28 +929,6 @@ let payload_to_yojson ~contract_version = function ("permitted_quantity", quantity permitted_quantity); ("price", price fill_price); ] - | Audit.Borrow_fee_applied - { - instrument_id = instrument; - quote_currency; - short_quantity; - reference_price; - borrow_bps; - period_start; - period_end; - fee; - } -> - `Assoc - [ - ("instrument_id", instrument_id instrument); - ("quote_currency", string quote_currency); - ("short_quantity", quantity short_quantity); - ("reference_price", price reference_price); - ("borrow_bps", `Int borrow_bps); - ("period_start", timestamp period_start); - ("period_end", timestamp period_end); - ("fee", money fee); - ] | Audit.Borrow_charge_applied { observation; @@ -1227,26 +986,17 @@ let payload_to_yojson ~contract_version = function ("closing_balance", money closing_balance); ] | Audit.Margin_call_triggered valuation | Audit.Margin_restored valuation -> - valuation_to_yojson ~contract_version valuation + valuation_to_yojson valuation | Audit.Intent_rejected reason -> `Assoc [ ("reason", string reason) ] - | Audit.Metric_emitted metric -> - if String.equal contract_version "16" then metric_to_yojson metric - else - let value = - match metric.Metric.value with - | Metric.String value -> value - | Metric.Numeric value -> Metric.numeric_to_string value - | Metric.Boolean value -> string_of_bool value - in - `Assoc [ ("name", string metric.name); ("value", string value) ] - | Audit.Valuation valuation -> valuation_to_yojson ~contract_version valuation + | Audit.Metric_emitted metric -> metric_to_yojson metric + | Audit.Valuation valuation -> valuation_to_yojson valuation | Audit.Run_completed { scenario_sha256; execution_model; valuation; order_counts } -> `Assoc [ ("scenario_sha256", string scenario_sha256); ("execution_model", string execution_model); - ("valuation", valuation_to_yojson ~contract_version valuation); + ("valuation", valuation_to_yojson valuation); ("order_counts", order_counts_to_yojson order_counts); ] @@ -1264,9 +1014,7 @@ let audit_to_yojson audit = ("run_id", string (Id.Run.to_string audit.run_id)); ("recorded_at", timestamp audit.recorded_at); ("event_type", string (Audit.event_name audit.event)); - ( "payload", - payload_to_yojson ~contract_version:audit.contract_version audit.event - ); + ("payload", payload_to_yojson audit.event); ] let audit_to_string audit = Yojson.Safe.to_string (audit_to_yojson audit) diff --git a/lib/codec.mli b/lib/codec.mli index ed39a34..86ff0a9 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -4,21 +4,9 @@ val ptime_to_string : Ptime.t -> string val ptime_of_string : string -> (Ptime.t, string) result val bar_to_yojson : Bar.t -> Yojson.Safe.t 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 market_slice_to_yojson_v14 : Market_slice.t -> Yojson.Safe.t -val market_slice_to_yojson_v15 : Market_slice.t -> Yojson.Safe.t -val market_slice_to_yojson_v16 : 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 -val fill_to_yojson_v9 : Fill.t -> Yojson.Safe.t val initial_portfolio_to_yojson : Initial_portfolio.t -> Yojson.Safe.t - -val account_valuation_to_yojson : - ?contract_version:string -> Account.valuation -> Yojson.Safe.t - +val account_valuation_to_yojson : Account.valuation -> Yojson.Safe.t val audit_to_yojson : Audit.t -> Yojson.Safe.t val audit_to_string : Audit.t -> string diff --git a/lib/contract.ml b/lib/contract.ml index d8682a4..06d6f5f 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,28 +1,7 @@ -let version = "16" -let previous_version = "15" -let legacy_journal_version = "3" - -let supported_versions = - [ - version; - previous_version; - "14"; - "13"; - "12"; - "11"; - "10"; - "9"; - "8"; - "7"; - "6"; - "5"; - "4"; - legacy_journal_version; - ] - +let version = "1" +let supported_versions = [ version ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "14" -let previous_strategy_protocol_version = "13" +let strategy_protocol_version = "1" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -36,22 +15,7 @@ let capabilities_to_yojson () = ("journal_formats", strings [ "jsonl" ]); ("execution_models", strings Execution_model.supported); ("execution_model_contracts", Execution_model.capabilities_to_yojson ()); - ( "strategy_protocol_versions", - strings - [ - strategy_protocol_version; - previous_strategy_protocol_version; - "12"; - "11"; - "10"; - "9"; - "8"; - "7"; - "6"; - "5"; - "4"; - "3"; - ] ); + ("strategy_protocol_versions", strings [ strategy_protocol_version ]); ("resource_limits", Resource_limits.to_yojson ()); ] diff --git a/lib/contract.mli b/lib/contract.mli index 967a91a..7e18e56 100644 --- a/lib/contract.mli +++ b/lib/contract.mli @@ -1,12 +1,9 @@ (** Version and capability identifiers for the process/file boundary. *) val version : string -val previous_version : string -val legacy_journal_version : string val supported_versions : string list val is_supported : string -> bool val strategy_protocol_version : string -val previous_strategy_protocol_version : string val engine_version : string val capabilities_to_yojson : unit -> Yojson.Safe.t val capabilities_to_string : unit -> string diff --git a/lib/engine.ml b/lib/engine.ml index c88493e..cbc4adf 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -6,13 +6,13 @@ type config = { venue_calendars : Venue_calendar.t list; execution_model : Execution_model.t; execution : Execution.t; - financing : Financing.policy option; - settlement : Settlement.policy option; + financing : Financing.policy; + settlement : Settlement.policy; max_internal_events : int; } -let make_config ~venue_calendars ~contract_version ~risk ~execution_model - ~execution ~financing ~settlement ~max_internal_events = +let config ~contract_version ~risk ~venue_calendars ~execution_model ~execution + ~financing ~settlement ~max_internal_events = if not (Contract.is_supported contract_version) then Error "engine contract version is unsupported" else if @@ -50,33 +50,6 @@ let make_config ~venue_calendars ~contract_version ~risk ~execution_model max_internal_events; } -let config ~contract_version ~risk ~execution_model ~execution - ~max_internal_events = - make_config ~venue_calendars:[] ~contract_version ~risk ~execution_model - ~execution ~financing:None ~max_internal_events ~settlement:None - -let config_v8 ~contract_version ~risk ~venue_calendars ~execution_model - ~execution ~max_internal_events = - make_config ~venue_calendars ~contract_version ~risk ~execution_model - ~execution ~financing:None ~max_internal_events ~settlement:None - -let config_v10 ~contract_version ~risk ~venue_calendars ~execution_model - ~execution ~financing ~max_internal_events = - make_config ~venue_calendars ~contract_version ~risk ~execution_model - ~execution ~financing:(Some financing) ~max_internal_events ~settlement:None - -let config_v11 ~contract_version ~risk ~venue_calendars ~execution_model - ~execution ~financing ~settlement ~max_internal_events = - make_config ~venue_calendars ~contract_version ~risk ~execution_model - ~execution ~financing:(Some financing) ~settlement:(Some settlement) - ~max_internal_events - -let config_v12 = config_v11 -let config_v13 = config_v12 -let config_v14 = config_v13 -let config_v15 = config_v14 -let config_v16 = config_v15 - let valid_sha256 value = String.length value = 64 && String.for_all @@ -108,7 +81,7 @@ module Interactive = struct latest_borrow : Financing.borrow_observation Id.Instrument.Map.t; latest_cash_rates : Financing.cash_rate_observation Currency_map.t; settlement_instructions : Settlement.instruction list; - initial_portfolio : Initial_portfolio.t option; + initial_portfolio : Initial_portfolio.t; applied_action_ids : Id.Corporate_action.Set.t; lifecycle : Instrument_lifecycle.t; desired_targets : desired_targets option; @@ -194,34 +167,7 @@ module Interactive = struct (Risk.instruments config.risk) |> List.sort_uniq String.compare - let create ~run_id ~scenario_sha256 ~config ~initial_cash = - if not (valid_sha256 scenario_sha256) then - Error "scenario SHA-256 must contain 64 lowercase hexadecimal characters" - else - let expected_currencies = expected_currencies config in - let supplied_currencies = - List.map fst initial_cash |> List.sort_uniq String.compare - in - if supplied_currencies <> expected_currencies then - Error "initial cash must contain every configured currency exactly once" - else - match - Account.create - ~base_currency:(Risk.base_currency config.risk) - ~initial_cash - with - | Error _ as error -> error - | Ok account -> - let base_rate = - Scalar.Price.of_decimal_string "1" |> Result.get_ok - in - create_state ~run_id ~scenario_sha256 ~config ~account - ~latest_marks:Id.Instrument.Map.empty - ~latest_fx_rates:[ (Risk.base_currency config.risk, base_rate) ] - ~initial_portfolio:None - - let create_with_portfolio ~run_id ~scenario_sha256 ~config ~initial_portfolio - = + let create ~run_id ~scenario_sha256 ~config ~initial_portfolio = if not (valid_sha256 scenario_sha256) then Error "scenario SHA-256 must contain 64 lowercase hexadecimal characters" else if @@ -250,8 +196,7 @@ module Interactive = struct in let* () = Risk.check_initial config.risk valuation in create_state ~run_id ~scenario_sha256 ~config ~account ~latest_marks - ~latest_fx_rates:initial_portfolio.fx_rates - ~initial_portfolio:(Some initial_portfolio) + ~latest_fx_rates:initial_portfolio.fx_rates ~initial_portfolio let account state = state.account let oms state = state.oms @@ -309,21 +254,16 @@ module Interactive = struct }) in let reduction = with_causes reduction [ event_id ] in - match reduction.state.initial_portfolio with - | None -> Ok reduction - | Some portfolio -> - let* account = value reduction.state in - let* margin = - Risk.margin_snapshot reduction.state.config.risk account - in - let valuation = Audit.{ account; margin } in - let* reduction, initial_event_id = - emit_with_id reduction - (Audit.Initial_state { portfolio; valuation }) - in - emit - (with_causes reduction [ initial_event_id ]) - (Audit.Valuation valuation) + let portfolio = reduction.state.initial_portfolio in + let* account = value reduction.state in + let* margin = Risk.margin_snapshot reduction.state.config.risk account in + let valuation = Audit.{ account; margin } in + let* reduction, initial_event_id = + emit_with_id reduction (Audit.Initial_state { portfolio; valuation }) + in + emit + (with_causes reduction [ initial_event_id ]) + (Audit.Valuation valuation) let enqueue reduction items = { reduction with pending = Pending_queue.enqueue reduction.pending items } @@ -432,13 +372,13 @@ module Interactive = struct Id.Instrument.Map.bindings reduction.state.latest_marks in let* () = + let policy = reduction.state.config.financing in match - ( reduction.state.config.financing, - request.Order.side, + ( request.Order.side, Account.position_quantity reduction.state.account request.instrument_id ) with - | Some policy, Order.Sell, position + | Order.Sell, position when policy.Financing.locate_policy = Financing.Reject_order && not (Scalar.Quantity.is_positive position) -> let available = @@ -530,7 +470,7 @@ module Interactive = struct let submit_recall_order reduction market_slice instrument quantity = let* request = - Order.request_v8 ~instrument_id:instrument.Instrument.id ~side:Order.Buy + Order.request ~instrument_id:instrument.Instrument.id ~side:Order.Buy ~quantity ~kind:Order.Market ~time_in_force:Order.Ioc ~origin:Order.Borrow_recall in @@ -1004,58 +944,6 @@ module Interactive = struct if Z.fits_int64 fee then Ok (Scalar.Money.of_micros (Z.to_int64 fee)) else Error "short borrow fee overflow" - let apply_legacy_borrow_fees reduction market_slice = - let span = - Ptime.diff market_slice.Market_slice.end_at market_slice.start_at - in - List.fold_left - (fun result instrument -> - let* reduction = result in - let quantity = - Account.position_quantity reduction.state.account - instrument.Instrument.id - in - if - (not (Scalar.Quantity.is_negative quantity)) - || Risk.short_borrow_bps reduction.state.config.risk = 0 - then Ok reduction - else - let* short_quantity = Scalar.Quantity.absolute quantity in - let* bar = - match Market_slice.bar market_slice instrument.id with - | Some value -> Ok value - | None -> Error "short position has no market slice bar" - in - let* notional = Scalar.Money.notional bar.open_price short_quantity in - let borrow_bps = Risk.short_borrow_bps reduction.state.config.risk in - let* fee = borrow_fee ~notional ~bps:borrow_bps span in - if Scalar.Money.equal fee Scalar.Money.zero then Ok reduction - else - let* account = - Account.apply_borrow_fee reduction.state.account - ~instrument_id:instrument.id - ~quote_currency:instrument.quote_currency ~fee - in - let reduction = - { reduction with state = { reduction.state with account } } - in - let causes = Option.to_list reduction.slice_event_id in - emit - (with_causes reduction causes) - (Audit.Borrow_fee_applied - { - instrument_id = instrument.id; - quote_currency = instrument.quote_currency; - short_quantity; - reference_price = bar.open_price; - borrow_bps; - period_start = market_slice.start_at; - period_end = market_slice.end_at; - fee; - })) - (Ok reduction) - (configured_instruments reduction.state) - let apply_observed_borrow_fees reduction market_slice policy = let span = Ptime.diff market_slice.Market_slice.end_at market_slice.start_at @@ -1242,85 +1130,73 @@ module Interactive = struct (configured_instruments reduction.state) let apply_financing reduction market_slice = - match reduction.state.config.financing with - | None -> apply_legacy_borrow_fees reduction market_slice - | Some policy -> - let* reduction = process_borrow_recalls reduction market_slice policy in - let* reduction = - apply_observed_borrow_fees reduction market_slice policy - in - apply_cash_interest reduction market_slice policy + let policy = reduction.state.config.financing in + let* reduction = process_borrow_recalls reduction market_slice policy in + let* reduction = apply_observed_borrow_fees reduction market_slice policy in + apply_cash_interest reduction market_slice policy let process_settlements reduction (market_slice : Market_slice.t) = - match reduction.state.config.settlement with - | None -> Ok reduction - | Some _ -> - List.fold_left - (fun result (instruction : Settlement.instruction) -> - let* reduction = result in - match instruction.status with - | Settlement.Settled _ | Settlement.Failed _ -> Ok reduction - | Settlement.Pending -> - if not (Settlement.is_due instruction market_slice.start_at) - then Ok reduction - else - let failure = - List.find_opt - (fun (failure : Settlement.failure) -> - String.equal failure.instruction_id - instruction.instruction_id) - market_slice.Market_slice.settlement_failures - in - let* instruction, account, event = - match failure with - | Some failure -> - let* instruction = - Settlement.fail instruction - ~failed_at:market_slice.start_at - ~reason:failure.reason - in - Ok - ( instruction, - reduction.state.account, - Audit.Settlement_failed instruction ) - | None -> - let* account = - Account.apply_settlement reduction.state.account - instruction - in - let* instruction = - Settlement.settle instruction - ~settled_at:market_slice.start_at - in - Ok - ( instruction, - account, - Audit.Settlement_completed instruction ) - in - let settlement_instructions = - List.map - (fun (current : Settlement.instruction) -> - if - String.equal current.instruction_id - instruction.instruction_id - then instruction - else current) - reduction.state.settlement_instructions - in - emit - (with_causes - { - reduction with - state = - { - reduction.state with - account; - settlement_instructions; - }; - } - (Option.to_list reduction.slice_event_id)) - event) - (Ok reduction) reduction.state.settlement_instructions + List.fold_left + (fun result (instruction : Settlement.instruction) -> + let* reduction = result in + match instruction.status with + | Settlement.Settled _ | Settlement.Failed _ -> Ok reduction + | Settlement.Pending -> + if not (Settlement.is_due instruction market_slice.start_at) then + Ok reduction + else + let failure = + List.find_opt + (fun (failure : Settlement.failure) -> + String.equal failure.instruction_id + instruction.instruction_id) + market_slice.Market_slice.settlement_failures + in + let* instruction, account, event = + match failure with + | Some failure -> + let* instruction = + Settlement.fail instruction + ~failed_at:market_slice.start_at ~reason:failure.reason + in + Ok + ( instruction, + reduction.state.account, + Audit.Settlement_failed instruction ) + | None -> + let* account = + Account.apply_settlement reduction.state.account + instruction + in + let* instruction = + Settlement.settle instruction + ~settled_at:market_slice.start_at + in + Ok + ( instruction, + account, + Audit.Settlement_completed instruction ) + in + let settlement_instructions = + List.map + (fun (current : Settlement.instruction) -> + if + String.equal current.instruction_id + instruction.instruction_id + then instruction + else current) + reduction.state.settlement_instructions + in + emit + (with_causes + { + reduction with + state = + { reduction.state with account; settlement_instructions }; + } + (Option.to_list reduction.slice_event_id)) + event) + (Ok reduction) reduction.state.settlement_instructions let validate_target_ids state ids = let expected = @@ -1637,18 +1513,15 @@ module Interactive = struct market_slice.cash_rate_observations in let settlement_failures_valid = - match state.config.settlement with - | None -> market_slice.settlement_failures = [] - | Some _ -> - List.for_all - (fun (failure : Settlement.failure) -> - List.exists - (fun (instruction : Settlement.instruction) -> - String.equal instruction.instruction_id failure.instruction_id - && instruction.status = Settlement.Pending - && Settlement.is_due instruction market_slice.start_at) - state.settlement_instructions) - market_slice.settlement_failures + List.for_all + (fun (failure : Settlement.failure) -> + List.exists + (fun (instruction : Settlement.instruction) -> + String.equal instruction.instruction_id failure.instruction_id + && instruction.status = Settlement.Pending + && Settlement.is_due instruction market_slice.start_at) + state.settlement_instructions) + market_slice.settlement_failures in if List.length ids <> List.length actual || actual <> expected then Error "market slice must contain each configured instrument exactly once" @@ -1702,12 +1575,8 @@ module Interactive = struct let create_execution_fill execution ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity ~price ~fee ~fee_components ~executed_at ~slice_sequence = - if Execution.fee_schedules execution = [] then - Fill.create ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity - ~price ~fee ~executed_at ~slice_sequence - else - Fill.create_v9 ~id ~order_id ~instrument_id ~quote_currency ~side - ~quantity ~price ~fee ~fee_components ~executed_at ~slice_sequence + Fill.create ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity + ~price ~fee ~fee_components ~executed_at ~slice_sequence let slice_open_marks market_slice = List.map @@ -1738,11 +1607,7 @@ module Interactive = struct ~executed_at:proposed.executed_at ~slice_sequence:market_slice.Market_slice.slice_sequence in - let* account = - match state.config.settlement with - | None -> Account.apply_fill state.account fill - | Some _ -> Account.apply_unsettled_fill state.account fill - in + let* account = Account.apply_unsettled_fill state.account fill in let after_position = Account.position_quantity account instrument.id in let* after = Account.value account ~instruments ~marks @@ -1755,8 +1620,9 @@ module Interactive = struct | Ok (fee_components, fee, account, after_position, after) -> ( let checked = let* () = - match (state.config.settlement, order.Order.request.side) with - | Some settlement, Order.Buy -> ( + let settlement = state.config.settlement in + match order.Order.request.side with + | Order.Buy -> ( let available = match settlement.Settlement.cash_buying_power with | Settlement.Total_cash -> @@ -1787,7 +1653,7 @@ module Interactive = struct (Risk.Settlement_cash_buying_power (instrument.quote_currency, available))) else Ok ()) - | Some settlement, Order.Sell + | Order.Sell when settlement.position_availability = Settlement.Settled_positions && Scalar.Quantity.is_positive before_position -> @@ -1805,7 +1671,7 @@ module Interactive = struct (Risk.Settlement_position_availability (instrument.id, available))) else Ok () - | None, _ | Some _, _ -> Ok () + | Order.Sell -> Ok () in let* () = Risk.check_post_fill_for state.config.risk @@ -1828,9 +1694,9 @@ module Interactive = struct | None -> Error "fill instrument has no risk policy" in let* borrow_constraint = - match (state.config.financing, order.Order.request.side) with - | Some policy, Order.Sell - when not (Scalar.Quantity.is_positive before_position) -> + let policy = state.config.financing in + match order.Order.request.side with + | Order.Sell when not (Scalar.Quantity.is_positive before_position) -> let available = match Id.Instrument.Map.find_opt instrument.id state.latest_borrow @@ -1856,7 +1722,7 @@ module Interactive = struct match policy.Financing.locate_policy with | Financing.Reject_order -> true | Financing.Clip_fill -> false )) - | _ -> Ok None + | Order.Sell | Order.Buy -> Ok None in let quantity_limit = let risk_limit = @@ -1963,12 +1829,8 @@ module Interactive = struct Error "newly allocated fill ID was duplicated" | Ok (oms, Oms.Applied order) -> ( match - match reduction.state.config.settlement with - | None -> - Account.apply_fill reduction.state.account fill - | Some _ -> - Account.apply_unsettled_fill - reduction.state.account fill + Account.apply_unsettled_fill reduction.state.account + fill with | Error _ as error -> error | Ok account -> ( @@ -1980,26 +1842,23 @@ module Interactive = struct | Error _ as error -> error | Ok (reduction, event_id) -> let* reduction = - match reduction.state.config.settlement with - | None -> Ok reduction - | Some policy -> - let* instruction = - Settlement.instruction policy fill - in - let state = - { - reduction.state with - settlement_instructions = - reduction.state - .settlement_instructions - @ [ instruction ]; - } - in - emit - (with_causes { reduction with state } - [ event_id ]) - (Audit.Settlement_instruction_created - instruction) + let* instruction = + Settlement.instruction + reduction.state.config.settlement fill + in + let state = + { + reduction.state with + settlement_instructions = + reduction.state.settlement_instructions + @ [ instruction ]; + } + in + emit + (with_causes { reduction with state } + [ event_id ]) + (Audit.Settlement_instruction_created + instruction) in let* fill_pending = notification reduction @@ -2055,30 +1914,16 @@ module Interactive = struct match limit with | None -> Ok reduction | Some limit -> - if - String.equal reduction.state.config.contract_version - Contract.legacy_journal_version - then - emit reduction - (Audit.Margin_limited - { - order_id = order.id; - instrument_id = order.request.instrument_id; - requested_quantity = proposed.quantity; - permitted_quantity; - price = proposed.price; - }) - else - emit reduction - (Audit.Fill_clipped - { - order_id = order.id; - instrument_id = order.request.instrument_id; - proposed_quantity = proposed.quantity; - permitted_quantity; - price = proposed.price; - limit; - }) + emit reduction + (Audit.Fill_clipped + { + order_id = order.id; + instrument_id = order.request.instrument_id; + proposed_quantity = proposed.quantity; + permitted_quantity; + price = proposed.price; + limit; + }) in if Scalar.Quantity.is_zero permitted_quantity then Ok (reduction, permitted_quantity) @@ -2096,12 +1941,8 @@ module Interactive = struct | None -> Error "immediate order disappeared during matching" | Some order -> ( let reason = - if - (not - (String.equal reduction.state.config.contract_version "8")) - && Order.is_market order - then Audit.Market_ioc - else if Order.is_fok order then Audit.Fill_or_kill + if Order.is_fok order then Audit.Fill_or_kill + else if Order.is_market order then Audit.Market_ioc else Audit.Immediate_or_cancel in let result = @@ -2222,7 +2063,8 @@ module Interactive = struct Error "target order limit cannot cover one instrument lot" | Ok quantity -> Order.request ~instrument_id ~side ~quantity - ~kind:Order.Market ~origin:Order.Target_rebalance + ~kind:Order.Market ~time_in_force:Order.Ioc + ~origin:Order.Target_rebalance |> Result.map Option.some)) let reconcile_targets reduction = @@ -2296,7 +2138,8 @@ module Interactive = struct in let* request = Order.request ~instrument_id:instrument.id ~side ~quantity - ~kind:Order.Market ~origin:Order.Margin_liquidation + ~kind:Order.Market ~time_in_force:Order.Ioc + ~origin:Order.Margin_liquidation in submit_order (with_causes reduction causes) request) (Ok reduction) @@ -2635,14 +2478,9 @@ module Make (Strategy_impl : Strategy.S) = struct type t = { engine : Interactive.t; strategy_state : Strategy_impl.state } - let create ~run_id ~scenario_sha256 ~config ~initial_cash ~strategy_state = - Interactive.create ~run_id ~scenario_sha256 ~config ~initial_cash - |> Result.map (fun engine -> { engine; strategy_state }) - - let create_with_portfolio ~run_id ~scenario_sha256 ~config ~initial_portfolio - ~strategy_state = - Interactive.create_with_portfolio ~run_id ~scenario_sha256 ~config - ~initial_portfolio + let create ~run_id ~scenario_sha256 ~config ~initial_portfolio ~strategy_state + = + Interactive.create ~run_id ~scenario_sha256 ~config ~initial_portfolio |> Result.map (fun engine -> { engine; strategy_state }) let account state = Interactive.account state.engine diff --git a/lib/engine.mli b/lib/engine.mli index dc47f1f..4014293 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -3,88 +3,6 @@ type config val config : - contract_version:string -> - risk:Risk.t -> - execution_model:Execution_model.t -> - execution:Execution.t -> - max_internal_events:int -> - (config, string) result - -val config_v8 : - contract_version:string -> - risk:Risk.t -> - venue_calendars:Venue_calendar.t list -> - execution_model:Execution_model.t -> - execution:Execution.t -> - max_internal_events:int -> - (config, string) result - -val config_v10 : - contract_version:string -> - risk:Risk.t -> - venue_calendars:Venue_calendar.t list -> - execution_model:Execution_model.t -> - execution:Execution.t -> - financing:Financing.policy -> - max_internal_events:int -> - (config, string) result - -val config_v11 : - 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 - -val config_v12 : - 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 - -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 - -val config_v14 : - contract_version:string -> - risk:Risk.t -> - venue_calendars:Venue_calendar.t list -> - execution_model:Execution_model.t -> - execution:Execution.t -> - financing:Financing.policy -> - settlement:Settlement.policy -> - max_internal_events:int -> - (config, string) result - -val config_v15 : - contract_version:string -> - risk:Risk.t -> - venue_calendars:Venue_calendar.t list -> - execution_model:Execution_model.t -> - execution:Execution.t -> - financing:Financing.policy -> - settlement:Settlement.policy -> - max_internal_events:int -> - (config, string) result - -val config_v16 : contract_version:string -> risk:Risk.t -> venue_calendars:Venue_calendar.t list -> @@ -100,13 +18,6 @@ module Interactive : sig type progress val create : - run_id:Id.Run.t -> - scenario_sha256:string -> - config:config -> - initial_cash:(string * Scalar.Money.t) list -> - (t, string) result - - val create_with_portfolio : run_id:Id.Run.t -> scenario_sha256:string -> config:config -> @@ -127,14 +38,6 @@ module Make (Strategy_impl : Strategy.S) : sig type t val create : - run_id:Id.Run.t -> - scenario_sha256:string -> - config:config -> - initial_cash:(string * Scalar.Money.t) list -> - strategy_state:Strategy_impl.state -> - (t, string) result - - val create_with_portfolio : run_id:Id.Run.t -> scenario_sha256:string -> config:config -> diff --git a/lib/execution.ml b/lib/execution.ml index 4e60910..49c5cdf 100644 --- a/lib/execution.ml +++ b/lib/execution.ml @@ -1,7 +1,3 @@ -type fee_configuration = - | Legacy of { fixed_fee : Scalar.Money.t; fee_bps : int } - | Schedules of Fee_schedule.t Id.Instrument.Map.t - type missing_volume_policy = Reject_missing_volume | Zero_impact type cost_model = { @@ -12,7 +8,7 @@ type cost_model = { type t = { participation_bps : int; - fee_configuration : fee_configuration; + fee_schedules : Fee_schedule.t Id.Instrument.Map.t; cost_model : cost_model option; book_depth_limit : int option; } @@ -55,23 +51,7 @@ let ( let* ) result function_ = let cursor next = Cursor (fun oms -> next ~oms) -let create ~participation_bps ~fixed_fee ~fee_bps = - if participation_bps < 0 || participation_bps > 10_000 then - Error "participation basis points must be between 0 and 10000" - else if Scalar.Money.compare fixed_fee Scalar.Money.zero < 0 then - Error "fixed fee must be nonnegative" - 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 }; - cost_model = None; - book_depth_limit = None; - } - -let create_v2 ~participation_bps ~fee_schedules = +let create ~participation_bps ~fee_schedules = if participation_bps < 0 || participation_bps > 10_000 then Error "participation basis points must be between 0 and 10000" else @@ -91,7 +71,7 @@ let create_v2 ~participation_bps ~fee_schedules = (fun schedules -> { participation_bps; - fee_configuration = Schedules schedules; + fee_schedules = schedules; cost_model = None; book_depth_limit = None; }) @@ -112,7 +92,7 @@ let create_conservative ~participation_bps ~fee_schedules ~half_spread_bps Some { half_spread_bps; impact_coefficient_bps; missing_volume_policy }; }) - (create_v2 ~participation_bps ~fee_schedules) + (create ~participation_bps ~fee_schedules) let create_order_book ~participation_bps ~fee_schedules ~max_depth_levels = if max_depth_levels <= 0 || max_depth_levels > 1024 then @@ -120,45 +100,24 @@ let create_order_book ~participation_bps ~fee_schedules ~max_depth_levels = else Result.map (fun state -> { state with book_depth_limit = Some max_depth_levels }) - (create_v2 ~participation_bps ~fee_schedules) + (create ~participation_bps ~fee_schedules) let participation_bps state = state.participation_bps let book_depth_limit state = state.book_depth_limit -let fixed_fee state = - match state.fee_configuration with - | Legacy { fixed_fee; _ } -> fixed_fee - | Schedules _ -> Scalar.Money.zero - -let fee_bps state = - match state.fee_configuration with - | Legacy { fee_bps; _ } -> fee_bps - | Schedules _ -> 0 - let fee_schedules state = - match state.fee_configuration with - | Legacy _ -> [] - | Schedules schedules -> Id.Instrument.Map.bindings schedules |> List.map snd + Id.Instrument.Map.bindings state.fee_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 } -> - let ( let* ) result function_ = - match result with - | Ok value -> function_ value - | Error _ as error -> error - in - let* fee = Scalar.Money.fee ~fixed:fixed_fee ~bps:fee_bps ~notional in - Ok ([], fee) - | Schedules schedules -> ( - match Id.Instrument.Map.find_opt instrument.Instrument.id schedules with - | None -> Error "execution instrument has no configured fee schedule" - | Some schedule -> - Fee_schedule.calculate schedule - ~quote_currency:instrument.quote_currency ~notional ~quantity - ~liquidity ~fx_rates) + match + Id.Instrument.Map.find_opt instrument.Instrument.id state.fee_schedules + with + | None -> Error "execution instrument has no configured fee schedule" + | Some schedule -> + Fee_schedule.calculate schedule ~quote_currency:instrument.quote_currency + ~notional ~quantity ~liquidity ~fx_rates type limit_fill_policy = Optimistic_touch | Next_open_only | Adverse_touch diff --git a/lib/execution.mli b/lib/execution.mli index da67d1c..70aeed3 100644 --- a/lib/execution.mli +++ b/lib/execution.mli @@ -44,12 +44,6 @@ val cursor : (oms:Oms.t -> (step, string) result) -> cursor (** Build an immutable cursor from one matching-step function. *) val create : - participation_bps:int -> - fixed_fee:Scalar.Money.t -> - fee_bps:int -> - (t, string) result - -val create_v2 : participation_bps:int -> fee_schedules:Fee_schedule.t list -> (t, string) result @@ -70,8 +64,6 @@ val create_order_book : val participation_bps : t -> int val book_depth_limit : t -> int option -val fixed_fee : t -> Scalar.Money.t -val fee_bps : t -> int val fee_schedules : t -> Fee_schedule.t list val cost_model : t -> cost_model option @@ -141,8 +133,8 @@ val fold_slice : (** Fold executable orders in liquidation-first, then sell-before-buy/FIFO order. The callback returns the quantity it actually applied; only that quantity consumes the shared per-instrument slice capacity. Returns an error - when a dormant stop triggers because this compatibility helper has no - callback through which to persist trigger state. *) + when a dormant stop triggers because the callback cannot persist trigger + state. *) val match_slice : t -> diff --git a/lib/execution_model.ml b/lib/execution_model.ml index 64c1712..c5ed8ba 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -13,10 +13,8 @@ type t = (module S) type configuration_contract = { version : string; - previous_versions : string list; scenario_contract_versions : string list; required_fields : string list; - legacy_required_fields : string list; supported_order_types : string list; data_requirements : string list; limits : Yojson.Safe.t; @@ -63,28 +61,9 @@ let supported = List.map name builtins let completed_bar_v1_contract = { - version = "2"; - previous_versions = [ "1" ]; - scenario_contract_versions = - [ - "16"; - "15"; - "14"; - "13"; - "12"; - "11"; - "10"; - "9"; - "8"; - "7"; - "6"; - "5"; - "4"; - "3"; - ]; + version = "1"; + scenario_contract_versions = [ "1" ]; required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; - legacy_required_fields = - [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ]; supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; data_requirements = [ "completed_ohlcv_bars" ]; limits = @@ -92,17 +71,13 @@ let completed_bar_v1_contract = [ ( "participation_bps", `Assoc [ ("minimum", `Int 0); ("maximum", `Int 10_000) ] ); - ("fee_bps", `Assoc [ ("minimum", `Int 0); ("maximum", `Int 10_000) ]); - ( "fixed_fee", - `Assoc [ ("minimum", `String "0"); ("unit", `String "money") ] ); ]; } let conservative_contract = { version = "1"; - previous_versions = []; - scenario_contract_versions = [ "16"; "15"; "14"; "13" ]; + scenario_contract_versions = [ "1" ]; required_fields = [ "version"; @@ -111,7 +86,6 @@ let conservative_contract = "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" ]; @@ -130,10 +104,8 @@ let conservative_contract = let quote_trade_contract = { version = "1"; - previous_versions = []; - scenario_contract_versions = [ "16"; "15"; "14" ]; + scenario_contract_versions = [ "1" ]; required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; - legacy_required_fields = []; supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; data_requirements = [ @@ -152,11 +124,9 @@ let quote_trade_contract = let order_book_contract = { version = "1"; - previous_versions = []; - scenario_contract_versions = [ "16"; "15" ]; + scenario_contract_versions = [ "1" ]; required_fields = [ "version"; "participation_bps"; "fee_schedules"; "max_depth_levels" ]; - legacy_required_fields = []; supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; data_requirements = [ @@ -190,13 +160,10 @@ let configuration_contract model = let supports_configuration model version = let contract = configuration_contract model in String.equal contract.version version - || List.mem version contract.previous_versions let required_fields model version = let contract = configuration_contract model in if String.equal version contract.version then Ok contract.required_fields - else if List.mem version contract.previous_versions then - Ok contract.legacy_required_fields else Error (Printf.sprintf @@ -216,18 +183,13 @@ let capabilities_to_yojson () = `Assoc [ ("name", `String (name model)); - ( "configuration_versions", - strings (contract.version :: contract.previous_versions) ); + ("configuration_versions", strings [ contract.version ]); ( "scenario_contract_versions", strings contract.scenario_contract_versions ); ("required_fields", strings contract.required_fields); ( "configuration_required_fields", - `Assoc - ((contract.version, strings contract.required_fields) - :: List.map - (fun version -> - (version, strings contract.legacy_required_fields)) - contract.previous_versions) ); + `Assoc [ (contract.version, strings contract.required_fields) ] + ); ("supported_order_types", strings contract.supported_order_types); ("data_requirements", strings contract.data_requirements); ("limits", contract.limits); diff --git a/lib/execution_model.mli b/lib/execution_model.mli index 25968aa..f8641be 100644 --- a/lib/execution_model.mli +++ b/lib/execution_model.mli @@ -21,10 +21,8 @@ type t type configuration_contract = private { version : string; - previous_versions : string list; scenario_contract_versions : string list; required_fields : string list; - legacy_required_fields : string list; supported_order_types : string list; data_requirements : string list; limits : Yojson.Safe.t; diff --git a/lib/external_replay.ml b/lib/external_replay.ml index 1c446ae..de770f5 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -62,7 +62,6 @@ let initialization_of_scenario ~scenario_sha256 (scenario : Scenario.t) = metadata = scenario.metadata; run_id = scenario.run_id; base_currency = scenario.base_currency; - initial_cash = scenario.initial_cash; initial_portfolio = scenario.initial_portfolio; instruments = scenario.instruments; venue_calendars = scenario.venue_calendars; @@ -82,7 +81,6 @@ let initialization_of_header ~scenario_sha256 (header : Scenario.stream_header) metadata = header.metadata; run_id = header.run_id; base_currency = header.base_currency; - initial_cash = header.initial_cash; initial_portfolio = header.initial_portfolio; instruments = header.instruments; venue_calendars = header.venue_calendars; @@ -95,47 +93,14 @@ let initialization_of_header ~scenario_sha256 (header : Scenario.stream_header) let create_runner ~contract_version ~run_id ~scenario_sha256 ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement - ~max_internal_events ~initial_cash ~initial_portfolio = + ~max_internal_events ~initial_portfolio = let* config = - (match (financing, settlement) with - | None, None -> - Engine.config_v8 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~max_internal_events - | Some financing, None -> - Engine.config_v10 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~financing ~max_internal_events - | Some financing, Some settlement -> - if List.mem contract_version [ "16"; "15" ] then - Engine.config_v16 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~financing ~settlement - ~max_internal_events - else if String.equal contract_version "14" then - Engine.config_v14 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~financing ~settlement - ~max_internal_events - else 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 - else - Engine.config_v11 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~financing ~settlement - ~max_internal_events - | None, Some _ -> Error "settlement requires financing configuration") + Engine.config ~contract_version ~risk ~venue_calendars ~execution_model + ~execution ~financing ~settlement ~max_internal_events |> reducer_result in - match initial_portfolio with - | None -> - Runner.create ~run_id ~scenario_sha256 ~config ~initial_cash - |> reducer_result - | Some initial_portfolio -> - Runner.create_with_portfolio ~run_id ~scenario_sha256 ~config - ~initial_portfolio - |> reducer_result + Runner.create ~run_id ~scenario_sha256 ~config ~initial_portfolio + |> reducer_result let append_events journal events = match journal with @@ -214,7 +179,6 @@ let run ?(durability = Artifact_writer.Buffered) ~env ~scenario_sha256 ~execution_model:scenario.execution_model ~execution:scenario.execution ~financing:scenario.financing ~settlement:scenario.settlement ~max_internal_events:scenario.max_internal_events - ~initial_cash:scenario.initial_cash ~initial_portfolio:scenario.initial_portfolio in let* journal, transcript = @@ -271,7 +235,6 @@ let validate_stream_pass ~scenario_sha256 channel = ~execution_model:header.execution_model ~execution:header.execution ~financing:header.financing ~settlement:header.settlement ~max_internal_events:header.max_internal_events - ~initial_cash:header.initial_cash ~initial_portfolio:header.initial_portfolio in Ok (runner, initialization_of_header ~scenario_sha256 header, 0L)) @@ -302,7 +265,6 @@ let replay_stream_pass ~scenario_sha256 ~journal ~session channel = ~execution_model:header.execution_model ~execution:header.execution ~financing:header.financing ~settlement:header.settlement ~max_internal_events:header.max_internal_events - ~initial_cash:header.initial_cash ~initial_portfolio:header.initial_portfolio in Ok { runner; journal = Some journal; audit_count = 0L }) diff --git a/lib/fill.ml b/lib/fill.ml index 2af8476..d315356 100644 --- a/lib/fill.ml +++ b/lib/fill.ml @@ -13,8 +13,8 @@ type t = { slice_sequence : int64; } -let create_internal ~allow_rebate ~fee_components ~id ~order_id ~instrument_id - ~quote_currency ~side ~quantity ~price ~fee ~executed_at ~slice_sequence = +let create ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity ~price + ~fee ~fee_components ~executed_at ~slice_sequence = if not (Scalar.Quantity.is_positive quantity) then Error "fill quantity must be positive" else if String.length quote_currency = 0 then @@ -27,8 +27,6 @@ let create_internal ~allow_rebate ~fee_components ~id ~order_id ~instrument_id code >= 0x21 && code <> 0x7f) quote_currency) then Error "fill quote currency must not contain whitespace" - else if (not allow_rebate) && Scalar.Money.compare fee Scalar.Money.zero < 0 - then Error "fill fee must be nonnegative" else if Int64.compare slice_sequence 0L <= 0 then Error "fill slice sequence must be positive" else @@ -68,18 +66,6 @@ let create_internal ~allow_rebate ~fee_components ~id ~order_id ~instrument_id slice_sequence; } -let create ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity ~price - ~fee ~executed_at ~slice_sequence = - create_internal ~allow_rebate:false ~fee_components:[] ~id ~order_id - ~instrument_id ~quote_currency ~side ~quantity ~price ~fee ~executed_at - ~slice_sequence - -let create_v9 ~id ~order_id ~instrument_id ~quote_currency ~side ~quantity - ~price ~fee ~fee_components ~executed_at ~slice_sequence = - create_internal ~allow_rebate:true ~fee_components ~id ~order_id - ~instrument_id ~quote_currency ~side ~quantity ~price ~fee ~executed_at - ~slice_sequence - let equal left right = Id.Fill.equal left.id right.id && Id.Order.equal left.order_id right.order_id diff --git a/lib/fill.mli b/lib/fill.mli index e9d6d9b..67e6aee 100644 --- a/lib/fill.mli +++ b/lib/fill.mli @@ -16,19 +16,6 @@ type t = private { } val create : - id:Id.Fill.t -> - order_id:Id.Order.t -> - instrument_id:Id.Instrument.t -> - quote_currency:string -> - side:Order.side -> - quantity:Scalar.Quantity.t -> - price:Scalar.Price.t -> - fee:Scalar.Money.t -> - executed_at:Ptime.t -> - slice_sequence:int64 -> - (t, string) result - -val create_v9 : id:Id.Fill.t -> order_id:Id.Order.t -> instrument_id:Id.Instrument.t -> diff --git a/lib/financing.ml b/lib/financing.ml index 2c44f33..c86fba2 100644 --- a/lib/financing.ml +++ b/lib/financing.ml @@ -39,11 +39,6 @@ let policy ~day_count ~compounding ~borrow_missing_data ~cash_missing_data recall_policy; } -let legacy_policy = - policy ~day_count:Actual_365 ~compounding:Simple ~borrow_missing_data:Zero - ~cash_missing_data:Zero ~locate_policy:Clip_fill - ~recall_policy:Reject_new_shorts - let valid_currency value = String.length value > 0 && String.for_all diff --git a/lib/financing.mli b/lib/financing.mli index 878072a..acb9c60 100644 --- a/lib/financing.mli +++ b/lib/financing.mli @@ -39,8 +39,6 @@ val policy : recall_policy:recall_policy -> policy -val legacy_policy : policy - val borrow_observation : instrument_id:Id.Instrument.t -> effective_at:Ptime.t -> diff --git a/lib/journal.ml b/lib/journal.ml index 2f07698..f7946aa 100644 --- a/lib/journal.ml +++ b/lib/journal.ml @@ -9,8 +9,7 @@ let audit_context event = Some (Id.Order.to_string order.Order.id) | Order_cancelled { order; _ } -> Some (Id.Order.to_string order.id) | Fill_applied fill -> Some (Id.Order.to_string fill.Fill.order_id) - | Margin_limited { order_id; _ } | Fill_clipped { order_id; _ } -> - Some (Id.Order.to_string order_id) + | Fill_clipped { order_id; _ } -> Some (Id.Order.to_string order_id) | _ -> None in (event_id, order_id, causation_ids) diff --git a/lib/market_slice.ml b/lib/market_slice.ml index f3dcd27..062b1e7 100644 --- a/lib/market_slice.ml +++ b/lib/market_slice.ml @@ -33,10 +33,9 @@ let fx_mark ~currency ~rate = let compare_bar left right = Id.Instrument.compare left.Bar.instrument_id right.Bar.instrument_id -let create_v15 ~slice_sequence ~start_at ~end_at ~available_at ~received_at - ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations ~settlement_failures ~lifecycle_events - ~market_events ~order_book_events = +let 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 ~market_events ~order_book_events = if Int64.compare slice_sequence 0L <= 0 then Error "market slice sequence must be positive" else if Ptime.compare start_at end_at >= 0 then @@ -186,46 +185,6 @@ let create_v15 ~slice_sequence ~start_at ~end_at ~available_at ~received_at settlement_failures; } -let create_v16 = create_v15 - -let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at - ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations ~settlement_failures ~lifecycle_events - ~market_events = - create_v15 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars - ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations - ~settlement_failures ~lifecycle_events ~market_events ~order_book_events:[] - -let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at - ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations ~settlement_failures ~lifecycle_events = - create_v15 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars - ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations - ~settlement_failures ~lifecycle_events ~market_events:[] - ~order_book_events:[] - -let create_v13 = create_v12 - -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 = - create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars - ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations - ~settlement_failures ~lifecycle_events:[] - -let create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at - ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations = - create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars - ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations - ~settlement_failures:[] - -let create ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars - ~fx_rates ~corporate_actions = - create_v10 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars - ~fx_rates ~corporate_actions ~borrow_observations:[] - ~cash_rate_observations:[] - let bar state instrument_id = List.find_opt (fun bar -> Id.Instrument.equal bar.Bar.instrument_id instrument_id) diff --git a/lib/market_slice.mli b/lib/market_slice.mli index 7182f96..8f4eee7 100644 --- a/lib/market_slice.mli +++ b/lib/market_slice.mli @@ -23,107 +23,6 @@ type t = private { } val create : - 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 -> - (t, string) result - -val create_v10 : - 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 -> - (t, string) result - -val create_v11 : - 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 -> - (t, string) result - -val create_v12 : - 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 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 create_v14 : - slice_sequence:int64 -> - start_at:Ptime.t -> - end_at:Ptime.t -> - available_at:Ptime.t -> - received_at:Ptime.t -> - bars:Bar.t list -> - fx_rates:fx_mark list -> - corporate_actions:Corporate_action.t list -> - borrow_observations:Financing.borrow_observation list -> - cash_rate_observations:Financing.cash_rate_observation list -> - settlement_failures:Settlement.failure list -> - lifecycle_events:Instrument_lifecycle.event list -> - market_events:Market_event.t list -> - (t, string) result - -val create_v15 : - slice_sequence:int64 -> - start_at:Ptime.t -> - end_at:Ptime.t -> - available_at:Ptime.t -> - received_at:Ptime.t -> - bars:Bar.t list -> - fx_rates:fx_mark list -> - corporate_actions:Corporate_action.t list -> - borrow_observations:Financing.borrow_observation list -> - cash_rate_observations:Financing.cash_rate_observation list -> - settlement_failures:Settlement.failure list -> - lifecycle_events:Instrument_lifecycle.event list -> - market_events:Market_event.t list -> - order_book_events:Order_book_event.t list -> - (t, string) result - -val create_v16 : slice_sequence:int64 -> start_at:Ptime.t -> end_at:Ptime.t -> diff --git a/lib/order.ml b/lib/order.ml index 8301f1e..6698210 100644 --- a/lib/order.ml +++ b/lib/order.ml @@ -55,14 +55,14 @@ type t = { status : status; } -let compatibility_time_in_force = function Market -> Ioc | _ -> Gtc +let default_time_in_force = function Market -> Ioc | _ -> Gtc let valid_stop_limit side trigger_price limit_price = match side with | Buy -> Scalar.Price.compare limit_price trigger_price >= 0 | Sell -> Scalar.Price.compare limit_price trigger_price <= 0 -let request_v8 ~instrument_id ~side ~quantity ~kind ~time_in_force ~origin = +let request ~instrument_id ~side ~quantity ~kind ~time_in_force ~origin = if not (Scalar.Quantity.is_positive quantity) then Error "order quantity must be positive" else @@ -74,11 +74,6 @@ let request_v8 ~instrument_id ~side ~quantity ~kind ~time_in_force ~origin = prices require limit <= trigger" | _ -> Ok { instrument_id; side; quantity; kind; time_in_force; origin } -let request ~instrument_id ~side ~quantity ~kind ~origin = - request_v8 ~instrument_id ~side ~quantity ~kind - ~time_in_force:(compatibility_time_in_force kind) - ~origin - let make ~id ~created_event_id ~sequence ~created_at ~eligible_after_slice_sequence ~request ~status = if Int64.compare sequence 0L < 0 then diff --git a/lib/order.mli b/lib/order.mli index 482409b..5f6bf0f 100644 --- a/lib/order.mli +++ b/lib/order.mli @@ -54,19 +54,9 @@ type t = private { status : status; } -val compatibility_time_in_force : kind -> time_in_force -(** Preserve the pre-v8 mapping: market orders are IOC and all other kinds are - GTC. *) +val default_time_in_force : kind -> time_in_force val request : - instrument_id:Id.Instrument.t -> - side:side -> - quantity:Scalar.Quantity.t -> - kind:kind -> - origin:origin -> - (request, string) result - -val request_v8 : instrument_id:Id.Instrument.t -> side:side -> quantity:Scalar.Quantity.t -> diff --git a/lib/replay.ml b/lib/replay.ml index aa9c7bb..cfdd9ec 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -70,35 +70,8 @@ let add_audit_count count events = let engine_config ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events = - match (financing, settlement) with - | None, None -> - Engine.config_v8 ~contract_version ~risk ~venue_calendars ~execution_model - ~execution ~max_internal_events - | Some financing, None -> - Engine.config_v10 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~financing ~max_internal_events - | Some financing, Some settlement -> - if List.mem contract_version [ "16"; "15" ] then - Engine.config_v16 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~financing ~settlement - ~max_internal_events - else if String.equal contract_version "14" then - Engine.config_v14 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~financing ~settlement - ~max_internal_events - else 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 - else - Engine.config_v11 ~contract_version ~risk ~venue_calendars - ~execution_model ~execution ~financing ~settlement - ~max_internal_events - | None, Some _ -> Error "settlement requires financing configuration" + Engine.config ~contract_version ~risk ~venue_calendars ~execution_model + ~execution ~financing ~settlement ~max_internal_events let run ~scenario_sha256 ?journal_path ?(durability = Artifact_writer.Buffered) scenario = @@ -114,13 +87,8 @@ let run ~scenario_sha256 ?journal_path ?(durability = Artifact_writer.Buffered) |> reducer_result in let* initial = - (match scenario.initial_portfolio with - | None -> - Runner.create ~run_id:scenario.run_id ~scenario_sha256 ~config - ~initial_cash:scenario.initial_cash ~strategy_state - | Some initial_portfolio -> - Runner.create_with_portfolio ~run_id:scenario.run_id ~scenario_sha256 - ~config ~initial_portfolio ~strategy_state) + Runner.create ~run_id:scenario.run_id ~scenario_sha256 ~config + ~initial_portfolio:scenario.initial_portfolio ~strategy_state |> reducer_result in let journal_result = @@ -197,15 +165,8 @@ let run_stream_pass ~scenario_sha256 ~journal channel = | Error _ as error -> error | Ok config -> ( match - (match header.initial_portfolio with - | None -> - Runner.create ~run_id:header.run_id ~scenario_sha256 - ~config ~initial_cash:header.initial_cash - ~strategy_state - | Some initial_portfolio -> - Runner.create_with_portfolio ~run_id:header.run_id - ~scenario_sha256 ~config ~initial_portfolio - ~strategy_state) + Runner.create ~run_id:header.run_id ~scenario_sha256 ~config + ~initial_portfolio:header.initial_portfolio ~strategy_state |> reducer_result with | Error _ as error -> error diff --git a/lib/risk.ml b/lib/risk.ml index 13378b9..8f5d058 100644 --- a/lib/risk.ml +++ b/lib/risk.ml @@ -1,5 +1,4 @@ type t = { - versioned : bool; base_currency : string; instruments : Instrument.t Id.Instrument.Map.t; instrument_policies : instrument_policy Id.Instrument.Map.t; @@ -11,7 +10,6 @@ type t = { max_leverage : Scalar.Ratio.t; initial_margin_bps : int; maintenance_margin_bps : int; - short_borrow_bps : int; } and instrument_policy = { @@ -194,102 +192,14 @@ let create_group ~group_id ~group_kind ~instrument_ids ~limits = limits; } -let create ~base_currency ~instruments ~max_order_quantity ~max_long_position - ~max_short_position ~max_gross_exposure ~max_leverage ~initial_margin_bps - ~maintenance_margin_bps ~short_borrow_bps = - if not (valid_label base_currency) then - Error "base currency must not be empty or contain whitespace" - else if not (Scalar.Quantity.is_positive max_order_quantity) then - Error "maximum order quantity must be positive" - else if not (Scalar.Quantity.is_positive max_long_position) then - Error "maximum long position must be positive" - else if not (Scalar.Quantity.is_positive max_short_position) then - Error "maximum short position must be positive" - else if Scalar.Money.compare max_gross_exposure Scalar.Money.zero <= 0 then - Error "maximum gross exposure must be positive" - else if initial_margin_bps <= 0 || initial_margin_bps > 10_000 then - Error "initial margin basis points must be between 1 and 10000" - else if maintenance_margin_bps <= 0 || maintenance_margin_bps > 10_000 then - Error "maintenance margin basis points must be between 1 and 10000" - else if initial_margin_bps < maintenance_margin_bps then - Error "initial margin must not be below maintenance margin" - else if short_borrow_bps < 0 || short_borrow_bps > 10_000 then - Error "short borrow basis points must be between 0 and 10000" - else if instruments = [] then Error "risk must define at least one instrument" - else if - List.exists - (fun instrument -> - Scalar.Quantity.compare max_order_quantity - instrument.Instrument.lot_size - < 0) - instruments - then Error "maximum order quantity must cover every instrument lot size" - else if - List.exists - (fun instrument -> - Scalar.Quantity.compare max_long_position instrument.Instrument.lot_size - < 0) - instruments - then Error "maximum long position must cover every instrument lot size" - else if - List.exists - (fun instrument -> - Scalar.Quantity.compare max_short_position - instrument.Instrument.lot_size - < 0) - instruments - then Error "maximum short position must cover every instrument lot size" - else - let add result instrument = - let* map = result in - if Id.Instrument.Map.mem instrument.Instrument.id map then - Error "instrument IDs must be unique" - else Ok (Id.Instrument.Map.add instrument.id instrument map) - in - let* instruments = - List.fold_left add (Ok Id.Instrument.Map.empty) instruments - in - let* instrument_policies = - Id.Instrument.Map.bindings instruments - |> List.fold_left - (fun result (_, instrument) -> - let* policies = result in - let* policy = - create_instrument_policy ~instrument ~max_order_quantity - ~max_long_position ~max_short_position - ~max_notional_exposure:None ~initial_margin_bps - ~maintenance_margin_bps ~shorting_allowed:true - in - Ok (Id.Instrument.Map.add instrument.Instrument.id policy policies)) - (Ok Id.Instrument.Map.empty) - in - Ok - { - versioned = false; - base_currency; - instruments; - instrument_policies; - groups = []; - max_order_quantity; - max_long_position; - max_short_position; - max_gross_exposure; - max_leverage; - initial_margin_bps; - maintenance_margin_bps; - short_borrow_bps; - } - -let create_v7 ~base_currency ~instruments +let create ~base_currency ~instruments ~(instrument_policies : instrument_policy list) ~(groups : group list) - ~max_gross_exposure ~max_leverage ~short_borrow_bps = + ~max_gross_exposure ~max_leverage = if not (valid_label base_currency) then Error "base currency must not be empty or contain whitespace" else if instruments = [] then Error "risk must define at least one instrument" else if Scalar.Money.compare max_gross_exposure Scalar.Money.zero <= 0 then Error "maximum gross exposure must be positive" - else if short_borrow_bps < 0 || short_borrow_bps > 10_000 then - Error "short borrow basis points must be between 0 and 10000" else let add_instrument result instrument = let* map = result in @@ -334,7 +244,6 @@ let create_v7 ~base_currency ~instruments let representative = List.hd instrument_policies in Ok { - versioned = true; base_currency; instruments = instrument_map; instrument_policies = policy_map; @@ -350,7 +259,6 @@ let create_v7 ~base_currency ~instruments max_leverage; initial_margin_bps = representative.initial_margin_bps; maintenance_margin_bps = representative.maintenance_margin_bps; - short_borrow_bps; } let base_currency state = state.base_currency @@ -375,7 +283,6 @@ let max_gross_exposure state = state.max_gross_exposure let max_leverage state = state.max_leverage let initial_margin_bps state = state.initial_margin_bps let maintenance_margin_bps state = state.maintenance_margin_bps -let short_borrow_bps state = state.short_borrow_bps let max_order_quantity_for state instrument_id = Option.map @@ -421,7 +328,7 @@ let group_exposure_from_positions group ~equity positions = concentration; } -let group_exposures state valuation = +let group_exposures state (valuation : Account.valuation) = List.fold_left (fun result group -> let* exposures = result in @@ -433,39 +340,28 @@ let group_exposures state valuation = (Ok []) state.groups |> Result.map List.rev -let margin_snapshot state valuation = +let margin_snapshot state (valuation : Account.valuation) = let requirements = - if not state.versioned then - let* initial = - Scalar.Money.bps_ceil valuation.Account.gross_exposure - ~bps:state.initial_margin_bps - in - let* maintenance = - Scalar.Money.bps_ceil valuation.gross_exposure - ~bps:state.maintenance_margin_bps - in - Ok (initial, maintenance) - else - List.fold_left - (fun result (position : Account.position_attribution) -> - let* initial, maintenance = result in - let* notional = Scalar.Money.absolute position.base_market_value in - let* policy = - match instrument_policy state position.instrument_id with - | Some policy -> Ok policy - | None -> Error "valuation position has no instrument risk policy" - in - let* item_initial = - Scalar.Money.bps_ceil notional ~bps:policy.initial_margin_bps - in - let* item_maintenance = - Scalar.Money.bps_ceil notional ~bps:policy.maintenance_margin_bps - in - let* initial = Scalar.Money.add initial item_initial in - let* maintenance = Scalar.Money.add maintenance item_maintenance in - Ok (initial, maintenance)) - (Ok (Scalar.Money.zero, Scalar.Money.zero)) - valuation.positions + List.fold_left + (fun result (position : Account.position_attribution) -> + let* initial, maintenance = result in + let* notional = Scalar.Money.absolute position.base_market_value in + let* policy = + match instrument_policy state position.instrument_id with + | Some policy -> Ok policy + | None -> Error "valuation position has no instrument risk policy" + in + let* item_initial = + Scalar.Money.bps_ceil notional ~bps:policy.initial_margin_bps + in + let* item_maintenance = + Scalar.Money.bps_ceil notional ~bps:policy.maintenance_margin_bps + in + let* initial = Scalar.Money.add initial item_initial in + let* maintenance = Scalar.Money.add maintenance item_maintenance in + Ok (initial, maintenance)) + (Ok (Scalar.Money.zero, Scalar.Money.zero)) + valuation.positions in let* initial_requirement, maintenance_requirement = requirements in let* initial_excess = @@ -486,139 +382,113 @@ let margin_snapshot state valuation = group_exposures; } -let check_initial_values state ~equity ~gross_exposure = - if Scalar.Money.compare gross_exposure state.max_gross_exposure > 0 then - Error "portfolio would exceed maximum gross exposure" - else - let* leveraged_equity = - Scalar.Money.multiply_ratio equity state.max_leverage - in - if Scalar.Money.compare gross_exposure leveraged_equity > 0 then - Error "portfolio would exceed maximum leverage" +let check_initial state valuation = + let* () = + if + Scalar.Money.compare valuation.Account.gross_exposure + state.max_gross_exposure + > 0 + then Error "portfolio would exceed maximum gross exposure" else - let* initial_requirement = - Scalar.Money.bps_ceil gross_exposure ~bps:state.initial_margin_bps + let* leveraged_equity = + Scalar.Money.multiply_ratio valuation.equity state.max_leverage in - let* initial_excess = Scalar.Money.subtract equity initial_requirement in - if Scalar.Money.compare initial_excess Scalar.Money.zero < 0 then - Error "portfolio would violate initial margin" - else Ok () - -let check_initial state valuation = - if not state.versioned then - check_initial_values state ~equity:valuation.Account.equity - ~gross_exposure:valuation.gross_exposure - else - let* () = - if - Scalar.Money.compare valuation.Account.gross_exposure - state.max_gross_exposure - > 0 - then Error "portfolio would exceed maximum gross exposure" - else - let* leveraged_equity = - Scalar.Money.multiply_ratio valuation.equity state.max_leverage - in - if Scalar.Money.compare valuation.gross_exposure leveraged_equity > 0 - then Error "portfolio would exceed maximum leverage" - else Ok () - in - let* () = - List.fold_left - (fun result (position : Account.position_attribution) -> - let* () = result in - let* policy = - match instrument_policy state position.instrument_id with - | Some policy -> Ok policy - | None -> Error "initial position has no instrument risk policy" - in - let* minimum_short = - Scalar.Quantity.negate policy.max_short_position - in - let* () = - if - Scalar.Quantity.compare position.quantity policy.max_long_position - > 0 - then Error "initial position exceeds its maximum long position" - else if Scalar.Quantity.compare position.quantity minimum_short < 0 - then Error "initial position exceeds its maximum short position" - else if - (not policy.shorting_allowed) - && Scalar.Quantity.is_negative position.quantity - then Error "initial position violates its shorting policy" - else Ok () - in - let* notional = Scalar.Money.absolute position.base_market_value in - match policy.max_notional_exposure with - | Some limit when Scalar.Money.compare notional limit > 0 -> - Error - "initial position exceeds the instrument maximum notional \ - exposure" - | _ -> Ok ()) - (Ok ()) valuation.positions - in - let* margin = margin_snapshot state valuation in - let* () = - if Scalar.Money.compare margin.initial_excess Scalar.Money.zero < 0 then - Error "portfolio would violate instrument initial margin requirements" + if Scalar.Money.compare valuation.gross_exposure leveraged_equity > 0 then + Error "portfolio would exceed maximum leverage" else Ok () - in + in + let* () = List.fold_left - (fun result (group : group) -> + (fun result (position : Account.position_attribution) -> let* () = result in - let* exposure = - match - List.find_opt - (fun item -> Id.Risk_group.equal item.group_id group.group_id) - margin.group_exposures - with - | Some exposure -> Ok exposure - | None -> Error "initial valuation omitted a configured risk group" + let* policy = + match instrument_policy state position.instrument_id with + | Some policy -> Ok policy + | None -> Error "initial position has no instrument risk policy" in - let* absolute_net = Scalar.Money.absolute exposure.net_exposure in - let exceeds option observed = - Option.exists - (fun limit -> Scalar.Money.compare observed limit > 0) - option + let* minimum_short = Scalar.Quantity.negate policy.max_short_position in + let* () = + if + Scalar.Quantity.compare position.quantity policy.max_long_position + > 0 + then Error "initial position exceeds its maximum long position" + else if Scalar.Quantity.compare position.quantity minimum_short < 0 + then Error "initial position exceeds its maximum short position" + else if + (not policy.shorting_allowed) + && Scalar.Quantity.is_negative position.quantity + then Error "initial position violates its shorting policy" + else Ok () in - let group_name = Id.Risk_group.to_string group.group_id in - if exceeds group.limits.max_gross_exposure exposure.gross_exposure then - Error - (Printf.sprintf - "initial portfolio exceeds group %s maximum gross exposure" - group_name) - else if exceeds group.limits.max_long_exposure exposure.long_exposure - then - Error - (Printf.sprintf - "initial portfolio exceeds group %s maximum long exposure" - group_name) - else if exceeds group.limits.max_short_exposure exposure.short_exposure - then - Error - (Printf.sprintf - "initial portfolio exceeds group %s maximum short exposure" - group_name) - else if exceeds group.limits.max_absolute_net_exposure absolute_net then - Error - (Printf.sprintf - "initial portfolio exceeds group %s maximum absolute net \ - exposure" - group_name) - else - match group.limits.max_concentration with - | None -> Ok () - | Some limit -> - let* threshold = - Scalar.Money.multiply_ratio valuation.equity limit - in - if Scalar.Money.compare exposure.gross_exposure threshold > 0 then - Error - (Printf.sprintf - "initial portfolio exceeds group %s maximum concentration" - group_name) - else Ok ()) - (Ok ()) state.groups + let* notional = Scalar.Money.absolute position.base_market_value in + match policy.max_notional_exposure with + | Some limit when Scalar.Money.compare notional limit > 0 -> + Error + "initial position exceeds the instrument maximum notional \ + exposure" + | _ -> Ok ()) + (Ok ()) valuation.positions + in + let* margin = margin_snapshot state valuation in + let* () = + if Scalar.Money.compare margin.initial_excess Scalar.Money.zero < 0 then + Error "portfolio would violate instrument initial margin requirements" + else Ok () + in + List.fold_left + (fun result (group : group) -> + let* () = result in + let* exposure = + match + List.find_opt + (fun item -> Id.Risk_group.equal item.group_id group.group_id) + margin.group_exposures + with + | Some exposure -> Ok exposure + | None -> Error "initial valuation omitted a configured risk group" + in + let* absolute_net = Scalar.Money.absolute exposure.net_exposure in + let exceeds option observed = + Option.exists + (fun limit -> Scalar.Money.compare observed limit > 0) + option + in + let group_name = Id.Risk_group.to_string group.group_id in + if exceeds group.limits.max_gross_exposure exposure.gross_exposure then + Error + (Printf.sprintf + "initial portfolio exceeds group %s maximum gross exposure" + group_name) + else if exceeds group.limits.max_long_exposure exposure.long_exposure then + Error + (Printf.sprintf + "initial portfolio exceeds group %s maximum long exposure" + group_name) + else if exceeds group.limits.max_short_exposure exposure.short_exposure + then + Error + (Printf.sprintf + "initial portfolio exceeds group %s maximum short exposure" + group_name) + else if exceeds group.limits.max_absolute_net_exposure absolute_net then + Error + (Printf.sprintf + "initial portfolio exceeds group %s maximum absolute net exposure" + group_name) + else + match group.limits.max_concentration with + | None -> Ok () + | Some limit -> + let* threshold = + Scalar.Money.multiply_ratio valuation.equity limit + in + if Scalar.Money.compare exposure.gross_exposure threshold > 0 then + Error + (Printf.sprintf + "initial portfolio exceeds group %s maximum concentration" + group_name) + else Ok ()) + (Ok ()) state.groups let invalid result = Result.map_error (fun message -> Invalid message) result @@ -742,10 +612,9 @@ let check_group_fill_limit state ~equity values = in check state.groups -let check_post_fill_for state ~instrument_id:_ ~before_position ~after_position - ~before ~after = - if state.versioned then Ok () - else check_post_fill state ~before_position ~after_position ~before ~after +let check_post_fill_for _state ~instrument_id:_ ~before_position:_ + ~after_position:_ ~before:_ ~after:_ = + Ok () let check_position state quantity = if Scalar.Quantity.compare quantity state.max_long_position > 0 then @@ -761,17 +630,11 @@ let check_position_for state instrument_id quantity = | None -> Error "position refers to an unknown instrument risk policy" | Some policy -> if Scalar.Quantity.compare quantity policy.max_long_position > 0 then - Error - (if state.versioned then - "position would exceed the instrument maximum long position" - else "position would exceed the maximum long position") + Error "position would exceed the instrument maximum long position" else let* minimum_short = Scalar.Quantity.negate policy.max_short_position in if Scalar.Quantity.compare quantity minimum_short < 0 then - Error - (if state.versioned then - "position would exceed the instrument maximum short position" - else "position would exceed the maximum short position") + Error "position would exceed the instrument maximum short position" else if (not policy.shorting_allowed) && Scalar.Quantity.is_negative quantity then Error "instrument policy does not allow short positions" @@ -970,9 +833,7 @@ let first_some checks = let check_projected_values state ~equity values = let* gross, _, _, _ = sum_values values in let* () = - if not state.versioned then - check_initial_values state ~equity ~gross_exposure:gross - else if Scalar.Money.compare gross state.max_gross_exposure > 0 then + if Scalar.Money.compare gross state.max_gross_exposure > 0 then Error "portfolio would exceed maximum gross exposure" else let* leveraged_equity = @@ -1080,100 +941,95 @@ let check_projected_values state ~equity values = let check_reserved_fill state ~account ~oms ~marks ~fx_rates ~(order : Order.t) ~filled_quantity ~after = - if not state.versioned then Ok () - else - let instrument_id = order.request.instrument_id in - let* quantities = - fill_projected_quantities state ~account ~oms ~order ~filled_quantity - |> invalid - in - let* values = - projected_values state ~marks ~fx_rates quantities |> invalid - in - let* policy = - match instrument_policy state instrument_id with - | Some policy -> Ok policy - | None -> Error (Invalid "fill has no instrument risk policy") - in - let* () = - List.fold_left - (fun result value -> - let* () = result in - let* item_policy = - match instrument_policy state value.instrument_id with - | Some policy -> Ok policy - | None -> Error (Invalid "fill projection has no risk policy") + let instrument_id = order.request.instrument_id in + let* quantities = + fill_projected_quantities state ~account ~oms ~order ~filled_quantity + |> invalid + in + let* values = projected_values state ~marks ~fx_rates quantities |> invalid in + let* policy = + match instrument_policy state instrument_id with + | Some policy -> Ok policy + | None -> Error (Invalid "fill has no instrument risk policy") + in + let* () = + List.fold_left + (fun result value -> + let* () = result in + let* item_policy = + match instrument_policy state value.instrument_id with + | Some policy -> Ok policy + | None -> Error (Invalid "fill projection has no risk policy") + in + if + Scalar.Quantity.compare value.quantity item_policy.max_long_position + > 0 + then + Error + (Limit + (Instrument_maximum_long_position + (value.instrument_id, item_policy.max_long_position))) + else + let* minimum_short = + Scalar.Quantity.negate item_policy.max_short_position |> invalid in - if - Scalar.Quantity.compare value.quantity item_policy.max_long_position - > 0 - then + if Scalar.Quantity.compare value.quantity minimum_short < 0 then Error (Limit - (Instrument_maximum_long_position - (value.instrument_id, item_policy.max_long_position))) + (Instrument_maximum_short_position + (value.instrument_id, item_policy.max_short_position))) + else if + (not item_policy.shorting_allowed) + && Scalar.Quantity.is_negative value.quantity + then Error (Limit (Instrument_shorting_disabled value.instrument_id)) else - let* minimum_short = - Scalar.Quantity.negate item_policy.max_short_position |> invalid - in - if Scalar.Quantity.compare value.quantity minimum_short < 0 then - Error - (Limit - (Instrument_maximum_short_position - (value.instrument_id, item_policy.max_short_position))) - else if - (not item_policy.shorting_allowed) - && Scalar.Quantity.is_negative value.quantity - then - Error (Limit (Instrument_shorting_disabled value.instrument_id)) - else - match item_policy.max_notional_exposure with - | Some limit - when Scalar.Money.compare value.absolute_value limit > 0 -> - Error - (Limit - (Instrument_maximum_notional (value.instrument_id, limit))) - | _ -> Ok ()) - (Ok ()) values - in - let* gross, _, _, _ = sum_values values |> invalid in - let* () = - if Scalar.Money.compare gross state.max_gross_exposure > 0 then - Error (Limit (Maximum_gross_exposure state.max_gross_exposure)) - else - let* leveraged_equity = - Scalar.Money.multiply_ratio after.Account.equity state.max_leverage + match item_policy.max_notional_exposure with + | Some limit + when Scalar.Money.compare value.absolute_value limit > 0 -> + Error + (Limit + (Instrument_maximum_notional (value.instrument_id, limit))) + | _ -> Ok ()) + (Ok ()) values + in + let* gross, _, _, _ = sum_values values |> invalid in + let* () = + if Scalar.Money.compare gross state.max_gross_exposure > 0 then + Error (Limit (Maximum_gross_exposure state.max_gross_exposure)) + else + let* leveraged_equity = + Scalar.Money.multiply_ratio after.Account.equity state.max_leverage + |> invalid + in + if Scalar.Money.compare gross leveraged_equity > 0 then + Error (Limit (Maximum_leverage state.max_leverage)) + else Ok () + in + let* initial_requirement = + List.fold_left + (fun result value -> + let* total = result in + let* item_policy = + match instrument_policy state value.instrument_id with + | Some policy -> Ok policy + | None -> Error (Invalid "fill projection has no risk policy") + in + let* requirement = + Scalar.Money.bps_ceil value.absolute_value + ~bps:item_policy.initial_margin_bps |> invalid in - if Scalar.Money.compare gross leveraged_equity > 0 then - Error (Limit (Maximum_leverage state.max_leverage)) - else Ok () - in - let* initial_requirement = - List.fold_left - (fun result value -> - let* total = result in - let* item_policy = - match instrument_policy state value.instrument_id with - | Some policy -> Ok policy - | None -> Error (Invalid "fill projection has no risk policy") - in - let* requirement = - Scalar.Money.bps_ceil value.absolute_value - ~bps:item_policy.initial_margin_bps - |> invalid - in - Scalar.Money.add total requirement |> invalid) - (Ok Scalar.Money.zero) values - in - let* excess = - Scalar.Money.subtract after.equity initial_requirement |> invalid - in - if Scalar.Money.compare excess Scalar.Money.zero < 0 then - Error - (Limit - (Instrument_initial_margin (instrument_id, policy.initial_margin_bps))) - else check_group_fill_limit state ~equity:after.equity values + Scalar.Money.add total requirement |> invalid) + (Ok Scalar.Money.zero) values + in + let* excess = + Scalar.Money.subtract after.equity initial_requirement |> invalid + in + if Scalar.Money.compare excess Scalar.Money.zero < 0 then + Error + (Limit + (Instrument_initial_margin (instrument_id, policy.initial_margin_bps))) + else check_group_fill_limit state ~equity:after.equity values let check state ~account ~oms ~marks ~fx_rates (request : Order.request) = match instrument state request.instrument_id with @@ -1187,11 +1043,7 @@ let check state ~account ~oms ~marks ~fx_rates (request : Order.request) = if Scalar.Quantity.compare request.Order.quantity policy.max_order_quantity > 0 - then - Error - (if state.versioned then - "order exceeds the instrument maximum order quantity" - else "order exceeds the maximum order quantity") + then Error "order exceeds the instrument maximum order quantity" else let* () = check_alignment instrument request in let* () = check_self_cross ~oms request in diff --git a/lib/risk.mli b/lib/risk.mli index 52d6455..6b0795c 100644 --- a/lib/risk.mli +++ b/lib/risk.mli @@ -74,14 +74,10 @@ type fill_check_error = Limit of fill_limit | Invalid of string val create : base_currency:string -> instruments:Instrument.t list -> - max_order_quantity:Scalar.Quantity.t -> - max_long_position:Scalar.Quantity.t -> - max_short_position:Scalar.Quantity.t -> + instrument_policies:instrument_policy list -> + groups:group list -> max_gross_exposure:Scalar.Money.t -> max_leverage:Scalar.Ratio.t -> - initial_margin_bps:int -> - maintenance_margin_bps:int -> - short_borrow_bps:int -> (t, string) result val create_instrument_policy : @@ -110,16 +106,6 @@ val create_group : limits:group_limits -> (group, string) result -val create_v7 : - base_currency:string -> - instruments:Instrument.t list -> - instrument_policies:instrument_policy list -> - groups:group list -> - max_gross_exposure:Scalar.Money.t -> - max_leverage:Scalar.Ratio.t -> - short_borrow_bps:int -> - (t, string) result - val base_currency : t -> string val instruments : t -> Instrument.t list val instrument : t -> Id.Instrument.t -> Instrument.t option @@ -133,7 +119,6 @@ val max_gross_exposure : t -> Scalar.Money.t val max_leverage : t -> Scalar.Ratio.t val initial_margin_bps : t -> int val maintenance_margin_bps : t -> int -val short_borrow_bps : t -> int val max_order_quantity_for : t -> Id.Instrument.t -> Scalar.Quantity.t option val check_position : t -> Scalar.Quantity.t -> (unit, string) result diff --git a/lib/scenario.ml b/lib/scenario.ml index 86a10f8..54d4607 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -3,15 +3,14 @@ type t = { metadata : Yojson.Safe.t; run_id : Id.Run.t; base_currency : string; - initial_cash : (string * Scalar.Money.t) list; - initial_portfolio : Initial_portfolio.t option; + initial_portfolio : Initial_portfolio.t; instruments : Instrument.t list; venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; - financing : Financing.policy option; - settlement : Settlement.policy option; + financing : Financing.policy; + settlement : Settlement.policy; max_internal_events : int; schedule : (int64 * Strategy.intent list) list; slices : Market_slice.t list; @@ -22,15 +21,14 @@ type stream_header = { metadata : Yojson.Safe.t; run_id : Id.Run.t; base_currency : string; - initial_cash : (string * Scalar.Money.t) list; - initial_portfolio : Initial_portfolio.t option; + initial_portfolio : Initial_portfolio.t; instruments : Instrument.t list; venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; - financing : Financing.policy option; - settlement : Settlement.policy option; + financing : Financing.policy; + settlement : Settlement.policy; max_internal_events : int; } @@ -327,48 +325,6 @@ let parse_instrument json = let* lot_size = parse_quantity ~name:"lot_size" lot_json in Instrument.create ~id ~symbol ~quote_currency ~tick_size ~lot_size -let parse_legacy_risk base_currency instruments json = - let* fields = - object_fields ~name:"risk" - ~expected: - [ - "max_order_quantity"; - "max_long_position"; - "max_short_position"; - "max_gross_exposure"; - "max_leverage"; - "initial_margin_bps"; - "maintenance_margin_bps"; - "short_borrow_bps"; - ] - json - in - let* order_json = field fields "max_order_quantity" in - let* max_order_quantity = - parse_quantity ~name:"max_order_quantity" order_json - in - let* long_json = field fields "max_long_position" in - let* max_long_position = parse_quantity ~name:"max_long_position" long_json in - let* short_json = field fields "max_short_position" in - let* max_short_position = - parse_quantity ~name:"max_short_position" short_json - in - let* gross_json = field fields "max_gross_exposure" in - let* max_gross_exposure = parse_money ~name:"max_gross_exposure" gross_json in - let* leverage_json = field fields "max_leverage" in - let* max_leverage = parse_ratio ~name:"max_leverage" leverage_json in - let* initial_json = field fields "initial_margin_bps" in - let* initial_margin_bps = integer ~name:"initial_margin_bps" initial_json in - let* maintenance_json = field fields "maintenance_margin_bps" in - let* maintenance_margin_bps = - integer ~name:"maintenance_margin_bps" maintenance_json - in - let* borrow_json = field fields "short_borrow_bps" in - let* short_borrow_bps = integer ~name:"short_borrow_bps" borrow_json in - Risk.create ~base_currency ~instruments ~max_order_quantity ~max_long_position - ~max_short_position ~max_gross_exposure ~max_leverage ~initial_margin_bps - ~maintenance_margin_bps ~short_borrow_bps - let parse_nullable parse ~name = function | `Null -> Ok None | json -> parse ~name json |> Result.map Option.some @@ -507,16 +463,12 @@ let parse_group json = in Risk.create_group ~group_id ~group_kind ~instrument_ids ~limits -let parse_v7_risk base_currency instruments json = +let parse_risk base_currency instruments json = let* fields = object_fields ~name:"risk" ~expected: [ - "max_gross_exposure"; - "max_leverage"; - "short_borrow_bps"; - "instrument_policies"; - "groups"; + "max_gross_exposure"; "max_leverage"; "instrument_policies"; "groups"; ] json in @@ -546,30 +498,8 @@ let parse_v7_risk base_currency instruments json = field fields "max_leverage" |> fun result -> Result.bind result (parse_ratio ~name:"max_leverage") in - let* short_borrow_bps = - field fields "short_borrow_bps" |> fun result -> - Result.bind result (integer ~name:"short_borrow_bps") - in - Risk.create_v7 ~base_currency ~instruments ~instrument_policies ~groups - ~max_gross_exposure ~max_leverage ~short_borrow_bps - -let parse_risk ~contract_version base_currency instruments json = - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] - then parse_v7_risk base_currency instruments json - else parse_legacy_risk base_currency instruments json - -let parse_execution_values fields = - let* participation_json = field fields "participation_bps" in - let* participation_bps = - integer ~name:"participation_bps" participation_json - in - let* fixed_json = field fields "fixed_fee" in - let* fixed_fee = parse_money ~name:"fixed_fee" fixed_json in - let* fee_json = field fields "fee_bps" in - let* fee_bps = integer ~name:"fee_bps" fee_json in - Execution.create ~participation_bps ~fixed_fee ~fee_bps + Risk.create ~base_currency ~instruments ~instrument_policies ~groups + ~max_gross_exposure ~max_leverage let parse_fee_component json = let* fields = @@ -683,11 +613,11 @@ let parse_execution_common instruments fields = in Ok (participation_bps, schedules) -let parse_execution_v2 instruments fields = +let parse_scheduled_execution instruments fields = let* participation_bps, schedules = parse_execution_common instruments fields in - Execution.create_v2 ~participation_bps ~fee_schedules:schedules + Execution.create ~participation_bps ~fee_schedules:schedules let parse_order_book_execution instruments fields = let* participation_bps, fee_schedules = @@ -755,28 +685,7 @@ let parse_conservative_execution instruments fields = 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" - ~expected:[ "model"; "participation_bps"; "fixed_fee"; "fee_bps" ] - json - in - let* model_json = field fields "model" in - let* model_name = string ~name:"execution model" model_json in - let* execution_model = Execution_model.find model_name in - let* () = - if Execution_model.supports_contract execution_model contract_version then - Ok () - else - Error - (Printf.sprintf - "execution model %S does not support scenario contract %S" model_name - contract_version) - in - let* execution = parse_execution_values fields in - Ok (execution_model, execution) - -let parse_versioned_execution ~contract_version ~instruments json = +let parse_execution ~contract_version ~instruments json = let* fields = object_fields ~name:"execution" ~expected:[ "model"; "configuration" ] json in @@ -819,20 +728,10 @@ let parse_versioned_execution ~contract_version ~instruments json = then parse_conservative_execution instruments configuration else if String.equal model_name "order_book_v1" then parse_order_book_execution instruments configuration - else if - String.equal model_name "quote_trade_v1" || String.equal version "2" - then parse_execution_v2 instruments configuration - else parse_execution_values configuration + else parse_scheduled_execution instruments configuration in Ok (execution_model, execution) -let parse_execution ~contract_version ~instruments json = - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] - then parse_versioned_execution ~contract_version ~instruments json - else parse_legacy_execution ~contract_version json - let parse_side json = let* value = string ~name:"side" json in match value with @@ -877,37 +776,23 @@ let parse_portfolio_intent ~name ~parse_target make json = let* targets = map_list parse_target targets_json in Ok (make targets) -let parse_submit_intent ~contract_version json = - let versioned = - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] - in +let parse_submit_intent json = let* fields = object_fields ~name:"submit_order intent" ~expected: - (if versioned then - [ - "type"; - "instrument_id"; - "side"; - "quantity"; - "order_kind"; - "trigger_price"; - "limit_price"; - "time_in_force"; - "venue_id"; - "calendar_id"; - "expires_at"; - ] - else - [ - "type"; - "instrument_id"; - "side"; - "quantity"; - "order_kind"; - "limit_price"; - ]) + [ + "type"; + "instrument_id"; + "side"; + "quantity"; + "order_kind"; + "trigger_price"; + "limit_price"; + "time_in_force"; + "venue_id"; + "calendar_id"; + "expires_at"; + ] json in let* instrument_json = field fields "instrument_id" in @@ -921,19 +806,17 @@ let parse_submit_intent ~contract_version json = let* kind_json = field fields "order_kind" in let* kind_name = string ~name:"order_kind" kind_json in let* limit_json = field fields "limit_price" in - let* trigger_json = - if versioned then field fields "trigger_price" else Ok `Null - in + let* trigger_json = field fields "trigger_price" in let* kind = match (kind_name, trigger_json, limit_json) with | "market", `Null, `Null -> Ok Order.Market | "limit", `Null, value -> let* limit = parse_price ~name:"limit_price" value in Ok (Order.Limit limit) - | "stop", trigger, `Null when versioned -> + | "stop", trigger, `Null -> let* trigger = parse_price ~name:"trigger_price" trigger in Ok (Order.Stop trigger) - | "stop_limit", trigger, limit when versioned -> + | "stop_limit", trigger, limit -> let* trigger_price = parse_price ~name:"trigger_price" trigger in let* limit_price = parse_price ~name:"limit_price" limit in Ok (Order.Stop_limit { trigger_price; limit_price }) @@ -941,32 +824,30 @@ let parse_submit_intent ~contract_version json = Error "market order trigger_price and limit_price must be null" | _ -> Error "invalid order_kind" in + let* tif_json = field fields "time_in_force" in + let* tif = string ~name:"time_in_force" tif_json in + let* venue_json = field fields "venue_id" in + let* calendar_json = field fields "calendar_id" in + let* expires_json = field fields "expires_at" in let* time_in_force = - if not versioned then Ok (Order.compatibility_time_in_force kind) - else - let* tif_json = field fields "time_in_force" in - let* tif = string ~name:"time_in_force" tif_json in - let* venue_json = field fields "venue_id" in - let* calendar_json = field fields "calendar_id" in - let* expires_json = field fields "expires_at" in - match (tif, venue_json, calendar_json, expires_json) with - | "gtc", `Null, `Null, `Null -> Ok Order.Gtc - | "ioc", `Null, `Null, `Null -> Ok Order.Ioc - | "fok", `Null, `Null, `Null -> Ok Order.Fok - | "day", venue, calendar, `Null -> - let* venue_id = parse_id Id.Venue.of_string ~name:"venue_id" venue in - let* calendar_id = - parse_id Id.Venue_calendar.of_string ~name:"calendar_id" calendar - in - Ok (Order.Day { venue_id; calendar_id }) - | "gtd", `Null, `Null, expires -> - let* value = string ~name:"expires_at" expires in - let* expires_at = Codec.ptime_of_string value in - Ok (Order.Gtd expires_at) - | _ -> Error "time_in_force companion fields are inconsistent" + match (tif, venue_json, calendar_json, expires_json) with + | "gtc", `Null, `Null, `Null -> Ok Order.Gtc + | "ioc", `Null, `Null, `Null -> Ok Order.Ioc + | "fok", `Null, `Null, `Null -> Ok Order.Fok + | "day", venue, calendar, `Null -> + let* venue_id = parse_id Id.Venue.of_string ~name:"venue_id" venue in + let* calendar_id = + parse_id Id.Venue_calendar.of_string ~name:"calendar_id" calendar + in + Ok (Order.Day { venue_id; calendar_id }) + | "gtd", `Null, `Null, expires -> + let* value = string ~name:"expires_at" expires in + let* expires_at = Codec.ptime_of_string value in + Ok (Order.Gtd expires_at) + | _ -> Error "time_in_force companion fields are inconsistent" in let* request = - Order.request_v8 ~instrument_id ~side ~quantity ~kind ~time_in_force + Order.request ~instrument_id ~side ~quantity ~kind ~time_in_force ~origin:Order.Direct in Ok (Strategy.Submit_order request) @@ -980,88 +861,73 @@ let parse_cancel_intent json = let* order_id = parse_id Id.Order.of_string ~name:"order_id" order_json in Ok (Strategy.Cancel_order order_id) -let parse_metric_intent ~contract_version json = - if not (String.equal contract_version "16") then - let* fields = - object_fields ~name:"emit_metric intent" - ~expected:[ "type"; "name"; "value" ] - json - in - let* name_json = field fields "name" in - let* name = string ~name:"metric name" name_json in - let* value_json = field fields "value" in - let* value = string ~name:"metric value" value_json in - let* metric = Metric.create ~name ~value:(Metric.String value) () in - Ok (Strategy.Emit_metric metric) - else - let* fields = - match json with - | `Assoc fields -> - let names = List.map fst fields in - let unique = List.sort_uniq String.compare names in - let allowed = - [ "aggregation"; "dimensions"; "name"; "type"; "unit"; "value" ] - in - if List.length names <> List.length unique then - Error "emit_metric intent must not contain duplicate fields" - else if - not - (List.for_all (fun name -> List.mem name allowed) unique - && List.for_all - (fun name -> List.mem name unique) - [ "type"; "name"; "value" ]) - then Error "emit_metric intent has unknown or missing fields" - else Ok fields - | _ -> Error "emit_metric intent must be a JSON object" - in - let* name_json = field fields "name" in - let* name = string ~name:"metric name" name_json in - let* value = - let* json = field fields "value" in - let* value_fields = - object_fields ~name:"metric value" ~expected:[ "type"; "value" ] json - in - let* type_json = field value_fields "type" in - let* value_type = string ~name:"metric value type" type_json in - let* value_json = field value_fields "value" in - match (value_type, value_json) with - | "numeric", `String value -> - Metric.numeric_of_string value - |> Result.map (fun value -> Metric.Numeric value) - | "string", `String value -> Ok (Metric.String value) - | "boolean", `Bool value -> Ok (Metric.Boolean value) - | _ -> Error "metric value does not match its declared type" - in - let* unit_ = - match List.assoc_opt "unit" fields with - | None -> Ok None - | Some json -> string ~name:"metric unit" json |> Result.map Option.some - in - let* dimensions = - match List.assoc_opt "dimensions" fields with - | None -> Ok [] - | Some (`Assoc dimensions) -> - List.fold_left - (fun result (key, json) -> - let* values = result in - let* value = string ~name:"metric dimension value" json in - Ok ((key, value) :: values)) - (Ok []) dimensions - | Some _ -> Error "metric dimensions must be an object" - in - let* aggregation = - match List.assoc_opt "aggregation" fields with - | None -> Ok None - | Some json -> - let* value = string ~name:"metric aggregation" json in - Metric.aggregation_of_string value |> Result.map Option.some - in - let* metric = - Metric.create ~name ~value ?unit_ ~dimensions ?aggregation () +let parse_metric_intent json = + let* fields = + match json with + | `Assoc fields -> + let names = List.map fst fields in + let unique = List.sort_uniq String.compare names in + let allowed = + [ "aggregation"; "dimensions"; "name"; "type"; "unit"; "value" ] + in + if List.length names <> List.length unique then + Error "emit_metric intent must not contain duplicate fields" + else if + not + (List.for_all (fun name -> List.mem name allowed) unique + && List.for_all + (fun name -> List.mem name unique) + [ "type"; "name"; "value" ]) + then Error "emit_metric intent has unknown or missing fields" + else Ok fields + | _ -> Error "emit_metric intent must be a JSON object" + in + let* name_json = field fields "name" in + let* name = string ~name:"metric name" name_json in + let* value = + let* json = field fields "value" in + let* value_fields = + object_fields ~name:"metric value" ~expected:[ "type"; "value" ] json in - Ok (Strategy.Emit_metric metric) - -let parse_intent ~contract_version json = + let* type_json = field value_fields "type" in + let* value_type = string ~name:"metric value type" type_json in + let* value_json = field value_fields "value" in + match (value_type, value_json) with + | "numeric", `String value -> + Metric.numeric_of_string value + |> Result.map (fun value -> Metric.Numeric value) + | "string", `String value -> Ok (Metric.String value) + | "boolean", `Bool value -> Ok (Metric.Boolean value) + | _ -> Error "metric value does not match its declared type" + in + let* unit_ = + match List.assoc_opt "unit" fields with + | None -> Ok None + | Some json -> string ~name:"metric unit" json |> Result.map Option.some + in + let* dimensions = + match List.assoc_opt "dimensions" fields with + | None -> Ok [] + | Some (`Assoc dimensions) -> + List.fold_left + (fun result (key, json) -> + let* values = result in + let* value = string ~name:"metric dimension value" json in + Ok ((key, value) :: values)) + (Ok []) dimensions + | Some _ -> Error "metric dimensions must be an object" + in + let* aggregation = + match List.assoc_opt "aggregation" fields with + | None -> Ok None + | Some json -> + let* value = string ~name:"metric aggregation" json in + Metric.aggregation_of_string value |> Result.map Option.some + in + let* metric = Metric.create ~name ~value ?unit_ ~dimensions ?aggregation () in + Ok (Strategy.Emit_metric metric) + +let parse_intent json = match json with | `Assoc fields -> ( match List.assoc_opt "type" fields with @@ -1075,22 +941,20 @@ let parse_intent ~contract_version json = ~parse_target:parse_quantity_target (fun targets -> Strategy.Target_quantities targets) json - | Some (`String "submit_order") -> - parse_submit_intent ~contract_version json + | Some (`String "submit_order") -> parse_submit_intent json | Some (`String "cancel_order") -> parse_cancel_intent json - | Some (`String "emit_metric") -> - parse_metric_intent ~contract_version json + | Some (`String "emit_metric") -> parse_metric_intent json | Some _ -> Error "unsupported intent type" | None -> Error "intent is missing type") | _ -> Error "intent must be a JSON object" -let intent_of_yojson ?(contract_version = Contract.previous_version) json = - parse_intent ~contract_version json +let intent_of_yojson json = + parse_intent json |> Result.map_error (fun message -> Diagnostic.make ~code:Diagnostic.Scenario_invalid ~phase:Diagnostic.Validation ~json_path:"$" message) -let parse_schedule_item ~contract_version json = +let parse_schedule_item json = let* fields = object_fields ~name:"schedule item" ~expected:[ "after_slice_sequence"; "intents" ] @@ -1105,7 +969,7 @@ let parse_schedule_item ~contract_version json = (Printf.sprintf "intent count is %d; limit is %d" (List.length intents_json) Resource_limits.intents_per_batch) else - let* intents = map_list (parse_intent ~contract_version) intents_json in + let* intents = map_list parse_intent intents_json in Ok (sequence, intents) let parse_volume = function @@ -1919,45 +1783,26 @@ let parse_order_book_event json = ~ingest_sequence ~book_sequence ~price ~quantity ~aggressor_side | _ -> assert false -let parse_slice ~contract_version json = - let financing_fields = - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] - then [ "borrow_observations"; "cash_rate_observations" ] - else [] - in - let settlement_fields = - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then - [ "settlement_failures" ] - else [] - in - let lifecycle_fields = - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12" ] then - [ "lifecycle_events" ] - else [] - in - let market_event_fields = - if List.mem contract_version [ "16"; "15"; "14" ] then [ "market_events" ] - else [] - in - let order_book_event_fields = - if List.mem contract_version [ "16"; "15" ] then [ "order_book_events" ] - else [] - in +let parse_slice json = let* fields = object_fields ~name:"market slice" ~expected: - ([ - "slice_sequence"; - "start_at"; - "end_at"; - "available_at"; - "received_at"; - "bars"; - "fx_rates"; - "corporate_actions"; - ] - @ financing_fields @ settlement_fields @ lifecycle_fields - @ market_event_fields @ order_book_event_fields) + [ + "slice_sequence"; + "start_at"; + "end_at"; + "available_at"; + "received_at"; + "bars"; + "fx_rates"; + "corporate_actions"; + "borrow_observations"; + "cash_rate_observations"; + "settlement_failures"; + "lifecycle_events"; + "market_events"; + "order_book_events"; + ] json in let* sequence_json = field fields "slice_sequence" in @@ -1979,81 +1824,46 @@ 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 [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] then - let* borrow_json = - Result.bind - (field fields "borrow_observations") - (list ~name:"borrow_observations") - in - let* borrow_observations = map_list parse_borrow_observation borrow_json in - let* cash_json = - Result.bind - (field fields "cash_rate_observations") - (list ~name:"cash_rate_observations") - in - let* cash_rate_observations = - map_list parse_cash_rate_observation cash_json - in - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then - let* failures_json = - Result.bind - (field fields "settlement_failures") - (list ~name:"settlement_failures") - in - let* settlement_failures = - map_list parse_settlement_failure failures_json - in - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12" ] then - let* lifecycle_json = - Result.bind - (field fields "lifecycle_events") - (list ~name:"lifecycle_events") - in - let* lifecycle_events = map_list parse_lifecycle_event lifecycle_json in - if List.mem contract_version [ "16"; "15"; "14" ] then - let* events_json = - Result.bind - (field fields "market_events") - (list ~name:"market_events") - in - let* market_events = map_list parse_market_event events_json in - if List.mem contract_version [ "16"; "15" ] then - let* book_events_json = - Result.bind - (field fields "order_book_events") - (list ~name:"order_book_events") - in - let* order_book_events = - map_list parse_order_book_event book_events_json - in - Market_slice.create_v15 ~slice_sequence ~start_at ~end_at - ~available_at ~received_at ~bars ~fx_rates ~corporate_actions - ~borrow_observations ~cash_rate_observations ~settlement_failures - ~lifecycle_events ~market_events ~order_book_events - else - Market_slice.create_v14 ~slice_sequence ~start_at ~end_at - ~available_at ~received_at ~bars ~fx_rates ~corporate_actions - ~borrow_observations ~cash_rate_observations ~settlement_failures - ~lifecycle_events ~market_events - else - let create = - if String.equal contract_version "13" then Market_slice.create_v13 - else Market_slice.create_v12 - in - create ~slice_sequence ~start_at ~end_at ~available_at ~received_at - ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations ~settlement_failures ~lifecycle_events - else - Market_slice.create_v11 ~slice_sequence ~start_at ~end_at ~available_at - ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations ~settlement_failures - else - Market_slice.create_v10 ~slice_sequence ~start_at ~end_at ~available_at - ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations - else - Market_slice.create ~slice_sequence ~start_at ~end_at ~available_at - ~received_at ~bars ~fx_rates ~corporate_actions + let* borrow_json = + Result.bind + (field fields "borrow_observations") + (list ~name:"borrow_observations") + in + let* borrow_observations = map_list parse_borrow_observation borrow_json in + let* cash_json = + Result.bind + (field fields "cash_rate_observations") + (list ~name:"cash_rate_observations") + in + let* cash_rate_observations = + map_list parse_cash_rate_observation cash_json + in + let* failures_json = + Result.bind + (field fields "settlement_failures") + (list ~name:"settlement_failures") + in + let* settlement_failures = map_list parse_settlement_failure failures_json in + let* lifecycle_json = + Result.bind + (field fields "lifecycle_events") + (list ~name:"lifecycle_events") + in + let* lifecycle_events = map_list parse_lifecycle_event lifecycle_json in + let* events_json = + Result.bind (field fields "market_events") (list ~name:"market_events") + in + let* market_events = map_list parse_market_event events_json in + let* book_events_json = + Result.bind + (field fields "order_book_events") + (list ~name:"order_book_events") + in + let* order_book_events = map_list parse_order_book_event book_events_json in + Market_slice.create ~slice_sequence ~start_at ~end_at ~available_at + ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations ~settlement_failures ~lifecycle_events + ~market_events ~order_book_events let child root field = root ^ "." ^ field @@ -2085,28 +1895,11 @@ let construct_header ~root ~contract_path ~contract_version string ~name:"base_currency" shape.base_currency |> at (child root "base_currency") in - let* initial_cash, initial_portfolio = - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] - then - let* portfolio = - parse_initial_portfolio ~base_currency shape.initial_state - |> at (child root "initial_portfolio") - in - Ok (portfolio.Initial_portfolio.cash, Some portfolio) - else - let* initial_cash_json = - list ~name:"initial_cash" shape.initial_state - |> at (child root "initial_cash") - in - let* initial_cash = - map_list_at - (child root "initial_cash") - parse_cash_balance initial_cash_json - in - Ok (initial_cash, None) + let* initial_portfolio = + parse_initial_portfolio ~base_currency shape.initial_state + |> at (child root "initial_portfolio") in + let initial_cash = initial_portfolio.Initial_portfolio.cash in let* instruments_json = list ~name:"instruments" shape.instruments |> at (child root "instruments") @@ -2116,7 +1909,8 @@ let construct_header ~root ~contract_path ~contract_version in let* venue_calendars = match shape.venue_calendars with - | None -> Ok [] + | None -> + Error "missing venue calendars" |> at (child root "venue_calendars") | Some calendars_json -> let* calendars_json = list ~name:"venue_calendars" calendars_json @@ -2131,45 +1925,30 @@ let construct_header ~root ~contract_path ~contract_version |> at (child root "max_internal_events") in let* currencies, catalog = - Scenario_validation.header ~root ~contract_version ~base_currency - ~initial_cash ~instruments ~venue_calendars ~max_internal_events + Scenario_validation.header ~root ~base_currency ~initial_cash ~instruments + ~venue_calendars ~max_internal_events in let* risk = - parse_risk ~contract_version base_currency instruments shape.risk - |> at (child root "risk") + parse_risk base_currency instruments shape.risk |> at (child root "risk") in let* () = - match initial_portfolio with - | None -> Ok () - | Some portfolio -> - Scenario_validation.initial_portfolio ~root ~currencies ~catalog - ~instruments ~risk portfolio + Scenario_validation.initial_portfolio ~root ~currencies ~catalog + ~instruments ~risk initial_portfolio in let* execution_model, execution = parse_execution ~contract_version ~instruments shape.execution |> at (child root "execution") in let* financing = - match (contract_version, shape.financing) with - | ("12" | "11" | "10"), Some json -> - parse_financing json |> at (child root "financing") - | ("12" | "11" | "10"), None -> - Error "missing financing policy" |> at (child root "financing") - | _, _ -> Ok Financing.legacy_policy - in - let financing = - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] - then Some financing - else None + match shape.financing with + | Some json -> parse_financing json |> at (child root "financing") + | None -> Error "missing financing policy" |> at (child root "financing") in let* settlement = - match (contract_version, shape.settlement) with - | ("12" | "11"), Some json -> - let* policy = parse_settlement json |> at (child root "settlement") in - Ok (Some policy) - | ("12" | "11"), None -> + match shape.settlement with + | Some json -> parse_settlement json |> at (child root "settlement") + | None -> Error "missing settlement policy" |> at (child root "settlement") - | _, _ -> Ok None in let header : stream_header = { @@ -2177,7 +1956,6 @@ let construct_header ~root ~contract_path ~contract_version metadata; run_id; base_currency; - initial_cash; initial_portfolio; instruments; venue_calendars; @@ -2203,15 +1981,9 @@ let construct_batch (shape : Scenario_shape.batch) = let* schedule_json = list ~name:"schedule" shape.schedule |> at "$.schedule" in - let* schedule = - map_list_at "$.schedule" - (parse_schedule_item ~contract_version) - schedule_json - in + let* schedule = map_list_at "$.schedule" parse_schedule_item schedule_json in let* slices_json = list ~name:"slices" shape.slices |> at "$.slices" in - let* slices = - map_list_at "$.slices" (parse_slice ~contract_version) slices_json - in + let* slices = map_list_at "$.slices" parse_slice slices_json in let* () = Scenario_validation.batch ~root ~base_currency:header.base_currency ~currencies ~instruments:header.instruments ~risk:header.risk ~catalog @@ -2223,7 +1995,6 @@ let construct_batch (shape : Scenario_shape.batch) = metadata = header.metadata; run_id = header.run_id; base_currency = header.base_currency; - initial_cash = header.initial_cash; initial_portfolio = header.initial_portfolio; instruments = header.instruments; venue_calendars = header.venue_calendars; @@ -2291,8 +2062,7 @@ let stream_header_of_yojson ~contract_version json = in let* () = check_stream_header_limits json in let* shape = - Scenario_shape.stream_header ~contract_version json - |> Result.map_error (diagnostic code) + Scenario_shape.stream_header json |> Result.map_error (diagnostic code) in construct_header ~root:"$.payload" ~contract_path:"$.contract_version" ~contract_version shape @@ -2306,7 +2076,7 @@ let stream_item_of_yojson header ~previous json = Scenario_shape.stream_item json |> Result.map_error (diagnostic code) in let* market_slice = - parse_slice ~contract_version:header.contract_version shape.market_slice + parse_slice shape.market_slice |> at "$.payload.market_slice" |> Result.map_error (diagnostic code) in @@ -2316,9 +2086,7 @@ let stream_item_of_yojson header ~previous json = |> Result.map_error (diagnostic code) in let* intents = - map_list_at "$.payload.intents" - (parse_intent ~contract_version:header.contract_version) - intents_json + map_list_at "$.payload.intents" parse_intent intents_json |> Result.map_error (diagnostic code) in let previous_slice, previous_intents, prior_action_ids = diff --git a/lib/scenario.mli b/lib/scenario.mli index 245281c..29ff2af 100644 --- a/lib/scenario.mli +++ b/lib/scenario.mli @@ -5,15 +5,14 @@ type t = private { metadata : Yojson.Safe.t; run_id : Id.Run.t; base_currency : string; - initial_cash : (string * Scalar.Money.t) list; - initial_portfolio : Initial_portfolio.t option; + initial_portfolio : Initial_portfolio.t; instruments : Instrument.t list; venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; - financing : Financing.policy option; - settlement : Settlement.policy option; + financing : Financing.policy; + settlement : Settlement.policy; max_internal_events : int; schedule : (int64 * Strategy.intent list) list; slices : Market_slice.t list; @@ -24,15 +23,14 @@ type stream_header = private { metadata : Yojson.Safe.t; run_id : Id.Run.t; base_currency : string; - initial_cash : (string * Scalar.Money.t) list; - initial_portfolio : Initial_portfolio.t option; + initial_portfolio : Initial_portfolio.t; instruments : Instrument.t list; venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; - financing : Financing.policy option; - settlement : Settlement.policy option; + financing : Financing.policy; + settlement : Settlement.policy; max_internal_events : int; } @@ -45,11 +43,7 @@ type stream_item = private { val of_yojson : Yojson.Safe.t -> (t, Diagnostic.t) result val of_string : string -> (t, Diagnostic.t) result val read_file : string -> (t, Diagnostic.t) result - -val intent_of_yojson : - ?contract_version:string -> - Yojson.Safe.t -> - (Strategy.intent, Diagnostic.t) result +val intent_of_yojson : Yojson.Safe.t -> (Strategy.intent, Diagnostic.t) result val stream_header_of_yojson : contract_version:string -> diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index 71de814..40bed59 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -65,38 +65,17 @@ let field ~root fields name = Error (error ~json_path:(root ^ "." ^ name) ("missing JSON field: " ^ name)) -let common ~root ~contract_version fields = +let common ~root fields = let* metadata = field ~root fields "metadata" in 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 - [ "16"; "15"; "14"; "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* initial_state = field ~root fields "initial_portfolio" in let* instruments = field ~root fields "instruments" in - let venue_calendars = - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] - then List.assoc_opt "venue_calendars" fields - else None - in + let venue_calendars = List.assoc_opt "venue_calendars" fields in let* risk = field ~root fields "risk" in let* execution = field ~root fields "execution" in - let financing = - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] - then List.assoc_opt "financing" fields - else None - in - let settlement = - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then - List.assoc_opt "settlement" fields - else None - in + let financing = List.assoc_opt "financing" fields in + let settlement = List.assoc_opt "settlement" fields in let* max_internal_events = field ~root fields "max_internal_events" in Ok { @@ -120,99 +99,55 @@ let batch json = | `Assoc fields -> field ~root fields "contract_version" | _ -> Error (error ~json_path:root "scenario must be a JSON object") in - let contract_version = - match preliminary with `String value -> value | _ -> "" - in - let calendar_fields = - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] - then [ "venue_calendars" ] - else [] - in - let initial_field = - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] - then "initial_portfolio" - else "initial_cash" - in + let _ = preliminary in let* fields = object_fields ~json_path:root ~name:"scenario" ~expected: - ([ - "contract_version"; - "metadata"; - "run_id"; - "base_currency"; - initial_field; - "instruments"; - "risk"; - "execution"; - "max_internal_events"; - "schedule"; - "slices"; - ] - @ calendar_fields - @ (if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] - then [ "financing" ] - else []) - @ - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then - [ "settlement" ] - else []) + [ + "contract_version"; + "metadata"; + "run_id"; + "base_currency"; + "initial_portfolio"; + "instruments"; + "venue_calendars"; + "risk"; + "execution"; + "financing"; + "settlement"; + "max_internal_events"; + "schedule"; + "slices"; + ] json in let* contract_version_json = field ~root fields "contract_version" in - let* common = common ~root ~contract_version fields in + let* common = common ~root fields in let* schedule = field ~root fields "schedule" in let* slices = field ~root fields "slices" in Ok { contract_version = contract_version_json; common; schedule; slices } -let stream_header ~contract_version json = +let stream_header json = let root = "$.payload" in - let calendar_fields = - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] - then [ "venue_calendars" ] - else [] - in - let initial_field = - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] - then "initial_portfolio" - else "initial_cash" - in let* fields = object_fields ~json_path:root ~name:"scenario stream header payload" ~expected: - ([ - "metadata"; - "run_id"; - "base_currency"; - initial_field; - "instruments"; - "risk"; - "execution"; - "max_internal_events"; - ] - @ calendar_fields - @ (if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10" ] - then [ "financing" ] - else []) - @ - if List.mem contract_version [ "16"; "15"; "14"; "13"; "12"; "11" ] then - [ "settlement" ] - else []) + [ + "metadata"; + "run_id"; + "base_currency"; + "initial_portfolio"; + "instruments"; + "venue_calendars"; + "risk"; + "execution"; + "financing"; + "settlement"; + "max_internal_events"; + ] json in - common ~root ~contract_version fields + common ~root fields let stream_item json = let root = "$.payload" in diff --git a/lib/scenario_shape.mli b/lib/scenario_shape.mli index dda3d07..8b32093 100644 --- a/lib/scenario_shape.mli +++ b/lib/scenario_shape.mli @@ -27,8 +27,5 @@ type stream_item = { market_slice : Yojson.Safe.t; intents : Yojson.Safe.t } val error : json_path:string -> string -> error val batch : Yojson.Safe.t -> (batch, error) result - -val stream_header : - contract_version:string -> Yojson.Safe.t -> (common, error) result - +val stream_header : Yojson.Safe.t -> (common, error) result val stream_item : Yojson.Safe.t -> (stream_item, error) result diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 06bed98..fa38b4d 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -45,18 +45,8 @@ let validate_venue_calendars ~root catalog venue_calendars = "venue calendars must cover every configured instrument exactly once" else Ok () -let header ~root ~contract_version ~base_currency ~initial_cash ~instruments - ~venue_calendars ~max_internal_events = - let* () = - if - List.mem contract_version - [ "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] - then Ok () - else - Account.create ~base_currency ~initial_cash - |> Result.map (fun _ -> ()) - |> at (child root "initial_cash") - in +let header ~root ~base_currency ~initial_cash ~instruments ~venue_calendars + ~max_internal_events = if instruments = [] then fail ~json_path:(child root "instruments") "scenario must define at least one instrument" @@ -68,15 +58,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments if Id.Instrument.Set.cardinal catalog <> List.length instruments then fail ~json_path:(child root "instruments") "instrument IDs must be unique" else - let* () = - if - List.mem contract_version - [ - "16"; "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; - ] - then validate_venue_calendars ~root catalog venue_calendars - else Ok () - in + let* () = validate_venue_calendars ~root catalog venue_calendars in let currencies = base_currency :: List.map @@ -89,25 +71,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments in if cash_currencies <> currencies then fail - ~json_path: - (child root - (if - List.mem contract_version - [ - "16"; - "15"; - "14"; - "13"; - "12"; - "11"; - "10"; - "9"; - "8"; - "7"; - "6"; - ] - then "initial_portfolio.cash" - else "initial_cash")) + ~json_path:(child root "initial_portfolio.cash") "initial cash must contain every scenario currency exactly once" else if max_internal_events <= 0 then fail diff --git a/lib/scenario_validation.mli b/lib/scenario_validation.mli index 3582d2d..63cf608 100644 --- a/lib/scenario_validation.mli +++ b/lib/scenario_validation.mli @@ -2,7 +2,6 @@ val header : root:string -> - contract_version:string -> base_currency:string -> initial_cash:(string * Scalar.Money.t) list -> instruments:Instrument.t list -> diff --git a/lib/strategy_process.ml b/lib/strategy_process.ml index a4ae59f..a6fe339 100644 --- a/lib/strategy_process.ml +++ b/lib/strategy_process.ml @@ -7,7 +7,6 @@ type t = { transcript : Strategy_transcript.t; effects : Boundary_effects.t; timeout : float; - protocol_version : string; mutable next_sequence : int64; } @@ -259,8 +258,7 @@ let exchange session ~stage ~expected_sequence request = in let* response = response in match - Strategy_protocol.response_of_string - ~protocol_version:session.protocol_version ~expected_sequence response + Strategy_protocol.response_of_string ~expected_sequence response |> Result.map_error (Diagnostic.annotate ~sequence:expected_sequence ~json_path:"$") with @@ -299,8 +297,7 @@ let on_event session context event = let* sequence = next_sequence session in let* response = exchange_at session ~stage:"strategy event" ~sequence (fun ~sequence -> - Strategy_protocol.event_message - ~protocol_version:session.protocol_version ~sequence context event) + Strategy_protocol.event_message ~sequence context event) in match response with | Strategy_protocol.Intents intents -> Ok intents @@ -317,8 +314,7 @@ let shutdown session = let* sequence = next_sequence session in let* response = exchange_at session ~stage:"strategy shutdown" ~sequence - (Strategy_protocol.shutdown_message_for - ~protocol_version:session.protocol_version) + Strategy_protocol.shutdown_message in match response with | Strategy_protocol.Stopped -> Ok () @@ -461,7 +457,6 @@ let run_session ~effects ~env ~command ~executable ~timeout ~transcript transcript; effects; timeout; - protocol_version = Strategy_protocol.protocol_version initialization; next_sequence = 1L; } in diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index 8cbc699..e08afdd 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -7,15 +7,14 @@ type initialization = { metadata : Yojson.Safe.t; run_id : Id.Run.t; base_currency : string; - initial_cash : (string * Scalar.Money.t) list; - initial_portfolio : Initial_portfolio.t option; + initial_portfolio : Initial_portfolio.t; instruments : Instrument.t list; venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; - financing : Financing.policy option; - settlement : Settlement.policy option; + financing : Financing.policy; + settlement : Settlement.policy; } type identity = { name : Id.Strategy.t; version : string option } @@ -41,10 +40,10 @@ let ratio value = string (Scalar.Ratio.to_decimal_string value) let timestamp value = string (Codec.ptime_to_string value) let instrument_id value = string (Id.Instrument.to_string value) -let message ~protocol_version ~sequence:message_sequence ~message_type payload = +let message ~sequence:message_sequence ~message_type payload = `Assoc [ - ("strategy_protocol_version", string protocol_version); + ("strategy_protocol_version", string version); ("strategy_sequence", sequence message_sequence); ("message_type", string message_type); ("payload", payload); @@ -102,10 +101,6 @@ let group_kind_to_string = function let nullable render = Option.fold ~none:`Null ~some:render -let modern_protocol protocol_version = - List.mem protocol_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] - let financing_to_yojson policy = `Assoc [ @@ -186,33 +181,19 @@ let group_to_yojson (group : Risk.group) = ] ); ] -let risk_to_yojson ~protocol_version risk = - if modern_protocol protocol_version then - `Assoc - [ - ("max_gross_exposure", money (Risk.max_gross_exposure risk)); - ("max_leverage", ratio (Risk.max_leverage risk)); - ("short_borrow_bps", `Int (Risk.short_borrow_bps risk)); - ( "instrument_policies", - `List - (List.map instrument_policy_to_yojson - (Risk.instrument_policies risk)) ); - ("groups", `List (List.map group_to_yojson (Risk.groups risk))); - ] - else - `Assoc - [ - ("max_order_quantity", quantity (Risk.max_order_quantity risk)); - ("max_long_position", quantity (Risk.max_long_position risk)); - ("max_short_position", quantity (Risk.max_short_position risk)); - ("max_gross_exposure", money (Risk.max_gross_exposure risk)); - ("max_leverage", ratio (Risk.max_leverage risk)); - ("initial_margin_bps", `Int (Risk.initial_margin_bps risk)); - ("maintenance_margin_bps", `Int (Risk.maintenance_margin_bps risk)); - ("short_borrow_bps", `Int (Risk.short_borrow_bps risk)); - ] +let risk_to_yojson risk = + `Assoc + [ + ("max_gross_exposure", money (Risk.max_gross_exposure risk)); + ("max_leverage", ratio (Risk.max_leverage risk)); + ( "instrument_policies", + `List + (List.map instrument_policy_to_yojson (Risk.instrument_policies risk)) + ); + ("groups", `List (List.map group_to_yojson (Risk.groups risk))); + ] -let execution_to_yojson ~protocol_version model execution = +let execution_to_yojson model execution = let fee_component_to_yojson component = let value = match Fee_schedule.component_basis component with @@ -253,149 +234,61 @@ let execution_to_yojson ~protocol_version model execution = (Fee_schedule.components schedule)) ); ] in - if - List.mem protocol_version [ "14"; "13"; "12"; "11" ] - && List.mem - (Execution_model.name model) - [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] - then - let costs = Execution.cost_model execution |> Option.get in - `Assoc - [ - ("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 [ "14"; "13"; "12" ] - && String.equal (Execution_model.name model) "quote_trade_v1" - then - `Assoc - [ - ("model", string (Execution_model.name model)); - ( "configuration", - `Assoc - [ - ("version", string "1"); - ("participation_bps", `Int (Execution.participation_bps execution)); - ( "fee_schedules", - `List - (List.map fee_schedule_to_yojson - (Execution.fee_schedules execution)) ); - ] ); - ] - else if - List.mem protocol_version [ "14"; "13" ] - && String.equal (Execution_model.name model) "order_book_v1" - then - `Assoc - [ - ("model", string (Execution_model.name model)); - ( "configuration", - `Assoc - [ - ("version", string "1"); - ("participation_bps", `Int (Execution.participation_bps execution)); - ( "fee_schedules", - `List - (List.map fee_schedule_to_yojson - (Execution.fee_schedules execution)) ); - ( "max_depth_levels", - `Int (Option.get (Execution.book_depth_limit execution)) ); - ] ); - ] - else if - List.mem protocol_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] - then - `Assoc - [ - ("model", string (Execution_model.name model)); - ( "configuration", - `Assoc - [ - ("version", string "2"); - ("participation_bps", `Int (Execution.participation_bps execution)); - ( "fee_schedules", - `List - (List.map fee_schedule_to_yojson - (Execution.fee_schedules execution)) ); - ] ); - ] - else if modern_protocol protocol_version then - `Assoc - [ - ("model", string (Execution_model.name model)); - ( "configuration", - `Assoc - [ - ("version", string "1"); - ("participation_bps", `Int (Execution.participation_bps execution)); - ("fixed_fee", money (Execution.fixed_fee execution)); - ("fee_bps", `Int (Execution.fee_bps execution)); - ] ); - ] - else - `Assoc - [ - ("model", string (Execution_model.name model)); - ("participation_bps", `Int (Execution.participation_bps execution)); - ("fixed_fee", money (Execution.fixed_fee execution)); - ("fee_bps", `Int (Execution.fee_bps execution)); - ] - -let protocol_version initialization = - match initialization.scenario_contract_version with - | "16" -> "14" - | "15" -> "13" - | "14" -> "12" - | "13" -> "11" - | "12" -> "10" - | "11" -> "9" - | "10" -> "8" - | "9" -> "7" - | "8" -> "6" - | "7" -> "5" - | "6" -> "4" - | _ -> "3" + let configuration = + [ + ("version", string "1"); + ("participation_bps", `Int (Execution.participation_bps execution)); + ( "fee_schedules", + `List + (List.map fee_schedule_to_yojson (Execution.fee_schedules execution)) + ); + ] + in + let configuration = + match Execution_model.name model with + | "completed_bar_next_open_v1" | "completed_bar_adverse_touch_v1" -> + let costs = Execution.cost_model execution |> Option.get in + configuration + @ [ + ( "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") ); + ] ); + ] + | "order_book_v1" -> + configuration + @ [ + ( "max_depth_levels", + `Int (Option.get (Execution.book_depth_limit execution)) ); + ] + | _ -> configuration + in + `Assoc + [ + ("model", string (Execution_model.name model)); + ("configuration", `Assoc configuration); + ] let initialize_message ~sequence:message_sequence initialization = - let protocol_version = protocol_version initialization in let instruments = List.sort (fun left right -> Id.Instrument.compare left.Instrument.id right.Instrument.id) initialization.instruments in - let initial_cash = - List.sort - (fun (left, _) (right, _) -> String.compare left right) - initialization.initial_cash - in let venue_calendars = List.sort (fun left right -> @@ -410,111 +303,48 @@ let initialize_message ~sequence:message_sequence initialization = ("scenario_sha256", string initialization.scenario_sha256); ("run_id", string (Id.Run.to_string initialization.run_id)); ("base_currency", string initialization.base_currency); - ("initial_cash", `List (List.map cash_balance_to_yojson initial_cash)); + ( "initial_portfolio", + Codec.initial_portfolio_to_yojson initialization.initial_portfolio ); + ( "venue_calendars", + `List (List.map venue_calendar_to_yojson venue_calendars) ); + ("financing", financing_to_yojson initialization.financing); + ("settlement", settlement_to_yojson initialization.settlement); ("instruments", `List (List.map instrument_to_yojson instruments)); - ("risk", risk_to_yojson ~protocol_version initialization.risk); + ("risk", risk_to_yojson initialization.risk); ( "execution", - execution_to_yojson ~protocol_version initialization.execution_model + execution_to_yojson initialization.execution_model initialization.execution ); ("metadata", initialization.metadata); ] in - let fields = - if - List.mem protocol_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] - then - let initial_portfolio = - Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson - initialization.initial_portfolio - in - List.concat - [ - List.take 6 fields; - [ - ("initial_portfolio", initial_portfolio); - ( "venue_calendars", - `List (List.map venue_calendar_to_yojson venue_calendars) ); - ]; - (if - List.mem protocol_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] - then - [ - ( "financing", - Option.fold ~none:`Null ~some:financing_to_yojson - initialization.financing ); - ] - else []); - (if List.mem protocol_version [ "14"; "13"; "12"; "11"; "10"; "9" ] - then - [ - ( "settlement", - Option.fold ~none:`Null ~some:settlement_to_yojson - initialization.settlement ); - ] - else []); - List.drop 6 fields; - ] - else if modern_protocol protocol_version then - let initial_portfolio = - Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson - initialization.initial_portfolio - in - List.concat - [ - List.take 6 fields; - [ ("initial_portfolio", initial_portfolio) ]; - List.drop 6 fields; - ] - else fields - in - message ~protocol_version ~sequence:message_sequence - ~message_type:"initialize" (`Assoc fields) + message ~sequence:message_sequence ~message_type:"initialize" (`Assoc fields) -let cash_attribution_to_yojson ~protocol_version - (balance : Account.cash_attribution) = +let cash_attribution_to_yojson (balance : Account.cash_attribution) = `Assoc - ([ - ("currency", string balance.currency); - ("amount", money balance.amount); - ("fx_rate", price balance.fx_rate); - ("base_value", money balance.base_value); - ] - @ (if List.mem protocol_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] - then - [ - ("interest", money balance.interest); - ("base_interest", money balance.base_interest); - ] - else []) - @ - if List.mem protocol_version [ "14"; "13"; "12"; "11"; "10"; "9" ] then - [ - ("settled_amount", money balance.settled_amount); - ("unsettled_amount", money balance.unsettled_amount); - ("base_settled_value", money balance.base_settled_value); - ("base_unsettled_value", money balance.base_unsettled_value); - ] - else []) + [ + ("currency", string balance.currency); + ("amount", money balance.amount); + ("fx_rate", price balance.fx_rate); + ("base_value", money balance.base_value); + ("interest", money balance.interest); + ("base_interest", money balance.base_interest); + ("settled_amount", money balance.settled_amount); + ("unsettled_amount", money balance.unsettled_amount); + ("base_settled_value", money balance.base_settled_value); + ("base_unsettled_value", money balance.base_unsettled_value); + ] -let marked_position_to_yojson ~protocol_version - (position : Strategy.marked_position) = +let marked_position_to_yojson (position : Strategy.marked_position) = `Assoc - ([ - ("instrument_id", instrument_id position.instrument_id); - ("quantity", quantity position.quantity); - ("mark", price position.mark); - ("base_market_value", money position.base_market_value); - ("weight", Option.fold ~none:`Null ~some:weight position.weight); - ] - @ - if List.mem protocol_version [ "14"; "13"; "12"; "11"; "10"; "9" ] then - [ - ("settled_quantity", quantity position.settled_quantity); - ("unsettled_quantity", quantity position.unsettled_quantity); - ] - else []) + [ + ("instrument_id", instrument_id position.instrument_id); + ("quantity", quantity position.quantity); + ("mark", price position.mark); + ("base_market_value", money position.base_market_value); + ("weight", Option.fold ~none:`Null ~some:weight position.weight); + ("settled_quantity", quantity position.settled_quantity); + ("unsettled_quantity", quantity position.unsettled_quantity); + ] let group_exposure_to_yojson (exposure : Risk.group_exposure) = `Assoc @@ -528,7 +358,7 @@ let group_exposure_to_yojson (exposure : Risk.group_exposure) = Option.fold ~none:`Null ~some:weight exposure.concentration ); ] -let context_to_yojson ~protocol_version context = +let context_to_yojson context = let portfolio = Strategy.portfolio context in let cash_balances = List.sort @@ -566,103 +396,50 @@ let context_to_yojson ~protocol_version context = ("weights_available", `Bool (Option.is_some portfolio.cash_weight)); ("cash_weight", Option.fold ~none:`Null ~some:weight portfolio.cash_weight); ( "cash_balances", - `List - (List.map - (cash_attribution_to_yojson ~protocol_version) - cash_balances) ); - ( "positions", - `List (List.map (marked_position_to_yojson ~protocol_version) positions) - ); + `List (List.map cash_attribution_to_yojson cash_balances) ); + ("positions", `List (List.map marked_position_to_yojson positions)); + ( "group_exposures", + `List (List.map group_exposure_to_yojson portfolio.group_exposures) ); ] in - let portfolio_fields = - if modern_protocol protocol_version then - portfolio_fields - @ [ - ( "group_exposures", - `List (List.map group_exposure_to_yojson portfolio.group_exposures) - ); - ] - else portfolio_fields - in `Assoc [ ("now", timestamp (Strategy.now context)); ("portfolio", `Assoc portfolio_fields); - ( "working_orders", - `List - (List.map - (if - List.mem protocol_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] - then Codec.order_to_yojson_v8 - else Codec.order_to_yojson) - working_orders) ); + ("working_orders", `List (List.map Codec.order_to_yojson working_orders)); ("latest_bars", `List (List.map Codec.bar_to_yojson latest_bars)); ] -let event_to_yojson ~protocol_version = function +let event_to_yojson = function | Strategy.Market_slice_closed market_slice -> `Assoc [ ("type", string "market_slice_closed"); - ( "market_slice", - if String.equal protocol_version "14" then - Codec.market_slice_to_yojson_v16 market_slice - else if String.equal protocol_version "13" then - Codec.market_slice_to_yojson_v15 market_slice - else if String.equal protocol_version "12" then - Codec.market_slice_to_yojson_v14 market_slice - else if String.equal protocol_version "11" then - Codec.market_slice_to_yojson_v13 market_slice - else if String.equal protocol_version "10" then - Codec.market_slice_to_yojson_v12 market_slice - else if String.equal protocol_version "9" then - Codec.market_slice_to_yojson_v11 market_slice - else if String.equal protocol_version "8" then - Codec.market_slice_to_yojson_v10 market_slice - else Codec.market_slice_to_yojson market_slice ); + ("market_slice", Codec.market_slice_to_yojson market_slice); ] | Strategy.Fill_received fill -> `Assoc [ - ("type", string "fill_received"); - ( "fill", - if - List.mem protocol_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] - then Codec.fill_to_yojson_v9 fill - else Codec.fill_to_yojson fill ); + ("type", string "fill_received"); ("fill", Codec.fill_to_yojson fill); ] | Strategy.Order_updated order -> `Assoc [ ("type", string "order_updated"); - ( "order", - if - List.mem protocol_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] - then Codec.order_to_yojson_v8 order - else Codec.order_to_yojson order ); + ("order", Codec.order_to_yojson order); ] | Strategy.Intent_rejected reason -> `Assoc [ ("type", string "intent_rejected"); ("reason", string reason) ] -let event_message ?(protocol_version = version) ~sequence:message_sequence - context event = - message ~protocol_version ~sequence:message_sequence ~message_type:"event" +let event_message ~sequence:message_sequence context event = + message ~sequence:message_sequence ~message_type:"event" (`Assoc [ - ("context", context_to_yojson ~protocol_version context); - ("event", event_to_yojson ~protocol_version event); + ("context", context_to_yojson context); ("event", event_to_yojson event); ]) -let shutdown_message_for ~protocol_version ~sequence:message_sequence = - message ~protocol_version ~sequence:message_sequence ~message_type:"shutdown" - (`Assoc []) - let shutdown_message ~sequence = - shutdown_message_for ~protocol_version:version ~sequence + message ~sequence ~message_type:"shutdown" (`Assoc []) let object_fields ~name ~expected = function | `Assoc fields -> @@ -712,7 +489,7 @@ let parse_ready_payload json = let* version = optional_string ~name:"strategy_version" version_json in Ok (Ready { name; version }) -let parse_intents_payload ~protocol_version json = +let parse_intents_payload json = let* fields = object_fields ~name:"strategy intents payload" ~expected:[ "intents" ] json in @@ -727,19 +504,7 @@ let parse_intents_payload ~protocol_version json = (fun result value -> let* intents = result in let* intent = - Scenario.intent_of_yojson - ~contract_version: - (if String.equal protocol_version "14" then "16" - else if List.mem protocol_version [ "14"; "13" ] then "15" - else if String.equal protocol_version "12" then "14" - else if String.equal protocol_version "11" then "13" - else if String.equal protocol_version "10" then "12" - else if String.equal protocol_version "9" then "11" - else if String.equal protocol_version "8" then "10" - else if String.equal protocol_version "7" then "9" - else if String.equal protocol_version "6" then "8" - else "7") - value + Scenario.intent_of_yojson value |> Result.map_error Diagnostic.to_human in Ok (intent :: intents)) @@ -751,7 +516,7 @@ let parse_stopped_payload json = let* _ = object_fields ~name:"strategy stopped payload" ~expected:[] json in Ok Stopped -let response_of_yojson_result ~protocol_version ~expected_sequence json = +let response_of_yojson_result ~expected_sequence json = let* fields = object_fields ~name:"strategy response" ~expected: @@ -767,7 +532,7 @@ let response_of_yojson_result ~protocol_version ~expected_sequence json = let* supplied_version = required_string ~name:"strategy_protocol_version" version_json in - if not (String.equal supplied_version protocol_version) then + if not (String.equal supplied_version version) then Error ("unsupported strategy protocol version: " ^ supplied_version) else let* sequence_json = field fields "strategy_sequence" in @@ -787,20 +552,19 @@ let response_of_yojson_result ~protocol_version ~expected_sequence json = let* payload = field fields "payload" in match message_type with | "ready" -> parse_ready_payload payload - | "intents" -> parse_intents_payload ~protocol_version payload + | "intents" -> parse_intents_payload payload | "stopped" -> parse_stopped_payload payload | "error" -> parse_error_payload payload |> Result.map (fun message -> Failed message) | value -> Error ("unsupported strategy response type: " ^ value) -let response_of_yojson ?(protocol_version = version) ~expected_sequence json = +let response_of_yojson ~expected_sequence json = let json_path = match json with | `Assoc fields -> ( match List.assoc_opt "strategy_protocol_version" fields with - | Some (`String supplied) - when not (String.equal supplied protocol_version) -> + | Some (`String supplied) when not (String.equal supplied version) -> "$.strategy_protocol_version" | _ -> ( match List.assoc_opt "strategy_sequence" fields with @@ -832,14 +596,13 @@ let response_of_yojson ?(protocol_version = version) ~expected_sequence json = (Printf.sprintf "intent count is %d; limit is %d" observed Resource_limits.intents_per_batch)) | _ -> - response_of_yojson_result ~protocol_version ~expected_sequence json + response_of_yojson_result ~expected_sequence json |> Result.map_error (fun message -> Diagnostic.make ~code:Diagnostic.Strategy_protocol ~phase:Diagnostic.Strategy ~sequence:expected_sequence ~json_path message) -let response_of_string ?(protocol_version = version) ~expected_sequence document - = +let response_of_string ~expected_sequence document = if String.length document > max_message_bytes then Error (Diagnostic.make ~code:Diagnostic.Resource_limit @@ -849,7 +612,7 @@ let response_of_string ?(protocol_version = version) ~expected_sequence document else try let json = Yojson.Safe.from_string document in - response_of_yojson ~protocol_version ~expected_sequence json + response_of_yojson ~expected_sequence json |> Result.map (fun response -> (response, json)) with Yojson.Json_error message as exception_ -> Error @@ -863,17 +626,9 @@ let direction_to_string = function | Strategy_to_engine -> "strategy_to_engine" let transcript_record ~transcript_sequence ~direction ~message = - let protocol_version = - match message with - | `Assoc fields -> ( - match List.assoc_opt "strategy_protocol_version" fields with - | Some (`String value) -> value - | _ -> version) - | _ -> version - in `Assoc [ - ("strategy_protocol_version", string protocol_version); + ("strategy_protocol_version", string version); ("transcript_sequence", sequence transcript_sequence); ("direction", string (direction_to_string direction)); ("message", message); diff --git a/lib/strategy_protocol.mli b/lib/strategy_protocol.mli index ab15974..54cc26f 100644 --- a/lib/strategy_protocol.mli +++ b/lib/strategy_protocol.mli @@ -9,15 +9,14 @@ type initialization = { metadata : Yojson.Safe.t; run_id : Id.Run.t; base_currency : string; - initial_cash : (string * Scalar.Money.t) list; - initial_portfolio : Initial_portfolio.t option; + initial_portfolio : Initial_portfolio.t; instruments : Instrument.t list; venue_calendars : Venue_calendar.t list; risk : Risk.t; execution_model : Execution_model.t; execution : Execution.t; - financing : Financing.policy option; - settlement : Settlement.policy option; + financing : Financing.policy; + settlement : Settlement.policy; } type identity = private { name : Id.Strategy.t; version : string option } @@ -30,29 +29,17 @@ type response = type direction = Engine_to_strategy | Strategy_to_engine -val protocol_version : initialization -> string val initialize_message : sequence:int64 -> initialization -> Yojson.Safe.t val event_message : - ?protocol_version:string -> - sequence:int64 -> - Strategy.context -> - Strategy.event -> - Yojson.Safe.t - -val shutdown_message_for : - protocol_version:string -> sequence:int64 -> Yojson.Safe.t + sequence:int64 -> Strategy.context -> Strategy.event -> Yojson.Safe.t val shutdown_message : sequence:int64 -> Yojson.Safe.t val response_of_yojson : - ?protocol_version:string -> - expected_sequence:int64 -> - Yojson.Safe.t -> - (response, Diagnostic.t) result + expected_sequence:int64 -> Yojson.Safe.t -> (response, Diagnostic.t) result val response_of_string : - ?protocol_version:string -> expected_sequence:int64 -> string -> (response * Yojson.Safe.t, Diagnostic.t) result diff --git a/mkdocs.yml b/mkdocs.yml index ec574e8..a5b8e0f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: Trading Engine -site_description: Deterministic execution, replay contracts, and OCaml API reference +site_description: Deterministic OCaml trading replay engine site_url: https://fallblu.github.io/trading-engine/ repo_url: https://github.com/fallblu/trading-engine repo_name: fallblu/trading-engine @@ -31,18 +31,9 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v16: contracts/v16/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 + - Current v1: contracts/v1/README.md - External strategy: - - Current v14: contracts/strategy/v14/README.md - - Historical v13: contracts/strategy/v13/README.md - - Historical v3: contracts/strategy/v3/README.md - - Historical v2: contracts/strategy/v2/README.md - - Historical v1: contracts/strategy/v1/README.md + - Current v1: contracts/strategy/v1/README.md - API reference: docs/api-reference.md - Engineering: - Continuous integration: docs/continuous-integration.md diff --git a/scripts/check-deterministic-journals b/scripts/check-deterministic-journals index b05c7df..55304ec 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -16,21 +16,18 @@ export TZ=UTC compare_journal() { name=$1 - scenario=$2 - expected=$3 actual="$temporary_root/$name.journal.jsonl" - standard_output="$temporary_root/$name.stdout" - standard_error="$temporary_root/$name.stderr" "$engine" \ - --input "$repository_root/$scenario" \ + --input "$repository_root/contracts/v1/fixtures/$name.scenario.json" \ --journal "$actual" \ - >"$standard_output" \ - 2>"$standard_error" + >"$temporary_root/$name.stdout" \ + 2>"$temporary_root/$name.stderr" - if ! cmp -s "$repository_root/$expected" "$actual"; then + expected="$repository_root/contracts/v1/fixtures/$name.journal.jsonl" + if ! cmp -s "$expected" "$actual"; then printf '%s\n' "error: $name journal differs from its canonical bytes" >&2 - diff -u "$repository_root/$expected" "$actual" >&2 || true + diff -u "$expected" "$actual" >&2 || true exit 1 fi @@ -38,111 +35,7 @@ compare_journal() { printf '%s\n' "$name: exact journal match ($byte_count bytes)" } -compare_journal \ - v3-demo \ - contracts/v3/fixtures/demo.scenario.json \ - contracts/v3/fixtures/demo.journal.jsonl -compare_journal \ - v5-demo \ - contracts/v5/fixtures/demo.scenario.json \ - contracts/v5/fixtures/demo.journal.jsonl -compare_journal \ - v5-fill-clipped \ - contracts/v5/fixtures/fill-clipped.scenario.json \ - contracts/v5/fixtures/fill-clipped.journal.jsonl -compare_journal \ - v8-demo \ - contracts/v8/fixtures/demo.scenario.json \ - contracts/v8/fixtures/demo.journal.jsonl -compare_journal \ - v8-fill-clipped \ - contracts/v8/fixtures/fill-clipped.scenario.json \ - contracts/v8/fixtures/fill-clipped.journal.jsonl -compare_journal \ - v9-demo \ - contracts/v9/fixtures/demo.scenario.json \ - contracts/v9/fixtures/demo.journal.jsonl -compare_journal \ - v9-fill-clipped \ - contracts/v9/fixtures/fill-clipped.scenario.json \ - contracts/v9/fixtures/fill-clipped.journal.jsonl -compare_journal \ - v10-demo \ - contracts/v10/fixtures/demo.scenario.json \ - contracts/v10/fixtures/demo.journal.jsonl -compare_journal \ - v10-fill-clipped \ - contracts/v10/fixtures/fill-clipped.scenario.json \ - contracts/v10/fixtures/fill-clipped.journal.jsonl -compare_journal \ - v11-demo \ - contracts/v11/fixtures/demo.scenario.json \ - contracts/v11/fixtures/demo.journal.jsonl -compare_journal \ - v11-fill-clipped \ - contracts/v11/fixtures/fill-clipped.scenario.json \ - contracts/v11/fixtures/fill-clipped.journal.jsonl -compare_journal \ - v12-demo \ - contracts/v12/fixtures/demo.scenario.json \ - contracts/v12/fixtures/demo.journal.jsonl -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 -compare_journal \ - v14-demo \ - contracts/v14/fixtures/demo.scenario.json \ - contracts/v14/fixtures/demo.journal.jsonl -compare_journal \ - v14-fill-clipped \ - contracts/v14/fixtures/fill-clipped.scenario.json \ - contracts/v14/fixtures/fill-clipped.journal.jsonl -compare_journal \ - v14-quote-trade \ - contracts/v14/fixtures/quote-trade.scenario.json \ - contracts/v14/fixtures/quote-trade.journal.jsonl -compare_journal \ - v15-demo \ - contracts/v15/fixtures/demo.scenario.json \ - contracts/v15/fixtures/demo.journal.jsonl -compare_journal \ - v15-fill-clipped \ - contracts/v15/fixtures/fill-clipped.scenario.json \ - contracts/v15/fixtures/fill-clipped.journal.jsonl -compare_journal \ - v15-quote-trade \ - contracts/v15/fixtures/quote-trade.scenario.json \ - contracts/v15/fixtures/quote-trade.journal.jsonl -compare_journal \ - v15-order-book \ - contracts/v15/fixtures/order-book.scenario.json \ - contracts/v15/fixtures/order-book.journal.jsonl - -compare_journal \ - v16-demo \ - contracts/v16/fixtures/demo.scenario.json \ - contracts/v16/fixtures/demo.journal.jsonl - -compare_journal \ - v16-fill-clipped \ - contracts/v16/fixtures/fill-clipped.scenario.json \ - contracts/v16/fixtures/fill-clipped.journal.jsonl - -compare_journal \ - v16-quote-trade \ - contracts/v16/fixtures/quote-trade.scenario.json \ - contracts/v16/fixtures/quote-trade.journal.jsonl - -compare_journal \ - v16-order-book \ - contracts/v16/fixtures/order-book.scenario.json \ - contracts/v16/fixtures/order-book.journal.jsonl +compare_journal demo +compare_journal fill-clipped +compare_journal quote-trade +compare_journal order-book diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index 5ae4a7b..dfe9d50 100644 --- a/scripts/check-documentation.py +++ b/scripts/check-documentation.py @@ -27,15 +27,7 @@ "SECURITY.md", "contracts/conformance/README.md", "contracts/cli/v1/README.md", - "contracts/v16/README.md", - "contracts/v5/README.md", - "contracts/v4/README.md", - "contracts/v3/README.md", - "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v14/README.md", - "contracts/strategy/v3/README.md", - "contracts/strategy/v2/README.md", "contracts/strategy/v1/README.md", "docs/api-reference.md", "docs/continuous-integration.md", @@ -166,9 +158,6 @@ def site_failures(root: Path = REPOSITORY_ROOT) -> list[str]: site / "docs" / "repository-governance" / "index.html", site / "SECURITY" / "index.html", site / "docs" / "api-reference" / "index.html", - site / "contracts" / "v4" / "index.html", - site / "contracts" / "v3" / "index.html", - site / "contracts" / "v2" / "index.html", site / "contracts" / "v1" / "index.html", site / "api" / "trading_engine" / "Trading_engine" / "index.html", ) diff --git a/scripts/release_artifacts.py b/scripts/release_artifacts.py index aceeddc..b967524 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/v16/scenario.schema.json", - "share/trading_engine/contracts/v16/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v1/scenario.schema.json", + "share/trading_engine/contracts/v1/fixtures/demo.scenario.json", "share/trading_engine/contracts/cli/v1/result.schema.json", "doc/trading_engine/README.md", ), @@ -389,7 +389,7 @@ def verify_release( ( "trading_engine.opam", "contracts/v1/scenario.schema.json", - "contracts/v16/fixtures/demo.scenario.json", + "contracts/v1/fixtures/demo.scenario.json", "contracts/cli/v1/result.schema.json", "docs/architecture.md", ".github/workflows/release-candidate.yml", @@ -402,8 +402,8 @@ def verify_release( ( "contracts/conformance/manifest.json", "contracts/v1/scenario.schema.json", - "contracts/v16/fixtures/demo.scenario.json", - "contracts/strategy/v14/message.schema.json", + "contracts/v1/fixtures/demo.scenario.json", + "contracts/strategy/v1/message.schema.json", "contracts/cli/v1/result.schema.json", ), epoch, @@ -415,7 +415,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v16/scenario.schema.json", + "contracts/v1/scenario.schema.json", "contracts/cli/v1/result.schema.json", "api/trading_engine/Trading_engine/index.html", ), diff --git a/test/cli.t b/test/cli.t index 22bbca7..87cbaf6 100644 --- a/test/cli.t +++ b/test/cli.t @@ -2,27 +2,27 @@ 1.0.0 $ ../bin/main.exe --capabilities - {"engine_version":"1.0.0","scenario_contract_versions":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1","order_book_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["16","15","14","13","12","11","10","9","8","7","6","5","4","3"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"2":["version","participation_bps","fee_schedules"],"1":["version","participation_bps","fixed_fee","fee_bps"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"fee_bps":{"minimum":0,"maximum":10000},"fixed_fee":{"minimum":"0","unit":"money"}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_adverse_touch_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15","14"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"order_book_v1","configuration_versions":["1"],"scenario_contract_versions":["16","15"],"required_fields":["version","participation_bps","fee_schedules","max_depth_levels"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","max_depth_levels"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["slice_open_level_two_snapshot","contiguous_absolute_level_updates","aggressor_classified_depth_consuming_trades","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"max_depth_levels":{"minimum":1,"maximum":1024}}}],"strategy_protocol_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"resource_limits":{"version":"1","scenario_record_bytes":1048576,"strategy_message_bytes":1048576,"internal_events":100000,"catalog_instruments":4096,"intents_per_batch":4096,"artifact_record_bytes":2097152}} + {"engine_version":"1.0.0","scenario_contract_versions":["1"],"journal_contract_versions":["1"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1","order_book_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"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":["1"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"order_book_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"required_fields":["version","participation_bps","fee_schedules","max_depth_levels"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","max_depth_levels"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["slice_open_level_two_snapshot","contiguous_absolute_level_updates","aggressor_classified_depth_consuming_trades","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"max_depth_levels":{"minimum":1,"maximum":1024}}}],"strategy_protocol_versions":["1"],"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 + $ ../bin/main.exe --validate-only --input ../contracts/v1/fixtures/demo.scenario.json + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=e2227af76072fab8151c3e1bd86f401293d16a736e32040efdaf8761cd397574 - $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl - valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=786f38d8bd10faac03b6b15c7aa8ae0a867eedc609ca6eaa75cfd93ae3ffdcae + $ ../bin/main.exe --validate-only --input-format jsonl --input ../contracts/v1/fixtures/demo.scenario.jsonl + valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=cb5cf2c600829b54be763809138137fe53ed61fd41f50c5b4d98cb27ee528eb4 - $ ../bin/main.exe --output-format json --validate-only --input ../contracts/v8/fixtures/demo.scenario.json | python3 -c 'import json, sys; result=json.load(sys.stdin); print(result["result_version"], result["status"], result["operation"], result["run_id"]); print(result["counts"]["instruments"], result["counts"]["slices"], result["counts"]["audits"], result["valuation"]["equity"]); print(result["hashes"]["journal_sha256"], result["artifacts"]["journal"])' + $ ../bin/main.exe --output-format json --validate-only --input ../contracts/v1/fixtures/demo.scenario.json | python3 -c 'import json, sys; result=json.load(sys.stdin); print(result["result_version"], result["status"], result["operation"], result["run_id"]); print(result["counts"]["instruments"], result["counts"]["slices"], result["counts"]["audits"], result["valuation"]["equity"]); print(result["hashes"]["journal_sha256"], result["artifacts"]["journal"])' 1 success validate demo - 1 4 22 10111.65392 + 1 4 34 10111.979929 None None - $ ../bin/main.exe --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts - run=demo audits=22 orders=3 active=0 filled=2 rejected=0 - cash=9846.65392 equity=10111.65392 gross=265 realized=18.965682 unrealized=7.688238 fees=3.16608 + $ ../bin/main.exe --input-format jsonl --input ../contracts/v1/fixtures/demo.scenario.jsonl --journal streamed.journal.jsonl --durable-artifacts + run=demo audits=34 orders=3 active=0 filled=2 rejected=0 + cash=9846.979929 equity=10111.979929 gross=265 realized=19.134573 unrealized=7.845356 fees=3.157896 journal=streamed.journal.jsonl $ wc -l < streamed.journal.jsonl - 22 + 34 - $ head -n 5 ../contracts/v8/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl + $ head -n 5 ../contracts/v1/fixtures/demo.scenario.jsonl > truncated.scenario.jsonl $ ../bin/main.exe --validate-only --input-format jsonl --input truncated.scenario.jsonl trading-engine: scenario_end must terminate the scenario stream [123] @@ -43,23 +43,23 @@ $ ../bin/main.exe --output-format json --validate-only --input missing.scenario.json 2>&1 >/dev/null | python3 -c 'import json, sys; diagnostic=json.load(sys.stdin); print(diagnostic["code"], diagnostic["phase"])' input.io input - $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v8/fixtures/demo.scenario.json > invalid-tick.json + $ sed 's/"open": "100"/"open": "100.001"/' ../contracts/v1/fixtures/demo.scenario.json > invalid-tick.json $ ../bin/main.exe --validate-only --input invalid-tick.json trading-engine: market prices and volumes must align with instrument increments [123] - $ ../bin/main.exe --input ../contracts/v8/fixtures/demo.scenario.json + $ ../bin/main.exe --input ../contracts/v1/fixtures/demo.scenario.json trading-engine: --journal is required unless --validate-only is set [123] - $ ../bin/main.exe --validate-only --input ../contracts/v8/fixtures/demo.scenario.json --journal validation.journal.jsonl + $ ../bin/main.exe --validate-only --input ../contracts/v1/fixtures/demo.scenario.json --journal validation.journal.jsonl trading-engine: --journal cannot be used with --validate-only [123] $ test ! -e validation.journal.jsonl - $ ../bin/main.exe --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl --journal piped-file.journal.jsonl >/dev/null - $ cat ../contracts/v8/fixtures/demo.scenario.jsonl | ../bin/main.exe --input-format jsonl --input - --journal - > piped-stdout.journal.jsonl 2> piped-summary.txt + $ ../bin/main.exe --input-format jsonl --input ../contracts/v1/fixtures/demo.scenario.jsonl --journal piped-file.journal.jsonl >/dev/null + $ cat ../contracts/v1/fixtures/demo.scenario.jsonl | ../bin/main.exe --input-format jsonl --input - --journal - > piped-stdout.journal.jsonl 2> piped-summary.txt $ cmp piped-file.journal.jsonl piped-stdout.journal.jsonl $ python3 - piped-summary.txt piped-stdout.journal.jsonl <<'PY' > import hashlib @@ -73,9 +73,9 @@ > print(len(journal_bytes.splitlines()), hashlib.sha256(journal_bytes).hexdigest() == hashlib.sha256(open("piped-file.journal.jsonl", "rb").read()).hexdigest()) > PY True run_completed - 22 True + 34 True - $ ../bin/main.exe --output-format json --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl --journal - > piped-json.journal.jsonl 2> piped-json-summary.json + $ ../bin/main.exe --output-format json --input-format jsonl --input ../contracts/v1/fixtures/demo.scenario.jsonl --journal - > piped-json.journal.jsonl 2> piped-json-summary.json $ python3 -c 'import json; result=json.load(open("piped-json-summary.json")); print(result["result_version"], result["operation"], result["artifacts"]["journal"], result["hashes"]["journal_sha256"] is not None)' 1 replay stdout True $ cmp piped-file.journal.jsonl piped-json.journal.jsonl @@ -84,43 +84,43 @@ trading-engine: standard input requires --input-format jsonl; batch JSON is not supported [123] - $ ../bin/main.exe --input-format jsonl --input ../contracts/v8/fixtures/demo.scenario.jsonl --journal - --durable-artifacts + $ ../bin/main.exe --input-format jsonl --input ../contracts/v1/fixtures/demo.scenario.jsonl --journal - --durable-artifacts trading-engine: --durable-artifacts cannot be used when --journal writes to standard output [123] - $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v8/fixtures/demo.scenario.json + $ ../bin/main.exe --validate-only --durable-artifacts --input ../contracts/v1/fixtures/demo.scenario.json trading-engine: --durable-artifacts cannot be used with --validate-only [123] - $ ../bin/main.exe --input ../contracts/v8/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 + $ ../bin/main.exe --input ../contracts/v1/fixtures/demo.scenario.json --journal ignored.journal.jsonl --strategy-timeout 5 trading-engine: --strategy-arg, --strategy-timeout, and --strategy-transcript require --strategy-executable [123] $ test ! -e ignored.journal.jsonl $ mkdir external - $ ../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external/run.strategy.jsonl --strategy-timeout 5 --durable-artifacts - run=external-demo audits=12 orders=1 active=0 filled=1 rejected=0 + $ ../bin/main.exe --input ../contracts/strategy/v1/fixtures/external.scenario.json --journal external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external/run.strategy.jsonl --strategy-timeout 5 --durable-artifacts + run=external-demo audits=13 orders=1 active=0 filled=1 rejected=0 cash=9793.544 equity=10007.544 gross=214 realized=0 unrealized=7.544 fees=0.456 journal=external/run.journal.jsonl strategy_transcript=external/run.strategy.jsonl $ python3 -c 'from pathlib import Path; print(len(Path("external/run.journal.jsonl").read_text().splitlines()), len(Path("external/run.strategy.jsonl").read_text().splitlines()))' - 12 14 - $ diff -u ../contracts/strategy/v6/fixtures/external.strategy.jsonl external/run.strategy.jsonl + 13 14 + $ diff -u ../contracts/strategy/v1/fixtures/external.strategy.jsonl external/run.strategy.jsonl $ mkdir external-json - $ ../bin/main.exe --output-format json --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal external-json/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-json/run.strategy.jsonl --strategy-timeout 5 | python3 -c 'import hashlib, json, sys; result=json.load(sys.stdin); digest=lambda path: hashlib.sha256(open(path, "rb").read()).hexdigest(); print(result["operation"], result["run_id"], result["artifacts"]["strategy_transcript"]); print(result["hashes"]["journal_sha256"] == digest(result["artifacts"]["journal"]), result["hashes"]["strategy_transcript_sha256"] == digest(result["artifacts"]["strategy_transcript"]))' + $ ../bin/main.exe --output-format json --input ../contracts/strategy/v1/fixtures/external.scenario.json --journal external-json/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-json/run.strategy.jsonl --strategy-timeout 5 | python3 -c 'import hashlib, json, sys; result=json.load(sys.stdin); digest=lambda path: hashlib.sha256(open(path, "rb").read()).hexdigest(); print(result["operation"], result["run_id"], result["artifacts"]["strategy_transcript"]); print(result["hashes"]["journal_sha256"] == digest(result["artifacts"]["journal"]), result["hashes"]["strategy_transcript_sha256"] == digest(result["artifacts"]["strategy_transcript"]))' replay external-demo external-json/run.strategy.jsonl True True - $ ../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal ignored-stdout.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript - + $ ../bin/main.exe --input ../contracts/strategy/v1/fixtures/external.scenario.json --journal ignored-stdout.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript - trading-engine: --strategy-transcript does not support standard output; choose a file path [123] $ test ! -e ignored-stdout.journal.jsonl $ mkdir callback-ordering - $ ../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal callback-ordering/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg cancel-next --strategy-transcript callback-ordering/run.strategy.jsonl --strategy-timeout 5 - run=external-demo audits=12 orders=2 active=0 filled=1 rejected=0 + $ ../bin/main.exe --input ../contracts/strategy/v1/fixtures/external.scenario.json --journal callback-ordering/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg cancel-next --strategy-transcript callback-ordering/run.strategy.jsonl --strategy-timeout 5 + run=external-demo audits=13 orders=2 active=0 filled=1 rejected=0 cash=9896.647 equity=10003.647 gross=107 realized=0 unrealized=3.647 fees=0.353 journal=callback-ordering/run.journal.jsonl strategy_transcript=callback-ordering/run.strategy.jsonl @@ -130,7 +130,7 @@ 107 107 $ mkdir failed-external - $ ../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal failed-external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg stall --strategy-transcript failed-external/run.strategy.jsonl --strategy-timeout 0.01 + $ ../bin/main.exe --input ../contracts/strategy/v1/fixtures/external.scenario.json --journal failed-external/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-arg stall --strategy-transcript failed-external/run.strategy.jsonl --strategy-timeout 0.01 trading-engine: strategy initialization: external strategy timed out [123] $ test ! -e failed-external/run.journal.jsonl @@ -143,7 +143,7 @@ > expected="$2" > directory="fault-$mode" > mkdir "$directory" - > output=$(../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 5 2>&1) + > output=$(../bin/main.exe --input ../contracts/strategy/v1/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 5 2>&1) > status=$? > test "$status" -eq 123 || return 1 > case "$output" in *"$expected"*) ;; *) return 1 ;; esac @@ -188,7 +188,7 @@ > if mode == "bad-sequence": > assert response["strategy_sequence"] == "999" > elif mode == "wrong-version": - > assert response["strategy_protocol_version"] == "1" + > assert response["strategy_protocol_version"] == "2" > elif mode == "unknown-field": > assert response["unexpected"] is True > PY @@ -220,7 +220,7 @@ > directory="process-tree-$mode" > mkdir "$directory" > pid_path="$directory/grandchild.pid" - > output=$(../bin/main.exe --input ../contracts/strategy/v6/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-arg "$pid_path" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 0.2 2>&1) + > output=$(../bin/main.exe --input ../contracts/strategy/v1/fixtures/external.scenario.json --journal "$directory/run.journal.jsonl" --strategy-executable ./fake_strategy.py --strategy-arg "$mode" --strategy-arg "$pid_path" --strategy-transcript "$directory/run.strategy.jsonl" --strategy-timeout 0.2 2>&1) > status=$? > test "$status" -eq 123 || return 1 > case "$output" in *"$expected"*) ;; *) return 1 ;; esac @@ -249,8 +249,8 @@ grandchild-malformed: process tree reaped $ mkdir external-stream - $ ../bin/main.exe --input-format jsonl --input ../contracts/strategy/v6/fixtures/external.scenario.jsonl --journal external-stream/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-stream/run.strategy.jsonl --strategy-timeout 5 - run=external-demo audits=12 orders=1 active=0 filled=1 rejected=0 + $ ../bin/main.exe --input-format jsonl --input ../contracts/strategy/v1/fixtures/external.scenario.jsonl --journal external-stream/run.journal.jsonl --strategy-executable ./fake_strategy.py --strategy-transcript external-stream/run.strategy.jsonl --strategy-timeout 5 + run=external-demo audits=13 orders=1 active=0 filled=1 rejected=0 cash=9793.544 equity=10007.544 gross=214 realized=0 unrealized=7.544 fees=0.456 journal=external-stream/run.journal.jsonl strategy_transcript=external-stream/run.strategy.jsonl diff --git a/test/dune b/test/dune index aba6db0..34999af 100644 --- a/test/dune +++ b/test/dune @@ -22,109 +22,7 @@ test_scenario test_engine) (deps - ../contracts/v7/fixtures/demo.journal.jsonl - ../contracts/v7/fixtures/demo.scenario.json - ../contracts/v7/fixtures/demo.scenario.jsonl - ../contracts/v7/fixtures/fill-clipped.journal.jsonl - ../contracts/v7/fixtures/fill-clipped.scenario.json - ../contracts/v7/journal.schema.json - ../contracts/v7/scenario-stream.schema.json - ../contracts/v7/scenario.schema.json - ../contracts/v8/fixtures/demo.journal.jsonl - ../contracts/v8/fixtures/demo.scenario.json - ../contracts/v8/fixtures/demo.scenario.jsonl - ../contracts/v8/fixtures/fill-clipped.journal.jsonl - ../contracts/v8/fixtures/fill-clipped.scenario.json - ../contracts/v8/journal.schema.json - ../contracts/v8/scenario-stream.schema.json - ../contracts/v8/scenario.schema.json - ../contracts/v9/fixtures/demo.journal.jsonl - ../contracts/v9/fixtures/demo.scenario.json - ../contracts/v9/fixtures/demo.scenario.jsonl - ../contracts/v9/fixtures/fill-clipped.journal.jsonl - ../contracts/v9/fixtures/fill-clipped.scenario.json - ../contracts/v9/journal.schema.json - ../contracts/v9/scenario-stream.schema.json - ../contracts/v9/scenario.schema.json - ../contracts/v10/fixtures/demo.journal.jsonl - ../contracts/v10/fixtures/demo.scenario.json - ../contracts/v10/fixtures/demo.scenario.jsonl - ../contracts/v10/fixtures/fill-clipped.journal.jsonl - ../contracts/v10/fixtures/fill-clipped.scenario.json - ../contracts/v10/journal.schema.json - ../contracts/v10/scenario-stream.schema.json - ../contracts/v10/scenario.schema.json - ../contracts/v11/fixtures/demo.journal.jsonl - ../contracts/v11/fixtures/demo.scenario.json - ../contracts/v11/fixtures/demo.scenario.jsonl - ../contracts/v11/fixtures/fill-clipped.journal.jsonl - ../contracts/v11/fixtures/fill-clipped.scenario.json - ../contracts/v11/journal.schema.json - ../contracts/v11/scenario-stream.schema.json - ../contracts/v11/scenario.schema.json - ../contracts/v12/fixtures/demo.journal.jsonl - ../contracts/v12/fixtures/demo.scenario.json - ../contracts/v12/fixtures/demo.scenario.jsonl - ../contracts/v12/fixtures/fill-clipped.journal.jsonl - ../contracts/v12/fixtures/fill-clipped.scenario.json - ../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/v15/fixtures/demo.journal.jsonl - ../contracts/v15/fixtures/demo.scenario.json - ../contracts/v15/fixtures/demo.scenario.jsonl - ../contracts/v15/fixtures/fill-clipped.journal.jsonl - ../contracts/v15/fixtures/fill-clipped.scenario.json - ../contracts/v15/fixtures/quote-trade.journal.jsonl - ../contracts/v15/fixtures/quote-trade.scenario.json - ../contracts/v15/fixtures/quote-trade.scenario.jsonl - ../contracts/v15/fixtures/order-book.journal.jsonl - ../contracts/v15/fixtures/order-book.scenario.json - ../contracts/v15/fixtures/order-book.scenario.jsonl - ../contracts/v15/journal.schema.json - ../contracts/v15/scenario-stream.schema.json - ../contracts/v15/scenario.schema.json - ../contracts/v6/fixtures/demo.scenario.json - ../contracts/v6/fixtures/demo.scenario.jsonl - ../contracts/v5/fixtures/demo.scenario.json - ../contracts/v5/fixtures/demo.scenario.jsonl - ../contracts/v4/fixtures/demo.scenario.json - ../contracts/v3/fixtures/demo.journal.jsonl - ../contracts/v3/fixtures/demo.scenario.json - ../contracts/v3/fixtures/demo.scenario.jsonl - ../contracts/conformance/cases.json - ../contracts/strategy/v5/fixtures/external.strategy.jsonl - ../contracts/strategy/v6/fixtures/external.strategy.jsonl - ../contracts/strategy/v7/fixtures/external.strategy.jsonl - ../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/v13/fixtures/external.strategy.jsonl - ../contracts/strategy/v14/fixtures/external.strategy.jsonl - ../contracts/v16/fixtures/demo.journal.jsonl - ../contracts/v16/fixtures/demo.scenario.json - ../contracts/v16/fixtures/demo.scenario.jsonl - ../contracts/v16/fixtures/fill-clipped.journal.jsonl - ../contracts/v16/fixtures/fill-clipped.scenario.json - ../contracts/v16/fixtures/order-book.journal.jsonl - ../contracts/v16/fixtures/order-book.scenario.json - ../contracts/v16/fixtures/order-book.scenario.jsonl - ../contracts/v16/fixtures/quote-trade.journal.jsonl - ../contracts/v16/fixtures/quote-trade.scenario.json - ../contracts/v16/fixtures/quote-trade.scenario.jsonl - ../contracts/v16/journal.schema.json - ../contracts/v16/scenario-stream.schema.json - ../contracts/v16/scenario.schema.json - ../contracts/strategy/v4/fixtures/external.strategy.jsonl + (source_tree ../contracts) fake_strategy.py) (libraries trading_engine @@ -146,358 +44,43 @@ (alias runtest) (deps validate_schemas.py - ../contracts/v15/fixtures/demo.journal.jsonl - ../contracts/v15/fixtures/demo.scenario.json - ../contracts/v15/fixtures/demo.scenario.jsonl - ../contracts/v15/journal.schema.json - ../contracts/v15/scenario-stream.schema.json - ../contracts/v15/scenario.schema.json) + ../contracts/v1/scenario.schema.json + ../contracts/v1/scenario-stream.schema.json + ../contracts/v1/journal.schema.json + ../contracts/v1/fixtures/demo.scenario.json + ../contracts/v1/fixtures/demo.scenario.jsonl + ../contracts/v1/fixtures/demo.journal.jsonl) (action (run python3 %{dep:validate_schemas.py} - %{dep:../contracts/v15/scenario.schema.json} - %{dep:../contracts/v15/scenario-stream.schema.json} - %{dep:../contracts/v15/journal.schema.json} - %{dep:../contracts/v15/fixtures/demo.scenario.json} - %{dep:../contracts/v15/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v15/fixtures/demo.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v15/fixtures/quote-trade.journal.jsonl - ../contracts/v15/fixtures/quote-trade.scenario.json - ../contracts/v15/fixtures/quote-trade.scenario.jsonl - ../contracts/v15/journal.schema.json - ../contracts/v15/scenario-stream.schema.json - ../contracts/v15/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v15/scenario.schema.json} - %{dep:../contracts/v15/scenario-stream.schema.json} - %{dep:../contracts/v15/journal.schema.json} - %{dep:../contracts/v15/fixtures/quote-trade.scenario.json} - %{dep:../contracts/v15/fixtures/quote-trade.scenario.jsonl} - %{dep:../contracts/v15/fixtures/quote-trade.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_strategy_schema.py - ../contracts/v15/scenario.schema.json - ../contracts/v15/journal.schema.json - ../contracts/diagnostic/v1/diagnostic.schema.json - ../contracts/strategy/v13/message.schema.json - ../contracts/strategy/v13/transcript.schema.json - ../contracts/strategy/v13/fixtures/external.strategy.jsonl) - (action - (run - python3 - %{dep:validate_strategy_schema.py} - %{dep:../contracts/v15/scenario.schema.json} - %{dep:../contracts/v15/journal.schema.json} - %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} - %{dep:../contracts/strategy/v13/message.schema.json} - %{dep:../contracts/strategy/v13/transcript.schema.json} - %{dep:../contracts/strategy/v13/fixtures/external.strategy.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v15/fixtures/order-book.journal.jsonl - ../contracts/v15/fixtures/order-book.scenario.json - ../contracts/v15/fixtures/order-book.scenario.jsonl - ../contracts/v15/journal.schema.json - ../contracts/v15/scenario-stream.schema.json - ../contracts/v15/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v15/scenario.schema.json} - %{dep:../contracts/v15/scenario-stream.schema.json} - %{dep:../contracts/v15/journal.schema.json} - %{dep:../contracts/v15/fixtures/order-book.scenario.json} - %{dep:../contracts/v15/fixtures/order-book.scenario.jsonl} - %{dep:../contracts/v15/fixtures/order-book.journal.jsonl}))) - -(rule - (alias runtest) - (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}))) + %{dep:../contracts/v1/scenario.schema.json} + %{dep:../contracts/v1/scenario-stream.schema.json} + %{dep:../contracts/v1/journal.schema.json} + %{dep:../contracts/v1/fixtures/demo.scenario.json} + %{dep:../contracts/v1/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v1/fixtures/demo.journal.jsonl}))) (rule (alias runtest) (deps validate_strategy_schema.py - ../contracts/v13/scenario.schema.json - ../contracts/v13/journal.schema.json + ../contracts/v1/scenario.schema.json + ../contracts/v1/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) + ../contracts/strategy/v1/message.schema.json + ../contracts/strategy/v1/transcript.schema.json + ../contracts/strategy/v1/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/v1/scenario.schema.json} + %{dep:../contracts/v1/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 - validate_schemas.py - ../contracts/v12/fixtures/demo.journal.jsonl - ../contracts/v12/fixtures/demo.scenario.json - ../contracts/v12/fixtures/demo.scenario.jsonl - ../contracts/v12/journal.schema.json - ../contracts/v12/scenario-stream.schema.json - ../contracts/v12/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v12/scenario.schema.json} - %{dep:../contracts/v12/scenario-stream.schema.json} - %{dep:../contracts/v12/journal.schema.json} - %{dep:../contracts/v12/fixtures/demo.scenario.json} - %{dep:../contracts/v12/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v12/fixtures/demo.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v12/fixtures/fill-clipped.journal.jsonl - ../contracts/v12/fixtures/fill-clipped.scenario.json - ../contracts/v12/fixtures/demo.scenario.jsonl - ../contracts/v12/journal.schema.json - ../contracts/v12/scenario-stream.schema.json - ../contracts/v12/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v12/scenario.schema.json} - %{dep:../contracts/v12/scenario-stream.schema.json} - %{dep:../contracts/v12/journal.schema.json} - %{dep:../contracts/v12/fixtures/fill-clipped.scenario.json} - %{dep:../contracts/v12/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v12/fixtures/fill-clipped.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_strategy_schema.py - ../contracts/v12/scenario.schema.json - ../contracts/v12/journal.schema.json - ../contracts/diagnostic/v1/diagnostic.schema.json - ../contracts/strategy/v10/message.schema.json - ../contracts/strategy/v10/transcript.schema.json - ../contracts/strategy/v10/fixtures/external.strategy.jsonl) - (action - (run - python3 - %{dep:validate_strategy_schema.py} - %{dep:../contracts/v12/scenario.schema.json} - %{dep:../contracts/v12/journal.schema.json} - %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} - %{dep:../contracts/strategy/v10/message.schema.json} - %{dep:../contracts/strategy/v10/transcript.schema.json} - %{dep:../contracts/strategy/v10/fixtures/external.strategy.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v11/fixtures/demo.journal.jsonl - ../contracts/v11/fixtures/demo.scenario.json - ../contracts/v11/fixtures/demo.scenario.jsonl - ../contracts/v11/journal.schema.json - ../contracts/v11/scenario-stream.schema.json - ../contracts/v11/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v11/scenario.schema.json} - %{dep:../contracts/v11/scenario-stream.schema.json} - %{dep:../contracts/v11/journal.schema.json} - %{dep:../contracts/v11/fixtures/demo.scenario.json} - %{dep:../contracts/v11/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v11/fixtures/demo.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v11/fixtures/fill-clipped.journal.jsonl - ../contracts/v11/fixtures/fill-clipped.scenario.json - ../contracts/v11/fixtures/demo.scenario.jsonl - ../contracts/v11/journal.schema.json - ../contracts/v11/scenario-stream.schema.json - ../contracts/v11/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v11/scenario.schema.json} - %{dep:../contracts/v11/scenario-stream.schema.json} - %{dep:../contracts/v11/journal.schema.json} - %{dep:../contracts/v11/fixtures/fill-clipped.scenario.json} - %{dep:../contracts/v11/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v11/fixtures/fill-clipped.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_strategy_schema.py - ../contracts/v11/scenario.schema.json - ../contracts/v11/journal.schema.json - ../contracts/diagnostic/v1/diagnostic.schema.json - ../contracts/strategy/v9/message.schema.json - ../contracts/strategy/v9/transcript.schema.json - ../contracts/strategy/v9/fixtures/external.strategy.jsonl) - (action - (run - python3 - %{dep:validate_strategy_schema.py} - %{dep:../contracts/v11/scenario.schema.json} - %{dep:../contracts/v11/journal.schema.json} - %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} - %{dep:../contracts/strategy/v9/message.schema.json} - %{dep:../contracts/strategy/v9/transcript.schema.json} - %{dep:../contracts/strategy/v9/fixtures/external.strategy.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v10/fixtures/demo.journal.jsonl - ../contracts/v10/fixtures/demo.scenario.json - ../contracts/v10/fixtures/demo.scenario.jsonl - ../contracts/v10/journal.schema.json - ../contracts/v10/scenario-stream.schema.json - ../contracts/v10/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v10/scenario.schema.json} - %{dep:../contracts/v10/scenario-stream.schema.json} - %{dep:../contracts/v10/journal.schema.json} - %{dep:../contracts/v10/fixtures/demo.scenario.json} - %{dep:../contracts/v10/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v10/fixtures/demo.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v10/fixtures/fill-clipped.journal.jsonl - ../contracts/v10/fixtures/fill-clipped.scenario.json - ../contracts/v10/fixtures/demo.scenario.jsonl - ../contracts/v10/journal.schema.json - ../contracts/v10/scenario-stream.schema.json - ../contracts/v10/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v10/scenario.schema.json} - %{dep:../contracts/v10/scenario-stream.schema.json} - %{dep:../contracts/v10/journal.schema.json} - %{dep:../contracts/v10/fixtures/fill-clipped.scenario.json} - %{dep:../contracts/v10/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v10/fixtures/fill-clipped.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v9/fixtures/demo.journal.jsonl - ../contracts/v9/fixtures/demo.scenario.json - ../contracts/v9/fixtures/demo.scenario.jsonl - ../contracts/v9/journal.schema.json - ../contracts/v9/scenario-stream.schema.json - ../contracts/v9/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v9/scenario.schema.json} - %{dep:../contracts/v9/scenario-stream.schema.json} - %{dep:../contracts/v9/journal.schema.json} - %{dep:../contracts/v9/fixtures/demo.scenario.json} - %{dep:../contracts/v9/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v9/fixtures/demo.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v9/fixtures/fill-clipped.journal.jsonl - ../contracts/v9/fixtures/fill-clipped.scenario.json - ../contracts/v9/fixtures/demo.scenario.jsonl - ../contracts/v9/journal.schema.json - ../contracts/v9/scenario-stream.schema.json - ../contracts/v9/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v9/scenario.schema.json} - %{dep:../contracts/v9/scenario-stream.schema.json} - %{dep:../contracts/v9/journal.schema.json} - %{dep:../contracts/v9/fixtures/fill-clipped.scenario.json} - %{dep:../contracts/v9/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v9/fixtures/fill-clipped.journal.jsonl}))) + %{dep:../contracts/strategy/v1/message.schema.json} + %{dep:../contracts/strategy/v1/transcript.schema.json} + %{dep:../contracts/strategy/v1/fixtures/external.strategy.jsonl}))) (rule (alias runtest) @@ -511,200 +94,11 @@ (deps ../bin/main.exe fake_strategy.py - ../contracts/strategy/v6/fixtures/external.scenario.json - ../contracts/strategy/v6/fixtures/external.scenario.jsonl - ../contracts/strategy/v6/fixtures/external.strategy.jsonl - ../contracts/v8/fixtures/demo.scenario.json - ../contracts/v8/fixtures/demo.scenario.jsonl)) - -(rule - (alias runtest) - (deps - validate_strategy_schema.py - ../contracts/v10/scenario.schema.json - ../contracts/v10/journal.schema.json - ../contracts/diagnostic/v1/diagnostic.schema.json - ../contracts/strategy/v8/message.schema.json - ../contracts/strategy/v8/transcript.schema.json - ../contracts/strategy/v8/fixtures/external.strategy.jsonl) - (action - (run - python3 - %{dep:validate_strategy_schema.py} - %{dep:../contracts/v10/scenario.schema.json} - %{dep:../contracts/v10/journal.schema.json} - %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} - %{dep:../contracts/strategy/v8/message.schema.json} - %{dep:../contracts/strategy/v8/transcript.schema.json} - %{dep:../contracts/strategy/v8/fixtures/external.strategy.jsonl}))) - -(rule - (alias runtest) - (deps - validate_strategy_schema.py - ../contracts/v9/scenario.schema.json - ../contracts/v9/journal.schema.json - ../contracts/diagnostic/v1/diagnostic.schema.json - ../contracts/strategy/v7/message.schema.json - ../contracts/strategy/v7/transcript.schema.json - ../contracts/strategy/v7/fixtures/external.strategy.jsonl) - (action - (run - python3 - %{dep:validate_strategy_schema.py} - %{dep:../contracts/v9/scenario.schema.json} - %{dep:../contracts/v9/journal.schema.json} - %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} - %{dep:../contracts/strategy/v7/message.schema.json} - %{dep:../contracts/strategy/v7/transcript.schema.json} - %{dep:../contracts/strategy/v7/fixtures/external.strategy.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v8/fixtures/demo.journal.jsonl - ../contracts/v8/fixtures/demo.scenario.json - ../contracts/v8/fixtures/demo.scenario.jsonl - ../contracts/v8/journal.schema.json - ../contracts/v8/scenario-stream.schema.json - ../contracts/v8/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v8/scenario.schema.json} - %{dep:../contracts/v8/scenario-stream.schema.json} - %{dep:../contracts/v8/journal.schema.json} - %{dep:../contracts/v8/fixtures/demo.scenario.json} - %{dep:../contracts/v8/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v8/fixtures/demo.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v8/fixtures/fill-clipped.journal.jsonl - ../contracts/v8/fixtures/fill-clipped.scenario.json - ../contracts/v8/fixtures/demo.scenario.jsonl - ../contracts/v8/journal.schema.json - ../contracts/v8/scenario-stream.schema.json - ../contracts/v8/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v8/scenario.schema.json} - %{dep:../contracts/v8/scenario-stream.schema.json} - %{dep:../contracts/v8/journal.schema.json} - %{dep:../contracts/v8/fixtures/fill-clipped.scenario.json} - %{dep:../contracts/v8/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v8/fixtures/fill-clipped.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v7/fixtures/demo.journal.jsonl - ../contracts/v7/fixtures/demo.scenario.json - ../contracts/v7/fixtures/demo.scenario.jsonl - ../contracts/v7/journal.schema.json - ../contracts/v7/scenario-stream.schema.json - ../contracts/v7/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v7/scenario.schema.json} - %{dep:../contracts/v7/scenario-stream.schema.json} - %{dep:../contracts/v7/journal.schema.json} - %{dep:../contracts/v7/fixtures/demo.scenario.json} - %{dep:../contracts/v7/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v7/fixtures/demo.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v7/fixtures/fill-clipped.journal.jsonl - ../contracts/v7/fixtures/fill-clipped.scenario.json - ../contracts/v7/fixtures/demo.scenario.jsonl - ../contracts/v7/journal.schema.json - ../contracts/v7/scenario-stream.schema.json - ../contracts/v7/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v7/scenario.schema.json} - %{dep:../contracts/v7/scenario-stream.schema.json} - %{dep:../contracts/v7/journal.schema.json} - %{dep:../contracts/v7/fixtures/fill-clipped.scenario.json} - %{dep:../contracts/v7/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v7/fixtures/fill-clipped.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_schemas.py - ../contracts/v3/fixtures/demo.journal.jsonl - ../contracts/v3/fixtures/demo.scenario.json - ../contracts/v3/fixtures/demo.scenario.jsonl - ../contracts/v3/journal.schema.json - ../contracts/v3/scenario-stream.schema.json - ../contracts/v3/scenario.schema.json) - (action - (run - python3 - %{dep:validate_schemas.py} - %{dep:../contracts/v3/scenario.schema.json} - %{dep:../contracts/v3/scenario-stream.schema.json} - %{dep:../contracts/v3/journal.schema.json} - %{dep:../contracts/v3/fixtures/demo.scenario.json} - %{dep:../contracts/v3/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v3/fixtures/demo.journal.jsonl}))) - -(rule - (alias runtest) - (deps - validate_strategy_schema.py - ../contracts/v8/scenario.schema.json - ../contracts/v8/journal.schema.json - ../contracts/diagnostic/v1/diagnostic.schema.json - ../contracts/strategy/v6/message.schema.json - ../contracts/strategy/v6/transcript.schema.json - ../contracts/strategy/v6/fixtures/external.strategy.jsonl) - (action - (run - python3 - %{dep:validate_strategy_schema.py} - %{dep:../contracts/v8/scenario.schema.json} - %{dep:../contracts/v8/journal.schema.json} - %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} - %{dep:../contracts/strategy/v6/message.schema.json} - %{dep:../contracts/strategy/v6/transcript.schema.json} - %{dep:../contracts/strategy/v6/fixtures/external.strategy.jsonl}))) - -(rule - (alias runtest) - (deps - validate_strategy_schema.py - ../contracts/v7/scenario.schema.json - ../contracts/v7/journal.schema.json - ../contracts/diagnostic/v1/diagnostic.schema.json - ../contracts/strategy/v5/message.schema.json - ../contracts/strategy/v5/transcript.schema.json - ../contracts/strategy/v5/fixtures/external.strategy.jsonl) - (action - (run - python3 - %{dep:validate_strategy_schema.py} - %{dep:../contracts/v7/scenario.schema.json} - %{dep:../contracts/v7/journal.schema.json} - %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} - %{dep:../contracts/strategy/v5/message.schema.json} - %{dep:../contracts/strategy/v5/transcript.schema.json} - %{dep:../contracts/strategy/v5/fixtures/external.strategy.jsonl}))) + ../contracts/strategy/v1/fixtures/external.scenario.json + ../contracts/strategy/v1/fixtures/external.scenario.jsonl + ../contracts/strategy/v1/fixtures/external.strategy.jsonl + ../contracts/v1/fixtures/demo.scenario.json + ../contracts/v1/fixtures/demo.scenario.jsonl)) (rule (alias runtest) @@ -752,7 +146,7 @@ (deps test_benchmark_replay.py ../bench/benchmark_replay.py - ../contracts/v9/fixtures/demo.scenario.json) + ../contracts/v1/fixtures/demo.scenario.json) (action (run python3 %{dep:test_benchmark_replay.py}))) @@ -761,14 +155,14 @@ (deps validate_cli_result.py ../contracts/cli/v1/result.schema.json - ../contracts/v16/journal.schema.json - ../contracts/v16/fixtures/demo.scenario.json + ../contracts/v1/journal.schema.json + ../contracts/v1/fixtures/demo.scenario.json ../bin/main.exe) (action (run python3 %{dep:validate_cli_result.py} %{dep:../contracts/cli/v1/result.schema.json} - %{dep:../contracts/v16/journal.schema.json} + %{dep:../contracts/v1/journal.schema.json} %{dep:../bin/main.exe} - %{dep:../contracts/v16/fixtures/demo.scenario.json}))) + %{dep:../contracts/v1/fixtures/demo.scenario.json}))) diff --git a/test/fake_strategy.py b/test/fake_strategy.py index 260725b..7590f31 100755 --- a/test/fake_strategy.py +++ b/test/fake_strategy.py @@ -98,7 +98,10 @@ def response(request: dict[str, object]) -> dict[str, object]: { "type": "emit_metric", "name": "fixture_signal", - "value": "2", + "value": {"type": "numeric", "value": "2"}, + "unit": "score", + "dimensions": {"source": "fixture"}, + "aggregation": "last", }, ] } @@ -151,7 +154,7 @@ def response(request: dict[str, object]) -> dict[str, object]: if MODE == "bad-sequence": message["strategy_sequence"] = "999" if MODE == "wrong-version": - message["strategy_protocol_version"] = "1" + message["strategy_protocol_version"] = "2" if MODE == "unknown-field": message["unexpected"] = True if MODE == "error" and request["message_type"] == "event": diff --git a/test/fuzz_protocol.ml b/test/fuzz_protocol.ml index 65442b8..8d74e4d 100644 --- a/test/fuzz_protocol.ml +++ b/test/fuzz_protocol.ml @@ -324,7 +324,7 @@ let hostile_seeds () = ("deep-nesting", deep_json 512); ("huge-token", huge_json_string); ( "duplicate-key", - "{\"contract_version\":\"4\",\"contract_version\":\"4\"}" ); + "{\"contract_version\":\"1\",\"contract_version\":\"1\"}" ); ("truncation", "{\"contract_version\":"); ] in diff --git a/test/test_accounting.ml b/test/test_accounting.ml index 39e93de..e611b32 100644 --- a/test/test_accounting.ml +++ b/test/test_accounting.ml @@ -249,7 +249,7 @@ let risk_reserves_partial_order_remainders () = (request ~side:T.Order.Buy ~quantity_value:"1" ())); Alcotest.(check string) "two units exceed the reserved long limit" - "position would exceed the maximum long position" + "position would exceed the instrument maximum long position" (risk_check configured ~account ~oms (request ~side:T.Order.Buy ~quantity_value:"2" ()) |> error) diff --git a/test/test_boundary_failures.ml b/test/test_boundary_failures.ml index 9319621..6cae1b4 100644 --- a/test/test_boundary_failures.ml +++ b/test/test_boundary_failures.ml @@ -323,15 +323,14 @@ let initialization () = metadata = `Assoc [ ("experiment", `String "boundary-failure") ]; run_id = run_id "boundary-failure"; base_currency = "USD"; - initial_cash = [ ("USD", money "10000") ]; - initial_portfolio = None; + initial_portfolio = initial_portfolio (); instruments = [ instrument ]; venue_calendars = []; risk = risk ~instruments:[ instrument ] (); execution_model = T.Execution_model.find "completed_bar_v1" |> ok; execution = execution (); - financing = None; - settlement = None; + financing = financing_policy (); + settlement = settlement_policy (); } let process_stages = diff --git a/test/test_checkpoint4.ml b/test/test_checkpoint4.ml index a350f59..e8f0852 100644 --- a/test/test_checkpoint4.ml +++ b/test/test_checkpoint4.ml @@ -2,37 +2,6 @@ open Test_support module T = Trading_engine module Runner = T.Engine.Make (T.Scripted_strategy) -module Margin_observing_strategy = struct - type state = { saw_liquidation_update : bool } - - let name = "margin-observer" - let initial = { saw_liquidation_update = false } - - let on_event state _context event = - match event with - | T.Strategy.Market_slice_closed market_slice - when Int64.equal market_slice.T.Market_slice.slice_sequence 1L -> - ( state, - [ - T.Strategy.Target_quantities - [ - T.Strategy. - { - instrument_id = instrument_id "test-equity"; - quantity = quantity "15"; - }; - ]; - ] ) - | T.Strategy.Order_updated order - when order.T.Order.request.origin = T.Order.Margin_liquidation -> - ({ saw_liquidation_update = true }, []) - | T.Strategy.Market_slice_closed _ | T.Strategy.Fill_received _ - | T.Strategy.Order_updated _ | T.Strategy.Intent_rejected _ -> - (state, []) -end - -module Margin_runner = T.Engine.Make (Margin_observing_strategy) - let euro_instrument () = instrument ~id:"euro-equity" ~symbol:"EURO" ~currency:"EUR" ~lot_size:"0.001" () @@ -51,6 +20,7 @@ let multi_currency_fractional_accounting () = T.Fill.create ~id:(fill_id "euro-fill") ~order_id:order.id ~instrument_id:euro.id ~quote_currency:"EUR" ~side:T.Order.Buy ~quantity:(quantity "1.5") ~price:(price "20") ~fee:(money "0.5") + ~fee_components:[] ~executed_at:(timestamp "2026-01-03T14:30:00Z") ~slice_sequence:2L |> ok @@ -146,7 +116,7 @@ let split_adjusts_working_order () = let config = engine_config () in let state = Runner.create ~run_id:(run_id "split-order") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "10000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "10000") ] ()) ~strategy_state |> ok in @@ -190,7 +160,7 @@ let split_caps_adjusted_market_fill () = Runner.create ~run_id:(run_id "split-market-cap") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "10000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "10000") ] ()) ~strategy_state |> ok in @@ -262,7 +232,7 @@ let split_caps_partially_filled_limit_remainder () = Runner.create ~run_id:(run_id "split-partial-limit") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "10000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "10000") ] ()) ~strategy_state |> ok in @@ -348,7 +318,7 @@ let reverse_split_restores_order_below_maximum () = Runner.create ~run_id:(run_id "reverse-split-order") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "10000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "10000") ] ()) ~strategy_state |> ok in @@ -390,98 +360,6 @@ let reverse_split_restores_order_below_maximum () = | _ -> false) events) -let margin_call_forces_deterministic_liquidation () = - let config = engine_config () in - let state = - Margin_runner.create ~run_id:(run_id "margin-call") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "1000") ] - ~strategy_state:Margin_observing_strategy.initial - |> ok - in - let state, _ = Margin_runner.process_slice state (market_slice 1L) |> ok in - let stressed_bar = - bar ~open_price:"100" ~high_price:"100" ~low_price:"40" ~close_price:"40" 2L - in - let state, call_events = - Margin_runner.process_slice state (market_slice ~bars:[ stressed_bar ] 2L) - |> ok - in - Alcotest.(check bool) - "margin call emitted" true - (List.exists - (fun event -> - String.equal (T.Audit.event_name event.T.Audit.event) "margin_call") - call_events); - let liquidation = - T.Oms.active_orders (Margin_runner.oms state) - |> List.find (fun order -> - order.T.Order.request.origin = T.Order.Margin_liquidation) - in - Alcotest.(check string) - "forced sell" "sell" - (T.Order.side_to_string liquidation.request.side); - Alcotest.(check bool) - "strategy observes liquidation order updates" true - (Margin_runner.strategy_state state).saw_liquidation_update; - let liquidation_bar = - bar ~open_price:"40" ~high_price:"40" ~low_price:"40" ~close_price:"40" 3L - in - let state, restored_events = - Margin_runner.process_slice state - (market_slice ~bars:[ liquidation_bar ] 3L) - |> ok - in - Alcotest.check quantity_testable "position flattened" T.Scalar.Quantity.zero - (T.Account.position_quantity - (Margin_runner.account state) - (instrument_id "test-equity")); - Alcotest.(check bool) - "margin restored emitted" true - (List.exists - (fun event -> - String.equal (T.Audit.event_name event.T.Audit.event) "margin_restored") - restored_events) - -let short_borrow_accrues_before_matching () = - let target = - T.Strategy.Target_quantities - [ - T.Strategy. - { - instrument_id = instrument_id "test-equity"; - quantity = quantity "-10"; - }; - ] - in - let strategy_state = T.Scripted_strategy.create [ (1L, [ target ]) ] |> ok in - let config = engine_config ~risk:(risk ~short_borrow_bps:3650 ()) () in - let state = - Runner.create ~run_id:(run_id "short-borrow") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "1000") ] - ~strategy_state - |> ok - in - let state, _ = Runner.process_slice state (market_slice 1L) |> ok in - let state, _ = Runner.process_slice state (market_slice 2L) |> ok in - let state, events = Runner.process_slice state (market_slice 3L) |> ok in - let borrow = - List.find_map - (fun event -> - match event.T.Audit.event with - | T.Audit.Borrow_fee_applied { fee; _ } -> Some fee - | _ -> None) - events - |> Option.get - in - Alcotest.(check bool) - "positive borrow fee" true - (T.Scalar.Money.compare borrow (money "0") > 0); - let position = - T.Account.position (Runner.account state) (instrument_id "test-equity") - in - Alcotest.check money_testable "borrow fee attributed" borrow - position.borrow_fees - let risk_allows_reducing_an_out_of_limit_position () = let configured_risk = risk ~max_long:"10" () in let account = test_account ~initial_cash:[ ("USD", money "10000") ] () in @@ -571,7 +449,8 @@ let engine_requires_complete_currency_ledgers () = "missing EUR ledger rejected" true (Result.is_error (Runner.create ~run_id:(run_id "missing-ledger") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "1000") ] + ~initial_portfolio: + (initial_portfolio ~cash:[ ("USD", money "1000") ] ()) ~strategy_state)) let tests = @@ -588,10 +467,6 @@ let tests = split_caps_partially_filled_limit_remainder; Alcotest.test_case "reverse split restores order below maximum" `Quick reverse_split_restores_order_below_maximum; - Alcotest.test_case "margin call forces liquidation" `Quick - margin_call_forces_deterministic_liquidation; - Alcotest.test_case "short borrow accrues" `Quick - short_borrow_accrues_before_matching; Alcotest.test_case "risk allows reduction above position cap" `Quick risk_allows_reducing_an_out_of_limit_position; Alcotest.test_case "fill clipping reason taxonomy is stable" `Quick diff --git a/test/test_contract_conformance.ml b/test/test_contract_conformance.ml index bd9b7f0..65e75dd 100644 --- a/test/test_contract_conformance.ml +++ b/test/test_contract_conformance.ml @@ -182,13 +182,7 @@ let runtime_result case = let expected_sequence = string_field "expected_sequence" case |> Int64.of_string in - let protocol_version = - match optional_field "protocol_version" case with - | Some (`String value) -> value - | _ -> T.Contract.strategy_protocol_version - in - T.Strategy_protocol.response_of_yojson ~protocol_version - ~expected_sequence response + T.Strategy_protocol.response_of_yojson ~expected_sequence response |> Result.map (fun _ -> ()) | kind -> Alcotest.failf "unsupported differential runtime kind %s" kind diff --git a/test/test_corporate_lifecycle.ml b/test/test_corporate_lifecycle.ml index afd7d58..ccffbba 100644 --- a/test/test_corporate_lifecycle.ml +++ b/test/test_corporate_lifecycle.ml @@ -320,7 +320,7 @@ let policy_and_transition_boundaries () = let lifecycle_slice ?(corporate_actions = []) ?(lifecycle_events = []) sequence = let date = Int64.to_int sequence + 1 in - T.Market_slice.create_v12 ~slice_sequence:sequence + T.Market_slice.create ~slice_sequence:sequence ~start_at:(timestamp (Printf.sprintf "2026-03-%02dT14:30:00Z" date)) ~end_at:(timestamp (Printf.sprintf "2026-03-%02dT21:00:00Z" date)) ~available_at:(timestamp (Printf.sprintf "2026-03-%02dT21:00:01Z" date)) @@ -328,14 +328,15 @@ let lifecycle_slice ?(corporate_actions = []) ?(lifecycle_events = []) sequence ~bars:[ bar sequence ] ~fx_rates:[ fx_mark () ] ~corporate_actions ~borrow_observations:[] ~cash_rate_observations:[] - ~settlement_failures:[] ~lifecycle_events + ~settlement_failures:[] ~lifecycle_events ~market_events:[] + ~order_book_events:[] |> ok let lifecycle_runner schedule run = - let config = engine_config ~contract_version:"12" () in + let config = engine_config ~contract_version:"1" () in let strategy_state = T.Scripted_strategy.create schedule |> ok in Runner.create ~run_id:(run_id run) ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "10000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "10000") ] ()) ~strategy_state |> ok diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 13ebe5b..89b337c 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -133,26 +133,10 @@ let capabilities_describe_execution_contracts () = | _ -> Alcotest.fail (name ^ " must be an array") in Alcotest.(check (list string)) - "configuration versions" [ "2"; "1" ] + "configuration versions" [ "1" ] (strings "configuration_versions"); Alcotest.(check (list string)) - "scenario contracts" - [ - "16"; - "15"; - "14"; - "13"; - "12"; - "11"; - "10"; - "9"; - "8"; - "7"; - "6"; - "5"; - "4"; - "3"; - ] + "scenario contracts" [ "1" ] (strings "scenario_contract_versions"); Alcotest.(check (list string)) "required fields" diff --git a/test/test_domain.ml b/test/test_domain.ml index 2a607ec..34e159d 100644 --- a/test/test_domain.ml +++ b/test/test_domain.ml @@ -95,7 +95,9 @@ let market_slice_validation () = ~received_at ~bars:[ bar 1L ] ~fx_rates:[ fx_mark () ] - ~corporate_actions:[] + ~corporate_actions:[] ~borrow_observations:[] ~cash_rate_observations:[] + ~settlement_failures:[] ~lifecycle_events:[] ~market_events:[] + ~order_book_events:[] in Alcotest.(check bool) "premature availability rejected" true (Result.is_error result) @@ -187,7 +189,7 @@ let market_event_validation () = Alcotest.(check bool) "nonmonotonic ingest rejected" true (Result.is_error - (T.Market_slice.create_v14 ~slice_sequence:base.slice_sequence + (T.Market_slice.create ~slice_sequence:base.slice_sequence ~start_at:base.start_at ~end_at:base.end_at ~available_at:base.available_at ~received_at:base.received_at ~bars:base.bars ~fx_rates:base.fx_rates @@ -196,7 +198,7 @@ let market_event_validation () = ~cash_rate_observations:base.cash_rate_observations ~settlement_failures:base.settlement_failures ~lifecycle_events:base.lifecycle_events - ~market_events:[ first; second ])) + ~market_events:[ first; second ] ~order_book_events:[])) let order_book_event_validation () = let instrument_id = instrument_id "book-validation" in @@ -477,23 +479,19 @@ let risk_limits_cover_lots () = Alcotest.(check bool) "order limit smaller than lot rejected" true (Result.is_error - (T.Risk.create ~base_currency:"USD" ~instruments:[ configured ] + (T.Risk.create_instrument_policy ~instrument:configured ~max_order_quantity:(quantity "5") ~max_long_position:(quantity "100") - ~max_short_position:(quantity "100") - ~max_gross_exposure:(money "1000000") - ~max_leverage:(T.Scalar.Ratio.of_decimal_string "2" |> ok) + ~max_short_position:(quantity "100") ~max_notional_exposure:None ~initial_margin_bps:5000 ~maintenance_margin_bps:2500 - ~short_borrow_bps:100)); + ~shorting_allowed:true)); Alcotest.(check bool) "position limit smaller than lot rejected" true (Result.is_error - (T.Risk.create ~base_currency:"USD" ~instruments:[ configured ] + (T.Risk.create_instrument_policy ~instrument:configured ~max_order_quantity:(quantity "100") ~max_long_position:(quantity "5") - ~max_short_position:(quantity "100") - ~max_gross_exposure:(money "1000000") - ~max_leverage:(T.Scalar.Ratio.of_decimal_string "2" |> ok) + ~max_short_position:(quantity "100") ~max_notional_exposure:None ~initial_margin_bps:5000 ~maintenance_margin_bps:2500 - ~short_borrow_bps:100)) + ~shorting_allowed:true)) let typed_metric_validation () = let numeric = T.Metric.numeric_of_string "-12.5" |> ok in diff --git a/test/test_execution.ml b/test/test_execution.ml index 24856fb..e088e97 100644 --- a/test/test_execution.ml +++ b/test/test_execution.ml @@ -63,7 +63,7 @@ let trade_event ?(sequence = 2L) ?(second = 2) ?(price_value = "100") let quote_trade_slice events = let base = market_slice 2L in - T.Market_slice.create_v14 ~slice_sequence:base.slice_sequence + T.Market_slice.create ~slice_sequence:base.slice_sequence ~start_at:base.start_at ~end_at:base.end_at ~available_at:base.available_at ~received_at:base.received_at ~bars:base.bars ~fx_rates:base.fx_rates ~corporate_actions:base.corporate_actions @@ -71,11 +71,12 @@ let quote_trade_slice events = ~cash_rate_observations:base.cash_rate_observations ~settlement_failures:base.settlement_failures ~lifecycle_events:base.lifecycle_events ~market_events:events + ~order_book_events:[] |> ok let quote_trade_execution ?(participation_bps = 10_000) () = let fees = conservative_execution () |> T.Execution.fee_schedules in - T.Execution.create_v2 ~participation_bps ~fee_schedules:fees |> ok + T.Execution.create ~participation_bps ~fee_schedules:fees |> ok let book_level price_value quantity_value = T.Order_book_event.level ~price:(price price_value) @@ -123,7 +124,7 @@ let book_trade ?(sequence = 2L) ?(second = 2) ?(price_value = "99") let order_book_slice events = let base = market_slice 2L in - T.Market_slice.create_v15 ~slice_sequence:base.slice_sequence + T.Market_slice.create ~slice_sequence:base.slice_sequence ~start_at:base.start_at ~end_at:base.end_at ~available_at:base.available_at ~received_at:base.received_at ~bars:base.bars ~fx_rates:base.fx_rates ~corporate_actions:base.corporate_actions @@ -231,8 +232,7 @@ let quote_trade_limits_fok_and_continuations () = (Result.is_error (continue (quantity "-1"))) | _ -> Alcotest.fail "marketable limit did not execute"); let oms, _ = - oms_with_order - (request_v8 ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) + oms_with_order (request ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) in let cursor = T.Execution.start_slice_quote_trade (quote_trade_execution ()) @@ -248,9 +248,7 @@ let quote_trade_limits_fok_and_continuations () = let quote_trade_stop_and_event_boundaries () = let oms, order = oms_with_order - (request_v8 - ~kind:(T.Order.Stop (price "100")) - ~time_in_force:T.Order.Gtc ()) + (request ~kind:(T.Order.Stop (price "100")) ~time_in_force:T.Order.Gtc ()) in let cursor = T.Execution.start_slice_quote_trade (quote_trade_execution ()) @@ -344,8 +342,7 @@ let order_book_walks_depth_and_rejects_inconsistent_updates () = second.price | _ -> Alcotest.fail "second ask did not produce a fill"); let fok_oms, _ = - oms_with_order - (request_v8 ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) + oms_with_order (request ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) in let fok_cursor = T.Execution.start_slice_order_book (order_book_execution ()) @@ -501,7 +498,7 @@ let order_book_walks_depth_and_rejects_inconsistent_updates () = let triggered_stop side trigger = let oms, order = oms_with_order - (request_v8 ~side + (request ~side ~kind:(T.Order.Stop (price trigger)) ~time_in_force:T.Order.Gtc ()) in @@ -527,9 +524,7 @@ let order_book_walks_depth_and_rejects_inconsistent_updates () = triggered_stop T.Order.Sell "100"; let waiting_stop_oms, _ = oms_with_order - (request_v8 - ~kind:(T.Order.Stop (price "200")) - ~time_in_force:T.Order.Gtc ()) + (request ~kind:(T.Order.Stop (price "200")) ~time_in_force:T.Order.Gtc ()) in let waiting_stop_cursor = T.Execution.start_slice_order_book (order_book_execution ()) @@ -542,8 +537,7 @@ let order_book_walks_depth_and_rejects_inconsistent_updates () = | T.Execution.Finished _ -> () | _ -> Alcotest.fail "untriggered order-book stop should remain dormant"); let shallow_fok_oms, _ = - oms_with_order - (request_v8 ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) + oms_with_order (request ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) in let shallow_fok_cursor = T.Execution.start_slice_order_book (order_book_execution ()) @@ -732,14 +726,14 @@ let conservative_configuration_is_bounded () = (Result.is_error (create spread impact))) [ (-1, 0); (10_001, 0); (0, -1); (0, 10_001) ]; Alcotest.(check bool) - "v2 participation bound enforced" true + "participation bound enforced" true (Result.is_error - (T.Execution.create_v2 ~participation_bps:(-1) ~fee_schedules:schedules)); + (T.Execution.create ~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 + (T.Execution.create ~participation_bps:10_000 ~fee_schedules:[ schedule; schedule ])); Alcotest.(check bool) "missing instrument fee schedule rejected" true diff --git a/test/test_fee_schedules.ml b/test/test_fee_schedules.ml index b94e6cb..6d6b6f4 100644 --- a/test/test_fee_schedules.ml +++ b/test/test_fee_schedules.ml @@ -106,7 +106,7 @@ let rebate_settles_and_is_attributed () = Alcotest.check money_testable "negative rebate" (money "-0.1") fee; let request = request ~quantity_value:"1" () in let fill = - T.Fill.create_v9 ~id:(fill_id "rebate-fill") + T.Fill.create ~id:(fill_id "rebate-fill") ~order_id:(order_id "rebate-order") ~instrument_id:request.instrument_id ~quote_currency:"USD" ~side:T.Order.Buy ~quantity:(quantity "1") ~price:(price "100") ~fee ~fee_components diff --git a/test/test_financing.ml b/test/test_financing.ml index f8908fd..fa40cb3 100644 --- a/test/test_financing.ml +++ b/test/test_financing.ml @@ -152,11 +152,13 @@ let financing_slice ?(borrow_observations = []) ?(cash_rate_observations = []) let end_at = timestamp (Printf.sprintf "2026-01-%02dT21:00:00Z" day) in let available_at = timestamp (Printf.sprintf "2026-01-%02dT21:00:01Z" day) in let received_at = timestamp (Printf.sprintf "2026-01-%02dT21:00:02Z" day) in - T.Market_slice.create_v10 ~slice_sequence:sequence ~start_at ~end_at - ~available_at ~received_at + T.Market_slice.create ~slice_sequence:sequence ~start_at ~end_at ~available_at + ~received_at ~bars:[ bar sequence ] ~fx_rates:[ fx_mark () ] ~corporate_actions:[] ~borrow_observations ~cash_rate_observations + ~settlement_failures:[] ~lifecycle_events:[] ~market_events:[] + ~order_book_events:[] |> ok let borrow_observation ?(available = "5") ?(rate = 3600) ?(recalled = false) @@ -173,11 +175,11 @@ let cash_rate effective_at = |> ok let financing_config financing = - T.Engine.config_v10 ~contract_version:T.Contract.version - ~risk:(risk ~short_borrow_bps:0 ()) + T.Engine.config ~contract_version:T.Contract.version ~risk:(risk ()) ~venue_calendars:[] ~execution_model:(T.Execution_model.find "completed_bar_v1" |> ok) - ~execution:(execution ()) ~financing ~max_internal_events:1000 + ~execution:(execution ()) ~financing ~settlement:(settlement_policy ()) + ~max_internal_events:1000 |> ok let empty_strategy () = T.Scripted_strategy.create [] |> ok @@ -186,7 +188,7 @@ let missing_data_policies_are_explicit () = let cash_state = Runner.create ~run_id:(run_id "missing-cash") ~scenario_sha256 ~config:(financing_config (policy ())) - ~initial_cash:[ ("USD", money "1000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "1000") ] ()) ~strategy_state:(empty_strategy ()) |> ok in @@ -213,8 +215,7 @@ let missing_data_policies_are_explicit () = |> ok in let borrow_state = - Runner.create_with_portfolio ~run_id:(run_id "missing-borrow") - ~scenario_sha256 + Runner.create ~run_id:(run_id "missing-borrow") ~scenario_sha256 ~config:(financing_config (policy ~cash_missing_data:T.Financing.Zero ())) ~initial_portfolio ~strategy_state:(empty_strategy ()) |> ok @@ -247,7 +248,7 @@ let reject_order_policy_uses_current_locate () = let state = Runner.create ~run_id:(run_id "reject-locate") ~scenario_sha256 ~config:(financing_config financing) - ~initial_cash:[ ("USD", money "1000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "1000") ] ()) ~strategy_state |> ok in @@ -294,8 +295,7 @@ let zero_missing_data_and_recall_retention () = ~recall_policy:T.Financing.Reject_new_shorts () in let state = - Runner.create_with_portfolio ~run_id:(run_id "retain-recall") - ~scenario_sha256 + Runner.create ~run_id:(run_id "retain-recall") ~scenario_sha256 ~config:(financing_config financing) ~initial_portfolio ~strategy_state:(empty_strategy ()) |> ok @@ -344,18 +344,19 @@ let availability_clips_and_recall_closes () = ] in let strategy_state = T.Scripted_strategy.create [ (1L, [ target ]) ] |> ok in - let configured_risk = risk ~short_borrow_bps:0 () in + let configured_risk = risk () in let financing = policy () in let config = - T.Engine.config_v10 ~contract_version:T.Contract.version - ~risk:configured_risk ~venue_calendars:[] + T.Engine.config ~contract_version:T.Contract.version ~risk:configured_risk + ~venue_calendars:[] ~execution_model:(T.Execution_model.find "completed_bar_v1" |> ok) - ~execution:(execution ()) ~financing ~max_internal_events:1000 + ~execution:(execution ()) ~financing ~settlement:(settlement_policy ()) + ~max_internal_events:1000 |> ok in let state = Runner.create ~run_id:(run_id "financing") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "1000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "1000") ] ()) ~strategy_state |> ok in diff --git a/test/test_order_lifetimes.ml b/test/test_order_lifetimes.ml index 2ba0f4c..a464ff5 100644 --- a/test/test_order_lifetimes.ml +++ b/test/test_order_lifetimes.ml @@ -9,8 +9,9 @@ let runner ?(initial_cash = "10000") ?(risk = risk ()) ?(venue_calendars = []) schedule = let strategy_state = T.Scripted_strategy.create schedule |> ok in Runner.create ~run_id:(run_id "lifetime-run") ~scenario_sha256 - ~config:(engine_config_v8 ~risk ~venue_calendars ()) - ~initial_cash:[ ("USD", money initial_cash) ] + ~config:(engine_config ~risk ~venue_calendars ()) + ~initial_portfolio: + (initial_portfolio ~cash:[ ("USD", money initial_cash) ] ()) ~strategy_state |> ok @@ -34,21 +35,11 @@ let calendar ?(venue = "XNAS") ?(covered_instrument = "test-equity") () = ~sessions:[ session ] |> ok -let compatibility_mapping () = - let market = request () in - let limit = request ~kind:(T.Order.Limit (price "100")) () in - Alcotest.(check string) - "legacy market is IOC" "ioc" - (T.Order.time_in_force_to_string market.time_in_force); - Alcotest.(check string) - "legacy limit is GTC" "gtc" - (T.Order.time_in_force_to_string limit.time_in_force) - let validates_stop_limit_and_gtd () = Alcotest.(check bool) "invalid buy stop-limit" true (Result.is_error - (T.Order.request_v8 + (T.Order.request ~instrument_id:(instrument_id "test-equity") ~side:T.Order.Buy ~quantity:(quantity "1") ~kind: @@ -56,9 +47,7 @@ let validates_stop_limit_and_gtd () = { trigger_price = price "100"; limit_price = price "99" }) ~time_in_force:T.Order.Gtc ~origin:T.Order.Direct)); let request = - request_v8 - ~time_in_force:(T.Order.Gtd (timestamp "2026-01-02T20:00:00Z")) - () + request ~time_in_force:(T.Order.Gtd (timestamp "2026-01-02T20:00:00Z")) () in Alcotest.(check bool) "expiry follows creation" true @@ -68,7 +57,7 @@ let validates_stop_limit_and_gtd () = ~created_at:(timestamp "2026-01-02T21:00:00Z") ~eligible_after_slice_sequence:1L request)) -let v8_intent_requires_explicit_companions () = +let intent_requires_explicit_companions () = let intent = `Assoc [ @@ -87,7 +76,7 @@ let v8_intent_requires_explicit_companions () = in Alcotest.(check bool) "explicit stop/GTD parses" true - (Result.is_ok (T.Scenario.intent_of_yojson ~contract_version:"8" intent)); + (Result.is_ok (T.Scenario.intent_of_yojson intent)); let missing_trigger = match intent with | `Assoc fields -> `Assoc (List.remove_assoc "trigger_price" fields) @@ -95,8 +84,7 @@ let v8_intent_requires_explicit_companions () = in Alcotest.(check bool) "missing companion is rejected" true - (Result.is_error - (T.Scenario.intent_of_yojson ~contract_version:"8" missing_trigger)); + (Result.is_error (T.Scenario.intent_of_yojson missing_trigger)); let submit kind trigger limit tif venue calendar expires = `Assoc [ @@ -125,34 +113,34 @@ let v8_intent_requires_explicit_companions () = List.iter (fun json -> Alcotest.(check bool) - "v8 order variant parses" true - (Result.is_ok (T.Scenario.intent_of_yojson ~contract_version:"8" json))) + "order variant parses" true + (Result.is_ok (T.Scenario.intent_of_yojson json))) valid; Alcotest.(check bool) "inconsistent TIF companions rejected" true (Result.is_error - (T.Scenario.intent_of_yojson ~contract_version:"8" + (T.Scenario.intent_of_yojson (submit "market" `Null `Null "gtc" (`String "XNAS") `Null `Null))) let order_validation_and_serialization_branches () = Alcotest.(check bool) "nonpositive quantity rejected" true (Result.is_error - (T.Order.request_v8 + (T.Order.request ~instrument_id:(instrument_id "test-equity") ~side:T.Order.Buy ~quantity:T.Scalar.Quantity.zero ~kind:T.Order.Market ~time_in_force:T.Order.Gtc ~origin:T.Order.Direct)); Alcotest.(check bool) "invalid sell stop-limit rejected" true (Result.is_error - (T.Order.request_v8 + (T.Order.request ~instrument_id:(instrument_id "test-equity") ~side:T.Order.Sell ~quantity:(quantity "1") ~kind: (T.Order.Stop_limit { trigger_price = price "100"; limit_price = price "101" }) ~time_in_force:T.Order.Gtc ~origin:T.Order.Direct)); - let ordinary = request_v8 () in + let ordinary = current_request () in Alcotest.(check bool) "negative accepted sequence rejected" true (Result.is_error @@ -198,7 +186,7 @@ let order_validation_and_serialization_branches () = in List.iteri (fun index (kind, time_in_force) -> - let request = request_v8 ~kind ~time_in_force () in + let request = current_request ~kind ~time_in_force () in ignore (T.Order.kind_to_string kind); ignore (T.Order.time_in_force_to_string time_in_force); let order = @@ -206,14 +194,14 @@ let order_validation_and_serialization_branches () = in ignore (T.Order.is_market order); ignore (T.Order.effective_kind order); - match T.Codec.order_to_yojson_v8 order with + match T.Codec.order_to_yojson order with | `Assoc fields -> Alcotest.(check bool) "TIF serialized" true (List.mem_assoc "time_in_force" fields) | _ -> Alcotest.fail "serialized order must be an object") cases; - let unconditional = accepted_order (request_v8 ()) in + let unconditional = accepted_order (current_request ()) in Alcotest.(check bool) "unconditional order cannot trigger" true (Result.is_error @@ -222,7 +210,7 @@ let order_validation_and_serialization_branches () = ~triggered_at:(timestamp "2026-01-03T20:00:00Z") ~triggered_slice_sequence:2L)); let conditional = - accepted_order (request_v8 ~kind:(T.Order.Stop (price "110")) ()) + accepted_order (current_request ~kind:(T.Order.Stop (price "110")) ()) in Alcotest.(check bool) "negative trigger sequence rejected" true @@ -237,7 +225,7 @@ let order_validation_and_serialization_branches () = ~triggered_slice_sequence:2L |> ok in - ignore (T.Codec.order_to_yojson_v8 triggered); + ignore (T.Codec.order_to_yojson triggered); Alcotest.(check bool) "duplicate trigger rejected" true (Result.is_error @@ -259,7 +247,9 @@ let order_validation_and_serialization_branches () = let trigger_then_execute_on_following_slice () = let request = - request_v8 ~kind:(T.Order.Stop (price "110")) ~time_in_force:T.Order.Gtc () + current_request + ~kind:(T.Order.Stop (price "110")) + ~time_in_force:T.Order.Gtc () in let oms, order = oms_with_order request in let trigger_slice = @@ -317,7 +307,7 @@ let trigger_then_execute_on_following_slice () = let stop_limit_uses_limit_after_trigger () = let request = - request_v8 + request ~kind: (T.Order.Stop_limit { trigger_price = price "110"; limit_price = price "111" }) @@ -363,7 +353,7 @@ let stop_limit_uses_limit_after_trigger () = let sell_stop_gap_and_partial_fill () = let request = - request_v8 ~side:T.Order.Sell ~quantity_value:"10" + current_request ~side:T.Order.Sell ~quantity_value:"10" ~kind:(T.Order.Stop (price "90")) () in @@ -412,7 +402,9 @@ let sell_stop_gap_and_partial_fill () = | _ -> Alcotest.fail "expected one partial sell-stop fill" let fok_is_all_or_cancel () = - let request = request_v8 ~quantity_value:"10" ~time_in_force:T.Order.Fok () in + let request = + current_request ~quantity_value:"10" ~time_in_force:T.Order.Fok () + in let oms, order = oms_with_order request in let matched = T.Execution.match_slice (execution ()) @@ -427,7 +419,7 @@ let fok_is_all_or_cancel () = let split_adjusts_stop_prices () = let request = - request_v8 ~quantity_value:"10" + current_request ~quantity_value:"10" ~kind: (T.Order.Stop_limit { trigger_price = price "110"; limit_price = price "112" }) @@ -450,7 +442,9 @@ let split_adjusts_stop_prices () = let engine_audits_trigger_and_defers_fill () = let request = - request_v8 ~kind:(T.Order.Stop (price "110")) ~time_in_force:T.Order.Gtc () + current_request + ~kind:(T.Order.Stop (price "110")) + ~time_in_force:T.Order.Gtc () in let state = runner [ (1L, [ T.Strategy.Submit_order request ]) ] in let state, _ = Runner.process_slice state (market_slice 1L) |> ok in @@ -482,7 +476,7 @@ let engine_audits_trigger_and_defers_fill () = let gtd_and_day_expire_deterministically () = let gtd = - request_v8 + request ~kind:(T.Order.Limit (price "90")) ~time_in_force:(T.Order.Gtd (timestamp "2026-01-03T20:00:00Z")) () @@ -509,7 +503,7 @@ let gtd_and_day_expire_deterministically () = (T.Audit.cancellation_reason_to_string reason); let calendar = calendar () in let day = - request_v8 + request ~kind:(T.Order.Stop (price "101")) ~time_in_force: (T.Order.Day @@ -555,7 +549,7 @@ let gtd_and_day_expire_deterministically () = (List.mem "fill_applied" (event_names expired)) let fok_rejects_risk_clipped_fill () = - let order = request_v8 ~time_in_force:T.Order.Fok () in + let order = current_request ~time_in_force:T.Order.Fok () in let state = runner ~initial_cash:"550" ~risk:(risk ~max_leverage:"1" ()) @@ -593,7 +587,7 @@ let fok_rejects_risk_clipped_fill () = let day_identity_is_validated () = let day venue = - request_v8 + request ~kind:(T.Order.Limit (price "90")) ~time_in_force: (T.Order.Day @@ -637,12 +631,10 @@ let day_identity_is_validated () = let tests = [ - Alcotest.test_case "legacy compatibility mapping" `Quick - compatibility_mapping; Alcotest.test_case "stop-limit and GTD validation" `Quick validates_stop_limit_and_gtd; - Alcotest.test_case "v8 intent companions" `Quick - v8_intent_requires_explicit_companions; + Alcotest.test_case "intent companions" `Quick + intent_requires_explicit_companions; Alcotest.test_case "order validation and serialization" `Quick order_validation_and_serialization_branches; Alcotest.test_case "stop triggers before later execution" `Quick diff --git a/test/test_reducer.ml b/test/test_reducer.ml index 46bbd74..06e9c1e 100644 --- a/test/test_reducer.ml +++ b/test/test_reducer.ml @@ -9,7 +9,8 @@ let runner ?contract_version ?(initial_cash = "10000") ?(risk = risk ()) engine_config ?contract_version ~risk ?execution_model ~execution () in Runner.create ~run_id:(run_id "test-run") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money initial_cash) ] + ~initial_portfolio: + (initial_portfolio ~cash:[ ("USD", money initial_cash) ] ()) ~strategy_state |> ok @@ -49,6 +50,8 @@ let market_order_retries_after_partial_fill () = "first audit order" [ "run_started"; + "initial_state"; + "valuation"; "market_slice_received"; "target_portfolio_requested"; "order_accepted"; @@ -74,6 +77,7 @@ let market_order_retries_after_partial_fill () = [ "market_slice_received"; "fill_applied"; + "settlement_instruction_created"; "order_cancelled"; "order_accepted"; "valuation"; @@ -186,112 +190,6 @@ let superseding_target_replaces_retry () = (event_names events))) | _ -> Alcotest.fail "expected one replacement order" -let fill_limit_clips_buy_to_lots ?(contract_version = T.Contract.version) () = - let constrained = risk ~max_leverage:"1" () in - let state = - runner ~contract_version ~initial_cash:"550" ~risk:constrained - ~execution:(execution ~fixed_fee:"10" ()) - [ (1L, [ target "10" ]) ] - in - let decision_bar = - bar ~open_price:"50" ~high_price:"50" ~low_price:"50" ~close_price:"50" 1L - in - let state, _ = - Runner.process_slice state (market_slice ~bars:[ decision_bar ] 1L) |> ok - in - let state, events = - Runner.process_slice state - (market_slice ~bars:[ bar ~open_price:"100" ~close_price:"100" 2L ] 2L) - |> ok - in - Alcotest.check quantity_testable "five risk-permitted shares" (quantity "5") - (T.Account.position_quantity (Runner.account state) - (instrument_id "test-equity")); - Alcotest.check money_testable "cash after clipped fill" (money "40") - (account_cash (Runner.account state)); - let limited = - List.find - (fun audit -> - String.equal (T.Audit.event_name audit.T.Audit.event) "fill_clipped") - events - in - Alcotest.(check string) - "fill-clipped contract version" contract_version limited.contract_version; - match limited.event with - | T.Audit.Fill_clipped - { - proposed_quantity; - permitted_quantity; - price = fill_price; - limit = T.Risk.Maximum_leverage threshold; - _; - } -> - Alcotest.check quantity_testable "ten proposed" (quantity "10") - proposed_quantity; - Alcotest.check quantity_testable "five permitted" (quantity "5") - permitted_quantity; - Alcotest.check price_testable "actual price" (price "100") fill_price; - Alcotest.(check string) - "leverage threshold" "1" - (T.Scalar.Ratio.to_decimal_string threshold) - | _ -> Alcotest.fail "expected leverage clipping audit" - -let v4_replays_keep_the_fill_clipped_record () = - fill_limit_clips_buy_to_lots ~contract_version:T.Contract.previous_version () - -let v3_replays_keep_the_legacy_clipping_record () = - let constrained = risk ~max_leverage:"1" () in - let state = - runner ~contract_version:T.Contract.legacy_journal_version - ~initial_cash:"550" ~risk:constrained - ~execution:(execution ~fixed_fee:"10" ()) - [ (1L, [ target "10" ]) ] - in - let decision_bar = - bar ~open_price:"50" ~high_price:"50" ~low_price:"50" ~close_price:"50" 1L - in - let state, _ = - Runner.process_slice state (market_slice ~bars:[ decision_bar ] 1L) |> ok - in - let _, events = - Runner.process_slice state - (market_slice ~bars:[ bar ~open_price:"100" ~close_price:"100" 2L ] 2L) - |> ok - in - let limited = - List.find - (fun audit -> - String.equal (T.Audit.event_name audit.T.Audit.event) "margin_limited") - events - in - Alcotest.(check string) - "legacy journal version" T.Contract.legacy_journal_version - limited.contract_version; - match limited.event with - | T.Audit.Margin_limited { requested_quantity; permitted_quantity; _ } -> - Alcotest.check quantity_testable "legacy requested" (quantity "10") - requested_quantity; - Alcotest.check quantity_testable "legacy permitted" (quantity "5") - permitted_quantity - | _ -> Alcotest.fail "expected legacy margin_limited audit" - -let invalid_fill_candidates_fail_instead_of_clipping () = - let constrained = risk ~max_leverage:"1" () in - let state = - runner ~initial_cash:"9223372036854.775807" ~risk:constrained - [ - ( 1L, - [ - T.Strategy.Submit_order - (request ~side:T.Order.Sell ~quantity_value:"1" ()); - ] ); - ] - in - let state, _ = Runner.process_slice state (market_slice 1L) |> ok in - Alcotest.(check string) - "account overflow is not a clipping policy" "int64 addition overflow" - (Runner.process_slice state (market_slice 2L) |> error) - let sells_precede_buys_in_the_same_slice () = let a = instrument ~id:"asset-a" ~symbol:"A" () in let b = instrument ~id:"asset-b" ~symbol:"B" () in @@ -311,6 +209,7 @@ let sells_precede_buys_in_the_same_slice () = in let state = runner ~initial_cash:"1000" ~risk:configured + ~execution:(execution ~instruments:[ a; b ] ()) [ (1L, [ portfolio "10" "0" ]); (2L, [ portfolio "0" "10" ]) ] in let state, _ = @@ -349,7 +248,7 @@ let interactive_market_slice_timeline_is_non_overlapping () = let initial = T.Engine.Interactive.create ~run_id:(run_id "timeline-test") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "10000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "10000") ] ()) |> ok in let rec finish progress = @@ -400,7 +299,7 @@ let internal_feedback_is_capped () = let config = engine_config ~max_internal_events:3 () in let state = Looping_runner.create ~run_id:(run_id "loop") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "10000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "10000") ] ()) ~strategy_state |> ok in @@ -413,7 +312,7 @@ let exact_internal_event_limit_succeeds () = let config = engine_config ~max_internal_events:1 () in let state = Runner.create ~run_id:(run_id "one-event") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "10000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "10000") ] ()) ~strategy_state |> ok in @@ -433,7 +332,7 @@ let reducer_feedback_queue_handles_large_batches () = let config = engine_config ~max_internal_events () in let state = Runner.create ~run_id:(run_id "large-feedback") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "10000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "10000") ] ()) ~strategy_state |> ok in @@ -448,7 +347,7 @@ let reducer_feedback_queue_handles_large_batches () = in Alcotest.(check int) "every intent rejected" batch_size rejection_count; Alcotest.(check int) - "batch completes at the exact feedback limit" (batch_size + 3) + "batch completes at the exact feedback limit" (batch_size + 5) (List.length events) let completed_run_is_terminal_and_hash_bound () = @@ -456,7 +355,7 @@ let completed_run_is_terminal_and_hash_bound () = let state, valuation, events = Runner.complete state |> ok in Alcotest.(check (list string)) "start and completion events" - [ "run_started"; "run_completed" ] + [ "run_started"; "initial_state"; "valuation"; "run_completed" ] (event_names events); Alcotest.check money_testable "initial equity" (money "10000") valuation.equity; @@ -476,18 +375,12 @@ let completed_run_is_terminal_and_hash_bound () = let invalid_initial_state_is_rejected () = let strategy_state = T.Scripted_strategy.create [] |> ok in let config = engine_config () in - Alcotest.(check bool) - "negative cash rejected" true - (Result.is_error - (Runner.create ~run_id:(run_id "bad-cash") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "-1") ] - ~strategy_state)); Alcotest.(check bool) "noncanonical hash rejected" true (Result.is_error (Runner.create ~run_id:(run_id "bad-hash") ~scenario_sha256:(String.make 64 'A') ~config - ~initial_cash:[ ("USD", money "1") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "1") ] ()) ~strategy_state)) let one_valuation_per_slice () = @@ -500,7 +393,7 @@ let one_valuation_per_slice () = (fun name -> String.equal name "valuation") (event_names events)) in - Alcotest.(check int) "first slice" 1 (count first); + Alcotest.(check int) "initial state and first slice" 2 (count first); Alcotest.(check int) "second slice" 1 (count second) module No_fill_execution = struct @@ -546,21 +439,19 @@ let execution_model_configuration_must_match () = |> ok in let configure contract_version execution_model execution = - T.Engine.config ~contract_version ~risk:(risk ()) ~execution_model - ~execution ~max_internal_events:1000 + T.Engine.config ~contract_version ~risk:(risk ()) ~venue_calendars:[] + ~execution_model ~execution ~financing:(financing_policy ()) + ~settlement:(settlement_policy ()) ~max_internal_events:1000 in Alcotest.(check bool) "conservative model requires pricing configuration" true - (Result.is_error (configure "13" next_open (execution ()))); + (Result.is_error (configure "1" 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)); + "completed-bar model rejects conservative pricing" true + (Result.is_error (configure "1" completed conservative)); Alcotest.(check bool) "matching conservative configuration accepted" true - (Result.is_ok (configure "13" next_open conservative)) + (Result.is_ok (configure "1" next_open conservative)) module Cancel_next_strategy = struct type state = { submitted : bool; cancelled : bool } @@ -592,8 +483,12 @@ module Cancel_next_runner = T.Engine.Make (Cancel_next_strategy) let callbacks_use_current_slice_and_apply_responses_before_matching () = let configured = instrument ~currency:"EUR" () in let configured_risk = risk ~instruments:[ configured ] () in - let config = engine_config ~risk:configured_risk () in - let initial_cash = [ ("USD", money "10000"); ("EUR", money "0") ] in + let config = + engine_config ~risk:configured_risk + ~execution:(execution ~instruments:[ configured ] ()) + () + in + let initial_cash = [ ("USD", money "0"); ("EUR", money "10000") ] in let first_slice = market_slice ~bars:[ bar ~close_price:"104" 1L ] @@ -616,7 +511,9 @@ let callbacks_use_current_slice_and_apply_responses_before_matching () = let scripted = Cancel_next_runner.create ~run_id:(run_id "callback-consistency") - ~scenario_sha256 ~config ~initial_cash ~strategy_state + ~scenario_sha256 ~config + ~initial_portfolio:(initial_portfolio ~cash:initial_cash ()) + ~strategy_state |> ok in let scripted, _ = @@ -628,7 +525,8 @@ let callbacks_use_current_slice_and_apply_responses_before_matching () = let interactive = T.Engine.Interactive.create ~run_id:(run_id "callback-consistency") - ~scenario_sha256 ~config ~initial_cash + ~scenario_sha256 ~config + ~initial_portfolio:(initial_portfolio ~cash:initial_cash ()) |> ok in let rec finish_first submitted progress = @@ -746,7 +644,7 @@ let interactive_reducer_matches_scripted_strategy () = let interactive = T.Engine.Interactive.create ~run_id:(run_id "test-run") ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money "10000") ] + ~initial_portfolio:(initial_portfolio ~cash:[ ("USD", money "10000") ] ()) |> ok in let progress = @@ -812,6 +710,7 @@ let explicit_phase_order_is_stable () = "market_slice_received"; "cash_dividend_applied"; "fill_applied"; + "settlement_instruction_created"; "metric_emitted"; "target_portfolio_requested"; "order_accepted"; @@ -830,14 +729,6 @@ let tests = bounded_target_orders_make_progress; Alcotest.test_case "superseding target replaces retry" `Quick superseding_target_replaces_retry; - Alcotest.test_case "fill clipping identifies leverage" `Quick - fill_limit_clips_buy_to_lots; - Alcotest.test_case "v4 keeps fill clipping records" `Quick - v4_replays_keep_the_fill_clipped_record; - Alcotest.test_case "v3 keeps legacy clipping records" `Quick - v3_replays_keep_the_legacy_clipping_record; - Alcotest.test_case "invalid fill candidates fail" `Quick - invalid_fill_candidates_fail_instead_of_clipping; Alcotest.test_case "same-slice sells precede buys" `Quick sells_precede_buys_in_the_same_slice; Alcotest.test_case "external ordering validation" `Quick diff --git a/test/test_reducer_properties.ml b/test/test_reducer_properties.ml index 9135a12..42edfd0 100644 --- a/test/test_reducer_properties.ml +++ b/test/test_reducer_properties.ml @@ -218,22 +218,17 @@ let gen_trace = (list_size (int_range 4 14) gen_step))) let make_risk trace = - T.Risk.create ~base_currency:"USD" ~instruments:[ primary; foreign ] - ~max_order_quantity:(quantity "50") ~max_long_position:(quantity "100") - ~max_short_position:(quantity "100") ~max_gross_exposure:(money "50000") - ~max_leverage: - (T.Scalar.Ratio.of_decimal_string - (decimal_of_scaled trace.leverage_tenths 10) - |> ok) + risk ~instruments:[ primary; foreign ] ~max_order:"50" ~max_long:"100" + ~max_short:"100" ~max_gross:"50000" + ~max_leverage:(decimal_of_scaled trace.leverage_tenths 10) ~initial_margin_bps:trace.initial_margin_bps - ~maintenance_margin_bps:trace.maintenance_margin_bps ~short_borrow_bps:250 - |> ok + ~maintenance_margin_bps:trace.maintenance_margin_bps () let make_config trace risk = engine_config ~risk ~execution: - (execution ~participation_bps:trace.participation_bps ~fixed_fee:"0.25" - ~fee_bps:5 ()) + (execution ~participation_bps:trace.participation_bps + ~instruments:[ primary; foreign ] ()) ~max_internal_events:5000 () let initial_cash = [ ("USD", money "10000"); ("EUR", money "5000") ] @@ -693,9 +688,10 @@ let slice_valuation audits = | T.Audit.Valuation value -> Some value | _ -> None) audits + |> List.rev |> function - | [ valuation ] -> Ok valuation - | _ -> Error "slice did not emit exactly one valuation" + | valuation :: _ -> Ok valuation + | [] -> Error "slice did not emit a valuation" let check_fill oms audit = match audit.T.Audit.event with @@ -743,7 +739,9 @@ let reducer_invariants_hold trace = let config = make_config trace risk in let state = Generated_runner.create ~run_id:(run_id "property-run") ~scenario_sha256 - ~config ~initial_cash ~strategy_state:(schedule trace) + ~config + ~initial_portfolio:(initial_portfolio ~cash:initial_cash ()) + ~strategy_state:(schedule trace) |> ok in let rec loop index history state = function @@ -841,13 +839,16 @@ let reducers_agree trace = let scripted = Generated_runner.create ~run_id:(run_id "property-equivalence") - ~scenario_sha256 ~config ~initial_cash ~strategy_state + ~scenario_sha256 ~config + ~initial_portfolio:(initial_portfolio ~cash:initial_cash ()) + ~strategy_state |> ok in let interactive = T.Engine.Interactive.create ~run_id:(run_id "property-equivalence") - ~scenario_sha256 ~config ~initial_cash + ~scenario_sha256 ~config + ~initial_portfolio:(initial_portfolio ~cash:initial_cash ()) |> ok in let compare_slice index scripted interactive audits scripted_audits = diff --git a/test/test_repository_metadata.py b/test/test_repository_metadata.py index 3b76053..c9b2b49 100644 --- a/test/test_repository_metadata.py +++ b/test/test_repository_metadata.py @@ -19,8 +19,7 @@ def test_repository_profile_is_specific_and_bounded(self) -> None: self.assertEqual( profile["description"], - "Deterministic event-driven OCaml execution engine with versioned replay contracts " - "and causal audit journals", + "Deterministic OCaml trading replay engine", ) self.assertEqual(profile["homepage"], "https://fallblu.github.io/trading-engine/") self.assertEqual( @@ -75,13 +74,13 @@ def test_label_manifest_covers_stable_planning_dimensions(self) -> None: self.assertEqual(len(names), len(set(names))) self.assertEqual( categories, - {"component": 10, "priority": 4, "effort": 3, "contract": 7, "dependency": 3}, + {"component": 10, "priority": 4, "effort": 3, "contract": 2, "dependency": 3}, ) self.assertTrue(all(re.fullmatch(r"[0-9a-f]{6}", label["color"]) for label in labels)) self.assertTrue(all(label["description"].strip() for label in labels)) self.assertIn("dependency: persistra", names) - self.assertIn("contract: scenario-v4", names) - self.assertIn("contract: strategy-v3", names) + self.assertIn("contract: scenario-v1", names) + self.assertIn("contract: strategy-v1", names) def test_structured_forms_reference_defined_labels_and_require_evidence(self) -> None: template_directory = GITHUB / "ISSUE_TEMPLATE" @@ -142,9 +141,11 @@ def test_compatibility_gate_is_pinned_and_canary_is_optional(self) -> None: self.assertTrue(all(re.fullmatch(r"[0-9a-f]{40}", revision) for revision in revisions)) compatibility = (REPOSITORY_ROOT / "docs/persistra.md").read_text(encoding="utf-8") - for guarantee in ("Engine", "Scenario", "Journal", "Strategy", "Persistra"): - self.assertIn(f"**{guarantee}:**", compatibility) - self.assertIn("Neither repository silently advances", compatibility) + self.assertIn("versioned files and processes", compatibility) + self.assertIn("scenario contract v1", compatibility) + self.assertIn("explicit pair of repository commits", compatibility) + self.assertIn("Neither repository", compatibility) + self.assertIn("silently follows a moving branch", compatibility) if __name__ == "__main__": diff --git a/test/test_risk_groups.ml b/test/test_risk_groups.ml index 1269510..891d1cb 100644 --- a/test/test_risk_groups.ml +++ b/test/test_risk_groups.ml @@ -40,10 +40,9 @@ let setup ?(groups = []) ?(shorting_allowed = true) () = ] in let risk = - T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + T.Risk.create ~base_currency:"USD" ~instruments:[ first; second ] ~instrument_policies:policies ~groups ~max_gross_exposure:(money "100000") ~max_leverage:(T.Scalar.Ratio.of_decimal_string "10" |> ok) - ~short_borrow_bps:0 |> ok in (first, second, risk) @@ -52,11 +51,10 @@ let exact_coverage_and_short_policy () = let first = instrument ~id:"first" ~symbol:"FIRST" () in let second = instrument ~id:"second" ~symbol:"SECOND" () in let result = - T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + T.Risk.create ~base_currency:"USD" ~instruments:[ first; second ] ~instrument_policies:[ policy first () ] ~groups:[] ~max_gross_exposure:(money "100000") ~max_leverage:(T.Scalar.Ratio.of_decimal_string "10" |> ok) - ~short_borrow_bps:0 in Alcotest.(check string) "policy required for every instrument" @@ -141,7 +139,7 @@ let initialized_positions_use_instrument_margin_and_groups () = let second = instrument ~id:"second" ~symbol:"SECOND" () in let group_limit = limits ~gross:"150" () in let risk = - T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + T.Risk.create ~base_currency:"USD" ~instruments:[ first; second ] ~instrument_policies: [ policy first ~initial_margin_bps:10_000 ~maintenance_margin_bps:5000 @@ -151,7 +149,6 @@ let initialized_positions_use_instrument_margin_and_groups () = ~groups:[ group "initial-group" [ first; second ] group_limit ] ~max_gross_exposure:(money "100000") ~max_leverage:(T.Scalar.Ratio.of_decimal_string "10" |> ok) - ~short_borrow_bps:0 |> ok in let account = test_account () in @@ -195,7 +192,7 @@ let reserved_result ?(side = T.Order.Buy) ?(initial_cash = "10000") group_limits) in let risk = - T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + T.Risk.create ~base_currency:"USD" ~instruments:[ first; second ] ~instrument_policies: [ policy first ~max_long ~max_short ~max_notional ~shorting_allowed @@ -206,7 +203,6 @@ let reserved_result ?(side = T.Order.Buy) ?(initial_cash = "10000") ] ~groups ~max_gross_exposure:(money max_gross) ~max_leverage:(T.Scalar.Ratio.of_decimal_string max_leverage |> ok) - ~short_borrow_bps:0 |> ok in let account = test_account ~initial_cash:[ ("USD", money initial_cash) ] () in @@ -328,7 +324,7 @@ let constructors_reject_ambiguous_policies () = ~group_kind:T.Risk.Custom ~instrument_ids:[ first.id; first.id ] ~limits:valid_limits)) -let create_v7_rejects_inconsistent_configuration () = +let create_rejects_inconsistent_configuration () = let first = instrument ~id:"first" ~symbol:"FIRST" () in let second = instrument ~id:"second" ~symbol:"SECOND" () in let first_policy = policy first () in @@ -341,22 +337,19 @@ let create_v7_rejects_inconsistent_configuration () = in let create ?(base_currency = "USD") ?(instruments = [ first; second ]) ?(policies = [ first_policy; second_policy ]) ?(groups = [ valid_group ]) - ?(max_gross = "100000") ?(short_borrow_bps = 0) () = - T.Risk.create_v7 ~base_currency ~instruments ~instrument_policies:policies + ?(max_gross = "100000") () = + T.Risk.create ~base_currency ~instruments ~instrument_policies:policies ~groups ~max_gross_exposure:(money max_gross) ~max_leverage:(T.Scalar.Ratio.of_decimal_string "10" |> ok) - ~short_borrow_bps in List.iter (fun result -> Alcotest.(check bool) - "invalid v7 configuration" true (Result.is_error result)) + "invalid risk configuration" true (Result.is_error result)) [ create ~base_currency:"" (); create ~instruments:[] ~policies:[] ~groups:[] (); create ~max_gross:"0" (); - create ~short_borrow_bps:(-1) (); - create ~short_borrow_bps:10_001 (); create ~instruments:[ first; first ] (); create ~policies:[ first_policy; unknown_policy ] (); create ~policies:[ first_policy; first_policy ] (); @@ -379,7 +372,7 @@ let admission_result ?(side = T.Order.Buy) ?(initial_cash = "10000") group_limits) in let risk = - T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + T.Risk.create ~base_currency:"USD" ~instruments:[ first; second ] ~instrument_policies: [ policy first ~max_order ~max_long ~max_short ~max_notional @@ -390,7 +383,6 @@ let admission_result ?(side = T.Order.Buy) ?(initial_cash = "10000") ] ~groups ~max_gross_exposure:(money max_gross) ~max_leverage:(T.Scalar.Ratio.of_decimal_string max_leverage |> ok) - ~short_borrow_bps:0 |> ok in T.Risk.check risk @@ -400,7 +392,7 @@ let admission_result ?(side = T.Order.Buy) ?(initial_cash = "10000") ~fx_rates:[ ("USD", price "1") ] (request ~instrument:first.id ~side ~quantity_value:"10" ()) -let admission_enforces_every_v7_limit () = +let admission_enforces_every_limit () = let check_error expected result = Alcotest.(check string) "admission error" expected (error result) in @@ -436,12 +428,11 @@ let group_exposures_include_short_and_zero_equity () = let first = instrument ~id:"first" ~symbol:"FIRST" () in let second = instrument ~id:"second" ~symbol:"SECOND" () in let risk = - T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + T.Risk.create ~base_currency:"USD" ~instruments:[ first; second ] ~instrument_policies:[ policy first (); policy second () ] ~groups:[ group "group" [ first ] (limits ~gross:"100000" ()) ] ~max_gross_exposure:(money "100000") ~max_leverage:(T.Scalar.Ratio.of_decimal_string "10" |> ok) - ~short_borrow_bps:0 |> ok in let order = @@ -476,7 +467,7 @@ let initial_result ?(side = T.Order.Buy) ?(initial_cash = "10000") group_limits) in let risk = - T.Risk.create_v7 ~base_currency:"USD" ~instruments:[ first; second ] + T.Risk.create ~base_currency:"USD" ~instruments:[ first; second ] ~instrument_policies: [ policy first ~max_long ~max_short ~max_notional ~shorting_allowed @@ -487,7 +478,6 @@ let initial_result ?(side = T.Order.Buy) ?(initial_cash = "10000") ] ~groups ~max_gross_exposure:(money max_gross) ~max_leverage:(T.Scalar.Ratio.of_decimal_string max_leverage |> ok) - ~short_borrow_bps:0 |> ok in let order = @@ -504,7 +494,7 @@ let initial_result ?(side = T.Order.Buy) ?(initial_cash = "10000") in T.Risk.check_initial risk valuation -let initial_portfolio_enforces_every_v7_limit () = +let initial_portfolio_enforces_every_limit () = let check_error expected result = Alcotest.(check string) "initial portfolio error" expected (error result) in @@ -535,25 +525,35 @@ let initial_portfolio_enforces_every_v7_limit () = initial_result ~group_limits:(limits ~concentration:"0.05" ()) () |> check_error "initial portfolio exceeds group group-a maximum concentration" -let legacy_and_policy_boundaries_are_rejected () = +let risk_and_policy_boundaries_are_rejected () = let first = instrument ~id:"first" ~symbol:"FIRST" () in let large_lot = instrument ~id:"large" ~symbol:"LARGE" ~lot_size:"2" () in let ratio = T.Scalar.Ratio.of_decimal_string "10" |> ok in let create ?(base_currency = "USD") ?(instruments = [ first ]) ?(max_order = "10") ?(max_long = "10") ?(max_short = "10") - ?(max_gross = "1000") ?(initial = 5000) ?(maintenance = 2500) - ?(borrow = 0) () = - T.Risk.create ~base_currency ~instruments - ~max_order_quantity:(quantity max_order) - ~max_long_position:(quantity max_long) - ~max_short_position:(quantity max_short) - ~max_gross_exposure:(money max_gross) ~max_leverage:ratio - ~initial_margin_bps:initial ~maintenance_margin_bps:maintenance - ~short_borrow_bps:borrow + ?(max_gross = "1000") ?(initial = 5000) ?(maintenance = 2500) () = + let instrument_policies = + List.map + (fun instrument -> + T.Risk.create_instrument_policy ~instrument + ~max_order_quantity:(quantity max_order) + ~max_long_position:(quantity max_long) + ~max_short_position:(quantity max_short) ~max_notional_exposure:None + ~initial_margin_bps:initial ~maintenance_margin_bps:maintenance + ~shorting_allowed:true) + instruments + in + match List.find_opt Result.is_error instrument_policies with + | Some (Error message) -> Error message + | Some (Ok _) -> assert false + | None -> + T.Risk.create ~base_currency ~instruments + ~instrument_policies:(List.map Result.get_ok instrument_policies) + ~groups:[] ~max_gross_exposure:(money max_gross) ~max_leverage:ratio in List.iter (fun result -> - Alcotest.(check bool) "invalid legacy risk" true (Result.is_error result)) + Alcotest.(check bool) "invalid risk" true (Result.is_error result)) [ create ~base_currency:"" (); create ~max_order:"0" (); @@ -565,8 +565,6 @@ let legacy_and_policy_boundaries_are_rejected () = create ~initial:10_001 (); create ~maintenance:10_001 (); create ~initial:1000 ~maintenance:2000 (); - create ~borrow:(-1) (); - create ~borrow:10_001 (); create ~instruments:[] (); create ~instruments:[ large_lot ] ~max_order:"1" (); create ~instruments:[ large_lot ] ~max_long:"1" (); @@ -594,29 +592,30 @@ let legacy_and_policy_boundaries_are_rejected () = ] let public_checks_cover_success_and_diagnostics () = - let legacy = risk ~max_order:"7" ~max_long:"7" ~max_short:"7" () in + let configured = risk ~max_order:"7" ~max_long:"7" ~max_short:"7" () in Alcotest.(check bool) "position accepted" true - (Result.is_ok (T.Risk.check_position legacy (quantity "7"))); + (Result.is_ok (T.Risk.check_position configured (quantity "7"))); Alcotest.(check string) "long rejected" "position would exceed the maximum long position" - (T.Risk.check_position legacy (quantity "8") |> error); + (T.Risk.check_position configured (quantity "8") |> error); Alcotest.(check string) "short rejected" "position would exceed the maximum short position" - (T.Risk.check_position legacy (quantity "-8") |> error); + (T.Risk.check_position configured (quantity "-8") |> error); Alcotest.(check string) "unknown policy" "position refers to an unknown instrument risk policy" - (T.Risk.check_position_for legacy (instrument_id "unknown") (quantity "1") + (T.Risk.check_position_for configured (instrument_id "unknown") + (quantity "1") |> error); let unknown_request = request ~instrument:(instrument_id "unknown") () in Alcotest.(check string) "unknown instrument" "order refers to an unknown instrument" - (T.Risk.check legacy ~account:(test_account ()) ~oms:T.Oms.empty ~marks:[] - ~fx_rates:[] unknown_request + (T.Risk.check configured ~account:(test_account ()) ~oms:T.Oms.empty + ~marks:[] ~fx_rates:[] unknown_request |> error); Alcotest.(check string) - "legacy order limit" "order exceeds the maximum order quantity" - (risk_check legacy ~account:(test_account ()) ~oms:T.Oms.empty + "order limit" "order exceeds the instrument maximum order quantity" + (risk_check configured ~account:(test_account ()) ~oms:T.Oms.empty (request ~quantity_value:"8" ()) |> error); (match reserved_result ~include_mark:false () with @@ -641,7 +640,7 @@ let public_checks_cover_success_and_diagnostics () = (Result.is_ok (admission_result ~group_limits:(limits ~gross:"10000" ()) ())) -let legacy_post_fill_covers_gross_and_reduction () = +let post_fill_covers_gross_and_reduction () = let first = instrument () in let before_account = test_account () in let before = @@ -660,7 +659,7 @@ let legacy_post_fill_covers_gross_and_reduction () = ~after_position:(quantity "10") ~before ~after with | Error (T.Risk.Limit (T.Risk.Maximum_gross_exposure _)) -> () - | _ -> Alcotest.fail "expected legacy gross fill limit"); + | _ -> Alcotest.fail "expected gross fill limit"); Alcotest.(check bool) "gross-reducing fill accepted" true (Result.is_ok @@ -681,18 +680,18 @@ let tests = clipping_taxonomy_is_exact; Alcotest.test_case "constructors reject ambiguous policies" `Quick constructors_reject_ambiguous_policies; - Alcotest.test_case "v7 rejects inconsistent configuration" `Quick - create_v7_rejects_inconsistent_configuration; - Alcotest.test_case "admission enforces every v7 limit" `Quick - admission_enforces_every_v7_limit; + Alcotest.test_case "current constructor rejects inconsistent configuration" + `Quick create_rejects_inconsistent_configuration; + Alcotest.test_case "admission enforces every current limit" `Quick + admission_enforces_every_limit; Alcotest.test_case "group exposures include short and zero equity" `Quick group_exposures_include_short_and_zero_equity; - Alcotest.test_case "initial portfolio enforces every v7 limit" `Quick - initial_portfolio_enforces_every_v7_limit; - Alcotest.test_case "legacy and policy boundaries are rejected" `Quick - legacy_and_policy_boundaries_are_rejected; + Alcotest.test_case "initial portfolio enforces every current limit" `Quick + initial_portfolio_enforces_every_limit; + Alcotest.test_case "risk and policy boundaries are rejected" `Quick + risk_and_policy_boundaries_are_rejected; Alcotest.test_case "public checks cover success and diagnostics" `Quick public_checks_cover_success_and_diagnostics; - Alcotest.test_case "legacy post-fill covers gross and reduction" `Quick - legacy_post_fill_covers_gross_and_reduction; + Alcotest.test_case "post-fill covers gross and reduction" `Quick + post_fill_covers_gross_and_reduction; ] diff --git a/test/test_scenario.ml b/test/test_scenario.ml index eebadba..1cf7428 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -2,21 +2,21 @@ open Test_support module T = Trading_engine let demo_document () = - In_channel.with_open_bin "../contracts/v16/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v1/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/v16/fixtures/demo.scenario.jsonl" -let quote_trade_path = "../contracts/v16/fixtures/quote-trade.scenario.json" +let stream_path = "../contracts/v1/fixtures/demo.scenario.jsonl" +let quote_trade_path = "../contracts/v1/fixtures/quote-trade.scenario.json" let quote_trade_stream_path = - "../contracts/v16/fixtures/quote-trade.scenario.jsonl" + "../contracts/v1/fixtures/quote-trade.scenario.jsonl" -let order_book_path = "../contracts/v16/fixtures/order-book.scenario.json" +let order_book_path = "../contracts/v1/fixtures/order-book.scenario.json" let order_book_stream_path = - "../contracts/v16/fixtures/order-book.scenario.jsonl" + "../contracts/v1/fixtures/order-book.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -84,8 +84,7 @@ let write_large_stream path slice_count = ~effective_at:start_at ~credit_rate_bps:0 ~debit_rate_bps:0 |> ok in - T.Market_slice.create_v15 ~slice_sequence:(Int64.of_int index) - ~start_at + T.Market_slice.create ~slice_sequence:(Int64.of_int index) ~start_at ~end_at:(add_seconds base (offset + 1)) ~available_at:(add_seconds base (offset + 2)) ~received_at:(add_seconds base (offset + 3)) @@ -104,7 +103,7 @@ let write_large_stream path slice_count = let payload = `Assoc [ - ("market_slice", T.Codec.market_slice_to_yojson_v16 market_slice); + ("market_slice", T.Codec.market_slice_to_yojson market_slice); ("intents", `List []); ] in @@ -149,9 +148,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v16/scenario.schema.json"; - check_schema "../contracts/v16/scenario-stream.schema.json"; - check_schema "../contracts/v16/journal.schema.json" + check_schema "../contracts/v1/scenario.schema.json"; + check_schema "../contracts/v1/scenario-stream.schema.json"; + check_schema "../contracts/v1/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -191,7 +190,7 @@ let map_root change = let replace_assoc name value fields = (name, value) :: List.remove_assoc name fields -let v12_distributions_and_lifecycle_parse () = +let distributions_and_lifecycle_parse () = let source = instrument_id "demo-equity-acme" in let child = instrument_id "demo-equity-child" in let action name distribution_type destination fractional_policy = @@ -212,7 +211,7 @@ let v12_distributions_and_lifecycle_parse () = |> ok in let market_slice = - T.Market_slice.create_v15 ~slice_sequence:1L + T.Market_slice.create ~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") @@ -351,7 +350,7 @@ let v12_distributions_and_lifecycle_parse () = | _ -> Alcotest.fail "demo slice must be an object" in `List - (T.Codec.market_slice_to_yojson_v16 market_slice + (T.Codec.market_slice_to_yojson market_slice :: List.map add_child_bar rest) | _ -> Alcotest.fail "demo slices must be nonempty" in @@ -441,8 +440,7 @@ 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 16, 15, 14, \ - 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 1)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -592,7 +590,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_v15 ~slice_sequence:(Int64.of_int index) ~start_at + T.Market_slice.create ~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)) @@ -607,7 +605,7 @@ let dense_schedule_document slice_count = ~cash_rate_observations:[ cash_rate_observation ] ~settlement_failures:[] ~lifecycle_events:[] ~market_events:[] ~order_book_events:[] - |> ok |> T.Codec.market_slice_to_yojson_v16) + |> ok |> T.Codec.market_slice_to_yojson) in let schedule = List.init slice_count (fun offset -> @@ -703,8 +701,10 @@ let configured_resources_are_bounded () = "reducer configuration limit" true (Result.is_error (T.Engine.config ~contract_version:T.Contract.version ~risk:(risk ()) + ~venue_calendars:[] ~execution_model:(T.Execution_model.find "completed_bar_v1" |> ok) - ~execution:(execution ()) + ~execution:(execution ()) ~financing:(financing_policy ()) + ~settlement:(settlement_policy ()) ~max_internal_events:(T.Resource_limits.internal_events + 1))) let scenario_with_second_slice_start start_at = @@ -1154,10 +1154,19 @@ let audit_ids_are_deterministic_and_causal () = "fill cites price selection" [ "demo-event-000000000012" ] (cause_strings (event 13L)); + let completion = List.hd (List.rev result.audits) in + let terminal_valuation = + result.audits + |> List.filter (fun audit -> + Int64.compare audit.T.Audit.engine_sequence completion.engine_sequence + < 0 + && String.equal (T.Audit.event_name audit.event) "valuation") + |> List.rev |> List.hd + in Alcotest.(check (list string)) "completion cites terminal valuation" - [ "demo-event-000000000028" ] - (cause_strings (event 29L)); + [ T.Id.Event.to_string terminal_valuation.event_id ] + (cause_strings completion); match (event 8L).event with | T.Audit.Order_accepted order -> Alcotest.(check string) @@ -1196,36 +1205,15 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v16/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v1/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual -let v3_replay_matches_frozen_golden_file () = - let document = - In_channel.with_open_bin "../contracts/v3/fixtures/demo.scenario.json" - In_channel.input_all - in - let scenario = T.Scenario.of_string document |> ok in - let result = - T.Replay.run ~scenario_sha256:(T.Sha256.digest_string document) scenario - |> ok - in - let actual = - result.audits |> List.map T.Codec.audit_to_string |> String.concat "\n" - |> fun value -> value ^ "\n" - in - let expected = - In_channel.with_open_bin "../contracts/v3/fixtures/demo.journal.jsonl" - In_channel.input_all - in - Alcotest.(check string) "frozen v3 audit contract" expected actual - let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v16/fixtures/fill-clipped.scenario.json" - In_channel.input_all + "../contracts/v1/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in let result = @@ -1238,8 +1226,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v16/fixtures/fill-clipped.journal.jsonl" - In_channel.input_all + "../contracts/v1/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual @@ -1258,7 +1245,7 @@ let quote_trade_replay_is_causal_and_stream_equivalent () = in let golden = In_channel.with_open_bin - "../contracts/v16/fixtures/quote-trade.journal.jsonl" In_channel.input_all + "../contracts/v1/fixtures/quote-trade.journal.jsonl" In_channel.input_all in Alcotest.(check string) "quote/trade golden journal" golden batch_journal; let fills = @@ -1315,8 +1302,8 @@ let order_book_replay_is_bounded_and_stream_equivalent () = |> fun value -> value ^ "\n" in let golden = - In_channel.with_open_bin - "../contracts/v16/fixtures/order-book.journal.jsonl" In_channel.input_all + In_channel.with_open_bin "../contracts/v1/fixtures/order-book.journal.jsonl" + In_channel.input_all in Alcotest.(check string) "order-book golden journal" golden actual; let fills = @@ -1435,9 +1422,11 @@ let journal_matches_in_memory_events () = let streamed_replay_matches_batch_semantics () = let scenario_sha256 = T.Sha256.digest_file stream_path |> ok in + let expected_result = T.Replay.run ~scenario_sha256 (demo ()) |> ok in let expected = - T.Replay.run ~scenario_sha256 (demo ()) |> ok |> fun result -> - result.audits |> List.map T.Codec.audit_to_string |> String.concat "\n" + expected_result.audits + |> List.map T.Codec.audit_to_string + |> String.concat "\n" |> fun value -> value ^ "\n" in let journal = Filename.temp_file "trading-engine-stream" ".jsonl" in @@ -1454,7 +1443,10 @@ 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) "twenty-nine audits" 29L result.audit_count; + Alcotest.(check int64) + "same audit count" + (Int64.of_int (List.length expected_result.audits)) + result.audit_count; Alcotest.check money_testable "same equity" (money "10111.979929") result.valuation.equity; Alcotest.(check string) @@ -1657,8 +1649,8 @@ let large_stream_replay_does_not_retain_audit_history () = let tests = [ Alcotest.test_case "demo contract parses" `Quick demo_contract_parses; - Alcotest.test_case "v12 distributions and lifecycle parse" `Quick - v12_distributions_and_lifecycle_parse; + Alcotest.test_case "distributions and lifecycle parse" `Quick + distributions_and_lifecycle_parse; Alcotest.test_case "schema artifacts parse" `Quick schema_artifacts_parse; Alcotest.test_case "timestamp precision is bounded" `Quick timestamp_precision_is_bounded; @@ -1697,8 +1689,6 @@ let tests = replay_ends_with_completion_summary; Alcotest.test_case "replay matches golden file" `Quick replay_matches_golden_file; - Alcotest.test_case "v3 replay matches frozen golden file" `Quick - v3_replay_matches_frozen_golden_file; Alcotest.test_case "fill clipping fixture reconciles" `Quick fill_clipping_fixture_reconciles; Alcotest.test_case "quote/trade replay is causal and stream equivalent" diff --git a/test/test_settlement.ml b/test/test_settlement.ml index eb7fe85..3b3b4c1 100644 --- a/test/test_settlement.ml +++ b/test/test_settlement.ml @@ -60,7 +60,7 @@ let calendar_and_trade_date_accounting () = let slice ?(settlement_failures = []) sequence = let date = match sequence with 1L -> "02" | 2L -> "03" | _ -> "04" in - T.Market_slice.create_v11 ~slice_sequence:sequence + T.Market_slice.create ~slice_sequence:sequence ~start_at:(timestamp ("2026-01-" ^ date ^ "T14:30:00Z")) ~end_at:(timestamp ("2026-01-" ^ date ^ "T21:00:00Z")) ~available_at:(timestamp ("2026-01-" ^ date ^ "T21:00:01Z")) @@ -68,15 +68,15 @@ let slice ?(settlement_failures = []) sequence = ~bars:[ bar sequence ] ~fx_rates:[ fx_mark () ] ~corporate_actions:[] ~borrow_observations:[] ~cash_rate_observations:[] - ~settlement_failures + ~settlement_failures ~lifecycle_events:[] ~market_events:[] + ~order_book_events:[] |> ok let runner ?(initial_cash = "1000") ?schedule policy run = let config = - T.Engine.config_v11 ~contract_version:"11" ~risk:(risk ()) - ~venue_calendars:[] + T.Engine.config ~contract_version:"1" ~risk:(risk ()) ~venue_calendars:[] ~execution_model:(T.Execution_model.find "completed_bar_v1" |> ok) - ~execution:(execution ()) ~financing:T.Financing.legacy_policy + ~execution:(execution ()) ~financing:(financing_policy ()) ~settlement:policy ~max_internal_events:1000 |> ok in @@ -98,7 +98,8 @@ let runner ?(initial_cash = "1000") ?schedule policy run = in let strategy_state = T.Scripted_strategy.create schedule |> ok in Runner.create ~run_id:(run_id run) ~scenario_sha256 ~config - ~initial_cash:[ ("USD", money initial_cash) ] + ~initial_portfolio: + (initial_portfolio ~cash:[ ("USD", money initial_cash) ] ()) ~strategy_state |> ok diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index bec7710..903bdec 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -22,18 +22,17 @@ let initialization () = metadata = `Assoc [ ("experiment", `String "demo") ]; run_id = run_id "test-run"; base_currency = "USD"; - initial_cash = [ ("USD", money "10000") ]; - initial_portfolio = None; + initial_portfolio = initial_portfolio (); instruments = [ instrument ]; venue_calendars = []; risk = risk ~instruments:[ instrument ] (); execution_model = T.Execution_model.find "completed_bar_v1" |> ok; execution = - T.Execution.create_v2 ~participation_bps:10_000 + T.Execution.create ~participation_bps:10_000 ~fee_schedules:[ fee_schedule ] |> ok; - financing = Some T.Financing.legacy_policy; - settlement = None; + financing = financing_policy (); + settlement = settlement_policy (); } let field name = function @@ -45,7 +44,7 @@ let initialize_message_is_complete () = T.Strategy_protocol.initialize_message ~sequence:1L (initialization ()) in Alcotest.(check string) - "protocol version" "14" + "protocol version" "1" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -148,36 +147,6 @@ let conservative_initialize_message_encodes_cost_models () = 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 = - { - (initialization ()) with - scenario_contract_version = T.Contract.legacy_journal_version; - } - in - let message = - T.Strategy_protocol.initialize_message ~sequence:1L initialization - in - Alcotest.(check string) - "legacy protocol version" "3" - (match field "strategy_protocol_version" message with - | `String value -> value - | _ -> Alcotest.fail "expected version string"); - let payload = field "payload" message in - Alcotest.(check bool) - "no v4 initial portfolio" false - (match payload with - | `Assoc fields -> List.mem_assoc "initial_portfolio" fields - | _ -> Alcotest.fail "expected payload object"); - let execution = field "execution" payload in - Alcotest.(check bool) - "flat v3 execution" true - (match execution with - | `Assoc fields -> - List.mem_assoc "participation_bps" fields - && not (List.mem_assoc "configuration" fields) - | _ -> Alcotest.fail "expected execution object") - let event_message_contains_complete_context () = let account = test_account () in let slice = market_slice 1L in @@ -265,7 +234,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "14"); + ("strategy_protocol_version", `String "1"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); @@ -365,8 +334,8 @@ let responses_are_strict_and_typed () = let duplicate = `Assoc [ - ("strategy_protocol_version", `String "7"); - ("strategy_protocol_version", `String "7"); + ("strategy_protocol_version", `String "1"); + ("strategy_protocol_version", `String "1"); ("strategy_sequence", `String "3"); ("message_type", `String "stopped"); ("payload", `Assoc []); @@ -380,20 +349,21 @@ let responses_are_strict_and_typed () = let wrong_version = `Assoc [ - ("strategy_protocol_version", `String "1"); + ("strategy_protocol_version", `String "unsupported"); ("strategy_sequence", `String "3"); ("message_type", `String "stopped"); ("payload", `Assoc []); ] in Alcotest.(check string) - "wrong version rejected" "unsupported strategy protocol version: 1" + "wrong version rejected" + "unsupported strategy protocol version: unsupported" (T.Strategy_protocol.response_of_yojson ~expected_sequence:3L wrong_version |> diagnostic_message); let unknown_field = `Assoc [ - ("strategy_protocol_version", `String "7"); + ("strategy_protocol_version", `String "unsupported"); ("strategy_sequence", `String "3"); ("message_type", `String "stopped"); ("payload", `Assoc []); @@ -581,8 +551,6 @@ let tests = 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 event_message_contains_complete_context; Alcotest.test_case "nonpositive equity omits weights" `Quick diff --git a/test/test_support.ml b/test/test_support.ml index 93402b4..21aaaef 100644 --- a/test/test_support.ml +++ b/test/test_support.ml @@ -54,6 +54,10 @@ let test_account ?(base_currency = "USD") ?initial_cash () = in T.Account.create ~base_currency ~initial_cash |> ok +let initial_portfolio ?(base_currency = "USD") ?cash () = + let cash = Option.value cash ~default:[ (base_currency, money "10000") ] in + T.Initial_portfolio.cash_only ~base_currency ~cash |> ok + let account_cash ?(currency = "USD") account = T.Account.cash account currency |> Option.get @@ -61,8 +65,11 @@ let account_value ?(instruments = [ instrument () ]) ?(fx_rates = [ ("USD", price "1") ]) account ~marks = T.Account.value account ~instruments ~marks ~fx_rates |> ok -let market_slice ?bars ?fx_rates ?(corporate_actions = []) ?start_at ?end_at - ?available_at ?received_at sequence = +let market_slice ?bars ?fx_rates ?(corporate_actions = []) + ?(borrow_observations = []) ?(cash_rate_observations = []) + ?(settlement_failures = []) ?(lifecycle_events = []) ?(market_events = []) + ?(order_book_events = []) ?start_at ?end_at ?available_at ?received_at + sequence = let day = day sequence in let start_at = Option.value start_at @@ -83,20 +90,25 @@ let market_slice ?bars ?fx_rates ?(corporate_actions = []) ?start_at ?end_at let bars = Option.value bars ~default:[ bar sequence ] in let fx_rates = Option.value fx_rates ~default:[ fx_mark () ] in T.Market_slice.create ~slice_sequence:sequence ~start_at ~end_at ~available_at - ~received_at ~bars ~fx_rates ~corporate_actions + ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations ~settlement_failures ~lifecycle_events + ~market_events ~order_book_events |> ok let request ?(instrument = instrument_id "test-equity") ?(side = T.Order.Buy) ?(quantity_value = "10") ?(kind = T.Order.Market) ?(origin = T.Order.Direct) - () = + ?time_in_force () = + let time_in_force = + Option.value time_in_force ~default:(T.Order.default_time_in_force kind) + in T.Order.request ~instrument_id:instrument ~side - ~quantity:(quantity quantity_value) ~kind ~origin + ~quantity:(quantity quantity_value) ~kind ~time_in_force ~origin |> ok -let request_v8 ?(instrument = instrument_id "test-equity") ?(side = T.Order.Buy) - ?(quantity_value = "10") ?(kind = T.Order.Market) +let current_request ?(instrument = instrument_id "test-equity") + ?(side = T.Order.Buy) ?(quantity_value = "10") ?(kind = T.Order.Market) ?(time_in_force = T.Order.Gtc) ?(origin = T.Order.Direct) () = - T.Order.request_v8 ~instrument_id:instrument ~side + T.Order.request ~instrument_id:instrument ~side ~quantity:(quantity quantity_value) ~kind ~time_in_force ~origin |> ok @@ -122,48 +134,94 @@ let fill ?(id = "fill-1") ?(price_value = "100") ?(quantity_value = "1") T.Fill.create ~id:(fill_id id) ~order_id:order.T.Order.id ~instrument_id:order.request.instrument_id ~side:order.request.side ~quote_currency:"USD" ~quantity:(quantity quantity_value) - ~price:(price price_value) ~fee:(money fee_value) ~executed_at - ~slice_sequence + ~price:(price price_value) ~fee:(money fee_value) ~fee_components:[] + ~executed_at ~slice_sequence |> ok -let execution ?(participation_bps = 10_000) ?(fixed_fee = "0") ?(fee_bps = 0) () - = - T.Execution.create ~participation_bps ~fixed_fee:(money fixed_fee) ~fee_bps +let zero_fee_schedule instrument = + let component = + T.Fee_schedule.create_component ~name:"zero" + ~currency:instrument.T.Instrument.quote_currency + ~basis:(T.Fee_schedule.Fixed T.Scalar.Money.zero) + ~rounding:T.Fee_schedule.Up ~applicability:T.Fee_schedule.Any + |> ok + in + T.Fee_schedule.create + ~schedule_id:(T.Id.Instrument.to_string instrument.id ^ "-fees-v1") + ~instrument_id:instrument.id ~settlement_currency:instrument.quote_currency + ~minimum:None ~maximum:None ~components:[ component ] |> ok +let execution ?(participation_bps = 10_000) ?fee_schedules + ?(instruments = [ instrument () ]) () = + let fee_schedules = + Option.value fee_schedules ~default:(List.map zero_fee_schedule instruments) + in + T.Execution.create ~participation_bps ~fee_schedules |> ok + let risk ?(base_currency = "USD") ?(instruments = [ instrument () ]) ?(max_order = "1000") ?(max_long = "1000") ?(max_short = "1000") ?(max_gross = "1000000000") ?(max_leverage = "2") - ?(initial_margin_bps = 5000) ?(maintenance_margin_bps = 2500) - ?(short_borrow_bps = 100) () = - T.Risk.create ~base_currency ~instruments - ~max_order_quantity:(quantity max_order) - ~max_long_position:(quantity max_long) - ~max_short_position:(quantity max_short) + ?(initial_margin_bps = 5000) ?(maintenance_margin_bps = 2500) () = + let instrument_policies = + List.map + (fun instrument -> + T.Risk.create_instrument_policy ~instrument + ~max_order_quantity:(quantity max_order) + ~max_long_position:(quantity max_long) + ~max_short_position:(quantity max_short) ~max_notional_exposure:None + ~initial_margin_bps ~maintenance_margin_bps ~shorting_allowed:true + |> ok) + instruments + in + T.Risk.create ~base_currency ~instruments ~instrument_policies ~groups:[] ~max_gross_exposure:(money max_gross) ~max_leverage:(T.Scalar.Ratio.of_decimal_string max_leverage |> ok) - ~initial_margin_bps ~maintenance_margin_bps ~short_borrow_bps |> ok -let engine_config ?(contract_version = T.Contract.version) ?(risk = risk ()) - ?execution_model ?(execution = execution ()) ?(max_internal_events = 1000) - () = - let execution_model = - Option.value execution_model - ~default:(T.Execution_model.find "completed_bar_v1" |> ok) +let financing_policy () = + T.Financing.policy ~day_count:T.Financing.Actual_365 + ~compounding:T.Financing.Simple ~borrow_missing_data:T.Financing.Zero + ~cash_missing_data:T.Financing.Zero ~locate_policy:T.Financing.Clip_fill + ~recall_policy:T.Financing.Reject_new_shorts + +let settlement_policy ?(instruments = [ instrument () ]) () = + let dates month count = + List.init count (fun index -> + Printf.sprintf "2026-%02d-%02d" month (index + 1)) in - T.Engine.config ~contract_version ~risk ~execution_model ~execution - ~max_internal_events + let calendar = + T.Settlement.calendar ~calendar_id:"test-settlement" ~version:"1" + ~business_dates:(dates 1 31 @ dates 2 28 @ dates 3 31) + |> ok + in + let rules = + List.map + (fun instrument -> + T.Settlement.rule ~instrument_id:instrument.T.Instrument.id + ~calendar_id:"test-settlement" ~lag_business_days:1 + |> ok) + instruments + in + T.Settlement.policy ~cash_buying_power:T.Settlement.Total_cash + ~position_availability:T.Settlement.Total_positions ~calendars:[ calendar ] + ~rules |> ok -let engine_config_v8 ?(risk = risk ()) ?(venue_calendars = []) ?execution_model - ?(execution = execution ()) ?(max_internal_events = 1000) () = +let engine_config ?(contract_version = T.Contract.version) ?(risk = risk ()) + ?(venue_calendars = []) ?execution_model ?(execution = execution ()) + ?(financing = financing_policy ()) ?settlement ?(max_internal_events = 1000) + () = let execution_model = Option.value execution_model ~default:(T.Execution_model.find "completed_bar_v1" |> ok) in - T.Engine.config_v8 ~contract_version:"8" ~risk ~venue_calendars - ~execution_model ~execution ~max_internal_events + let settlement = + Option.value settlement + ~default:(settlement_policy ~instruments:(T.Risk.instruments risk) ()) + in + T.Engine.config ~contract_version ~risk ~venue_calendars ~execution_model + ~execution ~financing ~settlement ~max_internal_events |> ok let risk_check risk ~account ~oms request = diff --git a/test/test_venue_calendar.ml b/test/test_venue_calendar.ml index 2b8b03b..8d028a6 100644 --- a/test/test_venue_calendar.ml +++ b/test/test_venue_calendar.ml @@ -95,7 +95,7 @@ let ambiguous_phase_policies_are_rejected () = let scenario_contract_requires_calendar_coverage () = let document = - Yojson.Safe.from_file "../contracts/v5/fixtures/demo.scenario.json" + Yojson.Safe.from_file "../contracts/v1/fixtures/demo.scenario.json" in let scenario = T.Scenario.of_yojson document |> ok in Alcotest.(check int) @@ -144,15 +144,6 @@ let scenario_contract_requires_calendar_coverage () = Alcotest.(check (option string)) "coverage path" (Some "$.venue_calendars") uncovered.context.json_path -let v4_remains_a_calendar_free_compatibility_contract () = - let scenario = - T.Scenario.read_file "../contracts/v4/fixtures/demo.scenario.json" |> ok - in - Alcotest.(check string) "v4 retained" "4" scenario.contract_version; - Alcotest.(check int) - "no inferred calendars" 0 - (List.length scenario.venue_calendars) - let tests = [ Alcotest.test_case "explicit policies and missing dates" `Quick @@ -161,6 +152,4 @@ let tests = ambiguous_phase_policies_are_rejected; Alcotest.test_case "scenario calendar coverage" `Quick scenario_contract_requires_calendar_coverage; - Alcotest.test_case "v4 compatibility does not infer calendars" `Quick - v4_remains_a_calendar_free_compatibility_contract; ] diff --git a/test/validate_contract_conformance.py b/test/validate_contract_conformance.py index 3aa3047..f2d1bf7 100644 --- a/test/validate_contract_conformance.py +++ b/test/validate_contract_conformance.py @@ -4,7 +4,6 @@ from __future__ import annotations import copy -import hashlib import json from pathlib import Path from typing import Any @@ -394,54 +393,11 @@ def verify_top_level_branches( ) -def verify_frozen_integrity() -> None: - ledger_path = CONFORMANCE / "frozen.sha256" - expected: dict[str, str] = {} - for line_number, line in enumerate( - ledger_path.read_text(encoding="utf-8").splitlines(), start=1 - ): - try: - digest, relative = line.split(" ", maxsplit=1) - except ValueError as error: - raise AssertionError( - f"{ledger_path.relative_to(ROOT)}:{line_number} is malformed" - ) from error - if relative in expected: - raise AssertionError(f"duplicate frozen artifact {relative}") - expected[relative] = digest - frozen_roots = [ - CONTRACTS / "v1", - CONTRACTS / "v2", - CONTRACTS / "strategy" / "v1", - CONTRACTS / "strategy" / "v2", - ] - discovered = { - str(path.relative_to(ROOT)) - for root in frozen_roots - for path in root.rglob("*") - if path.is_file() - and (path.name.endswith(".schema.json") or "fixtures" in path.parts) - } - if set(expected) != discovered: - raise AssertionError( - "frozen integrity ledger differs from archived artifacts: " - f"ledger={sorted(expected)} archived={sorted(discovered)}" - ) - for relative, digest in expected.items(): - actual = hashlib.sha256((ROOT / relative).read_bytes()).hexdigest() - if actual != digest: - raise AssertionError( - f"frozen artifact changed: {relative}; " - "update frozen.sha256 only for an intentional contract revision" - ) - - def main() -> None: schemas, registry = schema_registry() artifacts, _ = verify_manifest(schemas, registry) accepted_cases = verify_cases(artifacts, schemas, registry) verify_top_level_branches(artifacts, schemas, registry, accepted_cases) - verify_frozen_integrity() if __name__ == "__main__": diff --git a/test/validate_schemas.py b/test/validate_schemas.py index 827d76e..34e115a 100644 --- a/test/validate_schemas.py +++ b/test/validate_schemas.py @@ -78,7 +78,6 @@ def main() -> None: ) scenario = load(scenario_path) - contract_version = scenario["contract_version"] unsupported_version = "unsupported" scenario_validator.validate(scenario) stream_records = [ @@ -116,31 +115,25 @@ def main() -> None: unsupported_execution_model = copy.deepcopy(scenario) unsupported_execution_model["execution"]["model"] = "future_model" expect_invalid(scenario_validator, unsupported_execution_model) - if contract_version in {"5", "6"}: - missing_configuration_version = copy.deepcopy(scenario) - del missing_configuration_version["execution"]["configuration"][ - "version" - ] - expect_invalid(scenario_validator, missing_configuration_version) - unsupported_configuration_version = copy.deepcopy(scenario) - unsupported_configuration_version["execution"]["configuration"][ - "version" - ] = "2" - expect_invalid(scenario_validator, unsupported_configuration_version) - unknown_configuration_field = copy.deepcopy(scenario) - unknown_configuration_field["execution"]["configuration"]["future"] = True - expect_invalid(scenario_validator, unknown_configuration_field) - if contract_version in {"3", "4", "5", "6"}: - excessive_feedback_cap = copy.deepcopy(scenario) - excessive_feedback_cap["max_internal_events"] = 100001 - expect_invalid(scenario_validator, excessive_feedback_cap) - excessive_catalog = copy.deepcopy(scenario) - excessive_catalog["instruments"] = [scenario["instruments"][0]] * 4097 - expect_invalid(scenario_validator, excessive_catalog) - excessive_intents = copy.deepcopy(scenario) - intent = scenario["schedule"][0]["intents"][0] - excessive_intents["schedule"][0]["intents"] = [intent] * 4097 - expect_invalid(scenario_validator, excessive_intents) + missing_configuration_version = copy.deepcopy(scenario) + del missing_configuration_version["execution"]["configuration"]["version"] + expect_invalid(scenario_validator, missing_configuration_version) + unsupported_configuration_version = copy.deepcopy(scenario) + unsupported_configuration_version["execution"]["configuration"]["version"] = "2" + expect_invalid(scenario_validator, unsupported_configuration_version) + unknown_configuration_field = copy.deepcopy(scenario) + unknown_configuration_field["execution"]["configuration"]["future"] = True + expect_invalid(scenario_validator, unknown_configuration_field) + excessive_feedback_cap = copy.deepcopy(scenario) + excessive_feedback_cap["max_internal_events"] = 100001 + expect_invalid(scenario_validator, excessive_feedback_cap) + excessive_catalog = copy.deepcopy(scenario) + excessive_catalog["instruments"] = [scenario["instruments"][0]] * 4097 + expect_invalid(scenario_validator, excessive_catalog) + excessive_intents = copy.deepcopy(scenario) + intent = scenario["schedule"][0]["intents"][0] + excessive_intents["schedule"][0]["intents"] = [intent] * 4097 + expect_invalid(scenario_validator, excessive_intents) unversioned_stream_record = copy.deepcopy(stream_records[0]) del unversioned_stream_record["contract_version"] expect_invalid(stream_validator, unversioned_stream_record) @@ -150,23 +143,17 @@ def main() -> None: malformed_stream_slice = copy.deepcopy(stream_records[1]) malformed_stream_slice["payload"]["market_slice"]["unexpected"] = True expect_invalid(stream_validator, malformed_stream_slice) - if contract_version in {"3", "4", "5", "6"}: - excessive_stream_catalog = copy.deepcopy(stream_records[0]) - excessive_stream_catalog["payload"]["instruments"] = ( - [stream_records[0]["payload"]["instruments"][0]] * 4097 - ) - expect_invalid(stream_validator, excessive_stream_catalog) - excessive_stream_intents = copy.deepcopy(stream_records[1]) - intent = scenario["schedule"][0]["intents"][0] - excessive_stream_intents["payload"]["intents"] = [intent] * 4097 - expect_invalid(stream_validator, excessive_stream_intents) + excessive_stream_catalog = copy.deepcopy(stream_records[0]) + excessive_stream_catalog["payload"]["instruments"] = ( + [stream_records[0]["payload"]["instruments"][0]] * 4097 + ) + expect_invalid(stream_validator, excessive_stream_catalog) + excessive_stream_intents = copy.deepcopy(stream_records[1]) + intent = scenario["schedule"][0]["intents"][0] + excessive_stream_intents["payload"]["intents"] = [intent] * 4097 + expect_invalid(stream_validator, excessive_stream_intents) noncanonical = copy.deepcopy(scenario) - if contract_version in {"3", "4"}: - noncanonical["initial_cash"][0]["amount"] = "10000.0" - elif contract_version == "6": - noncanonical["initial_portfolio"]["cash"][0]["amount"] = "10000.0" - else: - noncanonical["initial_cash"] = "10000.0" + noncanonical["initial_portfolio"]["cash"][0]["amount"] = "10000.0" expect_invalid(scenario_validator, noncanonical) first_journal_record = json.loads( journal_path.read_text(encoding="utf-8").splitlines()[0] @@ -187,31 +174,30 @@ def main() -> None: json.loads(line) for line in journal_path.read_text(encoding="utf-8").splitlines() ] - if contract_version == "4": - fill_clipped = copy.deepcopy(first_journal_record) - fill_clipped["event_type"] = "fill_clipped" - fill_clipped["payload"] = { - "reason": { - "version": "1", - "policy": "max_leverage", - "threshold": {"unit": "ratio", "value": "2"}, - }, - "order_id": "fixture-order", - "instrument_id": "fixture-instrument", - "proposed_quantity": "10", - "permitted_quantity": "5", - "price": "100", - } - journal_validator.validate(fill_clipped) - mismatched_threshold = copy.deepcopy(fill_clipped) - mismatched_threshold["payload"]["reason"]["threshold"] = { - "unit": "money", - "value": "2", - } - expect_invalid(journal_validator, mismatched_threshold) - unknown_policy = copy.deepcopy(fill_clipped) - unknown_policy["payload"]["reason"]["policy"] = "future_policy" - expect_invalid(journal_validator, unknown_policy) + fill_clipped = copy.deepcopy(first_journal_record) + fill_clipped["event_type"] = "fill_clipped" + fill_clipped["payload"] = { + "reason": { + "version": "1", + "policy": "max_leverage", + "threshold": {"unit": "ratio", "value": "2"}, + }, + "order_id": "fixture-order", + "instrument_id": "fixture-instrument", + "proposed_quantity": "10", + "permitted_quantity": "5", + "price": "100", + } + journal_validator.validate(fill_clipped) + mismatched_threshold = copy.deepcopy(fill_clipped) + mismatched_threshold["payload"]["reason"]["threshold"] = { + "unit": "money", + "value": "2", + } + expect_invalid(journal_validator, mismatched_threshold) + unknown_policy = copy.deepcopy(fill_clipped) + unknown_policy["payload"]["reason"]["policy"] = "future_policy" + expect_invalid(journal_validator, unknown_policy) order_record = next( record for record in journal_records if record["event_type"] == "order_accepted" ) diff --git a/test/validate_strategy_schema.py b/test/validate_strategy_schema.py index f419b22..2df99de 100644 --- a/test/validate_strategy_schema.py +++ b/test/validate_strategy_schema.py @@ -93,7 +93,7 @@ def main() -> None: extra_field["unexpected"] = True expect_invalid(message_validator, extra_field) unsupported_version = copy.deepcopy(records[1]["message"]) - unsupported_version["strategy_protocol_version"] = "1" + unsupported_version["strategy_protocol_version"] = "2" expect_invalid(message_validator, unsupported_version) malformed_sequence = copy.deepcopy(records[1]["message"]) malformed_sequence["strategy_sequence"] = "01" From a6b50d1c8d95731b12d0bad8b7df886f6f03b367 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Wed, 26 Aug 2026 14:14:56 -0400 Subject: [PATCH 54/57] ci: pin Persistra v1 compatibility --- .github/workflows/ci.yml | 2 +- test/test_repository_metadata.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 787774c..5dea87b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,7 +108,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 env: - PERSISTRA_COMPAT_REVISION: ade8c05e435c56d8df8eba88fed1284652fd731b + PERSISTRA_COMPAT_REVISION: 6874e175098519eef9904edf0bb9f27b244982e4 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/test/test_repository_metadata.py b/test/test_repository_metadata.py index c9b2b49..7509720 100644 --- a/test/test_repository_metadata.py +++ b/test/test_repository_metadata.py @@ -119,7 +119,7 @@ def test_pull_request_and_support_templates_preserve_required_sections(self) -> def test_compatibility_gate_is_pinned_and_canary_is_optional(self) -> None: workflow = (GITHUB / "workflows/ci.yml").read_text(encoding="utf-8") - revision = "ade8c05e435c56d8df8eba88fed1284652fd731b" + revision = "6874e175098519eef9904edf0bb9f27b244982e4" self.assertIn(f"PERSISTRA_COMPAT_REVISION: {revision}", workflow) self.assertIn("ref: ${{ env.PERSISTRA_COMPAT_REVISION }}", workflow) From e5c3d0307bbc92c8338f0f0dea8a300ee9e2bf38 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Wed, 26 Aug 2026 14:22:32 -0400 Subject: [PATCH 55/57] test: cover current instrument validation --- test/test_domain.ml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/test_domain.ml b/test/test_domain.ml index 34e159d..5597946 100644 --- a/test/test_domain.ml +++ b/test/test_domain.ml @@ -12,6 +12,29 @@ let identifier_validation () = "round trip" "order-1" (T.Id.Order.of_string_exn "order-1" |> T.Id.Order.to_string) +let instrument_validation () = + let create ?(symbol = "TEST") ?(quote_currency = "USD") + ?(lot_size = quantity "1") () = + T.Instrument.create + ~id:(instrument_id "test-equity") + ~symbol ~quote_currency ~tick_size:(price "0.01") ~lot_size + in + Alcotest.(check bool) + "empty symbol rejected" true + (Result.is_error (create ~symbol:"" ())); + Alcotest.(check bool) + "symbol whitespace rejected" true + (Result.is_error (create ~symbol:"BAD SYMBOL" ())); + Alcotest.(check bool) + "quote currency whitespace rejected" true + (Result.is_error (create ~quote_currency:" BAD" ())); + Alcotest.(check bool) + "zero lot size rejected" true + (Result.is_error (create ~lot_size:T.Scalar.Quantity.zero ())); + Alcotest.(check string) + "rendered instrument" "TEST (test-equity)" + (Format.asprintf "%a" T.Instrument.pp (create () |> ok)) + let scalar_decimal_round_trip () = let values = [ "0"; "1"; "1.25"; "-0.5"; "999999.000001" ] in List.iter @@ -541,6 +564,7 @@ let typed_metric_validation () = let tests = [ Alcotest.test_case "identifier validation" `Quick identifier_validation; + Alcotest.test_case "instrument validation" `Quick instrument_validation; Alcotest.test_case "fixed-point decimal round trip" `Quick scalar_decimal_round_trip; Alcotest.test_case "checked overflow" `Quick scalar_overflow_is_rejected; From 62d4f5413414e7ca6ac31b0fac6431b375f0f1c2 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Wed, 26 Aug 2026 14:45:06 -0400 Subject: [PATCH 56/57] ci: pin merged Persistra compatibility --- .github/workflows/ci.yml | 2 +- test/test_repository_metadata.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dea87b..97e275a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,7 +108,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 env: - PERSISTRA_COMPAT_REVISION: 6874e175098519eef9904edf0bb9f27b244982e4 + PERSISTRA_COMPAT_REVISION: 2f8beeaab87f4c456f7040f327314afe67306a54 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/test/test_repository_metadata.py b/test/test_repository_metadata.py index 7509720..48cc887 100644 --- a/test/test_repository_metadata.py +++ b/test/test_repository_metadata.py @@ -119,7 +119,7 @@ def test_pull_request_and_support_templates_preserve_required_sections(self) -> def test_compatibility_gate_is_pinned_and_canary_is_optional(self) -> None: workflow = (GITHUB / "workflows/ci.yml").read_text(encoding="utf-8") - revision = "6874e175098519eef9904edf0bb9f27b244982e4" + revision = "2f8beeaab87f4c456f7040f327314afe67306a54" self.assertIn(f"PERSISTRA_COMPAT_REVISION: {revision}", workflow) self.assertIn("ref: ${{ env.PERSISTRA_COMPAT_REVISION }}", workflow) From eebcfe7bc58bccaa9d07af0f89e2abd682c5af68 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Wed, 26 Aug 2026 15:14:15 -0400 Subject: [PATCH 57/57] chore: prepare v1.1.0 release --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- contracts/strategy/v1/fixtures/external.strategy.jsonl | 2 +- dune-project | 2 +- lib/contract.ml | 2 +- test/cli.t | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index d32f298..3351944 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -27,7 +27,7 @@ body: id: version attributes: label: Engine revision or version - placeholder: v1.0.0 or a full commit SHA + placeholder: v1.1.0 or a full commit SHA validations: required: true - type: input diff --git a/contracts/strategy/v1/fixtures/external.strategy.jsonl b/contracts/strategy/v1/fixtures/external.strategy.jsonl index a74e5d5..36693da 100644 --- a/contracts/strategy/v1/fixtures/external.strategy.jsonl +++ b/contracts/strategy/v1/fixtures/external.strategy.jsonl @@ -1,4 +1,4 @@ -{"strategy_protocol_version":"1","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"1","scenario_sha256":"7c1991b8f4662c51faf8b3436999fdce8f0295ee6c0ef5d20a0658c960d04c4d","run_id":"external-demo","base_currency":"USD","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","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":"1","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":"1","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.1.0","scenario_contract_version":"1","scenario_sha256":"7c1991b8f4662c51faf8b3436999fdce8f0295ee6c0ef5d20a0658c960d04c4d","run_id":"external-demo","base_currency":"USD","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","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":"1","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":"1","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"1","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} {"strategy_protocol_version":"1","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"1","strategy_sequence":"2","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} {"strategy_protocol_version":"1","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"1","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":{"type":"numeric","value":"2"},"unit":"score","dimensions":{"source":"fixture"},"aggregation":"last"}]}}} diff --git a/dune-project b/dune-project index de35c8d..eb0b69d 100644 --- a/dune-project +++ b/dune-project @@ -1,7 +1,7 @@ (lang dune 3.24) (name trading_engine) -(version 1.0.0) +(version 1.1.0) (generate_opam_files false) (implicit_transitive_deps false) diff --git a/lib/contract.ml b/lib/contract.ml index 06d6f5f..202d0bb 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -2,7 +2,7 @@ let version = "1" let supported_versions = [ version ] let is_supported version = List.mem version supported_versions let strategy_protocol_version = "1" -let engine_version = "1.0.0" +let engine_version = "1.1.0" let strings values = `List (List.map (fun value -> `String value) values) let capabilities_to_yojson () = diff --git a/test/cli.t b/test/cli.t index 87cbaf6..33fada3 100644 --- a/test/cli.t +++ b/test/cli.t @@ -1,8 +1,8 @@ $ ../bin/main.exe --version - 1.0.0 + 1.1.0 $ ../bin/main.exe --capabilities - {"engine_version":"1.0.0","scenario_contract_versions":["1"],"journal_contract_versions":["1"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1","order_book_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"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":["1"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"order_book_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"required_fields":["version","participation_bps","fee_schedules","max_depth_levels"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","max_depth_levels"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["slice_open_level_two_snapshot","contiguous_absolute_level_updates","aggressor_classified_depth_consuming_trades","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"max_depth_levels":{"minimum":1,"maximum":1024}}}],"strategy_protocol_versions":["1"],"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.1.0","scenario_contract_versions":["1"],"journal_contract_versions":["1"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1","order_book_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"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":["1"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"order_book_v1","configuration_versions":["1"],"scenario_contract_versions":["1"],"required_fields":["version","participation_bps","fee_schedules","max_depth_levels"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","max_depth_levels"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["slice_open_level_two_snapshot","contiguous_absolute_level_updates","aggressor_classified_depth_consuming_trades","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"max_depth_levels":{"minimum":1,"maximum":1024}}}],"strategy_protocol_versions":["1"],"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/v1/fixtures/demo.scenario.json valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=e2227af76072fab8151c3e1bd86f401293d16a736e32040efdaf8761cd397574