From 20b6236eb4649218e8189a1f7fa93e421bbb500c Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 12 Sep 2026 22:09:30 +0800 Subject: [PATCH 1/5] Add native resting orders and causal execution relationships --- docs/pages/abi-stability.md | 34 +- docs/pages/native-engine.md | 31 +- .../pineforge/compat/pine/exit_activation.hpp | 2 +- .../pineforge/compat/pine/exit_lifecycle.hpp | 2 +- .../compat/pine/frozen_market_instruction.hpp | 2 +- include/pineforge/compat/pine/order_birth.hpp | 2 +- .../pineforge/compat/pine/order_priority.hpp | 2 +- .../compat/pine/reservation_expansion.hpp | 2 +- include/pineforge/engine.hpp | 8 +- include/pineforge/execution_consumer.hpp | 4 +- include/pineforge/market_driver.hpp | 2 +- include/pineforge/native_host.hpp | 133 +- include/pineforge/native_order.hpp | 881 +++++++- include/pineforge/native_order_identity.hpp | 58 + include/pineforge/native_run_spec.hpp | 2 +- scripts/check_aggregate_cpp_versions.py | 18 +- scripts/check_broker_state_hash_coverage.py | 16 +- scripts/check_native_cpp_abi.py | 93 +- scripts/check_native_cpp_versions.py | 57 +- scripts/check_script_cpp_abi.py | 37 +- scripts/test_aggregate_cpp_versions.py | 14 +- scripts/test_broker_state_hash_coverage.py | 26 +- scripts/test_native_cpp_versions.py | 50 +- src/engine_consumer.cpp | 4 +- src/engine_state_hash.cpp | 2 +- src/engine_stream.cpp | 2 +- src/native_execution_consumer.cpp | 1545 +++++++++++-- src/native_execution_consumer.hpp | 63 +- src/native_matching.hpp | 114 + src/native_order.cpp | 1966 +++++++++++++++-- tests/CMakeLists.txt | 3 + tests/fixtures/native_cpp_abi/README.md | 2 + .../native_cpp_abi/host-e7d023d/README.md | 9 + .../host-e7d023d/headers.json.gz | Bin 0 -> 172613 bytes .../native_cpp_abi/host-e7d023d/manifest.json | 181 ++ .../native_cpp_abi/order-e7d023d/README.md | 7 + .../order-e7d023d/headers.json.gz | Bin 0 -> 11950 bytes .../order-e7d023d/manifest.json | 36 + .../fixtures/script_cpp_abi/basev12/README.md | 8 + .../script_cpp_abi/basev12/headers.json.gz | Bin 0 -> 160017 bytes .../script_cpp_abi/basev12/manifest.json | 155 ++ .../test_native_calendar_driver_contract.cpp | 69 +- tests/test_native_host_repairs.cpp | 24 +- .../test_native_market_vertical_contract.cpp | 52 +- tests/test_native_order_core.cpp | 258 +-- tests/test_native_order_resting_core.cpp | 707 ++++++ tests/test_native_order_terminal.cpp | 122 +- tests/test_native_resting_contract.cpp | 281 +++ 48 files changed, 6272 insertions(+), 814 deletions(-) create mode 100644 include/pineforge/native_order_identity.hpp create mode 100644 src/native_matching.hpp create mode 100644 tests/fixtures/native_cpp_abi/host-e7d023d/README.md create mode 100644 tests/fixtures/native_cpp_abi/host-e7d023d/headers.json.gz create mode 100644 tests/fixtures/native_cpp_abi/host-e7d023d/manifest.json create mode 100644 tests/fixtures/native_cpp_abi/order-e7d023d/README.md create mode 100644 tests/fixtures/native_cpp_abi/order-e7d023d/headers.json.gz create mode 100644 tests/fixtures/native_cpp_abi/order-e7d023d/manifest.json create mode 100644 tests/fixtures/script_cpp_abi/basev12/README.md create mode 100644 tests/fixtures/script_cpp_abi/basev12/headers.json.gz create mode 100644 tests/fixtures/script_cpp_abi/basev12/manifest.json create mode 100644 tests/test_native_order_resting_core.cpp create mode 100644 tests/test_native_resting_contract.cpp diff --git a/docs/pages/abi-stability.md b/docs/pages/abi-stability.md index fb78f938..c1ef6eec 100644 --- a/docs/pages/abi-stability.md +++ b/docs/pages/abi-stability.md @@ -112,14 +112,20 @@ notice: - The shape of internal log lines (use them for humans, not parsers). Rebuild generated and native C++ objects against matching engine headers and -runtime. Deriving placement and opposite-predecessor facts from original -admission evidence changes the native layout and advances `PendingOrder` and -`BacktestEngine` to `engine_script_run_v12`. Exact pre-change fd4c686/v10 -headers are authenticated before native, generated-style and standalone -PendingOrder pairing checks. Current/old matching links must succeed and stale -pairings must fail for the expected qualified symbols. Earlier v2-v9 controls -remain; every translation unit must compile before any mismatch is accepted. -No pairing executable runs. +runtime. Resting native request values and expanded host event snapshots +advance `PendingOrder`, `BacktestEngine`, `NativeStrategyHost`, and the private +native consumer to `engine_script_run_v13`. Exact pre-change e7d023d/v12 +headers (and the e7d023d `native_order_v1` closure) are authenticated before +native, generated-style, standalone PendingOrder, and return-only +`native_events()` pairing checks. A v12 host that only calls `native_events()` +cannot silently decode a later `CommandEvent` layout: the method lives on the +v13 class even if `sizeof(NativeStrategyHost)` is unchanged. Allocation-free +`NativeFailureContext` (`NativeInRunCause` / `NativeInRunRecipient` / +`NativeInRunCursor`) is part of that same epoch13 host record; it does not +open a C ABI or new engine epoch. Current/old +matching links must succeed and stale pairings must fail for the expected +qualified symbols. Earlier v2–v10 and v12 controls remain; every translation +unit must compile before any mismatch is accepted. No pairing executable runs. New standalone lifecycle values and `Lifecycle` own the inline namespace `pineforge::exit_legs::lifecycle_v1`; new admission values, `Draft`, `Journal` @@ -159,13 +165,17 @@ its creating strategy module. A fully self-contained old module can still use its own matching runtime; this check does not turn it into a v11 module. The integrated representation advances the broker fingerprint domain to -`pineforge-broker-state/v11` and stream fingerprint version to 11. The original -admission observation and prior-book direction are hashed as canonical facts; -the three derived placement views add no redundant folds. Lifecycle definitions, generations, obligations and replay +`pineforge-broker-state/v13` and stream fingerprint version to 13. Native +consumer identity is `native-consumer/v4`; driver v3 shapes, `close_scope_v1`, +and `native_run_spec_v1` stay. Stable `RunIdentity` / `RequestHandle` / `Birth` +remain `native_order_v1`; new request, core, and event values own +`native_order_v2`. The original admission observation and prior-book direction +are hashed as canonical facts; the three derived placement views add no +redundant folds. Lifecycle definitions, generations, obligations and replay receipts, plus causal journal state remain represented. Existing reservation, Pine instruction, activation, quantity, predecessor and birth facts remain represented. The Pine component schema remains 1; it is -independent of the aggregate fingerprint version. Prior v2-v10 fingerprints are +independent of the aggregate fingerprint version. Prior v2–v12 fingerprints are not comparable. Fingerprints are replay checks, not serialized checkpoints or complete hashes of private strategy state. The native runner already binds its strategy-library SHA; its ledger format and Python provenance fingerprints diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 8993a21f..0687f0c6 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -2,13 +2,13 @@ @tableofcontents -Hand-written C++ strategies can run a **standalone native market-order** path: -one `NativeRunSpec`, one working request roster, one physical lot book, and +Hand-written C++ strategies can run a **standalone native** path: one +`NativeRunSpec`, one working request roster, one physical lot book, and close-only callbacks. Pine `strategy.*` commands, cap/priority adapters, default source sizing, and complete Pine policy extraction are **not** this surface. -Codegen and source adapters select those policies separately. Resting -limit/stop/bracket relationships are a later roadmap; do not infer them from -this slice. +Codegen and source adapters select those policies separately. Resting requests +use the general host commands; request-value members live in +`` and are not restated here. Subclass `pineforge::NativeStrategyHost`. Configure with `configure_native`, then `run` or `stream_*`. Submit from native begin/bar callbacks, or between @@ -66,8 +66,13 @@ host high-water. events, ordinals, floor, or identity at the handoff. `last_error()` is presentation text. `native_state().failure` is the durable -record (`code`, `operation`, optional `ordinal`, `discriminator`). See -`NativeFailureCode` / `NativeFailureOperation` in `native_host.hpp`. +record (`code`, `operation`, optional `ordinal`, `discriminator`, and +allocation-free `context`). Cause/recipient/cursor facts use +`NativeInRunCause` / `NativeInRunRecipient` / `NativeInRunCursor` selected by +`NativeFailureContextKind`; identifiers belong to the failed spec's +`RunIdentity` (`native_failure_context_in_run`, `native_failed_run_identity`). +A foreign run is dropped, not relabeled. Failure copy/move does not allocate. +See `NativeFailureCode` / `NativeFailureOperation` in `native_host.hpp`. ## NativeRunSpec @@ -125,17 +130,23 @@ not native spec fields. From a native callback in `Batch` / `Warmup` / `Realtime`: ```cpp +submit(request); +replace(handle, request); +cancel(handle); submit_market(request); replace_market(handle, request); -cancel(handle); ``` +`submit` / `replace` are the complete host commands. `submit_market` / +`replace_market` keep the existing market-only call sites and must reject +nondefault trigger, capacity, owner, or group extras rather than drop them. Serialized external C++ calls may command only **between realtime inputs**, never reentrantly during input processing. There is no C request API in this slice. -`native_order::Request` is `{ Action, label, comment }`. Label/comment are -inert text. `Action` is: +`native_order::Request` values belong to `native_order_v2`; identity types stay +`native_order_v1`. Label/comment remain inert text. The market default path +still constructs from: - `order_action::Transact{signed_units}` — finite nonzero - `order_action::Reduce{units}` — finite positive diff --git a/include/pineforge/compat/pine/exit_activation.hpp b/include/pineforge/compat/pine/exit_activation.hpp index 4ce38272..a62927dd 100644 --- a/include/pineforge/compat/pine/exit_activation.hpp +++ b/include/pineforge/compat/pine/exit_activation.hpp @@ -9,7 +9,7 @@ namespace pineforge { enum class PositionSide; -inline namespace engine_script_run_v12 { struct PendingOrder; } +inline namespace engine_script_run_v13 { struct PendingOrder; } } namespace pineforge::compat::pine { diff --git a/include/pineforge/compat/pine/exit_lifecycle.hpp b/include/pineforge/compat/pine/exit_lifecycle.hpp index a49f059b..c78f64b8 100644 --- a/include/pineforge/compat/pine/exit_lifecycle.hpp +++ b/include/pineforge/compat/pine/exit_lifecycle.hpp @@ -1,6 +1,6 @@ #pragma once #include "../../exit_leg_lifecycle.hpp" -namespace pineforge { inline namespace engine_script_run_v12 { struct PendingOrder; } } +namespace pineforge { inline namespace engine_script_run_v13 { struct PendingOrder; } } namespace pineforge::compat::pine { struct ExitSuspensionContext { exit_legs::Frame cause; diff --git a/include/pineforge/compat/pine/frozen_market_instruction.hpp b/include/pineforge/compat/pine/frozen_market_instruction.hpp index 9d43f4f3..8890ad48 100644 --- a/include/pineforge/compat/pine/frozen_market_instruction.hpp +++ b/include/pineforge/compat/pine/frozen_market_instruction.hpp @@ -9,7 +9,7 @@ #include namespace pineforge { -inline namespace engine_script_run_v12 { struct PendingOrder; } +inline namespace engine_script_run_v13 { struct PendingOrder; } } namespace pineforge::compat::pine { diff --git a/include/pineforge/compat/pine/order_birth.hpp b/include/pineforge/compat/pine/order_birth.hpp index f18ff1e9..aa48c054 100644 --- a/include/pineforge/compat/pine/order_birth.hpp +++ b/include/pineforge/compat/pine/order_birth.hpp @@ -1,7 +1,7 @@ #pragma once #include "../../order_birth.hpp" -namespace pineforge { inline namespace engine_script_run_v12 { struct PendingOrder; } } +namespace pineforge { inline namespace engine_script_run_v13 { struct PendingOrder; } } namespace pineforge::compat::pine { // Historical Pine permissions remain policy, not physical birth facts. diff --git a/include/pineforge/compat/pine/order_priority.hpp b/include/pineforge/compat/pine/order_priority.hpp index 36e107e6..86135165 100644 --- a/include/pineforge/compat/pine/order_priority.hpp +++ b/include/pineforge/compat/pine/order_priority.hpp @@ -5,7 +5,7 @@ #include #include -namespace pineforge { inline namespace engine_script_run_v12 { struct PendingOrder; } } +namespace pineforge { inline namespace engine_script_run_v13 { struct PendingOrder; } } namespace pineforge::compat::pine { struct OrderPriorityContext { diff --git a/include/pineforge/compat/pine/reservation_expansion.hpp b/include/pineforge/compat/pine/reservation_expansion.hpp index 2ce5dac0..1115a659 100644 --- a/include/pineforge/compat/pine/reservation_expansion.hpp +++ b/include/pineforge/compat/pine/reservation_expansion.hpp @@ -4,7 +4,7 @@ #include namespace pineforge { enum class PositionSide; -inline namespace engine_script_run_v12 { struct PendingOrder; } +inline namespace engine_script_run_v13 { struct PendingOrder; } } namespace pineforge::compat::pine { // Called only for omitted explicit exit qty. Pine owns the population selector; diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index be3574cf..3624e74d 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -429,7 +429,7 @@ enum class ShortSeedCollisionRole : uint8_t { // PendingOrder crosses out-of-line helper boundaries independently of the // engine class, so its changed layout must carry the same internal epoch. using ExitLegLifecycle = exit_legs::Lifecycle; -inline namespace engine_script_run_v12 { +inline namespace engine_script_run_v13 { struct PendingOrder { std::string id; std::string from_entry; // for exit orders @@ -957,7 +957,7 @@ inline bool placement_has_opposite_market_predecessor( return false; } - } // inline namespace engine_script_run_v12 (PendingOrder) + } // inline namespace engine_script_run_v13 (PendingOrder) // default_qty_type constants (matches TradingView) enum class QtyType { FIXED = 0, PERCENT_OF_EQUITY = 1, CASH = 2 }; @@ -1015,7 +1015,7 @@ struct StrategyOverrides { // v6 adds explicit owner-bound exit-leg activation and Pine placement evidence. // Version the mangled class name so older headers' member offsets/vtable cannot // silently bind out-of-line members of this different object layout. -inline namespace engine_script_run_v12 { +inline namespace engine_script_run_v13 { class BacktestEngine { protected: friend class LegacyCompatibilityConsumer; @@ -5284,5 +5284,5 @@ class BacktestEngine { void trace(const std::string& name, int value) { trace(name, static_cast(value)); } }; -} // inline namespace engine_script_run_v12 +} // inline namespace engine_script_run_v13 } // namespace pineforge diff --git a/include/pineforge/execution_consumer.hpp b/include/pineforge/execution_consumer.hpp index adf6776f..19e99314 100644 --- a/include/pineforge/execution_consumer.hpp +++ b/include/pineforge/execution_consumer.hpp @@ -13,7 +13,7 @@ namespace pineforge { struct SymInfo; struct StrategyOverrides; -inline namespace engine_script_run_v12 { +inline namespace engine_script_run_v13 { class BacktestEngine; @@ -66,5 +66,5 @@ class IExecutionConsumer { std::unique_ptr make_legacy_execution_consumer(); std::unique_ptr make_native_execution_consumer(); -} // inline namespace engine_script_run_v12 +} // inline namespace engine_script_run_v13 } // namespace pineforge diff --git a/include/pineforge/market_driver.hpp b/include/pineforge/market_driver.hpp index d46beab6..3e6c68fb 100644 --- a/include/pineforge/market_driver.hpp +++ b/include/pineforge/market_driver.hpp @@ -13,7 +13,7 @@ inline namespace native_driver_v3 { // Semantic versions hashed into native continuation identity. inline constexpr const char* kNativeDriverSemanticVersion = "native-driver/v3"; -inline constexpr const char* kNativeConsumerSemanticVersion = "native-consumer/v3"; +inline constexpr const char* kNativeConsumerSemanticVersion = "native-consumer/v4"; inline constexpr const char* kNativeCalendarSemanticVersion = "native-calendar/v1"; enum class NativePriceProvenance : std::uint8_t { diff --git a/include/pineforge/native_host.hpp b/include/pineforge/native_host.hpp index 5fe94d12..58573006 100644 --- a/include/pineforge/native_host.hpp +++ b/include/pineforge/native_host.hpp @@ -14,7 +14,7 @@ #include namespace pineforge { -inline namespace engine_script_run_v12 { +inline namespace engine_script_run_v13 { enum class NativeLifecycleKind : std::uint8_t { Unconfigured = 0, @@ -63,11 +63,124 @@ enum class NativeFailureOperation : std::uint16_t { Stream = 8, }; +// In-run failure references. Identifiers belong to NativeFailed.spec->identity; +// they never carry a second RunIdentity string. Kind is a bit-union of the +// typed alternatives below (Cause=1, Recipient=2, Cursor=4). +enum class NativeFailureContextKind : std::uint8_t { + None = 0, + Cause = 1, + Recipient = 2, + CauseRecipient = 3, + Cursor = 4, + CauseCursor = 5, + RecipientCursor = 6, + CauseRecipientCursor = 7, +}; + +struct NativeInRunCause { + std::uint64_t ordinal = 0; // 0 = absent +}; + +struct NativeInRunRecipient { + std::uint64_t incarnation = 0; // 0 = absent +}; + +struct NativeInRunCursor { + NativeCoordinate point{}; + double t = 0.0; +}; + +struct NativeFailureContext { + NativeFailureContextKind kind = NativeFailureContextKind::None; + NativeInRunCause cause{}; + NativeInRunRecipient recipient{}; + NativeInRunCursor cursor{}; +}; + +constexpr std::uint8_t native_failure_context_bits(NativeFailureContextKind kind) noexcept { + return static_cast(kind); +} +constexpr bool native_failure_has_cause(NativeFailureContextKind kind) noexcept { + return (native_failure_context_bits(kind) & 1u) != 0; +} +constexpr bool native_failure_has_recipient(NativeFailureContextKind kind) noexcept { + return (native_failure_context_bits(kind) & 2u) != 0; +} +constexpr bool native_failure_has_cursor(NativeFailureContextKind kind) noexcept { + return (native_failure_context_bits(kind) & 4u) != 0; +} +constexpr bool native_failure_has_cause(const NativeFailureContext& context) noexcept { + return native_failure_has_cause(context.kind); +} +constexpr bool native_failure_has_recipient(const NativeFailureContext& context) noexcept { + return native_failure_has_recipient(context.kind); +} +constexpr bool native_failure_has_cursor(const NativeFailureContext& context) noexcept { + return native_failure_has_cursor(context.kind); +} + +constexpr NativeFailureContextKind native_failure_context_kind( + bool cause, bool recipient, bool cursor) noexcept { + return static_cast( + (cause ? 1u : 0u) | (recipient ? 2u : 0u) | (cursor ? 4u : 0u)); +} + +inline NativeFailureContext native_failure_cause(std::uint64_t ordinal) noexcept { + NativeFailureContext context; + if (ordinal == 0) return context; + context.kind = NativeFailureContextKind::Cause; + context.cause.ordinal = ordinal; + return context; +} +inline NativeFailureContext native_failure_recipient(std::uint64_t incarnation) noexcept { + NativeFailureContext context; + if (incarnation == 0) return context; + context.kind = NativeFailureContextKind::Recipient; + context.recipient.incarnation = incarnation; + return context; +} +inline NativeFailureContext native_failure_cursor(NativeCoordinate point, double t = 0.0) noexcept { + NativeFailureContext context; + context.kind = NativeFailureContextKind::Cursor; + context.cursor.point = point; + context.cursor.t = t; + return context; +} + +// Copies only ordinals/incarnation/cursor scalars. Foreign run identities are +// dropped rather than stored under the failed spec's identity. +inline NativeFailureContext native_failure_context_in_run( + const native_order::RunIdentity& run, + const native_order::EventId* cause, + const native_order::RequestHandle* recipient, + const native_order::MatchCursor* cursor) noexcept { + NativeFailureContext context; + bool has_cause = false; + bool has_recipient = false; + bool has_cursor = false; + if (cause != nullptr && cause->ordinal != 0 && cause->run == run) { + has_cause = true; + context.cause.ordinal = cause->ordinal; + } + if (recipient != nullptr && recipient->incarnation != 0 && recipient->run == run) { + has_recipient = true; + context.recipient.incarnation = recipient->incarnation; + } + if (cursor != nullptr) { + has_cursor = true; + context.cursor.point = cursor->point; + context.cursor.t = cursor->t; + } + context.kind = native_failure_context_kind(has_cause, has_recipient, has_cursor); + return context; +} + struct NativeFailure { NativeFailureCode code = NativeFailureCode::None; NativeFailureOperation operation = NativeFailureOperation::None; std::uint64_t ordinal = 0; // 0 = absent std::uint32_t discriminator = 0; + NativeFailureContext context{}; }; struct NativeUnconfigured {}; @@ -79,7 +192,18 @@ struct NativeFailed { std::optional spec; NativeFailure failure; using NativeLifecycle = std::variant; +inline const native_order::RunIdentity* native_failed_run_identity( + const NativeFailed& failed) noexcept { + return failed.spec ? &failed.spec->identity : nullptr; +} + +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_trivially_copyable_v); static_assert(std::is_trivially_copyable_v); +static_assert(std::is_nothrow_copy_constructible_v); +static_assert(std::is_nothrow_copy_assignable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); @@ -133,7 +257,7 @@ struct NativeSetupResult { // same inline engine epoch as BacktestEngine so old-header/new-library // linkage cannot resolve an unversioned constructor against a different // base layout. -#define PINEFORGE_HAS_NATIVE_STRATEGY_HOST_V12 1 +#define PINEFORGE_HAS_NATIVE_STRATEGY_HOST_V13 1 class NativeStrategyHost : public BacktestEngine { public: NativeStrategyHost(); @@ -151,6 +275,9 @@ class NativeStrategyHost : public BacktestEngine { NativeSetupResult configure_native(const NativeRunSpec& spec); NativeStateView native_state() const; + native_order::SubmitResult submit(const native_order::Request& request); + native_order::ReplaceResult replace(const native_order::RequestHandle& target, + const native_order::Request& request); native_order::SubmitResult submit_market(const native_order::Request& request); native_order::ReplaceResult replace_market(const native_order::RequestHandle& target, const native_order::Request& request); @@ -168,5 +295,5 @@ class NativeStrategyHost : public BacktestEngine { friend class NativeExecutionConsumer; }; -} // inline namespace engine_script_run_v12 +} // inline namespace engine_script_run_v13 } // namespace pineforge diff --git a/include/pineforge/native_order.hpp b/include/pineforge/native_order.hpp index e042d10b..8f8ece5e 100644 --- a/include/pineforge/native_order.hpp +++ b/include/pineforge/native_order.hpp @@ -1,79 +1,290 @@ #pragma once #include "execution.hpp" +#include "execution_close_scope.hpp" +#include "market_driver.hpp" +#include "native_order_identity.hpp" #include #include #include #include #include +#include #include #include #include +#include #include #include namespace pineforge::native_order { -inline namespace native_order_v1 { +inline namespace native_order_v2 { // Isolated working-request/value core: current LIVE requests and immutable // command history. It does not own positions, cash, paid fees, matching, // calendar, host phase, or a second physical book. +// +// Identity types remain native_order_v1. Request/core/event values are v2. +// Physical execution::Action is unchanged; native Reduce uses a typed size +// source instead of a dummy units field. -struct RunIdentity { - std::string session_key; - uint64_t run_number = 0; +using Flatten = execution::Flatten; +using Transact = order_action::Transact; + +struct ExplicitUnits { + double units = 0.0; +}; +struct OwnerOpenedUnits {}; +using ReductionSize = std::variant; +struct Reduce { + ReductionSize size; +}; +using OrderIntent = std::variant; + +struct Market {}; +struct Limit { + double price = 0.0; +}; +struct Stop { + double price = 0.0; +}; +struct StopLimit { + double stop = 0.0; + double limit = 0.0; +}; +struct Trail { + double offset = 0.0; + std::optional arm_price; +}; +using Trigger = std::variant; + +struct ImmediateRemaining {}; +struct PointBudget { + double units = 0.0; +}; +using Capacity = std::variant; + +struct Independent {}; +struct WaitForApplied { + RequestHandle parent; }; +struct BindOpening { + RequestHandle opening; + int64_t cycle = 0; +}; +using Owner = std::variant; + +enum class GroupEffect : std::uint8_t { Cancel = 0, Reduce = 1 }; +struct NoGroup {}; +struct Member { + std::uint64_t group = 0; + std::int64_t cohort = 0; + GroupEffect effect = GroupEffect::Cancel; +}; +using Group = std::variant; + +// Aggregate field order keeps market construction: +// Request{Transact{1.0}, "buy", "comment"} +// Request{Flatten{}, "flat", ""} +// Request{Reduce{ExplicitUnits{3}}, "close", ""} +// New fields default to Market / ImmediateRemaining / Independent / NoGroup. +// market_request() converts a physical execution::Action (explicit Reduce +// units only) into that same market Request. Host market-only methods must +// reject nondefault extras; they must not drop them. +struct Request { + OrderIntent intent{}; + std::string label{}; + std::string comment{}; + Trigger trigger = Market{}; + Capacity capacity = ImmediateRemaining{}; + Owner owner = Independent{}; + Group group = NoGroup{}; +}; + +inline OrderIntent intent_from_physical(const execution::Action& action) { + if (std::holds_alternative(action)) return Flatten{}; + if (const auto* transact = std::get_if(&action)) { + return Transact{transact->signed_units}; + } + const auto& reduce = std::get(action); + return Reduce{ExplicitUnits{reduce.units}}; +} -inline bool operator==(const RunIdentity& a, const RunIdentity& b) { - return a.run_number == b.run_number && a.session_key == b.session_key; +inline Request market_request(execution::Action action, + std::string label = {}, + std::string comment = {}) { + return Request{intent_from_physical(action), std::move(label), std::move(comment)}; } -inline bool operator!=(const RunIdentity& a, const RunIdentity& b) { return !(a == b); } -struct RequestHandle { +inline bool has_market_defaults(const Request& request) noexcept { + return std::holds_alternative(request.trigger) + && std::holds_alternative(request.capacity) + && std::holds_alternative(request.owner) + && std::holds_alternative(request.group); +} + +struct EventId { RunIdentity run; - uint64_t incarnation = 0; + uint64_t ordinal = 0; }; -inline bool operator==(const RequestHandle& a, const RequestHandle& b) { - return a.incarnation == b.incarnation && a.run == b.run; +inline bool operator==(const EventId& a, const EventId& b) { + return a.ordinal == b.ordinal && a.run == b.run; } -inline bool operator!=(const RequestHandle& a, const RequestHandle& b) { return !(a == b); } +inline bool operator!=(const EventId& a, const EventId& b) { return !(a == b); } -struct Request { - execution::Action action; - std::string label; - std::string comment; +struct MatchCursor { + NativeCoordinate point{}; + double t = 0.0; }; -struct Birth { - uint64_t acceptance_ordinal = 0; - int64_t decision_time_lower_bound = 0; -}; +inline bool operator==(const MatchCursor& a, const MatchCursor& b) noexcept { + return a.point.ordinal == b.point.ordinal && a.t == b.t + && a.point.interval_index == b.point.interval_index + && a.point.effective_time_ms == b.point.effective_time_ms + && a.point.provenance == b.point.provenance + && a.point.path_phase == b.point.path_phase; +} +inline bool operator!=(const MatchCursor& a, const MatchCursor& b) noexcept { return !(a == b); } -inline bool operator==(const Birth& a, const Birth& b) { - return a.acceptance_ordinal == b.acceptance_ordinal - && a.decision_time_lower_bound == b.decision_time_lower_bound; +inline std::uint8_t display_waypoint(const MatchCursor& cursor) noexcept { + return static_cast(cursor.point.path_phase); } -struct LiveRequest { +enum class Side : std::uint8_t { Long = 0, Short = 1 }; + +struct RemainingUnbound {}; +struct RemainingFlattenAll {}; +struct RemainingUnits { + double q = 0.0; +}; +using Remaining = std::variant; + +struct RemainingProjectionUnbound {}; +struct RemainingProjectionFlattenAll {}; +struct RemainingProjectionUnits { + double q = 0.0; +}; +using RemainingProjection = + std::variant; + +struct BookTransaction {}; +struct Wait { + RequestHandle parent; +}; +struct ArmedTransaction { + RequestHandle parent; + EventId cause; + MatchCursor cause_cursor{}; +}; +struct UnboundBookClose {}; +struct BookClose { + int64_t cycle = 0; + Side side = Side::Long; + EventId binding_event; + MatchCursor binding_cursor{}; +}; +struct EnrollmentFromCommand { + EventId accepted; +}; +struct EnrollmentFromApplied { + EventId cause; + MatchCursor cursor{}; +}; +using Enrollment = std::variant; +struct OpeningClose { + RequestHandle opening; + int64_t cycle = 0; + Side side = Side::Long; + Enrollment enrollment; +}; +using Authority = std::variant; + +struct MarketReady {}; +struct LimitReady {}; +struct StopIdle {}; +struct StopActive {}; +struct StopLimitPending {}; +struct StopLimitLive {}; +struct TrailWaitArm {}; +struct TrailTrack { + double best = 0.0; +}; +struct TrailActive { + double best_at_trigger = 0.0; +}; +using TriggerState = std::variant; + +struct AllowanceUnset {}; +struct AllowanceUnits { + uint64_t point_ordinal = 0; + double initial = 0.0; + double left = 0.0; +}; +struct AllowanceAllScope { + uint64_t point_ordinal = 0; +}; +using Allowance = std::variant; + +struct PendingNone {}; +struct PendingDeferred { + double total = 0.0; + uint64_t count = 0; + EventId tail_receipt; +}; +using PendingAdjustments = std::variant; + +struct PositionFlat {}; +struct PositionNonflat { + int64_t cycle = 0; + Side side = Side::Long; +}; +using PositionIdentity = std::variant; + +struct OpeningObservation { + RequestHandle queried_opening; + int64_t queried_cycle = 0; + PositionIdentity current_position; + bool has_live_matching_lot = false; +}; + +struct TargetObservation { + PositionIdentity current_position; + std::optional opening; +}; + +struct RequestDefinition { RequestHandle handle; Request request; Birth birth; std::optional predecessor; }; +using DefinitionRef = std::shared_ptr; -inline bool point_eligible(const Birth& birth, - uint64_t point_ordinal, - int64_t effective_time_ms) noexcept { - return point_ordinal > birth.acceptance_ordinal - && effective_time_ms >= birth.decision_time_lower_bound; -} +struct LiveRequest { + DefinitionRef definition; + Remaining remaining = RemainingUnbound{}; + Authority authority = UnboundBookClose{}; + TriggerState trigger_state = MarketReady{}; + Allowance allowance = AllowanceUnset{}; + PendingAdjustments pending = PendingNone{}; + + const RequestHandle& handle() const noexcept { return definition->handle; } + const Request& request() const noexcept { return definition->request; } + const Birth& birth() const noexcept { return definition->birth; } + const std::optional& predecessor() const noexcept { + return definition->predecessor; + } +}; inline bool point_eligible(const LiveRequest& live, uint64_t point_ordinal, int64_t effective_time_ms) noexcept { - return point_eligible(live.birth, point_ordinal, effective_time_ms); + return point_eligible(live.birth(), point_ordinal, effective_time_ms); } // v2 exact binary64 grid: r=abs(q)/s, n=round(r) half away from zero, g=n*s. @@ -98,7 +309,15 @@ inline bool quantity_on_grid(double q, double step) noexcept { return err <= ulp_bound && err < half_step; } -enum class RequestRejectReason { InvalidQuantity, OffGrid }; +enum class RequestRejectReason : std::uint8_t { + InvalidQuantity = 0, + OffGrid = 1, + InvalidTrigger = 2, + InvalidCapacity = 3, + InvalidOwner = 4, + InvalidQuantityBasis = 5, + InvalidGroup = 6, +}; enum class SubmitStatus { Accepted, Rejected }; enum class ReplaceStatus { Replaced, ReplaceRejected, NotWorking, InvalidHandle }; @@ -123,40 +342,107 @@ struct CancelResult { uint64_t event_ordinal = 0; }; +enum class MatchRejectReason : std::uint8_t { + NonpositivePrice = 0, + OpeningDirection = 1, + MaxAbsUnits = 2, + MaxOpenLots = 3, + InitialMargin = 4, +}; + +enum class CancelReason : std::uint8_t { + User = 0, + Group = 1, + OwnerGone = 2, + UnsupportedRelation = 3, +}; + +enum class AppliedTerminalReason : std::uint8_t { + WorkingUnitsSatisfied = 0, + Flattened = 1, + TargetExhausted = 2, +}; + +enum class ActivationKind : std::uint8_t { + Stop = 0, + StopLimit = 1, + TrailArm = 2, + TrailTrigger = 3, +}; + +enum class CoreFailure : std::uint8_t { + Invariant = 0, + InvalidCause = 1, + NonrepresentableQuantity = 2, + MissingObservation = 3, + StaleHandle = 4, + UnsupportedTransition = 5, + ConflictingReceipt = 6, + UnrepresentableReservation = 7, + InvalidScope = 8, + InvalidProposal = 9, + ObservationMismatch = 10, +}; + +enum class DriverEligibilityClass : std::uint8_t { + ObservedPrint = 0, + CarriedOpen = 1, + TickAfterCalculation = 2, + ConfirmedOpen = 3, + ConfirmedExcursion = 4, + ConfirmedAfterCalculationClose = 5, +}; + +enum class CommandSurface : std::uint8_t { General = 0, MarketOnly = 1 }; + struct AcceptedEvent { uint64_t ordinal = 0; - RequestHandle handle; - Request request; - Birth birth; + DefinitionRef definition; + CommandSurface surface = CommandSurface::General; + const RequestHandle& handle() const noexcept { return definition->handle; } + const Request& request() const noexcept { return definition->request; } + const Birth& birth() const noexcept { return definition->birth; } }; struct RejectedEvent { uint64_t ordinal = 0; Request request; RequestRejectReason reason = RequestRejectReason::InvalidQuantity; + CommandSurface surface = CommandSurface::General; }; struct ReplacedEvent { uint64_t ordinal = 0; - RequestHandle predecessor; - Request predecessor_request; - RequestHandle successor; - Request successor_request; - Birth successor_birth; + DefinitionRef predecessor_definition; + DefinitionRef successor_definition; + CommandSurface surface = CommandSurface::General; + const RequestHandle& predecessor() const noexcept { return predecessor_definition->handle; } + const Request& predecessor_request() const noexcept { return predecessor_definition->request; } + const RequestHandle& successor() const noexcept { return successor_definition->handle; } + const Request& successor_request() const noexcept { return successor_definition->request; } + const Birth& successor_birth() const noexcept { return successor_definition->birth; } }; struct ReplaceRejectedEvent { uint64_t ordinal = 0; - RequestHandle target; - Request live_request; + DefinitionRef live_definition; Request attempted; RequestRejectReason reason = RequestRejectReason::InvalidQuantity; + CommandSurface surface = CommandSurface::General; + const RequestHandle& target() const noexcept { return live_definition->handle; } + const Request& live_request() const noexcept { return live_definition->request; } }; struct CancelledEvent { uint64_t ordinal = 0; - RequestHandle handle; - Request request; + DefinitionRef definition; + CancelReason reason = CancelReason::User; + std::optional cause; + Authority prior_authority = UnboundBookClose{}; + RemainingProjection unexecuted = RemainingProjectionUnbound{}; + PendingAdjustments pending = PendingNone{}; + const RequestHandle& handle() const noexcept { return definition->handle; } + const Request& request() const noexcept { return definition->request; } }; struct NotWorkingEvent { @@ -171,45 +457,127 @@ struct InvalidHandleEvent { std::optional attempted; }; -enum class MatchRejectReason : std::uint8_t { - NonpositivePrice = 0, - OpeningDirection = 1, - MaxAbsUnits = 2, - MaxOpenLots = 3, - InitialMargin = 4, -}; - struct NoEffectEvent { uint64_t ordinal = 0; - RequestHandle handle; - Request request; - Birth birth; + DefinitionRef definition; + RemainingProjection remaining = RemainingProjectionUnbound{}; + Authority authority = UnboundBookClose{}; + MatchCursor cursor{}; + const RequestHandle& handle() const noexcept { return definition->handle; } + const Request& request() const noexcept { return definition->request; } + const Birth& birth() const noexcept { return definition->birth; } }; struct MatchRejectedEvent { uint64_t ordinal = 0; - RequestHandle handle; - Request request; - Birth birth; MatchRejectReason reason = MatchRejectReason::OpeningDirection; + DefinitionRef definition; + RemainingProjection remaining = RemainingProjectionUnbound{}; + Authority authority = UnboundBookClose{}; + MatchCursor cursor{}; + const RequestHandle& handle() const noexcept { return definition->handle; } + const Request& request() const noexcept { return definition->request; } + const Birth& birth() const noexcept { return definition->birth; } +}; + +struct CloseBoundEvent { + uint64_t ordinal = 0; + DefinitionRef definition; + int64_t cycle = 0; + Side side = Side::Long; + MatchCursor cursor{}; + Authority before = UnboundBookClose{}; + Authority after = UnboundBookClose{}; +}; + +struct ActivatedEvent { + uint64_t ordinal = 0; + DefinitionRef definition; + ActivationKind kind = ActivationKind::Stop; + TriggerState before = StopIdle{}; + TriggerState after = StopActive{}; + double reached_price = 0.0; + MatchCursor cursor{}; }; struct ExecutionAppliedEvent { uint64_t ordinal = 0; - RequestHandle handle; - Request request; - Birth birth; - int64_t effective_time_ms = 0; - int64_t interval_open_ms = 0; - int64_t interval_last_traded_close_ms = 0; - int interval_index = 0; + DefinitionRef definition; double raw_price = 0.0; double resolved_price = 0.0; double current_ticket = 0.0; std::size_t first_trade_index = 0; std::size_t closed_trade_count = 0; uint64_t opened_lot_incarnation = 0; - std::uint8_t provenance = 0; + double closed_units = 0.0; + double opened_units = 0.0; + double filled_working = 0.0; + RemainingProjection remaining_before = RemainingProjectionUnbound{}; + RemainingProjection remaining_after = RemainingProjectionUnbound{}; + Allowance allowance_before = AllowanceUnset{}; + Allowance allowance_after = AllowanceUnset{}; + bool terminal = true; + std::optional terminal_reason; + int64_t cycle_before = 0; + int64_t cycle_after = 0; + execution::CloseScope scope = execution::Book{}; + MatchCursor cursor{}; + const RequestHandle& handle() const noexcept { return definition->handle; } + const Request& request() const noexcept { return definition->request; } + const Birth& birth() const noexcept { return definition->birth; } + int64_t effective_time_ms() const noexcept { return cursor.point.effective_time_ms; } + int64_t interval_open_ms() const noexcept { return cursor.point.open_ms; } + int64_t interval_last_traded_close_ms() const noexcept { + return cursor.point.last_traded_close_ms; + } + int interval_index() const noexcept { return cursor.point.interval_index; } + std::uint8_t provenance() const noexcept { + return static_cast(cursor.point.provenance); + } +}; + +struct ReservationReducedEvent { + uint64_t ordinal = 0; + DefinitionRef definition; + EventId cause; + RequestHandle recipient; + GroupEffect effect = GroupEffect::Reduce; + double requested_delta = 0.0; + double actual_deduction = 0.0; + RemainingUnits before{}; + RemainingProjection after = RemainingProjectionUnits{}; +}; + +struct DeferredGroupAdjustmentEvent { + uint64_t ordinal = 0; + DefinitionRef definition; + EventId cause; + RequestHandle recipient; + GroupEffect effect = GroupEffect::Reduce; + double deferred_delta = 0.0; + PendingAdjustments pending_before = PendingNone{}; + PendingDeferred pending_after{}; + std::optional previous_pending_receipt; +}; + +struct QuantityBoundEvent { + uint64_t ordinal = 0; + DefinitionRef definition; + EventId source; + double source_units = 0.0; + std::vector prior_adjustment_ids; + double pending_total = 0.0; + double effective_deduction = 0.0; + RemainingProjection remaining = RemainingProjectionUnits{}; +}; + +struct ArmedEvent { + uint64_t ordinal = 0; + DefinitionRef definition; + Authority before = Wait{}; + Authority after = ArmedTransaction{}; + Enrollment enrollment = EnrollmentFromApplied{}; + std::optional quantity_resolution; }; using CommandEvent = std::variant; + ExecutionAppliedEvent, + CloseBoundEvent, + ActivatedEvent, + ReservationReducedEvent, + DeferredGroupAdjustmentEvent, + QuantityBoundEvent, + ArmedEvent>; + +struct CommandContext { + int64_t decision_time_ms = 0; + std::optional quantity_grid; + std::optional opening; + CommandSurface surface = CommandSurface::General; +}; + +struct EvaluationContext { + MatchCursor cursor{}; + DriverEligibilityClass driver_class = DriverEligibilityClass::ObservedPrint; + bool existing_matching_bit = false; +}; + +struct BeginTrailTracking { + MatchCursor cursor{}; + double reached_price = 0.0; +}; +struct ObserveTrailExtremum { + MatchCursor cursor{}; + double reached_price = 0.0; +}; +struct ActivateStop { + MatchCursor cursor{}; + double reached_price = 0.0; +}; +struct ActivateStopLimit { + MatchCursor cursor{}; + double reached_price = 0.0; +}; +struct ActivateTrail { + MatchCursor cursor{}; + double reached_price = 0.0; +}; +using TriggerTransition = std::variant; + +struct ExecutionProposal { + MatchCursor cursor{}; + double raw_price = 0.0; + double resolved_price = 0.0; + execution::Action physical_action{}; + execution::CloseScope scope = execution::Book{}; + PositionIdentity pre_fill = PositionFlat{}; + double inspected_closed_units = 0.0; + double inspected_opened_units = 0.0; + double inspected_current_ticket = 0.0; +}; + +struct CommittedExecutionFacts { + execution::Result result{}; + int64_t cycle_before = 0; + int64_t cycle_after = 0; + TargetObservation post_target{}; + execution::Action committed_action{}; +}; + +struct EventRange { + std::size_t first_index = 0; + std::size_t count = 0; +}; + +struct Installed { + EventRange events{}; +}; + +enum class InstallError : std::uint8_t { + StalePreparation = 0, + WrongCoreOrRun = 1, + AlreadyConsumed = 2, +}; + +using InstallResult = std::variant; + +enum class NoChangeReason : std::uint8_t { + NoTransition = 0, + StillWaiting = 1, + NotEligible = 2, + NotWorking = 3, + AlreadyApplied = 4, +}; + +struct NoChange { + NoChangeReason reason = NoChangeReason::NoTransition; +}; + +struct PreparationError { + CoreFailure code = CoreFailure::Invariant; + EventId cause; + RequestHandle target; +}; + +template +using Preparation = std::variant; + +template +struct CommandInstalled { + R result{}; + EventRange events{}; +}; + +template +using InstalledCommand = std::variant, InstallError>; + +class PreparedSubmit; +class PreparedReplace; +class PreparedCancel; +class PreparedMutation; +class PreparedExecution; + +struct EligibilityFacts { + bool birth_ok = false; + bool waiting = false; + bool driver_ok = false; + bool needs_close_bind = false; + bool ready_to_match = false; + Side position_side = Side::Long; + bool is_buy = false; + const Trigger* trigger = nullptr; + const TriggerState* trigger_state = nullptr; + const Remaining* remaining = nullptr; + const Allowance* allowance = nullptr; + const Authority* authority = nullptr; +}; class WorkingRequestCore { public: @@ -240,7 +738,10 @@ class WorkingRequestCore { const std::vector& live() const noexcept { return live_; } const std::vector& history() const noexcept { return history_; } const LiveRequest* find_live(const RequestHandle& handle) const; + const CommandEvent* event_at(const EventId& id) const; + // R1 producer convenience: prepare then install one command. BindOpening + // enrollment still requires CommandContext.opening via prepare_submit. SubmitResult submit(const Request& request, int64_t decision_time_ms, uint64_t& next_order_incarnation, @@ -256,38 +757,249 @@ class WorkingRequestCore { CancelResult cancel(const RequestHandle& target, uint64_t& next_timeline_ordinal); - // Consumer-only terminal install. Reserve history before physical commit; - // install is the nonthrowing live-erase + history-append suffix. - friend struct TerminalCommit; + // Consumer-only typed prepare/install. Prepare may reserve storage and + // invalidate live/history references; it does not change logical state. + // Tokens are move-only, bound to this instance/run/epoch, and do not + // survive another prepare/install, reset, or move. + PreparedSubmit prepare_submit(const Request& request, + const CommandContext& context, + uint64_t& next_order_incarnation, + uint64_t& next_timeline_ordinal); + PreparedReplace prepare_replace(const RequestHandle& target, + const Request& request, + const CommandContext& context, + uint64_t& next_order_incarnation, + uint64_t& next_timeline_ordinal); + PreparedCancel prepare_cancel(const RequestHandle& target, uint64_t& next_timeline_ordinal); + + InstalledCommand install_submit(PreparedSubmit&& prepared) noexcept; + InstalledCommand install_replace(PreparedReplace&& prepared) noexcept; + InstalledCommand install_cancel(PreparedCancel&& prepared) noexcept; + + Preparation prepare_evaluation(const RequestHandle& target, + const EvaluationContext& context, + const TargetObservation& observation, + uint64_t& next_timeline_ordinal); + Preparation prepare_trigger(const RequestHandle& target, + const TriggerTransition& transition, + uint64_t& next_timeline_ordinal); + InstallResult install_mutation(PreparedMutation&& prepared) noexcept; + + Preparation prepare_execution(const RequestHandle& target, + const ExecutionProposal& proposal, + uint64_t& next_timeline_ordinal); + InstallResult install_execution(PreparedExecution&& prepared, + const CommittedExecutionFacts& facts) noexcept; + Preparation prepare_no_effect(const RequestHandle& target, + const EvaluationContext& context, + uint64_t& next_timeline_ordinal); + Preparation prepare_match_rejected(const RequestHandle& target, + const EvaluationContext& context, + MatchRejectReason reason, + uint64_t& next_timeline_ordinal); + + std::vector group_recipients(const EventId& applied) const; + std::vector waiting_children(const RequestHandle& parent) const; + std::vector bound_close_handles() const; + + Preparation prepare_group_effect(const EventId& applied, + const RequestHandle& recipient, + uint64_t& next_timeline_ordinal); + Preparation prepare_owner_applied( + const EventId& applied, + const RequestHandle& child, + const std::optional& observation, + uint64_t& next_timeline_ordinal); + Preparation prepare_bound_expiry(const EventId& physical_cause, + const RequestHandle& child, + const TargetObservation& observation, + uint64_t& next_timeline_ordinal); + Preparation prepare_parent_terminal(const EventId& terminal_or_replaced, + const RequestHandle& child, + uint64_t& next_timeline_ordinal); + + EligibilityFacts eligibility_facts(const LiveRequest& live, + const EvaluationContext& context) const noexcept; + bool evaluation_eligible(const LiveRequest& live, + const EvaluationContext& context) const noexcept; + bool trigger_permits_driver(const Trigger& trigger, + const TriggerState& state, + DriverEligibilityClass driver_class, + bool existing_matching_bit) const noexcept; + bool working_is_buy(const LiveRequest& live) const noexcept; private: enum class TargetKind { Live, NotWorking, InvalidHandle }; + friend class PreparedSubmit; + friend class PreparedReplace; + friend class PreparedCancel; + friend class PreparedMutation; + friend class PreparedExecution; static void require_identity(const RunIdentity& identity); static void require_grid(std::optional quantity_grid); static void require_distinct_counters(uint64_t& next_order_incarnation, uint64_t& next_timeline_ordinal); - static std::optional validate_request( - const Request& request, std::optional quantity_grid); + std::optional validate_request( + const Request& request, + const CommandContext& context, + const std::optional& replace_target) const; + LiveRequest make_live(DefinitionRef definition, + const CommandContext& context, + EventId accepted) const; + std::vector pending_receipt_ids(const PendingAdjustments& pending) const; + bool receipt_seen(const EventId& cause, const RequestHandle& recipient, GroupEffect effect, + uint64_t* outcome) const; + bool trail_level_ok(double best, double offset, bool is_buy, double* stop) const noexcept; uint64_t usable_ordinal(uint64_t next) const; uint64_t usable_incarnation(uint64_t next) const; - uint64_t last_command_ordinal() const; - uint64_t last_consumed_incarnation() const; TargetKind classify(const RequestHandle& handle, std::size_t* live_index) const; + bool bump_epoch() noexcept; + void require_epoch_room() const; + void clear_unbound() noexcept; + + struct InstanceBinding { + uint64_t generation = 1; + bool expired = false; + }; + struct MutationPlan { + std::weak_ptr instance; + uint64_t generation = 0; + uint64_t epoch = 0; + RunIdentity run; + std::size_t history_size = 0; + uint64_t last_ordinal = 0; + bool consumed = false; + std::vector events; + std::uint8_t live_change = 0; // 0 none, 1 push, 2 erase, 3 update + std::size_t live_index = 0; + LiveRequest live_row{}; + bool consume_incarnation = false; + uint64_t incarnation_used = 0; + bool add_receipt = false; + EventId receipt_cause{}; + RequestHandle receipt_recipient{}; + GroupEffect receipt_effect = GroupEffect::Reduce; + uint64_t receipt_outcome = 0; + }; + InstallResult commit(MutationPlan& plan) noexcept; + MutationPlan begin_plan() const; + void reserve_plan(const MutationPlan& plan); + void bind_plan(MutationPlan& plan) const; + void seal_plan(MutationPlan& plan); + PreparedMutation finish_mutation(MutationPlan plan); RunIdentity identity_; + std::shared_ptr instance_; std::vector live_; std::vector history_; + uint64_t last_ordinal_ = 0; + uint64_t last_incarnation_ = 0; + uint64_t epoch_ = 0; + std::vector> ordinal_index_; + struct ReceiptKey { + EventId cause; + RequestHandle recipient; + GroupEffect effect = GroupEffect::Reduce; + uint64_t outcome_ordinal = 0; + }; + std::vector receipts_; +}; + +class PreparedSubmit { +public: + PreparedSubmit() noexcept; + PreparedSubmit(PreparedSubmit&&) noexcept; + PreparedSubmit& operator=(PreparedSubmit&&) noexcept; + ~PreparedSubmit(); + PreparedSubmit(const PreparedSubmit&) = delete; + PreparedSubmit& operator=(const PreparedSubmit&) = delete; + explicit operator bool() const noexcept; + const SubmitResult& predicted() const; + EventId predicted_event_id() const; + +private: + friend class WorkingRequestCore; + struct Impl; + std::unique_ptr impl_; + explicit PreparedSubmit(std::unique_ptr impl) noexcept; +}; + +class PreparedReplace { +public: + PreparedReplace() noexcept; + PreparedReplace(PreparedReplace&&) noexcept; + PreparedReplace& operator=(PreparedReplace&&) noexcept; + ~PreparedReplace(); + PreparedReplace(const PreparedReplace&) = delete; + PreparedReplace& operator=(const PreparedReplace&) = delete; + explicit operator bool() const noexcept; + const ReplaceResult& predicted() const; + EventId predicted_event_id() const; + +private: + friend class WorkingRequestCore; + struct Impl; + std::unique_ptr impl_; + explicit PreparedReplace(std::unique_ptr impl) noexcept; +}; + +class PreparedCancel { +public: + PreparedCancel() noexcept; + PreparedCancel(PreparedCancel&&) noexcept; + PreparedCancel& operator=(PreparedCancel&&) noexcept; + ~PreparedCancel(); + PreparedCancel(const PreparedCancel&) = delete; + PreparedCancel& operator=(const PreparedCancel&) = delete; + explicit operator bool() const noexcept; + const CancelResult& predicted() const; + EventId predicted_event_id() const; + +private: + friend class WorkingRequestCore; + struct Impl; + std::unique_ptr impl_; + explicit PreparedCancel(std::unique_ptr impl) noexcept; +}; + +class PreparedMutation { +public: + PreparedMutation() noexcept; + PreparedMutation(PreparedMutation&&) noexcept; + PreparedMutation& operator=(PreparedMutation&&) noexcept; + ~PreparedMutation(); + PreparedMutation(const PreparedMutation&) = delete; + PreparedMutation& operator=(const PreparedMutation&) = delete; + explicit operator bool() const noexcept; + +private: + friend class WorkingRequestCore; + struct Impl; + std::unique_ptr impl_; + explicit PreparedMutation(std::unique_ptr impl) noexcept; +}; + +class PreparedExecution { +public: + PreparedExecution() noexcept; + PreparedExecution(PreparedExecution&&) noexcept; + PreparedExecution& operator=(PreparedExecution&&) noexcept; + ~PreparedExecution(); + PreparedExecution(const PreparedExecution&) = delete; + PreparedExecution& operator=(const PreparedExecution&) = delete; + explicit operator bool() const noexcept; + +private: + friend class WorkingRequestCore; + struct Impl; + std::unique_ptr impl_; + explicit PreparedExecution(std::unique_ptr impl) noexcept; }; -static_assert(std::is_nothrow_move_constructible_v); -static_assert(std::is_nothrow_move_assignable_v); -static_assert(std::is_nothrow_move_constructible_v); -static_assert(std::is_nothrow_move_assignable_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); -static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); static_assert(std::is_nothrow_move_constructible_v); @@ -298,14 +1010,7 @@ static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_constructible_v); -struct TerminalCommit { - static uint64_t usable_ordinal(const WorkingRequestCore& core, uint64_t next); - static void reserve_history(WorkingRequestCore& core); - static void install(WorkingRequestCore& core, - std::size_t live_index, - CommandEvent&& event) noexcept; -}; - -} // inline namespace native_order_v1 +} // inline namespace native_order_v2 } // namespace pineforge::native_order diff --git a/include/pineforge/native_order_identity.hpp b/include/pineforge/native_order_identity.hpp new file mode 100644 index 00000000..c0a6835c --- /dev/null +++ b/include/pineforge/native_order_identity.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +namespace pineforge::native_order { +inline namespace native_order_v1 { + +// Stable native-v1 identity leaf. Request/core/event values live in +// native_order_v2; do not duplicate these types there. + +struct RunIdentity { + std::string session_key; + uint64_t run_number = 0; +}; + +inline bool operator==(const RunIdentity& a, const RunIdentity& b) { + return a.run_number == b.run_number && a.session_key == b.session_key; +} +inline bool operator!=(const RunIdentity& a, const RunIdentity& b) { return !(a == b); } + +struct RequestHandle { + RunIdentity run; + uint64_t incarnation = 0; +}; + +inline bool operator==(const RequestHandle& a, const RequestHandle& b) { + return a.incarnation == b.incarnation && a.run == b.run; +} +inline bool operator!=(const RequestHandle& a, const RequestHandle& b) { return !(a == b); } + +struct Birth { + uint64_t acceptance_ordinal = 0; + int64_t decision_time_lower_bound = 0; +}; + +inline bool operator==(const Birth& a, const Birth& b) { + return a.acceptance_ordinal == b.acceptance_ordinal + && a.decision_time_lower_bound == b.decision_time_lower_bound; +} +inline bool operator!=(const Birth& a, const Birth& b) { return !(a == b); } + +inline bool point_eligible(const Birth& birth, + uint64_t point_ordinal, + int64_t effective_time_ms) noexcept { + return point_ordinal > birth.acceptance_ordinal + && effective_time_ms >= birth.decision_time_lower_bound; +} + +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_assignable_v); +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_assignable_v); +static_assert(std::is_nothrow_move_constructible_v); + +} // inline namespace native_order_v1 +} // namespace pineforge::native_order diff --git a/include/pineforge/native_run_spec.hpp b/include/pineforge/native_run_spec.hpp index 098f5694..1b5ed883 100644 --- a/include/pineforge/native_run_spec.hpp +++ b/include/pineforge/native_run_spec.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include diff --git a/scripts/check_aggregate_cpp_versions.py b/scripts/check_aggregate_cpp_versions.py index 16a9de31..88341456 100644 --- a/scripts/check_aggregate_cpp_versions.py +++ b/scripts/check_aggregate_cpp_versions.py @@ -44,21 +44,21 @@ def standalone_scope(text, outer, version): def check_texts(files): header = clean(files[FILES[0]]) namespaces = re.findall(r'inline\s+namespace\s+(engine_script_run_v\d+)\s*\{', header) - if namespaces != ["engine_script_run_v12", "engine_script_run_v12"]: - raise ValueError("PendingOrder/BacktestEngine require engine_script_run_v12") + if namespaces != ["engine_script_run_v13", "engine_script_run_v13"]: + raise ValueError("PendingOrder/BacktestEngine require engine_script_run_v13") broker = body(clean(files[FILES[1]]), r'uint64_t\s+BacktestEngine::broker_state_hash\(\)\s+const\s*\{', "broker hash") - if not re.match(r'\s*Fnv\s+f;\s*f\.s\("pineforge-broker-state/v12"\);', broker): - raise ValueError("broker entry requires v12 domain") + if not re.match(r'\s*Fnv\s+f;\s*f\.s\("pineforge-broker-state/v13"\);', broker): + raise ValueError("broker entry requires v13 domain") stream = body(clean(files[FILES[2]]), r'uint64_t\s+BacktestEngine::stream_state_hash\(\)\s+const\s*\{', "stream hash") compact = re.sub(r'\s+', '', stream) - fold = "integer(12);integer(broker_state_hash());" + fold = "integer(13);integer(broker_state_hash());" if compact.count(fold) != 1: - raise ValueError("stream entry requires v12 then broker hash") + raise ValueError("stream entry requires v13 then broker hash") prefix = compact[:compact.index(fold)] if prefix.count('{') != prefix.count('}') or (prefix and prefix[-1] not in ';}'): - raise ValueError("stream v12 fold must be unconditional") + raise ValueError("stream v13 fold must be unconditional") life = standalone_scope(files[FILES[3]], "pineforge::exit_legs", "lifecycle_v1") for name in ("Lifecycle", "Definition", "Action", "Frame", "Barrier", "Suspension"): if not re.search(r'\b(?:class|struct)\s+' + name + r'\s*\{', life): @@ -86,7 +86,7 @@ def check_texts(files): for name, text in files.items(): if name.startswith("include/pineforge/compat/pine/"): found = re.findall(r'inline\s+namespace\s+(engine_script_run_v\d+)\s*\{', clean(text)) - if any(value != "engine_script_run_v12" for value in found): + if any(value != "engine_script_run_v13" for value in found): raise ValueError(name + " has a stale PendingOrder forward declaration") @@ -103,4 +103,4 @@ def check(root=ROOT): if __name__ == "__main__": check() - print("aggregate v12, standalone admission v2 and lifecycle/cancellation v1 ownership verified") + print("aggregate v13, standalone admission v2 and lifecycle/cancellation v1 ownership verified") diff --git a/scripts/check_broker_state_hash_coverage.py b/scripts/check_broker_state_hash_coverage.py index 95db6ab1..e9b9b146 100644 --- a/scripts/check_broker_state_hash_coverage.py +++ b/scripts/check_broker_state_hash_coverage.py @@ -546,7 +546,7 @@ def _reservation_expansion_version_coverage(header: str, source: str) -> None: def _runtime_version_coverage(header: str, source: str, stream: str) -> None: - """The v11 layout and serialized-state contracts must advance together. + """The v13 layout and serialized-state contracts must advance together. Pin the actual hash entry points, rather than accepting a version string mentioned in a comment or an unrelated helper. Public C ABI versions have @@ -554,21 +554,21 @@ def _runtime_version_coverage(header: str, source: str, stream: str) -> None: """ header = _strip_cpp_comments(header) namespaces = re.findall(r"inline\s+namespace\s+(engine_script_run_v\d+)\s*\{", header) - if namespaces != ["engine_script_run_v12", "engine_script_run_v12"]: - raise ValueError("PendingOrder and BacktestEngine layouts require internal namespace engine_script_run_v12") + if namespaces != ["engine_script_run_v13", "engine_script_run_v13"]: + raise ValueError("PendingOrder and BacktestEngine layouts require internal namespace engine_script_run_v13") broker = _one_braced_body(source, r"uint64_t\s+BacktestEngine::broker_state_hash\(\)\s+const\s*\{", "broker hash") - if not re.match(r'\s*Fnv\s+f;\s*f\.s\("pineforge-broker-state/v12"\);', broker): - raise ValueError("broker hash must start with pineforge-broker-state/v12") + if not re.match(r'\s*Fnv\s+f;\s*f\.s\("pineforge-broker-state/v13"\);', broker): + raise ValueError("broker hash must start with pineforge-broker-state/v13") stream_body = _one_braced_body(_strip_cpp_comments(stream), r"uint64_t\s+BacktestEngine::stream_state_hash\(\)\s+const\s*\{", "stream hash") compact = re.sub(r"\s+", "", stream_body) - fold = "integer(12);integer(broker_state_hash());" + fold = "integer(13);integer(broker_state_hash());" if compact.count(fold) != 1: - raise ValueError("stream hash requires version 9 followed by the broker hash") + raise ValueError("stream hash requires version 13 followed by the broker hash") prefix = compact[:compact.index(fold)] if prefix.count("{") != prefix.count("}") or (prefix and prefix[-1] not in ";}"): - raise ValueError("stream v11 version fold must be unconditional at function scope") + raise ValueError("stream v13 version fold must be unconditional at function scope") def main(root: Path = ROOT) -> int: diff --git a/scripts/check_native_cpp_abi.py b/scripts/check_native_cpp_abi.py index bed6ff2d..4b18f465 100644 --- a/scripts/check_native_cpp_abi.py +++ b/scripts/check_native_cpp_abi.py @@ -31,8 +31,12 @@ SPEC_TREE = "fb00ed9ba02cede2fe08ee8113ae32f71fd26b00" CAL262_HEADER_SHA = "c47b3ee15d69587879cca8f491e73880cddae1ce3e7dfd0d1f4db16dfb8fcf4b" SPEC_HEADER_SHA = "b263a8bf3a68f58201c968ffa9ec8b54633fe3c752332df164ea11cf4bce3c7c" +V12_COMMIT = "e7d023dbdff1c98229155ec5bcdd1e4ac534f5fb" +V12_TREE = "0201bf052429490fb453bbfd6037e5afd1669626" +HOST_V12_HEADER_SHA = "871865715f084a0054c9d8e220cb9b957318bfdc0a0e100765d1cda85d7944b3" +ORDER_V1_HEADER_SHA = "b13006e99554ba9caa3e5b444cca3e4f2b5ebd5e372d3dcbe44bb6a677192a3e" -CURRENT_ORDER_VARIANT = 10 +CURRENT_ORDER_VARIANT = 16 CURRENT_CALENDAR_INTERVAL = 40 CURRENT_COORDINATE = 80 @@ -113,17 +117,45 @@ HOST_CALLER = '''#include #include static_assert(std::is_same_v); + pineforge::engine_script_run_v13::NativeStrategyHost>); static_assert(std::is_same_v); + pineforge::engine_script_run_v13::NativeStateView>); static_assert(std::is_same_v); + pineforge::engine_script_run_v13::NativeFailure>); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_trivially_copyable_v); int main(int argc, char** argv) { auto* host = reinterpret_cast(argv); auto state = host->native_state(); return int(state.kind); } ''' +# Return-only observation: native_events() return layout is not in the symbol. +HOST_EVENTS_CALLER = '''#include +#include +static_assert(std::is_same_v); +int main(int argc, char** argv) { + auto* host = reinterpret_cast(argv); + auto events = host->native_events(0); + return int(events.size()); +} +''' +OLD_HOST_EVENTS_CALLER = '''#include +#include +static_assert(std::is_same_v); +int main(int argc, char** argv) { + auto* host = reinterpret_cast(argv); + auto events = host->native_events(0); + return int(events.size()); +} +''' +HOST_EVENTS_SYMBOL_CONTROL = '''#include +namespace pineforge { +std::vector NativeStrategyHost::native_events(uint64_t) const { return {}; } +} +''' # Labeled minimal symbol control. Not a historical NativeRunSpec runtime. SPEC_SYMBOL_CONTROL = '''#include namespace pineforge { @@ -322,12 +354,16 @@ def main() -> int: "pineforge::native_calendar::native_calendar_v2::timezone_identity_descriptor("), ("pineforge::native_order::WorkingRequestCore::submit(", "pineforge::native_order::native_order_v1::WorkingRequestCore::submit("), + ("pineforge::native_order::native_order_v1::WorkingRequestCore::submit(", + "pineforge::native_order::native_order_v2::WorkingRequestCore::submit("), ("pineforge::validate_native_run_spec(", "pineforge::native_run_spec_v1::validate_native_run_spec("), ("pineforge::native_bar_structurally_valid(", "pineforge::native_driver_v3::native_bar_structurally_valid("), ("abi_accept_coordinate(pineforge::NativeCoordinate", "abi_accept_coordinate(pineforge::native_driver_v3::NativeCoordinate"), + ("pineforge::engine_script_run_v12::NativeStrategyHost::native_events(", + "pineforge::engine_script_run_v13::NativeStrategyHost::native_events("), ) for old, new in controls: if old in new: @@ -348,7 +384,7 @@ def main() -> int: "native_calendar": "pineforge::native_calendar::native_calendar_v2", "native_run_spec": "pineforge::native_run_spec_v1", "native_driver": "pineforge::native_driver_v3", - "native_host": "pineforge::engine_script_run_v12", + "native_host": "pineforge::engine_script_run_v13", }, "executable_runs": 0, "compiles": [], @@ -376,6 +412,12 @@ def main() -> int: "run-spec-262a280": { "source_commit": SPEC_COMMIT, "source_tree": SPEC_TREE, }, + "host-e7d023d": { + "source_commit": V12_COMMIT, "source_tree": V12_TREE, + }, + "order-e7d023d": { + "source_commit": V12_COMMIT, "source_tree": V12_TREE, + }, } unpacked = {} for name, expected in pins.items(): @@ -397,6 +439,14 @@ def main() -> int: header = dest / "include/pineforge/native_run_spec.hpp" if sha256(header.read_bytes()) != SPEC_HEADER_SHA: raise RuntimeError("262a280 run-spec header content SHA mismatch") + if name == "host-e7d023d": + header = dest / "include/pineforge/native_host.hpp" + if sha256(header.read_bytes()) != HOST_V12_HEADER_SHA: + raise RuntimeError("e7d023d host header content SHA mismatch") + if name == "order-e7d023d": + header = dest / "include/pineforge/native_order.hpp" + if sha256(header.read_bytes()) != ORDER_V1_HEADER_SHA: + raise RuntimeError("e7d023d order-v1 header content SHA mismatch") unpacked[name] = dest receipt["fixtures"][name] = { "source_commit": manifest.get("source_commit"), @@ -511,12 +561,15 @@ def link(name, objects, runtime, missing=None): current_coordinate_provider = compile_object( "current_coordinate_provider", COORDINATE_PROVIDER, include) current_host = compile_object("current_host_caller", HOST_CALLER, include) + current_host_events = compile_object("current_host_events_caller", HOST_EVENTS_CALLER, include) old_order_include = unpacked["order-1acaf33"] / "include" old_calendar_include = unpacked["calendar-draft-a8e34c"] / "include" old_desc_include = unpacked["calendar-262a280"] / "include" old_driver_include = unpacked["driver-08b5c88"] / "include" old_spec_include = unpacked["run-spec-262a280"] / "include" + old_host_include = unpacked["host-e7d023d"] / "include" + old_order_v1_include = unpacked["order-e7d023d"] / "include" old_order = compile_object("old_order_caller", ORDER_CALLER, old_order_include) old_calendar = compile_object("old_calendar_caller", CALENDAR_CALLER, old_calendar_include) @@ -542,6 +595,15 @@ def link(name, objects, runtime, missing=None): "old_bar_object", unpacked["driver-08b5c88"] / "src/market_driver.cpp", old_driver_include) old_spec_symbols = compile_object( "old_spec_symbol_control", SPEC_SYMBOL_CONTROL, old_spec_include) + old_host_events = compile_object( + "old_host_events_caller", OLD_HOST_EVENTS_CALLER, old_host_include) + old_host_events_symbols = compile_object( + "old_host_events_symbol_control", HOST_EVENTS_SYMBOL_CONTROL, old_host_include) + old_order_v1 = compile_object("old_order_v1_caller", ORDER_CALLER, old_order_v1_include) + old_order_v1_obj = compile_file( + "old_order_v1_object", + unpacked["order-e7d023d"] / "src/native_order.cpp", + old_order_v1_include) old_order_layout = compile_object( "old_order_layout", @@ -596,6 +658,7 @@ def link(name, objects, runtime, missing=None): link("current_bar_to_current_library", [current_bar], library) link("current_preflight_to_current_library", [current_preflight], library) link("current_host_to_current_library", [current_host], library) + link("current_host_events_to_current_library", [current_host_events], library) link("current_coordinate_to_current_provider", [current_coordinate], current_coordinate_provider) @@ -603,6 +666,8 @@ def link(name, objects, runtime, missing=None): link("old_calendar_to_old_objects", [old_calendar, old_timezone_obj], old_calendar_obj) link("old_parse_timeframe_to_old_objects", [old_parse, old_timezone_obj], old_calendar_obj) link("old_spec_to_old_symbol_control", [old_spec], old_spec_symbols) + link("old_host_events_to_old_symbols", [old_host_events], old_host_events_symbols) + link("old_order_v1_to_old_object", [old_order_v1], old_order_v1_obj) link("old_bar_to_old_object", [old_bar], old_bar_obj) link("old_coordinate_to_old_provider", [old_coordinate], old_coordinate_provider) @@ -617,14 +682,26 @@ def link(name, objects, runtime, missing=None): "pineforge::native_calendar::timezone_identity_descriptor(") link("old_spec_to_current_library", [old_spec], library, "pineforge::validate_native_run_spec(") + link("old_host_events_to_current_library", [old_host_events], library, + "pineforge::engine_script_run_v12::NativeStrategyHost::native_events(") link("old_bar_to_current_library", [old_bar], library, "pineforge::native_bar_structurally_valid(") link("old_coordinate_to_current_provider", [old_coordinate], current_coordinate_provider, ["abi_accept_coordinate(pineforge::NativeCoordinate", "abi_accept_decision(pineforge::NativeDecisionContext"]) - link("current_order_to_old_object", [current_order], old_order_obj, - "pineforge::native_order::native_order_v1::WorkingRequestCore::submit(") + order_text = (source_root / "include/pineforge/native_order.hpp").read_text() + if "inline namespace native_order_v2" in order_text: + current_order_submit = ( + "pineforge::native_order::native_order_v2::WorkingRequestCore::submit(") + link("old_order_v1_to_current_library", [old_order_v1], library, + "pineforge::native_order::native_order_v1::WorkingRequestCore::submit(") + link("current_order_to_old_v1_object", [current_order], old_order_v1_obj, + current_order_submit) + else: + current_order_submit = ( + "pineforge::native_order::native_order_v1::WorkingRequestCore::submit(") + link("current_order_to_old_object", [current_order], old_order_obj, current_order_submit) link("current_calendar_to_old_objects", [current_calendar, old_timezone_obj], old_calendar_obj, "pineforge::native_calendar::native_calendar_v2::parse_timeframe(") link("current_parse_timeframe_to_old_objects", [current_parse, old_timezone_obj], old_calendar_obj, @@ -633,6 +710,8 @@ def link(name, objects, runtime, missing=None): "pineforge::native_calendar::native_calendar_v2::timezone_identity_descriptor(") link("current_spec_to_old_symbol_control", [current_spec], old_spec_symbols, "pineforge::native_run_spec_v1::validate_native_run_spec(") + link("current_host_events_to_old_symbols", [current_host_events], old_host_events_symbols, + "pineforge::engine_script_run_v13::NativeStrategyHost::native_events(") link("current_bar_to_old_object", [current_bar], old_bar_obj, "pineforge::native_driver_v3::native_bar_structurally_valid(") link("current_coordinate_to_old_provider", [current_coordinate], old_coordinate_provider, diff --git a/scripts/check_native_cpp_versions.py b/scripts/check_native_cpp_versions.py index 71520197..ca2ab827 100644 --- a/scripts/check_native_cpp_versions.py +++ b/scripts/check_native_cpp_versions.py @@ -18,6 +18,7 @@ "include/pineforge/native_host.hpp", "src/native_execution_consumer.hpp", "src/native_execution_consumer.cpp", + "include/pineforge/native_order_identity.hpp", ) DRIVER_FORWARD = ( @@ -100,17 +101,21 @@ def versioned(text, outer, version): def check_texts(files): - order = versioned(files[FILES[0]], "pineforge::native_order", "native_order_v1") - require(order, ("RunIdentity", "WorkingRequestCore", "Request", "SubmitResult", - "AcceptedEvent", "NoEffectEvent", "MatchRejectedEvent", - "ExecutionAppliedEvent", "TerminalCommit"), + identity = versioned(files[FILES[11]], "pineforge::native_order", "native_order_v1") + require(identity, ("RunIdentity", "RequestHandle", "Birth"), "native_order_v1", r'\b(?:class|struct)\s+NAME\s*\{') + order = versioned(files[FILES[0]], "pineforge::native_order", "native_order_v2") + require(order, ("WorkingRequestCore", "Request", "SubmitResult", + "AcceptedEvent", "NoEffectEvent", "MatchRejectedEvent", + "ExecutionAppliedEvent"), + "native_order_v2", r'\b(?:class|struct)\s+NAME\s*\{') if "CommandEvent" not in re.findall(ALIAS_DEF, order): - raise ValueError("CommandEvent must belong to native_order_v1") - order_src = versioned(files[FILES[1]], "pineforge::native_order", "native_order_v1") - require(order_src, ("WorkingRequestCore::submit", "WorkingRequestCore::replace", - "WorkingRequestCore::cancel", "TerminalCommit::install"), - "native_order_v1", r'\bNAME\s*\(') + raise ValueError("CommandEvent must belong to native_order_v2") + if re.search(r'\b(?:class|struct)\s+RunIdentity\s*\{', order): + raise ValueError("RunIdentity must remain in native_order_v1, not native_order_v2") + order_src = versioned(files[FILES[1]], "pineforge::native_order", "native_order_v2") + require(order_src, ("WorkingRequestCore::reset", "WorkingRequestCore::find_live"), + "native_order_v2", r'\bNAME\s*\(') calendar = versioned(files[FILES[2]], "pineforge::native_calendar", "native_calendar_v2") require(calendar, ("Timeframe", "SessionCalendar", "NativeInterval", @@ -164,25 +169,39 @@ def check_texts(files): require_namespace_functions( driver, ("native_bar_structurally_valid", "preflight_native_inputs"), "native_driver_v3") + if 'kNativeConsumerSemanticVersion = "native-consumer/v4"' not in driver_text: + raise ValueError("consumer semantic marker must remain native-consumer/v4") driver_src = versioned(files[FILES[7]], "pineforge", "native_driver_v3") require_namespace_functions( driver_src, ("native_bar_structurally_valid", "preflight_native_inputs"), "native_driver_v3") - host = versioned(files[FILES[8]], "pineforge", "engine_script_run_v12") + host = versioned(files[FILES[8]], "pineforge", "engine_script_run_v13") require(host, ("NativeStrategyHost", "NativeStateView", "NativeLifecycleKind", - "NativeFailure", "NativeMarketEvent", "NativeSetupResult", - "NativePhysicalPosition", "NativeAccountObservation"), - "engine_script_run_v12", + "NativeFailure", "NativeFailureContext", "NativeInRunCause", + "NativeInRunRecipient", "NativeInRunCursor", "NativeMarketEvent", + "NativeSetupResult", "NativePhysicalPosition", "NativeAccountObservation"), + "engine_script_run_v13", r'\b(?:enum\s+class|class|struct)\s+NAME\s*(?::[^;{]+)?\{') - consumer = versioned(files[FILES[9]], "pineforge", "engine_script_run_v12") + if "native_failure_context_in_run" not in host: + raise ValueError("native_failure_context_in_run must belong to engine_script_run_v13") + if "native_failed_run_identity" not in host: + raise ValueError("native_failed_run_identity must belong to engine_script_run_v13") + if not re.search(r'\bSubmitResult\s+submit\s*\(\s*const\s+native_order::Request\s*&', host): + raise ValueError("general submit must belong to engine_script_run_v13") + if not re.search(r'\bReplaceResult\s+replace\s*\(\s*const\s+native_order::RequestHandle\s*&', + host): + raise ValueError("general replace must belong to engine_script_run_v13") + if "submit_market" not in host or "replace_market" not in host: + raise ValueError("market-only submit/replace must remain in engine_script_run_v13") + consumer = versioned(files[FILES[9]], "pineforge", "engine_script_run_v13") require(consumer, ("NativeExecutionConsumer",), - "engine_script_run_v12", r'\bclass\s+NAME\s*') - consumer_src = versioned(files[FILES[10]], "pineforge", "engine_script_run_v12") + "engine_script_run_v13", r'\bclass\s+NAME\s*') + consumer_src = versioned(files[FILES[10]], "pineforge", "engine_script_run_v13") require(consumer_src, ("NativeStrategyHost::configure_native", "NativeStrategyHost::native_state", "NativeStrategyHost::native_events"), - "engine_script_run_v12", r'\bNAME\s*\(') + "engine_script_run_v13", r'\bNAME\s*\(') def load(root=ROOT): @@ -195,5 +214,5 @@ def check(root=ROOT): if __name__ == "__main__": check() - print("native_order_v1, native_calendar_v2, native_run_spec_v1, " - "native_driver_v3 and host engine_script_run_v12 ownership verified") + print("native_order identity v1 / values v2, native_calendar_v2, native_run_spec_v1, " + "native_driver_v3 and host engine_script_run_v13 ownership verified") diff --git a/scripts/check_script_cpp_abi.py b/scripts/check_script_cpp_abi.py index 70c71e1f..3ab18368 100644 --- a/scripts/check_script_cpp_abi.py +++ b/scripts/check_script_cpp_abi.py @@ -16,7 +16,7 @@ BASE_COMMIT = "38dc73e5503fe5395458e5f8df2a2ad78054a1ae" BASE_ENGINE_SHA256 = "06c937a1ccd31815ca7775268ac699ffdfddb1a1f19de4628b777f37e9a6d193" -CURRENT_NAMESPACE = "engine_script_run_v12" +CURRENT_NAMESPACE = "engine_script_run_v13" BASE_NAMESPACE = "engine_script_run_v2" V8_COMMIT = "79921099a9357cb5bbace907a9319479f6640d89" V8_TREE = "e141657c572b4a3855dfee607f9951e331b961d6" @@ -27,6 +27,9 @@ V10_COMMIT = "fd4c68685ebd612397e42bced731d7b7ee06c1b7" V10_TREE = "24fb6e11d979c737561c5ce8a657c5ff703fc01e" V10_ENGINE_SHA256 = "53b2cc6deb96143afe572581d92f0c5d8d0f67d83e307153be4b67798f4db998" +V12_COMMIT = "e7d023dbdff1c98229155ec5bcdd1e4ac534f5fb" +V12_TREE = "0201bf052429490fb453bbfd6037e5afd1669626" +V12_ENGINE_SHA256 = "210a43d3166e9788dfa8bb0e670776a243af136ef8fd026f340b52911484c8bf" FIXTURE = Path(__file__).resolve().parents[1] / "tests/fixtures/script_cpp_abi/base38" @@ -216,6 +219,9 @@ def main(): v10_include = root / "basev10/include" frozen_headers(v10_include, FIXTURE.parent / "basev10", V10_COMMIT, "engine_script_run_v10", V10_ENGINE_SHA256, V10_TREE) + v12_include = root / "basev12/include" + frozen_headers(v12_include, FIXTURE.parent / "basev12", V12_COMMIT, + "engine_script_run_v12", V12_ENGINE_SHA256, V12_TREE) standalone_draft_include = root / "standalone-draft/include" frozen_standalone_headers(standalone_draft_include) # The exact pre-v11 aggregate closure supplies the frozen @@ -318,6 +324,12 @@ def compile_object(name, source, include): BASE_SYMBOL_CONTROL.replace("engine_script_run_v2", "engine_script_run_v10"), v10_include) v10_priority = compile_object("basev10_pending_priority", priority_caller, v10_include) v10_priority_symbols = compile_object("basev10_pending_priority_symbols", priority_symbols, v10_include) + v12_native = compile_object("basev12_native", caller("engine_script_run_v12"), v12_include) + v12_generated = compile_object("basev12_generated", caller("engine_script_run_v12", True), v12_include) + v12_symbols = compile_object("basev12_symbol_control", + BASE_SYMBOL_CONTROL.replace("engine_script_run_v2", "engine_script_run_v12"), v12_include) + v12_priority = compile_object("basev12_pending_priority", priority_caller, v12_include) + v12_priority_symbols = compile_object("basev12_pending_priority_symbols", priority_symbols, v12_include) reservation_caller = '''#include int main(int argc, char**) { @@ -559,28 +571,39 @@ def link(name, obj, runtime, missing_namespace=None): link("current_native_to_v10_symbols", current_native, v10_symbols, CURRENT_NAMESPACE) link("current_generated_to_v10_symbols", current_generated, v10_symbols, CURRENT_NAMESPACE) link("basev10_pending_priority_to_v10_symbols", v10_priority, v10_priority_symbols) + link("basev12_native_to_v12_symbols", v12_native, v12_symbols) + link("basev12_generated_to_v12_symbols", v12_generated, v12_symbols) + link("basev12_native_to_current", v12_native, args.library, "engine_script_run_v12") + link("basev12_generated_to_current", v12_generated, args.library, "engine_script_run_v12") + link("current_native_to_v12_symbols", current_native, v12_symbols, CURRENT_NAMESPACE) + link("current_generated_to_v12_symbols", current_generated, v12_symbols, CURRENT_NAMESPACE) + link("basev12_pending_priority_to_v12_symbols", v12_priority, v12_priority_symbols) for name, obj, runtime, expected in [ ("basecc0_pending_priority_to_current", cc0_priority, args.library, "pineforge::engine_script_run_v7::PendingOrder"), - ("current_pending_priority_to_v7_symbols", current_priority, cc0_priority_symbols, "pineforge::engine_script_run_v12::PendingOrder"), + ("current_pending_priority_to_v7_symbols", current_priority, cc0_priority_symbols, "pineforge::engine_script_run_v13::PendingOrder"), ("baseff54_pending_priority_to_current", shipped_priority, args.library, "pineforge::engine_script_run_v6::PendingOrder"), - ("current_pending_priority_to_v6_symbols", current_priority, shipped_priority_symbols, "pineforge::engine_script_run_v12::PendingOrder"), + ("current_pending_priority_to_v6_symbols", current_priority, shipped_priority_symbols, "pineforge::engine_script_run_v13::PendingOrder"), ("base149_pending_priority_to_current", activation_priority, args.library, "pineforge::engine_script_run_v5::PendingOrder"), - ("current_pending_priority_to_v5_symbols", current_priority, activation_priority_symbols, "pineforge::engine_script_run_v12::PendingOrder"), + ("current_pending_priority_to_v5_symbols", current_priority, activation_priority_symbols, "pineforge::engine_script_run_v13::PendingOrder"), ("basec45_pending_priority_to_current", prior_priority, args.library, "pineforge::PendingOrder"), ("current_pending_priority_to_v4_symbols", current_priority, prior_priority_symbols, - "pineforge::engine_script_run_v12::PendingOrder"), + "pineforge::engine_script_run_v13::PendingOrder"), ("basev8_pending_priority_to_current", v8_priority, args.library, "pineforge::engine_script_run_v8::PendingOrder"), ("current_pending_priority_to_v8_symbols", current_priority, v8_priority_symbols, - "pineforge::engine_script_run_v12::PendingOrder"), + "pineforge::engine_script_run_v13::PendingOrder"), ("basev9_pending_priority_to_current", v9_priority, args.library, "pineforge::engine_script_run_v9::PendingOrder"), ("current_pending_priority_to_v9_symbols", current_priority, v9_priority_symbols, - "pineforge::engine_script_run_v12::PendingOrder"), + "pineforge::engine_script_run_v13::PendingOrder"), ("basev10_pending_priority_to_current", v10_priority, args.library, "pineforge::engine_script_run_v10::PendingOrder"), ("current_pending_priority_to_v10_symbols", current_priority, v10_priority_symbols, + "pineforge::engine_script_run_v13::PendingOrder"), + ("basev12_pending_priority_to_current", v12_priority, args.library, "pineforge::engine_script_run_v12::PendingOrder"), + ("current_pending_priority_to_v12_symbols", current_priority, v12_priority_symbols, + "pineforge::engine_script_run_v13::PendingOrder"), ]: result = subprocess.run([*common, str(obj), str(runtime), "-pthread", "-o", str(root / name)], capture_output=True, text=True, timeout=60) diff --git a/scripts/test_aggregate_cpp_versions.py b/scripts/test_aggregate_cpp_versions.py index 86aa491c..672ab8c8 100644 --- a/scripts/test_aggregate_cpp_versions.py +++ b/scripts/test_aggregate_cpp_versions.py @@ -19,16 +19,16 @@ def test_current(self): def test_engine_and_forward_declarations(self): for path, text in DATA.items(): - if 'engine_script_run_v12' in text: + if 'engine_script_run_v13' in text: with self.subTest(path=path): - self.reject(path, 'engine_script_run_v12', 'engine_script_run_v11') + self.reject(path, 'engine_script_run_v13', 'engine_script_run_v12') def test_broker_and_stream_entry_points(self): - self.reject('src/engine_state_hash.cpp', 'pineforge-broker-state/v12', 'pineforge-broker-state/v11') - self.reject('src/engine_stream.cpp', 'integer(12); integer(broker_state_hash());', - 'integer(11); integer(broker_state_hash());') - self.reject('src/engine_stream.cpp', 'integer(12); integer(broker_state_hash());', - 'if (false) { integer(12); integer(broker_state_hash()); }') + self.reject('src/engine_state_hash.cpp', 'pineforge-broker-state/v13', 'pineforge-broker-state/v12') + self.reject('src/engine_stream.cpp', 'integer(13); integer(broker_state_hash());', + 'integer(12); integer(broker_state_hash());') + self.reject('src/engine_stream.cpp', 'integer(13); integer(broker_state_hash());', + 'if (false) { integer(13); integer(broker_state_hash()); }') def test_standalone_owners(self): for path, namespace in ( diff --git a/scripts/test_broker_state_hash_coverage.py b/scripts/test_broker_state_hash_coverage.py index 60f1abc3..1f29c8a9 100644 --- a/scripts/test_broker_state_hash_coverage.py +++ b/scripts/test_broker_state_hash_coverage.py @@ -176,26 +176,26 @@ def test_quantity_model_additions_and_variant_drift_fail_closed(self): self.assertIn(old, QUANTITY) self.assertEqual(self.check(quantity=QUANTITY.replace(old, new))[0], 1) - def test_layout_and_hash_versions_must_match_v12_contract(self): - self.assertIn("engine_script_run_v12", HEADER) - self.assertIn('f.s("pineforge-broker-state/v12");', SOURCE) - self.assertIn("integer(12); integer(broker_state_hash());", STREAM) - self.assertEqual(self.check(header=HEADER.replace("engine_script_run_v12", "engine_script_run_v11"))[0], 1) - for replacement in ['f.s("pineforge-broker-state/v11");', '', - '// f.s("pineforge-broker-state/v12");']: + def test_layout_and_hash_versions_must_match_v13_contract(self): + self.assertIn("engine_script_run_v13", HEADER) + self.assertIn('f.s("pineforge-broker-state/v13");', SOURCE) + self.assertIn("integer(13); integer(broker_state_hash());", STREAM) + self.assertEqual(self.check(header=HEADER.replace("engine_script_run_v13", "engine_script_run_v12"))[0], 1) + for replacement in ['f.s("pineforge-broker-state/v12");', '', + '// f.s("pineforge-broker-state/v13");']: self.assertEqual(self.check(source=SOURCE.replace( - 'f.s("pineforge-broker-state/v12");', replacement))[0], 1) - for replacement in ["integer(11); integer(broker_state_hash());", + 'f.s("pineforge-broker-state/v13");', replacement))[0], 1) + for replacement in ["integer(12); integer(broker_state_hash());", "integer(broker_state_hash());", - "if (false) { integer(12); integer(broker_state_hash()); }"]: + "if (false) { integer(13); integer(broker_state_hash()); }"]: self.assertEqual(self.check(stream=STREAM.replace( - "integer(12); integer(broker_state_hash());", replacement))[0], 1) + "integer(13); integer(broker_state_hash());", replacement))[0], 1) def test_version_folds_in_unrelated_helpers_do_not_cover_entry_points(self): - broker_fold = 'f.s("pineforge-broker-state/v12");' + broker_fold = 'f.s("pineforge-broker-state/v13");' altered = SOURCE.replace(broker_fold, '') + '\nvoid other() { ' + broker_fold + ' }\n' self.assertEqual(self.check(source=altered)[0], 1) - stream_fold = "integer(12); integer(broker_state_hash());" + stream_fold = "integer(13); integer(broker_state_hash());" altered = STREAM.replace(stream_fold, '') + '\nvoid other() { ' + stream_fold + ' }\n' self.assertEqual(self.check(stream=altered)[0], 1) diff --git a/scripts/test_native_cpp_versions.py b/scripts/test_native_cpp_versions.py index d08f2b4d..1c1afbb2 100644 --- a/scripts/test_native_cpp_versions.py +++ b/scripts/test_native_cpp_versions.py @@ -23,28 +23,30 @@ def test_current(self): def test_stale_wrapper(self): for path, namespace, stale in ( - (FILES[0], "native_order_v1", "native_order_v2"), - (FILES[1], "native_order_v1", "native_order_v2"), + (FILES[0], "native_order_v2", "native_order_v1"), + (FILES[1], "native_order_v2", "native_order_v1"), + (FILES[11], "native_order_v1", "native_order_v2"), (FILES[2], "native_calendar_v2", "native_calendar_v1"), (FILES[3], "native_calendar_v2", "native_calendar_v3"), (FILES[4], "native_run_spec_v1", "native_run_spec_v2"), (FILES[5], "native_run_spec_v1", "native_run_spec_v2"), (FILES[6], "native_driver_v3", "native_driver_v2"), (FILES[7], "native_driver_v3", "native_driver_v4"), - (FILES[8], "engine_script_run_v12", "engine_script_run_v11"), - (FILES[9], "engine_script_run_v12", "engine_script_run_v11"), - (FILES[10], "engine_script_run_v12", "engine_script_run_v13"), + (FILES[8], "engine_script_run_v13", "engine_script_run_v12"), + (FILES[9], "engine_script_run_v13", "engine_script_run_v12"), + (FILES[10], "engine_script_run_v13", "engine_script_run_v12"), ): with self.subTest(path=path, namespace=namespace): self.reject(path, namespace, stale) def test_duplicate_wrapper(self): for path, namespace in ( - (FILES[0], "native_order_v1"), + (FILES[0], "native_order_v2"), (FILES[2], "native_calendar_v2"), (FILES[4], "native_run_spec_v1"), (FILES[6], "native_driver_v3"), - (FILES[8], "engine_script_run_v12"), + (FILES[8], "engine_script_run_v13"), + (FILES[11], "native_order_v1"), ): with self.subTest(path=path): opening = "inline namespace " + namespace + " {" @@ -52,11 +54,12 @@ def test_duplicate_wrapper(self): def test_empty_namespace_is_not_ownership(self): for path, namespace in ( - (FILES[0], "native_order_v1"), + (FILES[0], "native_order_v2"), (FILES[2], "native_calendar_v2"), (FILES[4], "native_run_spec_v1"), (FILES[6], "native_driver_v3"), - (FILES[8], "engine_script_run_v12"), + (FILES[8], "engine_script_run_v13"), + (FILES[11], "native_order_v1"), ): with self.subTest(path=path): self.reject(path, "inline namespace " + namespace + " {", @@ -64,9 +67,10 @@ def test_empty_namespace_is_not_ownership(self): def test_comment_only_namespace_is_not_ownership(self): for path, namespace, decoy in ( - (FILES[0], "native_order_v1", "struct WorkingRequestCore"), + (FILES[0], "native_order_v2", "struct WorkingRequestCore"), + (FILES[11], "native_order_v1", "struct RunIdentity"), (FILES[2], "native_calendar_v2", "parse_timeframe NativeInterval"), - (FILES[8], "engine_script_run_v12", "class NativeStrategyHost"), + (FILES[8], "engine_script_run_v13", "class NativeStrategyHost"), ): with self.subTest(path=path): self.reject( @@ -75,7 +79,9 @@ def test_comment_only_namespace_is_not_ownership(self): "inline namespace " + namespace + " { /* " + decoy + " */ } namespace misplaced {") def test_type_moved_outside_owner(self): - self.reject(FILES[0], "struct RunIdentity {", + self.reject(FILES[0], "struct Request {", + "} struct Request {") + self.reject(FILES[11], "struct RunIdentity {", "} struct RunIdentity {") self.reject(FILES[2], "struct NativeInterval {", "} struct NativeInterval {") @@ -171,11 +177,11 @@ def test_out_of_line_function_escape(self): changed = dict(DATA) src = FILES[1] - needle = "WorkingRequestCore::submit(" + needle = "WorkingRequestCore::reset(" self.assertIn(needle, changed[src]) changed[src] = changed[src].replace( - "} // inline namespace native_order_v1", - "} // inline namespace native_order_v1\nSubmitResult WorkingRequestCore::submit(const Request&, int64_t, uint64_t&, uint64_t&, std::optional) { return {}; }\n", + "} // inline namespace native_order_v2", + "} // inline namespace native_order_v2\nvoid WorkingRequestCore::reset(RunIdentity) {}\n", 1) with self.assertRaises(ValueError): check_texts(changed) @@ -191,18 +197,28 @@ def test_driver_native_run_spec_forward_must_stay_outside(self): DRIVER_FORWARD, "inline namespace native_run_spec_v1 { struct NativeRunSpec {}; }") - def test_host_public_values_cannot_leave_v12(self): + def test_host_public_values_cannot_leave_v13(self): self.reject(FILES[8], "struct NativeStateView {", "} struct NativeStateView {") self.reject(FILES[8], "struct NativeFailure {", "} struct NativeFailure {") + self.reject(FILES[8], "struct NativeFailureContext {", "} struct NativeFailureContext {") self.reject(FILES[9], "class NativeExecutionConsumer final : public IExecutionConsumer {", "} class NativeExecutionConsumer final : public IExecutionConsumer {") + self.reject( + FILES[8], + "native_order::SubmitResult submit(const native_order::Request& request);", + "") + self.reject( + FILES[6], + 'kNativeConsumerSemanticVersion = "native-consumer/v4"', + 'kNativeConsumerSemanticVersion = "native-consumer/v3"') class FixtureAuthentication(unittest.TestCase): def test_authentic_fixtures_unpack(self): from check_native_cpp_abi import FIXTURE, authenticate_fixture for name in ("order-1acaf33", "calendar-draft-a8e34c", "calendar-262a280", - "driver-08b5c88", "run-spec-262a280"): + "driver-08b5c88", "run-spec-262a280", "host-e7d023d", + "order-e7d023d"): with self.subTest(name=name): with tempfile.TemporaryDirectory(prefix="pf-native-abi-ok-") as temp: authenticate_fixture(FIXTURE / name, Path(temp) / name) diff --git a/src/engine_consumer.cpp b/src/engine_consumer.cpp index 9a804656..f30d0ba5 100644 --- a/src/engine_consumer.cpp +++ b/src/engine_consumer.cpp @@ -7,7 +7,7 @@ #include namespace pineforge { -inline namespace engine_script_run_v12 { +inline namespace engine_script_run_v13 { class LegacyCompatibilityConsumer final : public IExecutionConsumer { public: @@ -169,5 +169,5 @@ bool BacktestEngine::stream_end(bool finalize_partial_input_bar) { return execution_consumer().stream_end(*this, finalize_partial_input_bar); } -} // inline namespace engine_script_run_v12 +} // inline namespace engine_script_run_v13 } // namespace pineforge diff --git a/src/engine_state_hash.cpp b/src/engine_state_hash.cpp index 3c8a5d7f..5c163628 100644 --- a/src/engine_state_hash.cpp +++ b/src/engine_state_hash.cpp @@ -92,7 +92,7 @@ uint64_t BacktestEngine::broker_state_hash() const { Fnv f; // v6 includes resolved exit-leg owner/coordinates and immutable Pine placement evidence. // It is a serialization boundary, independent of the public C ABI version. - f.s("pineforge-broker-state/v12"); + f.s("pineforge-broker-state/v13"); f.u(execution_consumer().continuation_hash()); // --- Position core --- diff --git a/src/engine_stream.cpp b/src/engine_stream.cpp index dcfc990c..8a42c19e 100644 --- a/src/engine_stream.cpp +++ b/src/engine_stream.cpp @@ -678,7 +678,7 @@ uint64_t BacktestEngine::stream_state_hash() const { integer(static_cast(bar.timestamp)); real(bar.open); real(bar.high); real(bar.low); real(bar.close); real(bar.volume); }; - integer(12); integer(broker_state_hash()); + integer(13); integer(broker_state_hash()); integer(static_cast(stream_phase_)); integer(static_cast(stream_input_mode_)); integer(static_cast(stream_input_tf_ms_)); diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index de9194b2..ed9d1420 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -1,19 +1,23 @@ #include "native_execution_consumer.hpp" #include "engine_internal.hpp" +#include "native_matching.hpp" +#include #include #include #include +#include #include #include +#include #include #include #include #include namespace pineforge { -inline namespace engine_script_run_v12 { +inline namespace engine_script_run_v13 { namespace { struct Fnv { @@ -31,6 +35,10 @@ struct Fnv { void s(const std::string& v) noexcept { u(v.size()); bytes(v.data(), v.size()); } }; +void hash_coordinate(Fnv& f, const NativeCoordinate& c) noexcept; +void hash_birth(Fnv& f, const native_order::Birth& birth) noexcept; +void hash_optional_handle(Fnv& f, const std::optional& handle) noexcept; + void hash_spec(Fnv& f, const NativeRunSpec& spec) noexcept { f.s(spec.identity.session_key); f.u(spec.identity.run_number); f.s(spec.input_tf); f.s(spec.script_tf); @@ -48,22 +56,174 @@ void hash_spec(Fnv& f, const NativeRunSpec& spec) noexcept { if (spec.initial_margin_fraction) f.d(*spec.initial_margin_fraction); } -void hash_action(Fnv& f, const execution::Action& action) noexcept { - f.u(action.index()); - if (const auto* r = std::get_if(&action)) f.d(r->units); - if (const auto* t = std::get_if(&action)) f.d(t->signed_units); -} - void hash_handle(Fnv& f, const native_order::RequestHandle& handle) noexcept { f.s(handle.run.session_key); f.u(handle.run.run_number); f.u(handle.incarnation); } +void hash_event_id(Fnv& f, const native_order::EventId& id) noexcept { + f.s(id.run.session_key); + f.u(id.run.run_number); + f.u(id.ordinal); +} + +void hash_cursor(Fnv& f, const native_order::MatchCursor& cursor) noexcept { + hash_coordinate(f, cursor.point); + f.d(cursor.t); +} + +void hash_surface(Fnv& f, native_order::CommandSurface surface) noexcept { + f.u(static_cast(surface)); +} + +void hash_intent(Fnv& f, const native_order::OrderIntent& intent) noexcept { + f.u(intent.index()); + if (const auto* reduce = std::get_if(&intent)) { + f.u(reduce->size.index()); + if (const auto* units = std::get_if(&reduce->size)) { + f.d(units->units); + } + } + if (const auto* transact = std::get_if(&intent)) { + f.d(transact->signed_units); + } +} + +void hash_trigger(Fnv& f, const native_order::Trigger& trigger) noexcept { + f.u(trigger.index()); + if (const auto* limit = std::get_if(&trigger)) f.d(limit->price); + if (const auto* stop = std::get_if(&trigger)) f.d(stop->price); + if (const auto* sl = std::get_if(&trigger)) { + f.d(sl->stop); + f.d(sl->limit); + } + if (const auto* trail = std::get_if(&trigger)) { + f.d(trail->offset); + f.b(trail->arm_price.has_value()); + if (trail->arm_price) f.d(*trail->arm_price); + } +} + +void hash_capacity(Fnv& f, const native_order::Capacity& capacity) noexcept { + f.u(capacity.index()); + if (const auto* budget = std::get_if(&capacity)) f.d(budget->units); +} + +void hash_owner(Fnv& f, const native_order::Owner& owner) noexcept { + f.u(owner.index()); + if (const auto* wait = std::get_if(&owner)) { + hash_handle(f, wait->parent); + } + if (const auto* bind = std::get_if(&owner)) { + hash_handle(f, bind->opening); + f.i(bind->cycle); + } +} + +void hash_group(Fnv& f, const native_order::Group& group) noexcept { + f.u(group.index()); + if (const auto* member = std::get_if(&group)) { + f.u(member->group); + f.i(member->cohort); + f.u(static_cast(member->effect)); + } +} + void hash_request(Fnv& f, const native_order::Request& request) noexcept { - hash_action(f, request.action); + hash_intent(f, request.intent); f.s(request.label); f.s(request.comment); + hash_trigger(f, request.trigger); + hash_capacity(f, request.capacity); + hash_owner(f, request.owner); + hash_group(f, request.group); +} + +void hash_remaining(Fnv& f, const native_order::Remaining& remaining) noexcept { + f.u(remaining.index()); + if (const auto* units = std::get_if(&remaining)) f.d(units->q); +} + +void hash_remaining_projection(Fnv& f, const native_order::RemainingProjection& remaining) noexcept { + f.u(remaining.index()); + if (const auto* units = std::get_if(&remaining)) { + f.d(units->q); + } +} + +void hash_authority(Fnv& f, const native_order::Authority& authority) noexcept { + f.u(authority.index()); + if (const auto* wait = std::get_if(&authority)) { + hash_handle(f, wait->parent); + } else if (const auto* armed = std::get_if(&authority)) { + hash_handle(f, armed->parent); + hash_event_id(f, armed->cause); + hash_cursor(f, armed->cause_cursor); + } else if (const auto* close = std::get_if(&authority)) { + f.i(close->cycle); + f.u(static_cast(close->side)); + hash_event_id(f, close->binding_event); + hash_cursor(f, close->binding_cursor); + } else if (const auto* opening = std::get_if(&authority)) { + hash_handle(f, opening->opening); + f.i(opening->cycle); + f.u(static_cast(opening->side)); + f.u(opening->enrollment.index()); + if (const auto* from_cmd = std::get_if(&opening->enrollment)) { + hash_event_id(f, from_cmd->accepted); + } + if (const auto* from_app = std::get_if(&opening->enrollment)) { + hash_event_id(f, from_app->cause); + hash_cursor(f, from_app->cursor); + } + } +} + +void hash_trigger_state(Fnv& f, const native_order::TriggerState& state) noexcept { + f.u(state.index()); + if (const auto* track = std::get_if(&state)) f.d(track->best); + if (const auto* active = std::get_if(&state)) { + f.d(active->best_at_trigger); + } +} + +void hash_allowance(Fnv& f, const native_order::Allowance& allowance) noexcept { + f.u(allowance.index()); + if (const auto* units = std::get_if(&allowance)) { + f.u(units->point_ordinal); + f.d(units->initial); + f.d(units->left); + } + if (const auto* all = std::get_if(&allowance)) { + f.u(all->point_ordinal); + } +} + +void hash_pending(Fnv& f, const native_order::PendingAdjustments& pending) noexcept { + f.u(pending.index()); + if (const auto* deferred = std::get_if(&pending)) { + f.d(deferred->total); + f.u(deferred->count); + hash_event_id(f, deferred->tail_receipt); + } +} + +void hash_definition(Fnv& f, const native_order::DefinitionRef& definition) noexcept { + f.b(static_cast(definition)); + if (!definition) return; + hash_handle(f, definition->handle); + hash_request(f, definition->request); + hash_birth(f, definition->birth); + hash_optional_handle(f, definition->predecessor); +} + +void hash_scope(Fnv& f, const execution::CloseScope& scope) noexcept { + f.u(scope.index()); + if (const auto* opening = std::get_if(&scope)) { + f.u(opening->incarnation); + f.i(opening->cycle); + } } void hash_birth(Fnv& f, const native_order::Birth& birth) noexcept { @@ -86,6 +246,11 @@ void hash_failure(Fnv& f, const NativeFailure& failure) noexcept { f.u(static_cast(failure.operation)); f.u(failure.ordinal); f.u(failure.discriminator); + f.u(static_cast(failure.context.kind)); + f.u(failure.context.cause.ordinal); + f.u(failure.context.recipient.incarnation); + hash_coordinate(f, failure.context.cursor.point); + f.d(failure.context.cursor.t); } void hash_coordinate(Fnv& f, const NativeCoordinate& c) noexcept { @@ -122,30 +287,33 @@ void hash_command(Fnv& f, const native_order::CommandEvent& event) noexcept { f.u(payload.ordinal); if constexpr (std::is_same_v) { f.u(1); - hash_handle(f, payload.handle); - hash_request(f, payload.request); - hash_birth(f, payload.birth); + hash_definition(f, payload.definition); + hash_surface(f, payload.surface); } else if constexpr (std::is_same_v) { f.u(2); hash_request(f, payload.request); f.u(static_cast(payload.reason)); + hash_surface(f, payload.surface); } else if constexpr (std::is_same_v) { f.u(3); - hash_handle(f, payload.predecessor); - hash_request(f, payload.predecessor_request); - hash_handle(f, payload.successor); - hash_request(f, payload.successor_request); - hash_birth(f, payload.successor_birth); + hash_definition(f, payload.predecessor_definition); + hash_definition(f, payload.successor_definition); + hash_surface(f, payload.surface); } else if constexpr (std::is_same_v) { f.u(4); - hash_handle(f, payload.target); - hash_request(f, payload.live_request); + hash_definition(f, payload.live_definition); hash_request(f, payload.attempted); f.u(static_cast(payload.reason)); + hash_surface(f, payload.surface); } else if constexpr (std::is_same_v) { f.u(5); - hash_handle(f, payload.handle); - hash_request(f, payload.request); + hash_definition(f, payload.definition); + f.u(static_cast(payload.reason)); + f.b(payload.cause.has_value()); + if (payload.cause) hash_event_id(f, *payload.cause); + hash_authority(f, payload.prior_authority); + hash_remaining_projection(f, payload.unexecuted); + hash_pending(f, payload.pending); } else if constexpr (std::is_same_v) { f.u(6); hash_handle(f, payload.target); @@ -156,31 +324,110 @@ void hash_command(Fnv& f, const native_order::CommandEvent& event) noexcept { hash_optional_request(f, payload.attempted); } else if constexpr (std::is_same_v) { f.u(8); - hash_handle(f, payload.handle); - hash_request(f, payload.request); - hash_birth(f, payload.birth); + hash_definition(f, payload.definition); + hash_remaining_projection(f, payload.remaining); + hash_authority(f, payload.authority); + hash_cursor(f, payload.cursor); } else if constexpr (std::is_same_v) { f.u(9); - hash_handle(f, payload.handle); - hash_request(f, payload.request); - hash_birth(f, payload.birth); f.u(static_cast(payload.reason)); + hash_definition(f, payload.definition); + hash_remaining_projection(f, payload.remaining); + hash_authority(f, payload.authority); + hash_cursor(f, payload.cursor); } else if constexpr (std::is_same_v) { f.u(10); - hash_handle(f, payload.handle); - hash_request(f, payload.request); - hash_birth(f, payload.birth); - f.i(payload.effective_time_ms); - f.i(payload.interval_open_ms); - f.i(payload.interval_last_traded_close_ms); - f.i(payload.interval_index); + hash_definition(f, payload.definition); f.d(payload.raw_price); f.d(payload.resolved_price); f.d(payload.current_ticket); f.u(payload.first_trade_index); f.u(payload.closed_trade_count); f.u(payload.opened_lot_incarnation); - f.u(payload.provenance); + f.d(payload.closed_units); + f.d(payload.opened_units); + f.d(payload.filled_working); + hash_remaining_projection(f, payload.remaining_before); + hash_remaining_projection(f, payload.remaining_after); + hash_allowance(f, payload.allowance_before); + hash_allowance(f, payload.allowance_after); + f.b(payload.terminal); + f.b(payload.terminal_reason.has_value()); + if (payload.terminal_reason) { + f.u(static_cast(*payload.terminal_reason)); + } + f.i(payload.cycle_before); + f.i(payload.cycle_after); + hash_scope(f, payload.scope); + hash_cursor(f, payload.cursor); + } else if constexpr (std::is_same_v) { + f.u(11); + hash_definition(f, payload.definition); + f.i(payload.cycle); + f.u(static_cast(payload.side)); + hash_cursor(f, payload.cursor); + hash_authority(f, payload.before); + hash_authority(f, payload.after); + } else if constexpr (std::is_same_v) { + f.u(12); + hash_definition(f, payload.definition); + f.u(static_cast(payload.kind)); + hash_trigger_state(f, payload.before); + hash_trigger_state(f, payload.after); + f.d(payload.reached_price); + hash_cursor(f, payload.cursor); + } else if constexpr (std::is_same_v) { + f.u(13); + hash_definition(f, payload.definition); + hash_event_id(f, payload.cause); + hash_handle(f, payload.recipient); + f.u(static_cast(payload.effect)); + f.d(payload.requested_delta); + f.d(payload.actual_deduction); + f.d(payload.before.q); + hash_remaining_projection(f, payload.after); + } else if constexpr (std::is_same_v) { + f.u(14); + hash_definition(f, payload.definition); + hash_event_id(f, payload.cause); + hash_handle(f, payload.recipient); + f.u(static_cast(payload.effect)); + f.d(payload.deferred_delta); + hash_pending(f, payload.pending_before); + f.d(payload.pending_after.total); + f.u(payload.pending_after.count); + hash_event_id(f, payload.pending_after.tail_receipt); + f.b(payload.previous_pending_receipt.has_value()); + if (payload.previous_pending_receipt) { + hash_event_id(f, *payload.previous_pending_receipt); + } + } else if constexpr (std::is_same_v) { + f.u(15); + hash_definition(f, payload.definition); + hash_event_id(f, payload.source); + f.d(payload.source_units); + f.u(payload.prior_adjustment_ids.size()); + for (const auto& id : payload.prior_adjustment_ids) hash_event_id(f, id); + f.d(payload.pending_total); + f.d(payload.effective_deduction); + hash_remaining_projection(f, payload.remaining); + } else if constexpr (std::is_same_v) { + f.u(16); + hash_definition(f, payload.definition); + hash_authority(f, payload.before); + hash_authority(f, payload.after); + f.u(payload.enrollment.index()); + if (const auto* from_cmd = std::get_if(&payload.enrollment)) { + hash_event_id(f, from_cmd->accepted); + } + if (const auto* from_app = std::get_if(&payload.enrollment)) { + hash_event_id(f, from_app->cause); + hash_cursor(f, from_app->cursor); + } + f.b(payload.quantity_resolution.has_value()); + if (payload.quantity_resolution) hash_event_id(f, *payload.quantity_resolution); + } else { + static_assert(!sizeof(T), "unhashed native command event"); } }, event); } @@ -224,10 +471,8 @@ CommissionType fee_to_commission(NativeFeeKind kind) { return CommissionType::PERCENT; } -bool buying_action(const native_order::Request& request, PositionSide side) { - if (const auto* t = std::get_if(&request.action)) - return t->signed_units > 0.0; - return side == PositionSide::SHORT; +uint64_t command_ordinal(const native_order::CommandEvent& event) { + return std::visit([](const auto& payload) { return payload.ordinal; }, event); } } // namespace @@ -418,10 +663,12 @@ uint64_t NativeExecutionConsumer::continuation_hash() const noexcept { f.u(requests_.identity().run_number); f.u(requests_.live().size()); for (const auto& live : requests_.live()) { - hash_handle(f, live.handle); - hash_request(f, live.request); - hash_birth(f, live.birth); - hash_optional_handle(f, live.predecessor); + hash_definition(f, live.definition); + hash_remaining(f, live.remaining); + hash_authority(f, live.authority); + hash_trigger_state(f, live.trigger_state); + hash_allowance(f, live.allowance); + hash_pending(f, live.pending); } sync_history_digest(); if (driver_digest_.count != driver_log_.size()) { @@ -794,27 +1041,129 @@ void NativeExecutionConsumer::apply_excursion(BacktestEngine& engine, double pri } } -double NativeExecutionConsumer::resolve_price(const native_order::Request& request, - double raw) const { - const auto* spec = spec_ptr(); - if (!spec || !std::isfinite(raw)) return std::numeric_limits::quiet_NaN(); - (void)request; - return raw; +native_order::DriverEligibilityClass NativeExecutionConsumer::classify_driver( + const NativeDriverPoint& point, bool continuous) const noexcept { + using Class = native_order::DriverEligibilityClass; + switch (point.coordinate.provenance) { + case NativePriceProvenance::ObservedPrint: + return Class::ObservedPrint; + case NativePriceProvenance::CarriedOpen: + return Class::CarriedOpen; + case NativePriceProvenance::AfterCalculationClose: + return input_mode_ == InputMode::ObservedTicks + ? Class::TickAfterCalculation + : Class::ConfirmedAfterCalculationClose; + case NativePriceProvenance::ModeledOHLCOpen: + return Class::ConfirmedOpen; + case NativePriceProvenance::Confirmed: + return continuous ? Class::ConfirmedExcursion : Class::ConfirmedOpen; + case NativePriceProvenance::ModeledOHLCClose: + return continuous ? Class::ConfirmedExcursion : Class::ConfirmedAfterCalculationClose; + case NativePriceProvenance::PartialFinalized: + case NativePriceProvenance::Calculation: + return Class::ConfirmedAfterCalculationClose; + } + return Class::ObservedPrint; +} + +native_order::MatchCursor NativeExecutionConsumer::make_cursor( + const NativeDriverPoint& point, double t) const noexcept { + native_order::MatchCursor cursor; + cursor.point = point.coordinate; + cursor.t = t; + return cursor; +} + +native_order::PositionIdentity NativeExecutionConsumer::read_position( + const BacktestEngine& engine) const { + if (engine.position_side_ == PositionSide::FLAT || engine.pyramid_entries_.empty() + || engine.position_cycle_seq_ <= 0) { + return native_order::PositionFlat{}; + } + const native_order::Side side = engine.position_side_ == PositionSide::SHORT + ? native_order::Side::Short : native_order::Side::Long; + return native_order::PositionNonflat{engine.position_cycle_seq_, side}; +} + +native_order::OpeningObservation NativeExecutionConsumer::read_opening( + const BacktestEngine& engine, const native_order::RequestHandle& opening, + int64_t cycle) const { + native_order::OpeningObservation out; + out.queried_opening = opening; + out.queried_cycle = cycle; + out.current_position = read_position(engine); + out.has_live_matching_lot = false; + if (opening.run != requests_.identity()) return out; + if (cycle != engine.position_cycle_seq_ || engine.position_cycle_seq_ <= 0) return out; + if (engine.position_side_ == PositionSide::FLAT) return out; + for (const auto& lot : engine.pyramid_entries_) { + if (lot.entry_incarnation == opening.incarnation) { + out.has_live_matching_lot = true; + break; + } + } + return out; +} + +native_order::TargetObservation NativeExecutionConsumer::read_target( + const BacktestEngine& engine, const native_order::LiveRequest* live) const { + native_order::TargetObservation out; + out.current_position = read_position(engine); + if (!live) return out; + if (const auto* opening = std::get_if(&live->authority)) { + out.opening = read_opening(engine, opening->opening, opening->cycle); + } else if (const auto* bind = std::get_if(&live->request().owner)) { + out.opening = read_opening(engine, bind->opening, bind->cycle); + } + return out; } -bool NativeExecutionConsumer::admit_opening( - const BacktestEngine& engine, +native_order::CommandContext NativeExecutionConsumer::make_command_context( + const BacktestEngine& engine, const native_order::Request& request, + native_order::CommandSurface surface) const { + native_order::CommandContext ctx; + ctx.decision_time_ms = decision_floor(); + if (const auto* spec = spec_ptr()) ctx.quantity_grid = spec->quantity_grid; + ctx.surface = surface; + if (const auto* bind = std::get_if(&request.owner)) { + ctx.opening = read_opening(engine, bind->opening, bind->cycle); + } + return ctx; +} + +void NativeExecutionConsumer::refresh_target_scalars( + const BacktestEngine& engine, native_order::TargetObservation& target) const noexcept { + target.current_position = read_position(engine); + if (!target.opening) return; + target.opening->current_position = target.current_position; + target.opening->has_live_matching_lot = false; + const auto& opening = target.opening->queried_opening; + if (opening.run != requests_.identity()) return; + if (target.opening->queried_cycle != engine.position_cycle_seq_ + || engine.position_cycle_seq_ <= 0) { + return; + } + if (engine.position_side_ == PositionSide::FLAT) return; + for (const auto& lot : engine.pyramid_entries_) { + if (lot.entry_incarnation == opening.incarnation) { + target.opening->has_live_matching_lot = true; + break; + } + } +} + +bool NativeExecutionConsumer::admit_opening_inspect( + const BacktestEngine& engine, double resolved_price, const execution::SettlementInspection& inspect, native_order::MatchRejectReason* reason) const { const auto* spec = spec_ptr(); if (!spec || !inspect.would_open) return true; const auto mask = static_cast(spec->allowed_open_directions); - const bool want_short = inspect.incoming_short; - if (want_short && (mask & 2u) == 0) { + if (inspect.incoming_short && (mask & 2u) == 0) { if (reason) *reason = native_order::MatchRejectReason::OpeningDirection; return false; } - if (!want_short && (mask & 1u) == 0) { + if (!inspect.incoming_short && (mask & 1u) == 0) { if (reason) *reason = native_order::MatchRejectReason::OpeningDirection; return false; } @@ -827,190 +1176,810 @@ bool NativeExecutionConsumer::admit_opening( return false; } if (spec->initial_margin_fraction) { - const double mark = engine.marked_equity(std::abs(inspect.opened_units) > 0 - ? engine.syminfo_.mintick : engine.syminfo_.mintick); - (void)mark; - const double equity = engine.marked_equity( - inspect.resulting_abs_units == 0.0 ? spec->price_tick : spec->price_tick); - (void)equity; - } - if (spec->initial_margin_fraction) { - // Compare resulting notional * fraction to marked equity at resolved - // price less this execution's current charge. Caller stamped fill price - // into inspect via the quote; recompute mark at the inspection's implied - // resulting book using marked_equity on the current book is wrong for a - // crossing. Use inspect facts: marked_equity(resolved) - ticket. - // The consumer match_point passes the resolved price through Fill. + const double equity = engine.marked_equity(resolved_price) - inspect.current_ticket; + const double required = inspect.resulting_abs_notional * *spec->initial_margin_fraction; + if (!std::isfinite(equity) || !std::isfinite(required) || required > equity) { + if (reason) *reason = native_order::MatchRejectReason::InitialMargin; + return false; + } } return true; } -void NativeExecutionConsumer::terminal_no_effect(BacktestEngine& engine, std::size_t live_index) { - (void)engine; - const auto live = requests_.live()[live_index]; - const uint64_t ordinal = native_order::TerminalCommit::usable_ordinal( - requests_, next_timeline_ordinal_); - native_order::TerminalCommit::reserve_history(requests_); - native_order::NoEffectEvent event{ordinal, live.handle, live.request, live.birth}; - native_order::TerminalCommit::install(requests_, live_index, native_order::CommandEvent{std::move(event)}); +void NativeExecutionConsumer::fail_preparation( + BacktestEngine& engine, const native_order::PreparationError& error, + NativeFailureOperation operation) { + NativeFailureCode code = NativeFailureCode::Contract; + switch (error.code) { + case native_order::CoreFailure::NonrepresentableQuantity: + case native_order::CoreFailure::UnrepresentableReservation: + case native_order::CoreFailure::InvalidProposal: + case native_order::CoreFailure::InvalidScope: + code = NativeFailureCode::SettlementFailure; + break; + case native_order::CoreFailure::MissingObservation: + case native_order::CoreFailure::ObservationMismatch: + code = NativeFailureCode::SettlementFailure; + break; + default: + code = NativeFailureCode::Contract; + break; + } + NativeFailure failure{code, operation, error.cause.ordinal, + static_cast(error.code)}; + failure.context = native_failure_context_in_run( + requests_.identity(), &error.cause, &error.target, nullptr); + fail(engine, failure); + render(engine, "native working-request preparation failed"); +} + +void NativeExecutionConsumer::catch_up_timeline() noexcept { + if (requests_.history().empty()) return; + const uint64_t last = command_ordinal(requests_.history().back()); + if (last >= next_timeline_ordinal_) next_timeline_ordinal_ = last + 1; +} + +bool NativeExecutionConsumer::install_mutation( + BacktestEngine& engine, native_order::PreparedMutation&& prepared, + NativeFailureOperation operation, uint64_t ordinal) { + if (!prepared) { + fail(engine, NativeFailure{NativeFailureCode::Contract, operation, ordinal}); + render(engine, "native mutation install missing preparation"); + return false; + } + const auto result = requests_.install_mutation(std::move(prepared)); + if (const auto* err = std::get_if(&result)) { + fail(engine, NativeFailure{NativeFailureCode::Contract, operation, ordinal, + static_cast(*err)}); + render(engine, "native mutation install failed"); + return false; + } sync_history_digest(); - ++next_timeline_ordinal_; + catch_up_timeline(); + return true; } -void NativeExecutionConsumer::terminal_reject(BacktestEngine& engine, std::size_t live_index, - native_order::MatchRejectReason reason) { - (void)engine; - const auto live = requests_.live()[live_index]; - const uint64_t ordinal = native_order::TerminalCommit::usable_ordinal( - requests_, next_timeline_ordinal_); - native_order::TerminalCommit::reserve_history(requests_); - native_order::MatchRejectedEvent event{ordinal, live.handle, live.request, live.birth, reason}; - native_order::TerminalCommit::install(requests_, live_index, native_order::CommandEvent{std::move(event)}); +bool NativeExecutionConsumer::install_execution( + BacktestEngine& engine, native_order::PreparedExecution&& prepared, + const native_order::CommittedExecutionFacts& facts, uint64_t ordinal) { + if (!prepared) { + fail(engine, NativeFailure{NativeFailureCode::Contract, + NativeFailureOperation::Settlement, ordinal}); + render(engine, "native execution install missing preparation"); + return false; + } + const auto result = requests_.install_execution(std::move(prepared), facts); + if (const auto* err = std::get_if(&result)) { + fail(engine, NativeFailure{NativeFailureCode::Contract, + NativeFailureOperation::Settlement, ordinal, + static_cast(*err)}); + render(engine, "native execution install failed after settlement"); + return false; + } sync_history_digest(); - ++next_timeline_ordinal_; + catch_up_timeline(); + return true; +} + +void NativeExecutionConsumer::drain_dependency_queue( + BacktestEngine& engine, + std::vector> seeds, + NativeFailureOperation operation) { + if (failed() || seeds.empty()) return; + try { + struct Item { + uint64_t cause_ordinal = 0; + native_order::EventId cause; + native_order::RequestHandle child; + }; + std::vector queue; + auto less_item = [](const Item& a, const Item& b) { + if (a.cause_ordinal != b.cause_ordinal) return a.cause_ordinal < b.cause_ordinal; + return a.child.incarnation < b.child.incarnation; + }; + auto enqueue_children = [&](const native_order::EventId& cause, + const native_order::RequestHandle& parent, + std::size_t sort_from) { + auto children = requests_.waiting_children(parent); + for (auto& child : children) { + queue.push_back(Item{cause.ordinal, cause, std::move(child)}); + } + if (sort_from < queue.size()) { + std::sort(queue.begin() + static_cast(sort_from), queue.end(), + less_item); + } + }; + for (auto& seed : seeds) { + enqueue_children(seed.first, seed.second, queue.size()); + } + std::sort(queue.begin(), queue.end(), less_item); + for (std::size_t i = 0; i < queue.size() && !failed(); ++i) { + const native_order::EventId cause = queue[i].cause; + const native_order::RequestHandle child = queue[i].child; + auto prep = requests_.prepare_parent_terminal(cause, child, next_timeline_ordinal_); + if (const auto* err = std::get_if(&prep)) { + fail_preparation(engine, *err, operation); + return; + } + if (std::holds_alternative(prep)) continue; + auto* mutation = std::get_if(&prep); + if (!mutation || !install_mutation(engine, std::move(*mutation), operation, + cause.ordinal)) { + return; + } + if (requests_.find_live(child) == nullptr && !requests_.history().empty()) { + native_order::EventId next_cause = cause; + next_cause.ordinal = command_ordinal(requests_.history().back()); + enqueue_children(next_cause, child, i + 1); + } + } + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, operation, + seeds.empty() ? 0 : seeds.front().first.ordinal}); + render(engine, e.what()); + } +} + +void NativeExecutionConsumer::drain_parent_terminal( + BacktestEngine& engine, const native_order::EventId& cause, + const native_order::RequestHandle& parent, + NativeFailureOperation operation) { + std::vector> seeds; + try { + seeds.push_back({cause, parent}); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, operation, cause.ordinal}); + render(engine, e.what()); + return; + } + drain_dependency_queue(engine, std::move(seeds), operation); +} + +void NativeExecutionConsumer::drain_after_applied( + BacktestEngine& engine, const native_order::EventId& applied, + const native_order::RequestHandle& filler) { + try { + std::vector> seeds; + auto note_absent = [&](const native_order::RequestHandle& parent) { + if (requests_.find_live(parent) != nullptr || requests_.history().empty()) return; + native_order::EventId cause = applied; + cause.ordinal = command_ordinal(requests_.history().back()); + seeds.push_back({std::move(cause), parent}); + }; + + const auto recipients = requests_.group_recipients(applied); + for (const auto& recipient : recipients) { + if (failed()) return; + auto prep = requests_.prepare_group_effect(applied, recipient, next_timeline_ordinal_); + if (const auto* err = std::get_if(&prep)) { + fail_preparation(engine, *err, NativeFailureOperation::Settlement); + return; + } + if (std::holds_alternative(prep)) continue; + auto* mutation = std::get_if(&prep); + if (!mutation || !install_mutation(engine, std::move(*mutation), + NativeFailureOperation::Settlement, + applied.ordinal)) { + return; + } + note_absent(recipient); + } + + const auto children = requests_.waiting_children(filler); + for (const auto& child : children) { + if (failed()) return; + std::optional observation; + if (requests_.find_live(child)) { + observation = read_opening(engine, filler, engine.position_cycle_seq_); + } + auto prep = requests_.prepare_owner_applied(applied, child, observation, + next_timeline_ordinal_); + if (const auto* err = std::get_if(&prep)) { + fail_preparation(engine, *err, NativeFailureOperation::Settlement); + return; + } + if (std::holds_alternative(prep)) continue; + auto* mutation = std::get_if(&prep); + if (!mutation || !install_mutation(engine, std::move(*mutation), + NativeFailureOperation::Settlement, + applied.ordinal)) { + return; + } + note_absent(child); + } + + const auto bound = requests_.bound_close_handles(); + for (const auto& handle : bound) { + if (failed()) return; + const auto* live = requests_.find_live(handle); + auto target = read_target(engine, live); + auto prep = requests_.prepare_bound_expiry(applied, handle, target, + next_timeline_ordinal_); + if (const auto* err = std::get_if(&prep)) { + fail_preparation(engine, *err, NativeFailureOperation::Settlement); + return; + } + if (std::holds_alternative(prep)) continue; + auto* mutation = std::get_if(&prep); + if (!mutation || !install_mutation(engine, std::move(*mutation), + NativeFailureOperation::Settlement, + applied.ordinal)) { + return; + } + note_absent(handle); + } + + seeds.push_back({applied, filler}); + drain_dependency_queue(engine, std::move(seeds), NativeFailureOperation::Settlement); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Settlement, applied.ordinal}); + render(engine, e.what()); + } +} + +void NativeExecutionConsumer::observe_trails( + BacktestEngine& engine, const native_order::MatchCursor& cursor, double price) { + if (!std::isfinite(price) || failed()) return; + std::vector handles; + handles.reserve(requests_.live().size()); + for (const auto& live : requests_.live()) handles.push_back(live.handle()); + for (const auto& handle : handles) { + if (failed()) return; + const auto* live = requests_.find_live(handle); + if (!live) continue; + const auto* track = std::get_if(&live->trigger_state); + if (!track) continue; + const bool buy = requests_.working_is_buy(*live); + if (!native_matching::trail_best_improves(track->best, price, buy)) continue; + native_order::Preparation prep; + try { + prep = requests_.prepare_trigger( + handle, native_order::ObserveTrailExtremum{cursor, price}, next_timeline_ordinal_); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Settlement, cursor.point.ordinal}); + render(engine, e.what()); + return; + } + if (const auto* err = std::get_if(&prep)) { + fail_preparation(engine, *err, NativeFailureOperation::Settlement); + return; + } + if (std::holds_alternative(prep)) continue; + auto* mutation = std::get_if(&prep); + if (!mutation || !install_mutation(engine, std::move(*mutation), + NativeFailureOperation::Settlement, + cursor.point.ordinal)) { + return; + } + } } void NativeExecutionConsumer::match_point(BacktestEngine& engine, const NativeDriverPoint& point) { - if (!point.matching || failed()) return; - std::vector order; - order.reserve(requests_.live().size()); - for (const auto& live : requests_.live()) order.push_back(live.handle); + match_discrete(engine, point); +} + +void NativeExecutionConsumer::match_discrete(BacktestEngine& engine, const NativeDriverPoint& point) { + match_path(engine, point, false, point.raw_price, point.raw_price); +} + +void NativeExecutionConsumer::match_segment( + BacktestEngine& engine, const NativeDriverPoint& dest, double from_price) { + match_path(engine, dest, true, from_price, dest.raw_price); +} + +void NativeExecutionConsumer::match_path( + BacktestEngine& engine, const NativeDriverPoint& point, + bool continuous, double from_price, double to_price) { + if (failed()) return; const auto* spec = spec_ptr(); if (!spec) return; - for (const auto& handle : order) { - if (failed()) return; - std::size_t live_index = 0; - bool found = false; - for (std::size_t i = 0; i < requests_.live().size(); ++i) { - if (requests_.live()[i].handle == handle) { - live_index = i; - found = true; - break; + const auto driver_class = classify_driver(point, continuous); + const uint64_t P = point.coordinate.ordinal; + double t_cursor = 0.0; + native_order::MatchCursor path_cursor = make_cursor(point, t_cursor); + observe_trails(engine, path_cursor, native_matching::price_at(from_price, to_price, t_cursor)); + + enum class Kind : std::uint8_t { + Evaluate = 0, + ActivateStop = 1, + ActivateStopLimit = 2, + BeginTrail = 3, + ActivateTrail = 4, + Fill = 5, + }; + struct Candidate { + native_order::RequestHandle handle; + uint64_t incarnation = 0; + double t = 0.0; + double price = 0.0; + Kind kind = Kind::Fill; + }; + + auto cause_floor = [&](const native_order::LiveRequest& live) { + double t_min = t_cursor; + if (const auto* armed = std::get_if(&live.authority)) { + if (armed->cause_cursor.point.ordinal == P) t_min = std::max(t_min, armed->cause_cursor.t); + } + if (const auto* opening = std::get_if(&live.authority)) { + if (const auto* from = std::get_if(&opening->enrollment)) { + if (from->cursor.point.ordinal == P) t_min = std::max(t_min, from->cursor.t); } } - if (!found) continue; - const auto& live = requests_.live()[live_index]; - if (!native_order::point_eligible(live, point.coordinate.ordinal, - point.coordinate.effective_time_ms)) { + return t_min; + }; + + auto needs_evaluation = [&](const native_order::LiveRequest& live, + const native_order::EligibilityFacts& facts) { + if (facts.needs_close_bind) return true; + if (std::holds_alternative(live.allowance)) return true; + if (const auto* units = std::get_if(&live.allowance)) { + return units->point_ordinal != P; + } + if (const auto* all = std::get_if(&live.allowance)) { + return all->point_ordinal != P; + } + return false; + }; + + std::set> skipped; + double skip_t = t_cursor; + auto skip_key = [](uint64_t incarnation, Kind kind) { + return std::pair{incarnation, static_cast(kind)}; + }; + + while (!failed()) { + if (skip_t != t_cursor) { + skipped.clear(); + skip_t = t_cursor; + } + native_order::EvaluationContext eval; + eval.cursor = make_cursor(point, t_cursor); + eval.driver_class = driver_class; + eval.existing_matching_bit = point.matching; + std::optional winner; + std::vector snapshot; + snapshot.reserve(requests_.live().size()); + for (const auto& live : requests_.live()) snapshot.push_back(live.handle()); + for (const auto& handle : snapshot) { + const auto* live = requests_.find_live(handle); + if (!live) continue; + const auto facts = requests_.eligibility_facts(*live, eval); + if (!facts.birth_ok || facts.waiting || !facts.driver_ok) continue; + const double t_min = cause_floor(*live); + if (t_min > 1.0) continue; + Candidate row; + row.handle = handle; + row.incarnation = handle.incarnation; + if (needs_evaluation(*live, facts)) { + row.t = t_min; + row.price = native_matching::price_at(from_price, to_price, t_min); + row.kind = Kind::Evaluate; + } else { + const bool buy = requests_.working_is_buy(*live); + const auto& trigger = live->request().trigger; + const auto& state = live->trigger_state; + std::optional hit; + Kind kind = Kind::Fill; + if (std::holds_alternative(state)) { + const auto* stop = std::get_if(&trigger); + if (!stop) continue; + hit = native_matching::first_region_entry( + from_price, to_price, t_min, stop->price, !buy, true); + kind = Kind::ActivateStop; + } else if (std::holds_alternative(state)) { + const auto* sl = std::get_if(&trigger); + if (!sl) continue; + hit = native_matching::first_region_entry( + from_price, to_price, t_min, sl->stop, !buy, true); + kind = Kind::ActivateStopLimit; + } else if (std::holds_alternative(state)) { + const auto* trail = std::get_if(&trigger); + if (!trail) continue; + if (!trail->arm_price) { + hit = native_matching::GeometricHit{t_min, + native_matching::price_at(from_price, to_price, t_min)}; + } else { + hit = native_matching::first_region_entry( + from_price, to_price, t_min, *trail->arm_price, buy, true); + } + kind = Kind::BeginTrail; + } else if (const auto* track = std::get_if(&state)) { + const auto* trail = std::get_if(&trigger); + if (!trail) continue; + double stop = 0.0; + if (!native_matching::checked_trail_stop(track->best, trail->offset, buy, &stop)) { + fail(engine, NativeFailure{NativeFailureCode::SettlementFailure, + NativeFailureOperation::Settlement, P}); + render(engine, "native trailing offset is not representable"); + return; + } + hit = native_matching::trail_stop_hit( + from_price, to_price, t_min, track->best, trail->offset, buy); + kind = Kind::ActivateTrail; + } else if (std::holds_alternative(state) + || std::holds_alternative(state)) { + double level = 0.0; + if (const auto* limit = std::get_if(&trigger)) { + level = limit->price; + } else if (const auto* sl = std::get_if(&trigger)) { + level = sl->limit; + } else { + continue; + } + hit = native_matching::first_region_entry( + from_price, to_price, t_min, level, buy, true); + kind = Kind::Fill; + } else if (std::holds_alternative(state) + || std::holds_alternative(state) + || std::holds_alternative(state)) { + if (!facts.ready_to_match) continue; + if (std::holds_alternative(state) && continuous) { + continue; + } + hit = native_matching::GeometricHit{ + t_min, native_matching::price_at(from_price, to_price, t_min)}; + kind = Kind::Fill; + } else { + continue; + } + if (!hit) continue; + if (kind == Kind::Fill) { + if (const auto* units = std::get_if(&live->allowance)) { + if (units->point_ordinal == P && units->left == 0.0) continue; + } + if (std::holds_alternative(live->remaining)) { + continue; + } + } + row.t = hit->t; + row.price = hit->price; + row.kind = kind; + } + if (!std::isfinite(row.price) || row.t < t_cursor || row.t > 1.0) continue; + if (skipped.count(skip_key(row.incarnation, row.kind))) continue; + if (!winner + || row.t < winner->t + || (row.t == winner->t && row.incarnation < winner->incarnation) + || (row.t == winner->t && row.incarnation == winner->incarnation + && static_cast(row.kind) < static_cast(winner->kind))) { + winner = row; + } + } + if (!winner) break; + if (continuous && winner->t > t_cursor) { + apply_excursion(engine, winner->price); + path_cursor = make_cursor(point, winner->t); + observe_trails(engine, path_cursor, winner->price); + } + t_cursor = winner->t; + path_cursor = make_cursor(point, t_cursor); + eval.cursor = path_cursor; + const auto* live = requests_.find_live(winner->handle); + if (!live) continue; + if (winner->kind == Kind::Evaluate) { + native_order::Preparation prep; + try { + prep = requests_.prepare_evaluation( + winner->handle, eval, read_target(engine, live), next_timeline_ordinal_); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Settlement, P}); + render(engine, e.what()); + return; + } + if (const auto* err = std::get_if(&prep)) { + fail_preparation(engine, *err, NativeFailureOperation::Settlement); + return; + } + if (std::holds_alternative(prep)) { + skipped.insert(skip_key(winner->incarnation, winner->kind)); + continue; + } + auto* mutation = std::get_if(&prep); + if (!mutation || !install_mutation(engine, std::move(*mutation), + NativeFailureOperation::Settlement, P)) { + return; + } + if (requests_.find_live(winner->handle) == nullptr && !requests_.history().empty()) { + try { + drain_parent_terminal( + engine, + native_order::EventId{winner->handle.run, + command_ordinal(requests_.history().back())}, + winner->handle, NativeFailureOperation::Settlement); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Settlement, P}); + render(engine, e.what()); + return; + } + } continue; } + if (winner->kind != Kind::Fill) { + native_order::TriggerTransition transition; + if (winner->kind == Kind::ActivateStop) { + transition = native_order::ActivateStop{path_cursor, winner->price}; + } else if (winner->kind == Kind::ActivateStopLimit) { + transition = native_order::ActivateStopLimit{path_cursor, winner->price}; + } else if (winner->kind == Kind::BeginTrail) { + transition = native_order::BeginTrailTracking{path_cursor, winner->price}; + } else { + transition = native_order::ActivateTrail{path_cursor, winner->price}; + } + native_order::Preparation prep; + try { + prep = requests_.prepare_trigger(winner->handle, transition, next_timeline_ordinal_); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Settlement, P}); + render(engine, e.what()); + return; + } + if (const auto* err = std::get_if(&prep)) { + fail_preparation(engine, *err, NativeFailureOperation::Settlement); + return; + } + if (std::holds_alternative(prep)) { + skipped.insert(skip_key(winner->incarnation, winner->kind)); + continue; + } + auto* mutation = std::get_if(&prep); + if (!mutation || !install_mutation(engine, std::move(*mutation), + NativeFailureOperation::Settlement, P)) { + return; + } + continue; + } + + live = requests_.find_live(winner->handle); + if (!live) continue; + const bool buy = requests_.working_is_buy(*live); const double slip = static_cast(spec->slippage_ticks) * spec->price_tick; - const bool buy = buying_action(live.request, engine.position_side_); - double resolved = buy ? point.raw_price + slip : point.raw_price - slip; + double resolved = native_matching::apply_slippage(winner->price, slip, buy); + const auto& trigger = live->request().trigger; + // Limit protection applies only after finite slippage arithmetic. + // min/max must not turn an overflowed price into an executable limit. + if (std::isfinite(resolved) + && (std::holds_alternative(live->trigger_state) + || std::holds_alternative(live->trigger_state))) { + double level = 0.0; + if (const auto* limit = std::get_if(&trigger)) level = limit->price; + else if (const auto* sl = std::get_if(&trigger)) level = sl->limit; + resolved = native_matching::protect_limit(resolved, level, buy); + } if (!std::isfinite(resolved) || resolved <= 0.0) { - terminal_reject(engine, live_index, native_order::MatchRejectReason::NonpositivePrice); + native_order::Preparation prep; + try { + prep = requests_.prepare_match_rejected( + winner->handle, eval, native_order::MatchRejectReason::NonpositivePrice, + next_timeline_ordinal_); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Settlement, P}); + render(engine, e.what()); + return; + } + if (const auto* err = std::get_if(&prep)) { + fail_preparation(engine, *err, NativeFailureOperation::Settlement); + return; + } + if (auto* mutation = std::get_if(&prep)) { + if (!install_mutation(engine, std::move(*mutation), + NativeFailureOperation::Settlement, P)) { + return; + } + try { + drain_parent_terminal( + engine, + native_order::EventId{winner->handle.run, + command_ordinal(requests_.history().back())}, + winner->handle, NativeFailureOperation::Settlement); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Settlement, P}); + render(engine, e.what()); + return; + } + } continue; } - execution::Fill probe{resolved, live.request.label, live.request.comment, - live.handle.incarnation, std::nullopt}; + + execution::Action physical = execution::Flatten{}; + execution::CloseScope scope = execution::Book{}; + if (const auto* opening = std::get_if(&live->authority)) { + if (opening->opening.run != requests_.identity()) { + fail(engine, NativeFailure{NativeFailureCode::Contract, + NativeFailureOperation::Settlement, P}); + render(engine, "native opening scope identity mismatch"); + return; + } + scope = execution::OpeningExposure{opening->opening.incarnation, opening->cycle}; + } + if (std::holds_alternative(live->remaining) + || std::holds_alternative(live->request().intent)) { + physical = execution::Flatten{}; + } else { + double qty = 0.0; + if (const auto* units = std::get_if(&live->remaining)) { + qty = units->q; + } + if (const auto* allow = std::get_if(&live->allowance)) { + if (allow->point_ordinal == P) qty = std::min(qty, allow->left); + } + if (!std::isfinite(qty) || qty <= 0.0) { + skipped.insert(skip_key(winner->incarnation, winner->kind)); + continue; + } + if (std::holds_alternative(live->request().intent)) { + physical = native_order::Transact{buy ? qty : -qty}; + } else { + physical = order_action::Reduce{qty}; + } + } + + execution::Fill probe{resolved, live->request().label, live->request().comment, + live->handle().incarnation, std::nullopt}; execution::SettlementInspection inspect; try { - inspect = engine.inspect_native_settlement(live.request.action, probe); + inspect = engine.inspect_native_settlement_scoped(physical, probe, scope); } catch (const std::exception& e) { fail(engine, NativeFailure{NativeFailureCode::Allocation, - NativeFailureOperation::Settlement, - point.coordinate.ordinal}); + NativeFailureOperation::Settlement, P}); render(engine, e.what()); return; } if (inspect.status == execution::Status::NoEffect) { - terminal_no_effect(engine, live_index); + native_order::Preparation prep; + try { + prep = requests_.prepare_no_effect(winner->handle, eval, next_timeline_ordinal_); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Settlement, P}); + render(engine, e.what()); + return; + } + if (const auto* err = std::get_if(&prep)) { + fail_preparation(engine, *err, NativeFailureOperation::Settlement); + return; + } + if (std::holds_alternative(prep)) { + skipped.insert(skip_key(winner->incarnation, winner->kind)); + continue; + } + if (auto* mutation = std::get_if(&prep)) { + if (!install_mutation(engine, std::move(*mutation), + NativeFailureOperation::Settlement, P)) { + return; + } + try { + drain_parent_terminal( + engine, + native_order::EventId{winner->handle.run, + command_ordinal(requests_.history().back())}, + winner->handle, NativeFailureOperation::Settlement); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Settlement, P}); + render(engine, e.what()); + return; + } + } continue; } if (inspect.status != execution::Status::Applied) { fail(engine, NativeFailure{NativeFailureCode::SettlementFailure, - NativeFailureOperation::Settlement, - point.coordinate.ordinal, + NativeFailureOperation::Settlement, P, static_cast(inspect.status)}); render(engine, "native settlement inspection failed"); return; } - if (inspect.would_open) { - native_order::MatchRejectReason reason{}; - const auto mask = static_cast(spec->allowed_open_directions); - bool admitted = true; - if (inspect.incoming_short && (mask & 2u) == 0) { - admitted = false; reason = native_order::MatchRejectReason::OpeningDirection; - } else if (!inspect.incoming_short && (mask & 1u) == 0) { - admitted = false; reason = native_order::MatchRejectReason::OpeningDirection; - } else if (spec->max_abs_units && inspect.resulting_abs_units > *spec->max_abs_units) { - admitted = false; reason = native_order::MatchRejectReason::MaxAbsUnits; - } else if (spec->max_open_lots && inspect.resulting_lot_count > *spec->max_open_lots) { - admitted = false; reason = native_order::MatchRejectReason::MaxOpenLots; - } else if (spec->initial_margin_fraction) { - const double equity = engine.marked_equity(resolved) - inspect.current_ticket; - const double required = inspect.resulting_abs_notional * *spec->initial_margin_fraction; - if (!std::isfinite(equity) || !std::isfinite(required) || required > equity) { - admitted = false; reason = native_order::MatchRejectReason::InitialMargin; - } + native_order::MatchRejectReason admit_reason{}; + if (inspect.would_open && !admit_opening_inspect(engine, resolved, inspect, &admit_reason)) { + native_order::Preparation prep; + try { + prep = requests_.prepare_match_rejected( + winner->handle, eval, admit_reason, next_timeline_ordinal_); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Settlement, P}); + render(engine, e.what()); + return; } - if (!admitted) { - terminal_reject(engine, live_index, reason); - continue; + if (const auto* err = std::get_if(&prep)) { + fail_preparation(engine, *err, NativeFailureOperation::Settlement); + return; } + if (auto* mutation = std::get_if(&prep)) { + if (!install_mutation(engine, std::move(*mutation), + NativeFailureOperation::Settlement, P)) { + return; + } + try { + drain_parent_terminal( + engine, + native_order::EventId{winner->handle.run, + command_ordinal(requests_.history().back())}, + winner->handle, NativeFailureOperation::Settlement); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Settlement, P}); + render(engine, e.what()); + return; + } + } + continue; } - execution::Fill fill = probe; - fill.commission_account = inspect.current_ticket; - execution::PhysicalExecutionContext ctx; - ctx.effective_time_ms = point.coordinate.effective_time_ms; - ctx.interval_index = point.coordinate.interval_index; - native_order::ExecutionAppliedEvent applied; - NativeAccountObservation observation; + + native_order::ExecutionProposal proposal; + proposal.cursor = path_cursor; + proposal.raw_price = winner->price; + proposal.resolved_price = resolved; + proposal.physical_action = physical; + proposal.scope = scope; + proposal.pre_fill = read_position(engine); + proposal.inspected_closed_units = inspect.closed_units; + proposal.inspected_opened_units = inspect.opened_units; + proposal.inspected_current_ticket = inspect.current_ticket; + const int64_t cycle_before = engine.position_cycle_seq_; + native_order::EventId applied_id; + native_order::TargetObservation staged_target; + native_order::Preparation prepared; try { - applied.ordinal = next_timeline_ordinal_; - applied.handle = live.handle; - applied.request = live.request; - applied.birth = live.birth; - applied.effective_time_ms = point.coordinate.effective_time_ms; - applied.interval_open_ms = point.coordinate.open_ms; - applied.interval_last_traded_close_ms = point.coordinate.last_traded_close_ms; - applied.interval_index = point.coordinate.interval_index; - applied.raw_price = point.raw_price; - applied.resolved_price = resolved; - applied.provenance = static_cast(point.coordinate.provenance); - observation.ordinal = applied.ordinal; - observation.effective_time_ms = applied.effective_time_ms; - native_order::TerminalCommit::usable_ordinal(requests_, next_timeline_ordinal_); - native_order::TerminalCommit::reserve_history(requests_); + applied_id = native_order::EventId{winner->handle.run, next_timeline_ordinal_}; + staged_target = read_target(engine, live); + prepared = requests_.prepare_execution(winner->handle, proposal, next_timeline_ordinal_); account_log_.reserve(account_log_.size() + 1); } catch (const std::exception& e) { fail(engine, NativeFailure{NativeFailureCode::Allocation, - NativeFailureOperation::Settlement, - point.coordinate.ordinal}); + NativeFailureOperation::Settlement, P}); render(engine, e.what()); return; } + if (const auto* err = std::get_if(&prepared)) { + fail_preparation(engine, *err, NativeFailureOperation::Settlement); + return; + } + if (std::holds_alternative(prepared)) { + skipped.insert(skip_key(winner->incarnation, winner->kind)); + continue; + } + auto* token = std::get_if(&prepared); + if (!token) continue; + execution::Fill fill = probe; + fill.commission_account = inspect.current_ticket; + execution::PhysicalExecutionContext ctx; + ctx.effective_time_ms = point.coordinate.effective_time_ms; + ctx.interval_index = point.coordinate.interval_index; execution::Result settled; try { - settled = engine.settle_native_execution_at(live.request.action, fill, ctx); + settled = engine.settle_native_execution_scoped_at(physical, fill, ctx, scope); } catch (const std::exception& e) { fail(engine, NativeFailure{NativeFailureCode::SettlementFailure, - NativeFailureOperation::Settlement, - point.coordinate.ordinal}); + NativeFailureOperation::Settlement, P}); render(engine, e.what()); return; } - if (settled.status != execution::Status::Applied - && settled.status != execution::Status::NoEffect) { + if (settled.status != execution::Status::Applied) { fail(engine, NativeFailure{NativeFailureCode::SettlementFailure, - NativeFailureOperation::Settlement, - point.coordinate.ordinal, + NativeFailureOperation::Settlement, P, static_cast(settled.status)}); render(engine, "native settlement commit failed"); return; } - if (settled.status == execution::Status::NoEffect) { - terminal_no_effect(engine, live_index); - continue; - } - applied.current_ticket = settled.current_ticket; - applied.first_trade_index = settled.first_trade_index; - applied.closed_trade_count = settled.closed_trade_count; - applied.opened_lot_incarnation = settled.opened_lot_incarnation; - native_order::TerminalCommit::install(requests_, live_index, - native_order::CommandEvent{std::move(applied)}); - sync_history_digest(); - ++next_timeline_ordinal_; + refresh_target_scalars(engine, staged_target); + native_order::CommittedExecutionFacts facts; + facts.result = settled; + facts.cycle_before = cycle_before; + facts.cycle_after = engine.position_cycle_seq_; + facts.post_target = std::move(staged_target); + facts.committed_action = physical; + if (!install_execution(engine, std::move(*token), facts, P)) return; + NativeAccountObservation observation; + observation.ordinal = applied_id.ordinal; + observation.effective_time_ms = point.coordinate.effective_time_ms; observation.marked_equity = engine.marked_equity(resolved); observation.realized_balance = engine.initial_capital_ + engine.net_profit_sum_; observation.signed_units = 0.0; @@ -1022,6 +1991,11 @@ void NativeExecutionConsumer::match_point(BacktestEngine& engine, const NativeDr fold_account_digest(observation); engine.current_bar_.timestamp = point.coordinate.effective_time_ms; engine.bar_index_ = point.coordinate.interval_index; + drain_after_applied(engine, applied_id, winner->handle); + } + if (continuous && t_cursor < 1.0 && !failed()) { + apply_excursion(engine, to_price); + observe_trails(engine, make_cursor(point, 1.0), to_price); } } @@ -1064,11 +2038,11 @@ void NativeExecutionConsumer::invoke_callback(BacktestEngine& engine, const Bar& void NativeExecutionConsumer::deliver_confirmed_script(BacktestEngine& engine, const Bar& bar, const NativeCoordinate& base) { - // Native AUTO depends on this input, including when another legacy host - // has installed a forced path around a nested native run. + // Native AUTO is local to this input; another legacy host may have a + // thread-local forced path installed around a nested native run. const bool high_first = std::abs(bar.high - bar.open) < std::abs(bar.open - bar.low); - auto emit_match = [&](double price, int64_t time, NativePriceProvenance provenance, - NativePathPhase phase) { + auto emit_discrete = [&](double price, int64_t time, NativePriceProvenance provenance, + NativePathPhase phase, bool matching) { NativeDriverPoint point; point.coordinate = base; point.coordinate.ordinal = take_ordinal(engine); @@ -1077,41 +2051,49 @@ void NativeExecutionConsumer::deliver_confirmed_script(BacktestEngine& engine, c point.coordinate.provenance = provenance; point.coordinate.path_phase = phase; point.raw_price = price; - point.matching = true; + point.matching = matching; record_driver(point); - match_point(engine, point); + match_discrete(engine, point); raise_floor(time); }; - auto emit_excursion = [&](double price, int64_t time, NativePathPhase phase) { + auto emit_segment = [&](double from, double to, int64_t time, NativePathPhase phase) { NativeDriverPoint point; point.coordinate = base; point.coordinate.ordinal = take_ordinal(engine); point.coordinate.effective_time_ms = time; + point.coordinate.source_price_time_ms = time; point.coordinate.provenance = NativePriceProvenance::Confirmed; point.coordinate.path_phase = phase; - point.raw_price = price; + point.raw_price = to; + point.matching = false; point.excursion = true; record_driver(point); - apply_excursion(engine, price); + match_segment(engine, point, from); raise_floor(time); }; const int64_t open_time = script_.first_source_time_ms != 0 ? script_.first_source_time_ms : (script_.first_open_ms != 0 ? script_.first_open_ms : bar.timestamp); const int64_t close_time = calculation_time(base); - emit_match(bar.open, open_time, NativePriceProvenance::ModeledOHLCOpen, NativePathPhase::Open); + emit_discrete(bar.open, open_time, NativePriceProvenance::ModeledOHLCOpen, + NativePathPhase::Open, true); if (failed()) return; + double prev = bar.open; if (high_first) { - emit_excursion(bar.high, open_time, NativePathPhase::High); + emit_segment(prev, bar.high, open_time, NativePathPhase::High); if (failed()) return; - emit_excursion(bar.low, open_time, NativePathPhase::Low); + prev = bar.high; + emit_segment(prev, bar.low, open_time, NativePathPhase::Low); + prev = bar.low; } else { - emit_excursion(bar.low, open_time, NativePathPhase::Low); + emit_segment(prev, bar.low, open_time, NativePathPhase::Low); if (failed()) return; - emit_excursion(bar.high, open_time, NativePathPhase::High); + prev = bar.low; + emit_segment(prev, bar.high, open_time, NativePathPhase::High); + prev = bar.high; } if (failed()) return; - emit_excursion(bar.close, close_time, NativePathPhase::Close); + emit_segment(prev, bar.close, close_time, NativePathPhase::Close); if (failed()) return; NativeCoordinate calc = base; calc.ordinal = take_ordinal(engine); @@ -1126,8 +2108,8 @@ void NativeExecutionConsumer::deliver_confirmed_script(BacktestEngine& engine, c if (failed()) return; const auto* spec = spec_ptr(); if (spec && spec->close_execution == NativeCloseExecution::AfterCalculation) { - emit_match(bar.close, close_time, NativePriceProvenance::AfterCalculationClose, - NativePathPhase::Close); + emit_discrete(bar.close, close_time, NativePriceProvenance::AfterCalculationClose, + NativePathPhase::Close, true); } } @@ -1811,43 +2793,163 @@ bool NativeExecutionConsumer::stream_end(BacktestEngine& engine, bool finalize_p } } -native_order::SubmitResult NativeExecutionConsumer::submit(BacktestEngine& engine, - const native_order::Request& request) { +native_order::SubmitResult NativeExecutionConsumer::submit_with_surface( + BacktestEngine& engine, const native_order::Request& request, + native_order::CommandSurface surface) { if (!commands_allowed()) { throw std::runtime_error("native submit refused outside allowed phase"); } - const int64_t floor = decision_floor(); - auto result = requests_.submit(request, floor, engine.next_order_incarnation_, - next_timeline_ordinal_, - spec_ptr() ? spec_ptr()->quantity_grid : std::nullopt); + native_order::CommandContext ctx; + native_order::PreparedSubmit prepared; + try { + ctx = make_command_context(engine, request, surface); + prepared = requests_.prepare_submit( + request, ctx, engine.next_order_incarnation_, next_timeline_ordinal_); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, NativeFailureOperation::Command}); + render(engine, e.what()); + throw; + } + if (!prepared) { + fail(engine, NativeFailure{NativeFailureCode::Contract, NativeFailureOperation::Command}); + render(engine, "native submit produced no preparation"); + throw std::runtime_error("native submit produced no preparation"); + } + auto installed = requests_.install_submit(std::move(prepared)); + if (const auto* err = std::get_if(&installed)) { + fail(engine, NativeFailure{NativeFailureCode::Contract, NativeFailureOperation::Command, + 0, static_cast(*err)}); + render(engine, "native submit install failed"); + throw std::runtime_error("native submit install failed"); + } + auto& ok = std::get>(installed); sync_history_digest(); - return result; + catch_up_timeline(); + if (ok.result.status == native_order::SubmitStatus::Accepted) { + ++engine.next_order_incarnation_; + } + return std::move(ok.result); } -native_order::ReplaceResult NativeExecutionConsumer::replace( - BacktestEngine& engine, - const native_order::RequestHandle& target, - const native_order::Request& request) { +native_order::ReplaceResult NativeExecutionConsumer::replace_with_surface( + BacktestEngine& engine, const native_order::RequestHandle& target, + const native_order::Request& request, native_order::CommandSurface surface) { if (!commands_allowed()) { throw std::runtime_error("native replace refused outside allowed phase"); } - const int64_t floor = decision_floor(); - auto result = requests_.replace(target, request, floor, engine.next_order_incarnation_, - next_timeline_ordinal_, - spec_ptr() ? spec_ptr()->quantity_grid : std::nullopt); + native_order::CommandContext ctx; + native_order::PreparedReplace prepared; + try { + ctx = make_command_context(engine, request, surface); + prepared = requests_.prepare_replace( + target, request, ctx, engine.next_order_incarnation_, next_timeline_ordinal_); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, NativeFailureOperation::Command}); + render(engine, e.what()); + throw; + } + if (!prepared) { + fail(engine, NativeFailure{NativeFailureCode::Contract, NativeFailureOperation::Command}); + render(engine, "native replace produced no preparation"); + throw std::runtime_error("native replace produced no preparation"); + } + const native_order::EventId predicted = prepared.predicted_event_id(); + const auto predicted_status = prepared.predicted().status; + auto installed = requests_.install_replace(std::move(prepared)); + if (const auto* err = std::get_if(&installed)) { + fail(engine, NativeFailure{NativeFailureCode::Contract, NativeFailureOperation::Command, + predicted.ordinal, static_cast(*err)}); + render(engine, "native replace install failed"); + throw std::runtime_error("native replace install failed"); + } + auto& ok = std::get>(installed); sync_history_digest(); - return result; + catch_up_timeline(); + if (predicted_status == native_order::ReplaceStatus::Replaced) { + ++engine.next_order_incarnation_; + try { + drain_parent_terminal(engine, predicted, target, NativeFailureOperation::Command); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Command, predicted.ordinal}); + render(engine, e.what()); + throw; + } + if (failed()) { + throw std::runtime_error("native replace dependency cleanup failed"); + } + } + return std::move(ok.result); +} + +native_order::SubmitResult NativeExecutionConsumer::submit(BacktestEngine& engine, + const native_order::Request& request) { + return submit_with_surface(engine, request, native_order::CommandSurface::General); +} + +native_order::ReplaceResult NativeExecutionConsumer::replace( + BacktestEngine& engine, + const native_order::RequestHandle& target, + const native_order::Request& request) { + return replace_with_surface(engine, target, request, native_order::CommandSurface::General); +} + +native_order::SubmitResult NativeExecutionConsumer::submit_market( + BacktestEngine& engine, const native_order::Request& request) { + return submit_with_surface(engine, request, native_order::CommandSurface::MarketOnly); +} + +native_order::ReplaceResult NativeExecutionConsumer::replace_market( + BacktestEngine& engine, + const native_order::RequestHandle& target, + const native_order::Request& request) { + return replace_with_surface(engine, target, request, native_order::CommandSurface::MarketOnly); } native_order::CancelResult NativeExecutionConsumer::cancel( BacktestEngine& engine, const native_order::RequestHandle& target) { - (void)engine; if (!commands_allowed()) { throw std::runtime_error("native cancel refused outside allowed phase"); } - auto result = requests_.cancel(target, next_timeline_ordinal_); + native_order::PreparedCancel prepared; + try { + prepared = requests_.prepare_cancel(target, next_timeline_ordinal_); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, NativeFailureOperation::Command}); + render(engine, e.what()); + throw; + } + if (!prepared) { + fail(engine, NativeFailure{NativeFailureCode::Contract, NativeFailureOperation::Command}); + render(engine, "native cancel produced no preparation"); + throw std::runtime_error("native cancel produced no preparation"); + } + const auto predicted_status = prepared.predicted().status; + const native_order::EventId predicted = prepared.predicted_event_id(); + auto installed = requests_.install_cancel(std::move(prepared)); + if (const auto* err = std::get_if(&installed)) { + fail(engine, NativeFailure{NativeFailureCode::Contract, NativeFailureOperation::Command, + predicted.ordinal, static_cast(*err)}); + render(engine, "native cancel install failed"); + throw std::runtime_error("native cancel install failed"); + } + auto& ok = std::get>(installed); sync_history_digest(); - return result; + catch_up_timeline(); + if (predicted_status == native_order::CancelStatus::Cancelled) { + try { + drain_parent_terminal(engine, predicted, target, NativeFailureOperation::Command); + } catch (const std::exception& e) { + fail(engine, NativeFailure{NativeFailureCode::Allocation, + NativeFailureOperation::Command, predicted.ordinal}); + render(engine, e.what()); + throw; + } + if (failed()) { + throw std::runtime_error("native cancel dependency cleanup failed"); + } + } + return std::move(ok.result); } NativePhysicalPosition NativeExecutionConsumer::position(const BacktestEngine& engine) const { @@ -1941,15 +3043,24 @@ NativeStateView NativeStrategyHost::native_state() const { return as_native_consumer(const_cast(execution_consumer())).view(); } -native_order::SubmitResult NativeStrategyHost::submit_market(const native_order::Request& request) { +native_order::SubmitResult NativeStrategyHost::submit(const native_order::Request& request) { return as_native_consumer(execution_consumer()).submit(*this, request); } -native_order::ReplaceResult NativeStrategyHost::replace_market( +native_order::ReplaceResult NativeStrategyHost::replace( const native_order::RequestHandle& target, const native_order::Request& request) { return as_native_consumer(execution_consumer()).replace(*this, target, request); } +native_order::SubmitResult NativeStrategyHost::submit_market(const native_order::Request& request) { + return as_native_consumer(execution_consumer()).submit_market(*this, request); +} + +native_order::ReplaceResult NativeStrategyHost::replace_market( + const native_order::RequestHandle& target, const native_order::Request& request) { + return as_native_consumer(execution_consumer()).replace_market(*this, target, request); +} + native_order::CancelResult NativeStrategyHost::cancel(const native_order::RequestHandle& target) { return as_native_consumer(execution_consumer()).cancel(*this, target); } @@ -1980,5 +3091,5 @@ uint64_t NativeStrategyHost::native_continuation_hash() const { .continuation_hash(); } -} // inline namespace engine_script_run_v12 +} // inline namespace engine_script_run_v13 } // namespace pineforge diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 61548020..4149322d 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -7,10 +7,11 @@ #include #include #include +#include #include namespace pineforge { -inline namespace engine_script_run_v12 { +inline namespace engine_script_run_v13 { class NativeExecutionConsumer final : public IExecutionConsumer { public: @@ -53,6 +54,11 @@ class NativeExecutionConsumer final : public IExecutionConsumer { native_order::ReplaceResult replace(BacktestEngine& engine, const native_order::RequestHandle& target, const native_order::Request& request); + native_order::SubmitResult submit_market(BacktestEngine& engine, + const native_order::Request& request); + native_order::ReplaceResult replace_market(BacktestEngine& engine, + const native_order::RequestHandle& target, + const native_order::Request& request); native_order::CancelResult cancel(BacktestEngine& engine, const native_order::RequestHandle& target); NativePhysicalPosition position(const BacktestEngine& engine) const; @@ -128,17 +134,56 @@ class NativeExecutionConsumer final : public IExecutionConsumer { const NativeCoordinate& base); int64_t calculation_time(const NativeCoordinate& base) const noexcept; void match_point(BacktestEngine& engine, const NativeDriverPoint& point); + void match_discrete(BacktestEngine& engine, const NativeDriverPoint& point); + void match_segment(BacktestEngine& engine, const NativeDriverPoint& dest, double from_price); + void match_path(BacktestEngine& engine, const NativeDriverPoint& point, + bool continuous, double from_price, double to_price); void apply_excursion(BacktestEngine& engine, double price); void invoke_callback(BacktestEngine& engine, const Bar& bar, const NativeCoordinate& coordinate); uint64_t take_ordinal(BacktestEngine& engine); void raise_floor(int64_t t); - double resolve_price(const native_order::Request& request, double raw) const; - bool admit_opening(const BacktestEngine& engine, - const execution::SettlementInspection& inspect, - native_order::MatchRejectReason* reason) const; - void terminal_no_effect(BacktestEngine& engine, std::size_t live_index); - void terminal_reject(BacktestEngine& engine, std::size_t live_index, - native_order::MatchRejectReason reason); + native_order::DriverEligibilityClass classify_driver( + const NativeDriverPoint& point, bool continuous) const noexcept; + native_order::MatchCursor make_cursor(const NativeDriverPoint& point, double t) const noexcept; + native_order::PositionIdentity read_position(const BacktestEngine& engine) const; + native_order::OpeningObservation read_opening( + const BacktestEngine& engine, const native_order::RequestHandle& opening, + int64_t cycle) const; + native_order::TargetObservation read_target( + const BacktestEngine& engine, const native_order::LiveRequest* live) const; + native_order::CommandContext make_command_context( + const BacktestEngine& engine, const native_order::Request& request, + native_order::CommandSurface surface) const; + void refresh_target_scalars(const BacktestEngine& engine, + native_order::TargetObservation& target) const noexcept; + bool admit_opening_inspect(const BacktestEngine& engine, double resolved_price, + const execution::SettlementInspection& inspect, + native_order::MatchRejectReason* reason) const; + void fail_preparation(BacktestEngine& engine, const native_order::PreparationError& error, + NativeFailureOperation operation); + void catch_up_timeline() noexcept; + bool install_mutation(BacktestEngine& engine, native_order::PreparedMutation&& prepared, + NativeFailureOperation operation, uint64_t ordinal); + bool install_execution(BacktestEngine& engine, native_order::PreparedExecution&& prepared, + const native_order::CommittedExecutionFacts& facts, + uint64_t ordinal); + native_order::SubmitResult submit_with_surface( + BacktestEngine& engine, const native_order::Request& request, + native_order::CommandSurface surface); + native_order::ReplaceResult replace_with_surface( + BacktestEngine& engine, const native_order::RequestHandle& target, + const native_order::Request& request, native_order::CommandSurface surface); + void drain_after_applied(BacktestEngine& engine, const native_order::EventId& applied, + const native_order::RequestHandle& filler); + void drain_parent_terminal(BacktestEngine& engine, const native_order::EventId& cause, + const native_order::RequestHandle& parent, + NativeFailureOperation operation); + void drain_dependency_queue( + BacktestEngine& engine, + std::vector> seeds, + NativeFailureOperation operation); + void observe_trails(BacktestEngine& engine, const native_order::MatchCursor& cursor, + double price); void record_driver(const NativeDriverPoint& point); NativeCoordinate coordinate_from(const native_calendar::NativeInterval& interval, int index, int64_t effective, @@ -202,5 +247,5 @@ inline NativeExecutionConsumer& as_native_consumer(IExecutionConsumer& consumer) return static_cast(consumer); } -} // inline namespace engine_script_run_v12 +} // inline namespace engine_script_run_v13 } // namespace pineforge diff --git a/src/native_matching.hpp b/src/native_matching.hpp new file mode 100644 index 00000000..e4b9d7f3 --- /dev/null +++ b/src/native_matching.hpp @@ -0,0 +1,114 @@ +#pragma once + +// Private R2 path geometry. Driver delivery and the consumer matcher include +// this header; it is not a public native API and does not own working-request +// state, fees, or settlement. + +#include +#include +#include + +namespace pineforge { +inline namespace engine_script_run_v13 { +namespace native_matching { + +struct GeometricHit { + double t = 0.0; + double price = 0.0; +}; + +inline bool finite_positive(double v) noexcept { + return std::isfinite(v) && v > 0.0; +} + +inline double price_at(double from, double to, double t) noexcept { + return from + t * (to - from); +} + +inline std::optional t_for_price(double from, double to, double price) noexcept { + const double denom = to - from; + if (denom == 0.0) { + if (price == from) return 0.0; + return std::nullopt; + } + const double t = (price - from) / denom; + if (!std::isfinite(t)) return std::nullopt; + return t; +} + +// Region is price <= level when le is true, otherwise price >= level. +inline bool in_region(double price, double level, bool le) noexcept { + return le ? price <= level : price >= level; +} + +// First remaining-suffix entry into a closed price region. include_current +// reports t_start when the cursor is already inside; otherwise the first +// later crossing uses the threshold as the modeled raw price. +inline std::optional first_region_entry( + double from, double to, double t_start, double level, bool le, + bool include_current) noexcept { + if (!std::isfinite(from) || !std::isfinite(to) || !std::isfinite(level) + || !std::isfinite(t_start) || t_start < 0.0 || t_start > 1.0) { + return std::nullopt; + } + const double current = price_at(from, to, t_start); + if (!std::isfinite(current)) return std::nullopt; + if (include_current && in_region(current, level, le)) { + return GeometricHit{t_start, current}; + } + if (from == to) return std::nullopt; + const auto t_cross = t_for_price(from, to, level); + if (!t_cross) return std::nullopt; + if (!(*t_cross > t_start) || *t_cross > 1.0) return std::nullopt; + const double crossed = price_at(from, to, *t_cross); + if (!in_region(crossed, level, le) && !in_region(level, level, le)) { + return std::nullopt; + } + return GeometricHit{*t_cross, level}; +} + +inline double apply_slippage(double raw, double slip, bool buy) noexcept { + return buy ? raw + slip : raw - slip; +} + +inline double protect_limit(double slipped, double limit, bool buy) noexcept { + return buy ? std::min(slipped, limit) : std::max(slipped, limit); +} + +// Sell trail: stop = best - offset, must sit strictly below best. +// Buy trail: stop = best + offset, must sit strictly above best. +// Returns false on nonfinite/nonpositive offset or an absorbed/overflowed +// level. A finite nonpositive stop is still reported; callers do not clamp it. +inline bool checked_trail_stop(double best, double offset, bool buy, double* stop_out) noexcept { + if (stop_out) *stop_out = std::numeric_limits::quiet_NaN(); + if (!std::isfinite(best) || !std::isfinite(offset) || offset <= 0.0) return false; + const double stop = buy ? best + offset : best - offset; + if (!std::isfinite(stop)) return false; + if (buy && !(stop > best)) return false; + if (!buy && !(stop < best)) return false; + if (stop_out) *stop_out = stop; + return true; +} + +inline bool trail_best_improves(double best, double price, bool buy) noexcept { + if (!std::isfinite(best) || !std::isfinite(price)) return false; + return buy ? price < best : price > best; +} + +// Adverse remaining suffix against a frozen best. Favorable monotonic +// motion cannot hit the trailing stop on one linear segment. +inline std::optional trail_stop_hit( + double from, double to, double t_start, double best, double offset, bool buy) noexcept { + double stop = 0.0; + if (!checked_trail_stop(best, offset, buy, &stop)) return std::nullopt; + if (!std::isfinite(from) || !std::isfinite(to) || !std::isfinite(t_start) + || t_start < 0.0 || t_start > 1.0) { + return std::nullopt; + } + const bool le = !buy; + return first_region_entry(from, to, t_start, stop, le, true); +} + +} // namespace native_matching +} // inline namespace engine_script_run_v13 +} // namespace pineforge diff --git a/src/native_order.cpp b/src/native_order.cpp index 1d1175d7..45387527 100644 --- a/src/native_order.cpp +++ b/src/native_order.cpp @@ -1,36 +1,298 @@ #include #include +#include #include +#include #include #include #include namespace pineforge::native_order { -inline namespace native_order_v1 { +inline namespace native_order_v2 { namespace { +constexpr std::uint8_t kLivePush = 1; +constexpr std::uint8_t kLiveErase = 2; +constexpr std::uint8_t kLiveUpdate = 3; +constexpr std::uint8_t kLiveErasePush = 4; + template -void reserve_one(std::vector& values) { +void reserve_n(std::vector& values, std::size_t n) { static_assert(std::is_nothrow_move_constructible_v); - if (values.size() == values.max_size()) { + if (n == 0) return; + if (values.size() > values.max_size() - n) { throw std::length_error("native working-request capacity exhausted"); } - const std::size_t required = values.size() + 1; + const std::size_t required = values.size() + n; if (required <= values.capacity()) return; - const std::size_t grown = values.capacity() == 0 - ? 1 - : (values.capacity() > values.max_size() / 2 ? values.max_size() - : values.capacity() * 2); - values.reserve(std::max(required, grown)); + std::size_t cap = values.capacity() == 0 ? 1 : values.capacity(); + while (cap < required) { + if (cap > values.max_size() / 2) { + cap = values.max_size(); + break; + } + cap *= 2; + } + values.reserve(std::max(required, cap)); } uint64_t event_ordinal(const CommandEvent& event) { return std::visit([](const auto& payload) { return payload.ordinal; }, event); } +bool finite_positive(double value) noexcept { + return std::isfinite(value) && value > 0.0; +} + +bool finite_nonzero(double value) noexcept { + return std::isfinite(value) && value != 0.0; +} + +const Flatten* as_flatten(const OrderIntent& intent) noexcept { + return std::get_if(&intent); +} +const Reduce* as_reduce(const OrderIntent& intent) noexcept { + return std::get_if(&intent); +} +const Transact* as_transact(const OrderIntent& intent) noexcept { + return std::get_if(&intent); +} + +const ExplicitUnits* explicit_size(const Reduce& reduce) noexcept { + return std::get_if(&reduce.size); +} + +bool is_owner_opened(const Reduce& reduce) noexcept { + return std::holds_alternative(reduce.size); +} + +double working_units(const Remaining& remaining) noexcept { + if (const auto* units = std::get_if(&remaining)) return units->q; + return 0.0; +} + +RemainingProjection project_remaining(const Remaining& remaining) { + if (std::holds_alternative(remaining)) return RemainingProjectionUnbound{}; + if (std::holds_alternative(remaining)) { + return RemainingProjectionFlattenAll{}; + } + return RemainingProjectionUnits{working_units(remaining)}; +} + +bool checked_add_positive(double a, double b, double* out) noexcept { + if (!std::isfinite(a) || !std::isfinite(b) || a < 0.0 || b < 0.0) return false; + const double sum = a + b; + if (!std::isfinite(sum)) return false; + if (a > 0.0 && b > 0.0 && !(sum > a && sum > b)) return false; + *out = sum; + return true; +} + +bool checked_sub_cap(double before, double deduct, double* after, bool* exhausted) noexcept { + if (!std::isfinite(before) || !std::isfinite(deduct) || before <= 0.0 || deduct <= 0.0) { + return false; + } + if (deduct == before) { + *after = 0.0; + *exhausted = true; + return true; + } + if (deduct > before) return false; + const double next = before - deduct; + if (!std::isfinite(next) || !(next > 0.0 && next < before)) return false; + *after = next; + *exhausted = false; + return true; +} + +TriggerState trigger_state_from(const Trigger& trigger) { + if (std::holds_alternative(trigger)) return MarketReady{}; + if (std::holds_alternative(trigger)) return LimitReady{}; + if (std::holds_alternative(trigger)) return StopIdle{}; + if (std::holds_alternative(trigger)) return StopLimitPending{}; + return TrailWaitArm{}; +} + +bool fillable_state(const TriggerState& state) noexcept { + return std::holds_alternative(state) || std::holds_alternative(state) + || std::holds_alternative(state) || std::holds_alternative(state) + || std::holds_alternative(state); +} + +Side side_from_opened(double opened_units) noexcept { + return opened_units < 0.0 ? Side::Short : Side::Long; +} + +bool position_matches(const PositionIdentity& position, int64_t cycle, Side side) noexcept { + const auto* nonflat = std::get_if(&position); + return nonflat && nonflat->cycle == cycle && nonflat->side == side && cycle > 0; +} + +bool opening_alive(const OpeningObservation& observation, + const RequestHandle& opening, + int64_t cycle) noexcept { + return observation.queried_opening == opening && observation.queried_cycle == cycle + && observation.has_live_matching_lot && cycle > 0; +} + +bool book_close_alive(const TargetObservation& observation, const BookClose& close) noexcept { + return position_matches(observation.current_position, close.cycle, close.side); +} + +bool opening_close_alive(const TargetObservation& observation, const OpeningClose& close) noexcept { + return observation.opening && opening_alive(*observation.opening, close.opening, close.cycle); +} + +const ExecutionAppliedEvent* as_applied(const CommandEvent& event) noexcept { + return std::get_if(&event); +} + +int receipt_cmp(uint64_t oa, uint64_t ia, GroupEffect ea, uint64_t ob, uint64_t ib, + GroupEffect eb) noexcept { + if (oa < ob) return -1; + if (oa > ob) return 1; + if (ia < ib) return -1; + if (ia > ib) return 1; + const auto a = static_cast(ea); + const auto b = static_cast(eb); + if (a < b) return -1; + if (a > b) return 1; + return 0; +} + +std::optional validate_levels(const Trigger& trigger) { + if (const auto* limit = std::get_if(&trigger)) { + if (!finite_positive(limit->price)) return RequestRejectReason::InvalidTrigger; + return std::nullopt; + } + if (const auto* stop = std::get_if(&trigger)) { + if (!finite_positive(stop->price)) return RequestRejectReason::InvalidTrigger; + return std::nullopt; + } + if (const auto* stop_limit = std::get_if(&trigger)) { + if (!finite_positive(stop_limit->stop) || !finite_positive(stop_limit->limit)) { + return RequestRejectReason::InvalidTrigger; + } + return std::nullopt; + } + if (const auto* trail = std::get_if(&trigger)) { + if (!finite_positive(trail->offset)) return RequestRejectReason::InvalidTrigger; + if (trail->arm_price && !finite_positive(*trail->arm_price)) { + return RequestRejectReason::InvalidTrigger; + } + } + return std::nullopt; +} + +bool on_optional_grid(double value, std::optional grid) { + return !grid || quantity_on_grid(value, *grid); +} + +void fill_applied_cursor(ExecutionAppliedEvent& event, const MatchCursor& cursor) { + event.cursor = cursor; +} + +CancelledEvent make_cancelled(uint64_t ordinal, const LiveRequest& live, CancelReason reason, + std::optional cause) { + CancelledEvent event; + event.ordinal = ordinal; + event.definition = live.definition; + event.reason = reason; + event.cause = std::move(cause); + event.prior_authority = live.authority; + event.unexecuted = project_remaining(live.remaining); + event.pending = live.pending; + return event; +} + } // namespace +struct PreparedSubmit::Impl { + WorkingRequestCore::MutationPlan plan; + SubmitResult result; +}; +PreparedSubmit::PreparedSubmit() noexcept = default; +PreparedSubmit::PreparedSubmit(PreparedSubmit&&) noexcept = default; +PreparedSubmit& PreparedSubmit::operator=(PreparedSubmit&&) noexcept = default; +PreparedSubmit::PreparedSubmit(std::unique_ptr impl) noexcept : impl_(std::move(impl)) {} +PreparedSubmit::~PreparedSubmit() = default; +PreparedSubmit::operator bool() const noexcept { return static_cast(impl_); } +const SubmitResult& PreparedSubmit::predicted() const { + if (!impl_) throw std::logic_error("empty prepared submit"); + return impl_->result; +} +EventId PreparedSubmit::predicted_event_id() const { + if (!impl_) throw std::logic_error("empty prepared submit"); + return EventId{impl_->plan.run, impl_->result.event_ordinal}; +} + +struct PreparedReplace::Impl { + WorkingRequestCore::MutationPlan plan; + ReplaceResult result; +}; +PreparedReplace::PreparedReplace() noexcept = default; +PreparedReplace::PreparedReplace(PreparedReplace&&) noexcept = default; +PreparedReplace& PreparedReplace::operator=(PreparedReplace&&) noexcept = default; +PreparedReplace::PreparedReplace(std::unique_ptr impl) noexcept : impl_(std::move(impl)) {} +PreparedReplace::~PreparedReplace() = default; +PreparedReplace::operator bool() const noexcept { return static_cast(impl_); } +const ReplaceResult& PreparedReplace::predicted() const { + if (!impl_) throw std::logic_error("empty prepared replace"); + return impl_->result; +} +EventId PreparedReplace::predicted_event_id() const { + if (!impl_) throw std::logic_error("empty prepared replace"); + return EventId{impl_->plan.run, impl_->result.event_ordinal}; +} + +struct PreparedCancel::Impl { + WorkingRequestCore::MutationPlan plan; + CancelResult result; +}; +PreparedCancel::PreparedCancel() noexcept = default; +PreparedCancel::PreparedCancel(PreparedCancel&&) noexcept = default; +PreparedCancel& PreparedCancel::operator=(PreparedCancel&&) noexcept = default; +PreparedCancel::PreparedCancel(std::unique_ptr impl) noexcept : impl_(std::move(impl)) {} +PreparedCancel::~PreparedCancel() = default; +PreparedCancel::operator bool() const noexcept { return static_cast(impl_); } +const CancelResult& PreparedCancel::predicted() const { + if (!impl_) throw std::logic_error("empty prepared cancel"); + return impl_->result; +} +EventId PreparedCancel::predicted_event_id() const { + if (!impl_) throw std::logic_error("empty prepared cancel"); + return EventId{impl_->plan.run, impl_->result.event_ordinal}; +} + +struct PreparedMutation::Impl { + WorkingRequestCore::MutationPlan plan; +}; +PreparedMutation::PreparedMutation() noexcept = default; +PreparedMutation::PreparedMutation(PreparedMutation&&) noexcept = default; +PreparedMutation& PreparedMutation::operator=(PreparedMutation&&) noexcept = default; +PreparedMutation::PreparedMutation(std::unique_ptr impl) noexcept : impl_(std::move(impl)) {} +PreparedMutation::~PreparedMutation() = default; +PreparedMutation::operator bool() const noexcept { return static_cast(impl_); } + +struct PreparedExecution::Impl { + WorkingRequestCore::MutationPlan terminal; + CommandEvent retain_event{ExecutionAppliedEvent{}}; + LiveRequest retain_live{}; + bool can_retain = false; + bool flatten = false; + bool opening = false; + BookClose book_close{}; + OpeningClose opening_close{}; + ExecutionProposal proposal{}; +}; +PreparedExecution::PreparedExecution() noexcept = default; +PreparedExecution::PreparedExecution(PreparedExecution&&) noexcept = default; +PreparedExecution& PreparedExecution::operator=(PreparedExecution&&) noexcept = default; +PreparedExecution::PreparedExecution(std::unique_ptr impl) noexcept : impl_(std::move(impl)) {} +PreparedExecution::~PreparedExecution() = default; +PreparedExecution::operator bool() const noexcept { return static_cast(impl_); } + void WorkingRequestCore::require_identity(const RunIdentity& identity) { if (identity.session_key.empty() || identity.run_number == 0) { throw std::invalid_argument( @@ -46,21 +308,11 @@ void WorkingRequestCore::require_distinct_counters(uint64_t& next_order_incarnat } } -uint64_t WorkingRequestCore::last_command_ordinal() const { - if (history_.empty()) return 0; - return event_ordinal(history_.back()); -} - -uint64_t WorkingRequestCore::last_consumed_incarnation() const { - for (auto it = history_.rbegin(); it != history_.rend(); ++it) { - if (const auto* accepted = std::get_if(&*it)) { - return accepted->handle.incarnation; - } - if (const auto* replaced = std::get_if(&*it)) { - return replaced->successor.incarnation; - } +void WorkingRequestCore::require_grid(std::optional quantity_grid) { + if (!quantity_grid) return; + if (!std::isfinite(*quantity_grid) || *quantity_grid <= 0.0) { + throw std::invalid_argument("quantity grid requires a finite positive step"); } - return 0; } uint64_t WorkingRequestCore::usable_ordinal(uint64_t next) const { @@ -70,7 +322,7 @@ uint64_t WorkingRequestCore::usable_ordinal(uint64_t next) const { if (next == std::numeric_limits::max()) { throw std::overflow_error("native timeline ordinal exhausted"); } - if (next <= last_command_ordinal()) { + if (next <= last_ordinal_) { throw std::invalid_argument("native timeline ordinal reused or regressed"); } return next; @@ -83,45 +335,204 @@ uint64_t WorkingRequestCore::usable_incarnation(uint64_t next) const { if (next == std::numeric_limits::max()) { throw std::overflow_error("native order incarnation exhausted"); } - if (next <= last_consumed_incarnation()) { + if (next <= last_incarnation_) { throw std::invalid_argument("native order incarnation reused or regressed"); } return next; } -void WorkingRequestCore::require_grid(std::optional quantity_grid) { - if (!quantity_grid) return; - if (!std::isfinite(*quantity_grid) || *quantity_grid <= 0.0) { - throw std::invalid_argument("quantity grid requires a finite positive step"); +bool WorkingRequestCore::bump_epoch() noexcept { + if (epoch_ == std::numeric_limits::max()) return false; + ++epoch_; + return true; +} + +void WorkingRequestCore::require_epoch_room() const { + if (epoch_ >= std::numeric_limits::max() - 1) { + throw std::overflow_error("native working-request epoch exhausted"); } } -std::optional WorkingRequestCore::validate_request( - const Request& request, std::optional quantity_grid) { - require_grid(quantity_grid); - if (std::holds_alternative(request.action)) return std::nullopt; - if (const auto* reduce = std::get_if(&request.action)) { - if (!std::isfinite(reduce->units) || reduce->units <= 0.0) { - return RequestRejectReason::InvalidQuantity; - } - if (quantity_grid && !quantity_on_grid(reduce->units, *quantity_grid)) { - return RequestRejectReason::OffGrid; +void WorkingRequestCore::clear_unbound() noexcept { + identity_.session_key.clear(); + identity_.run_number = 0; + live_.clear(); + history_.clear(); + last_ordinal_ = 0; + last_incarnation_ = 0; + ordinal_index_.clear(); + receipts_.clear(); + epoch_ = 0; + if (instance_) instance_->expired = true; + instance_.reset(); +} + +void WorkingRequestCore::bind_plan(MutationPlan& plan) const { + plan.instance = instance_; + plan.generation = instance_ ? instance_->generation : 0; +} + +WorkingRequestCore::MutationPlan WorkingRequestCore::begin_plan() const { + require_identity(identity_); + if (!instance_ || instance_->expired) { + throw std::invalid_argument( + "native run identity requires a nonempty session key and positive run number"); + } + require_epoch_room(); + MutationPlan plan; + plan.run = identity_; + plan.history_size = history_.size(); + plan.last_ordinal = last_ordinal_; + bind_plan(plan); + plan.epoch = epoch_ + 1; + return plan; +} + +void WorkingRequestCore::seal_plan(MutationPlan& plan) { + require_epoch_room(); + reserve_plan(plan); + if (!bump_epoch()) { + throw std::overflow_error("native working-request epoch exhausted"); + } + plan.epoch = epoch_; + bind_plan(plan); +} + +void WorkingRequestCore::reserve_plan(const MutationPlan& plan) { + reserve_n(history_, plan.events.size()); + reserve_n(ordinal_index_, plan.events.size()); + if (plan.live_change == kLivePush || plan.live_change == kLiveErasePush) reserve_n(live_, 1); + if (plan.add_receipt) reserve_n(receipts_, 1); +} + +PreparedMutation WorkingRequestCore::finish_mutation(MutationPlan plan) { + seal_plan(plan); + auto impl = std::make_unique(); + impl->plan = std::move(plan); + return PreparedMutation(std::move(impl)); +} + +WorkingRequestCore::TargetKind WorkingRequestCore::classify( + const RequestHandle& handle, std::size_t* live_index) const { + if (handle.incarnation == 0 || handle.run.run_number == 0 || handle.run.session_key.empty() + || handle.run != identity_) { + return TargetKind::InvalidHandle; + } + for (std::size_t i = 0; i < live_.size(); ++i) { + if (live_[i].handle().incarnation == handle.incarnation) { + if (live_index) *live_index = i; + return TargetKind::Live; } - return std::nullopt; } - const auto* transact = std::get_if(&request.action); - if (!transact || !std::isfinite(transact->signed_units) || transact->signed_units == 0.0) { - return RequestRejectReason::InvalidQuantity; + return TargetKind::NotWorking; +} + +const LiveRequest* WorkingRequestCore::find_live(const RequestHandle& handle) const { + std::size_t index = 0; + if (classify(handle, &index) != TargetKind::Live) return nullptr; + return &live_[index]; +} + +const CommandEvent* WorkingRequestCore::event_at(const EventId& id) const { + if (id.run != identity_ || id.ordinal == 0) return nullptr; + const auto it = std::lower_bound( + ordinal_index_.begin(), ordinal_index_.end(), id.ordinal, + [](const std::pair& row, uint64_t ordinal) { + return row.first < ordinal; + }); + if (it == ordinal_index_.end() || it->first != id.ordinal) return nullptr; + if (it->second >= history_.size()) return nullptr; + const CommandEvent* event = &history_[it->second]; + if (event_ordinal(*event) != id.ordinal) return nullptr; + return event; +} + +std::vector WorkingRequestCore::pending_receipt_ids( + const PendingAdjustments& pending) const { + std::vector ids; + const auto* deferred = std::get_if(&pending); + if (!deferred) return ids; + EventId cursor = deferred->tail_receipt; + for (uint64_t i = 0; i < deferred->count; ++i) { + const CommandEvent* event = event_at(cursor); + const auto* payload = event ? std::get_if(event) : nullptr; + if (!payload) break; + ids.push_back(cursor); + if (!payload->previous_pending_receipt) break; + cursor = *payload->previous_pending_receipt; } - if (quantity_grid && !quantity_on_grid(transact->signed_units, *quantity_grid)) { - return RequestRejectReason::OffGrid; + std::reverse(ids.begin(), ids.end()); + return ids; +} + +bool WorkingRequestCore::receipt_seen(const EventId& cause, const RequestHandle& recipient, + GroupEffect effect, uint64_t* outcome) const { + for (const auto& row : receipts_) { + if (row.cause == cause && row.recipient == recipient && row.effect == effect) { + const CommandEvent* event = event_at(EventId{identity_, row.outcome_ordinal}); + if (!event) return false; + if (outcome) *outcome = row.outcome_ordinal; + return true; + } } - return std::nullopt; + return false; +} + +bool WorkingRequestCore::trail_level_ok(double best, double offset, bool is_buy, + double* stop) const noexcept { + if (!finite_positive(offset) || !std::isfinite(best)) return false; + const double level = is_buy ? best + offset : best - offset; + if (!std::isfinite(level)) return false; + if (is_buy && !(level > best)) return false; + if (!is_buy && !(level < best)) return false; + if (stop) *stop = level; + return true; +} + +InstallResult WorkingRequestCore::commit(MutationPlan& plan) noexcept { + if (plan.consumed) return InstallError::AlreadyConsumed; + const auto locked = plan.instance.lock(); + if (!locked || locked != instance_ || !instance_ || instance_->expired + || plan.generation != instance_->generation || plan.run != identity_) { + return InstallError::WrongCoreOrRun; + } + if (plan.epoch != epoch_ || plan.history_size != history_.size() + || plan.last_ordinal != last_ordinal_) { + return InstallError::StalePreparation; + } + if (epoch_ == std::numeric_limits::max()) return InstallError::StalePreparation; + const std::size_t first = history_.size(); + const std::size_t count = plan.events.size(); + for (auto& event : plan.events) { + const uint64_t ordinal = event_ordinal(event); + history_.push_back(std::move(event)); + ordinal_index_.push_back({ordinal, history_.size() - 1}); + last_ordinal_ = ordinal; + } + if (plan.live_change == kLivePush) { + live_.push_back(std::move(plan.live_row)); + } else if (plan.live_change == kLiveErase) { + live_.erase(live_.begin() + static_cast(plan.live_index)); + } else if (plan.live_change == kLiveUpdate) { + live_[plan.live_index] = std::move(plan.live_row); + } else if (plan.live_change == kLiveErasePush) { + live_.erase(live_.begin() + static_cast(plan.live_index)); + live_.push_back(std::move(plan.live_row)); + } + if (plan.add_receipt) { + receipts_.push_back(ReceiptKey{std::move(plan.receipt_cause), std::move(plan.receipt_recipient), + plan.receipt_effect, plan.receipt_outcome}); + } + if (plan.consume_incarnation) last_incarnation_ = plan.incarnation_used; + plan.consumed = true; + bump_epoch(); + return Installed{EventRange{first, count}}; } WorkingRequestCore::WorkingRequestCore(RunIdentity identity) { require_identity(identity); identity_ = std::move(identity); + instance_ = std::make_shared(); } WorkingRequestCore::WorkingRequestCore(WorkingRequestCore&& other) noexcept { @@ -133,10 +544,29 @@ WorkingRequestCore& WorkingRequestCore::operator=(WorkingRequestCore&& other) no identity_ = std::move(other.identity_); live_ = std::move(other.live_); history_ = std::move(other.history_); + last_ordinal_ = other.last_ordinal_; + last_incarnation_ = other.last_incarnation_; + epoch_ = other.epoch_; + ordinal_index_ = std::move(other.ordinal_index_); + receipts_ = std::move(other.receipts_); + instance_ = std::move(other.instance_); + if (instance_) { + if (instance_->generation != std::numeric_limits::max()) { + ++instance_->generation; + } else { + instance_->expired = true; + } + } other.identity_.session_key.clear(); other.identity_.run_number = 0; other.live_.clear(); other.history_.clear(); + other.last_ordinal_ = 0; + other.last_incarnation_ = 0; + other.epoch_ = 0; + other.ordinal_index_.clear(); + other.receipts_.clear(); + other.instance_.reset(); return *this; } @@ -146,168 +576,1370 @@ void WorkingRequestCore::reset(RunIdentity identity) { *this = std::move(next); } -WorkingRequestCore::TargetKind WorkingRequestCore::classify( - const RequestHandle& handle, std::size_t* live_index) const { - if (handle.incarnation == 0 || handle.run.run_number == 0 || handle.run.session_key.empty() - || handle.run != identity_) { - return TargetKind::InvalidHandle; +std::optional WorkingRequestCore::validate_request( + const Request& request, + const CommandContext& context, + const std::optional& replace_target) const { + require_grid(context.quantity_grid); + if (as_flatten(request.intent)) { + } else if (const auto* reduce = as_reduce(request.intent)) { + if (const auto* units = explicit_size(*reduce)) { + if (!finite_positive(units->units)) return RequestRejectReason::InvalidQuantity; + if (!on_optional_grid(units->units, context.quantity_grid)) { + return RequestRejectReason::OffGrid; + } + } + } else if (const auto* transact = as_transact(request.intent)) { + if (!finite_nonzero(transact->signed_units)) return RequestRejectReason::InvalidQuantity; + if (!on_optional_grid(transact->signed_units, context.quantity_grid)) { + return RequestRejectReason::OffGrid; + } + } else { + return RequestRejectReason::InvalidQuantity; } - for (std::size_t i = 0; i < live_.size(); ++i) { - if (live_[i].handle.incarnation == handle.incarnation) { - if (live_index) *live_index = i; - return TargetKind::Live; + + const bool market_only = context.surface == CommandSurface::MarketOnly; + if (market_only && !std::holds_alternative(request.trigger)) { + return RequestRejectReason::InvalidTrigger; + } + if (const auto reason = validate_levels(request.trigger)) return reason; + + const bool flatten = as_flatten(request.intent) != nullptr; + const bool owner_opened = + as_reduce(request.intent) && is_owner_opened(*as_reduce(request.intent)); + if (market_only && !std::holds_alternative(request.capacity)) { + return RequestRejectReason::InvalidCapacity; + } + if (const auto* budget = std::get_if(&request.capacity)) { + if (flatten || owner_opened) return RequestRejectReason::InvalidCapacity; + if (!finite_positive(budget->units)) return RequestRejectReason::InvalidCapacity; + if (!on_optional_grid(budget->units, context.quantity_grid)) { + return RequestRejectReason::OffGrid; } } - return TargetKind::NotWorking; + + if (market_only && !std::holds_alternative(request.owner)) { + return RequestRejectReason::InvalidOwner; + } + if (std::holds_alternative(request.owner)) { + } else if (const auto* wait = std::get_if(&request.owner)) { + if (replace_target && wait->parent == *replace_target) { + return RequestRejectReason::InvalidOwner; + } + if (!find_live(wait->parent)) return RequestRejectReason::InvalidOwner; + } else if (const auto* bind = std::get_if(&request.owner)) { + if (as_transact(request.intent)) return RequestRejectReason::InvalidOwner; + if (bind->cycle <= 0 || bind->opening.incarnation == 0 || bind->opening.run != identity_) { + return RequestRejectReason::InvalidOwner; + } + if (!context.opening) return RequestRejectReason::InvalidOwner; + const auto& observation = *context.opening; + if (!opening_alive(observation, bind->opening, bind->cycle)) { + return RequestRejectReason::InvalidOwner; + } + if (std::holds_alternative(observation.current_position)) { + return RequestRejectReason::InvalidOwner; + } + } else { + return RequestRejectReason::InvalidOwner; + } + + if (owner_opened) { + const bool wait_reduce = std::holds_alternative(request.owner) + && as_reduce(request.intent); + if (!wait_reduce) return RequestRejectReason::InvalidQuantityBasis; + } + + if (market_only && !std::holds_alternative(request.group)) { + return RequestRejectReason::InvalidGroup; + } + if (const auto* member = std::get_if(&request.group)) { + if (member->group == 0) return RequestRejectReason::InvalidGroup; + } + return std::nullopt; } -const LiveRequest* WorkingRequestCore::find_live(const RequestHandle& handle) const { - std::size_t index = 0; - if (classify(handle, &index) != TargetKind::Live) return nullptr; - return &live_[index]; +LiveRequest WorkingRequestCore::make_live(DefinitionRef definition, const CommandContext& context, + EventId accepted) const { + LiveRequest live; + live.definition = std::move(definition); + const Request& request = live.request(); + live.trigger_state = trigger_state_from(request.trigger); + live.allowance = AllowanceUnset{}; + live.pending = PendingNone{}; + if (std::holds_alternative(request.owner)) { + if (const auto* transact = as_transact(request.intent)) { + live.remaining = RemainingUnits{std::abs(transact->signed_units)}; + live.authority = BookTransaction{}; + } else if (const auto* reduce = as_reduce(request.intent)) { + live.remaining = RemainingUnits{explicit_size(*reduce)->units}; + live.authority = UnboundBookClose{}; + } else { + live.remaining = RemainingFlattenAll{}; + live.authority = UnboundBookClose{}; + } + return live; + } + if (const auto* wait = std::get_if(&request.owner)) { + live.authority = Wait{wait->parent}; + if (const auto* transact = as_transact(request.intent)) { + live.remaining = RemainingUnits{std::abs(transact->signed_units)}; + } else if (const auto* reduce = as_reduce(request.intent)) { + if (is_owner_opened(*reduce)) live.remaining = RemainingUnbound{}; + else live.remaining = RemainingUnits{explicit_size(*reduce)->units}; + } else { + live.remaining = RemainingFlattenAll{}; + } + return live; + } + const auto& bind = std::get(request.owner); + const auto& nonflat = std::get(context.opening->current_position); + OpeningClose close; + close.opening = bind.opening; + close.cycle = bind.cycle; + close.side = nonflat.side; + close.enrollment = EnrollmentFromCommand{accepted}; + live.authority = close; + if (const auto* reduce = as_reduce(request.intent)) { + live.remaining = RemainingUnits{explicit_size(*reduce)->units}; + } else { + live.remaining = RemainingFlattenAll{}; + } + return live; } -SubmitResult WorkingRequestCore::submit(const Request& request, - int64_t decision_time_ms, - uint64_t& next_order_incarnation, - uint64_t& next_timeline_ordinal, - std::optional quantity_grid) { +bool WorkingRequestCore::trigger_permits_driver(const Trigger& trigger, + const TriggerState& state, + DriverEligibilityClass driver_class, + bool existing_matching_bit) const noexcept { + const bool market = std::holds_alternative(trigger) + && !std::holds_alternative(state) + && !std::holds_alternative(state); + switch (driver_class) { + case DriverEligibilityClass::ObservedPrint: + return market ? existing_matching_bit : true; + case DriverEligibilityClass::CarriedOpen: + if (market) return existing_matching_bit; + return std::holds_alternative(trigger); + case DriverEligibilityClass::TickAfterCalculation: + if (market) return existing_matching_bit; + return true; + case DriverEligibilityClass::ConfirmedOpen: + return market ? existing_matching_bit : true; + case DriverEligibilityClass::ConfirmedExcursion: + return !market; + case DriverEligibilityClass::ConfirmedAfterCalculationClose: + return market ? existing_matching_bit : false; + } + return false; +} + +bool WorkingRequestCore::working_is_buy(const LiveRequest& live) const noexcept { + if (const auto* transact = as_transact(live.request().intent)) { + return transact->signed_units > 0.0; + } + if (const auto* close = std::get_if(&live.authority)) { + return close->side == Side::Short; + } + if (const auto* close = std::get_if(&live.authority)) { + return close->side == Side::Short; + } + return false; +} + +EligibilityFacts WorkingRequestCore::eligibility_facts( + const LiveRequest& live, const EvaluationContext& context) const noexcept { + EligibilityFacts facts; + facts.trigger = &live.request().trigger; + facts.trigger_state = &live.trigger_state; + facts.remaining = &live.remaining; + facts.allowance = &live.allowance; + facts.authority = &live.authority; + facts.waiting = std::holds_alternative(live.authority); + facts.needs_close_bind = std::holds_alternative(live.authority); + facts.birth_ok = point_eligible(live.birth(), context.cursor.point.ordinal, + context.cursor.point.effective_time_ms); + if (facts.waiting) { + facts.driver_ok = false; + facts.ready_to_match = false; + facts.is_buy = working_is_buy(live); + return facts; + } + facts.driver_ok = trigger_permits_driver(live.request().trigger, live.trigger_state, + context.driver_class, context.existing_matching_bit); + facts.is_buy = working_is_buy(live); + if (const auto* close = std::get_if(&live.authority)) { + facts.position_side = close->side; + } else if (const auto* close = std::get_if(&live.authority)) { + facts.position_side = close->side; + } + facts.ready_to_match = facts.birth_ok && facts.driver_ok && !facts.waiting; + return facts; +} + +bool WorkingRequestCore::evaluation_eligible(const LiveRequest& live, + const EvaluationContext& context) const noexcept { + const EligibilityFacts facts = eligibility_facts(live, context); + return facts.ready_to_match; +} + +std::vector WorkingRequestCore::waiting_children(const RequestHandle& parent) const { + std::vector handles; + for (const auto& live : live_) { + if (const auto* wait = std::get_if(&live.authority)) { + if (wait->parent == parent) handles.push_back(live.handle()); + } + } + std::sort(handles.begin(), handles.end(), + [](const RequestHandle& a, const RequestHandle& b) { + return a.incarnation < b.incarnation; + }); + return handles; +} + +std::vector WorkingRequestCore::bound_close_handles() const { + std::vector handles; + for (const auto& live : live_) { + if (std::holds_alternative(live.authority) + || std::holds_alternative(live.authority)) { + handles.push_back(live.handle()); + } + } + std::sort(handles.begin(), handles.end(), + [](const RequestHandle& a, const RequestHandle& b) { + return a.incarnation < b.incarnation; + }); + return handles; +} + +std::vector WorkingRequestCore::group_recipients(const EventId& applied) const { + std::vector handles; + const CommandEvent* event = event_at(applied); + const auto* payload = event ? as_applied(*event) : nullptr; + if (!payload || !payload->definition) return handles; + const auto* member = std::get_if(&payload->definition->request.group); + if (!member) return handles; + if (member->effect == GroupEffect::Cancel && !payload->terminal) return handles; + if (member->effect == GroupEffect::Reduce && !(payload->filled_working > 0.0)) return handles; + for (const auto& live : live_) { + const auto* other = std::get_if(&live.request().group); + if (!other || other->group != member->group || other->cohort == member->cohort) continue; + if (live.handle() == payload->handle()) continue; + if (live.birth().acceptance_ordinal >= payload->ordinal) continue; + handles.push_back(live.handle()); + } + std::sort(handles.begin(), handles.end(), + [](const RequestHandle& a, const RequestHandle& b) { + return a.incarnation < b.incarnation; + }); + return handles; +} + +PreparedSubmit WorkingRequestCore::prepare_submit(const Request& request, + const CommandContext& context, + uint64_t& next_order_incarnation, + uint64_t& next_timeline_ordinal) { require_identity(identity_); require_distinct_counters(next_order_incarnation, next_timeline_ordinal); - Request staged_request = request; - const auto reason = validate_request(staged_request, quantity_grid); + Request staged = request; + MutationPlan plan = begin_plan(); + const auto reason = validate_request(staged, context, std::nullopt); + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + auto impl = std::make_unique(); if (reason) { - const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); - SubmitResult result{SubmitStatus::Rejected, ordinal, std::nullopt, reason}; - CommandEvent event{RejectedEvent{ordinal, std::move(staged_request), *reason}}; - reserve_one(history_); - history_.push_back(std::move(event)); - ++next_timeline_ordinal; - return result; + impl->result = SubmitResult{SubmitStatus::Rejected, ordinal, std::nullopt, reason}; + plan.events.emplace_back( + RejectedEvent{ordinal, std::move(staged), *reason, context.surface}); + seal_plan(plan); + impl->plan = std::move(plan); + impl->result.event_ordinal = ordinal; + return PreparedSubmit(std::move(impl)); } - const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); const uint64_t incarnation = usable_incarnation(next_order_incarnation); RequestHandle handle{identity_, incarnation}; - Birth birth{ordinal, decision_time_ms}; - LiveRequest live_row{handle, staged_request, birth, std::nullopt}; - CommandEvent event{AcceptedEvent{ordinal, handle, std::move(staged_request), birth}}; - SubmitResult result{SubmitStatus::Accepted, ordinal, std::move(handle), std::nullopt}; - reserve_one(live_); - reserve_one(history_); - live_.push_back(std::move(live_row)); - history_.push_back(std::move(event)); - ++next_timeline_ordinal; - ++next_order_incarnation; - return result; + Birth birth{ordinal, context.decision_time_ms}; + auto definition = std::make_shared( + RequestDefinition{handle, staged, birth, std::nullopt}); + LiveRequest live = make_live(definition, context, EventId{identity_, ordinal}); + AcceptedEvent accepted; + accepted.ordinal = ordinal; + accepted.definition = definition; + accepted.surface = context.surface; + plan.events.emplace_back(std::move(accepted)); + plan.live_change = kLivePush; + plan.live_row = std::move(live); + plan.consume_incarnation = true; + plan.incarnation_used = incarnation; + seal_plan(plan); + impl->plan = std::move(plan); + impl->result = SubmitResult{SubmitStatus::Accepted, ordinal, handle, std::nullopt}; + return PreparedSubmit(std::move(impl)); } -ReplaceResult WorkingRequestCore::replace(const RequestHandle& target, - const Request& request, - int64_t decision_time_ms, - uint64_t& next_order_incarnation, - uint64_t& next_timeline_ordinal, - std::optional quantity_grid) { +PreparedReplace WorkingRequestCore::prepare_replace(const RequestHandle& target, + const Request& request, + const CommandContext& context, + uint64_t& next_order_incarnation, + uint64_t& next_timeline_ordinal) { require_identity(identity_); require_distinct_counters(next_order_incarnation, next_timeline_ordinal); RequestHandle staged_target = target; - Request staged_request = request; + Request staged = request; std::size_t live_index = 0; const TargetKind kind = classify(staged_target, &live_index); + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + MutationPlan plan = begin_plan(); + auto impl = std::make_unique(); if (kind != TargetKind::Live) { - const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); const ReplaceStatus status = kind == TargetKind::InvalidHandle ? ReplaceStatus::InvalidHandle : ReplaceStatus::NotWorking; - ReplaceResult result{status, ordinal, std::nullopt, std::nullopt}; - CommandEvent event = kind == TargetKind::InvalidHandle - ? CommandEvent{InvalidHandleEvent{ordinal, std::move(staged_target), - std::move(staged_request)}} - : CommandEvent{NotWorkingEvent{ordinal, std::move(staged_target), - std::move(staged_request)}}; - reserve_one(history_); - history_.push_back(std::move(event)); - ++next_timeline_ordinal; - return result; + impl->result = ReplaceResult{status, ordinal, std::nullopt, std::nullopt}; + if (kind == TargetKind::InvalidHandle) { + plan.events.emplace_back( + InvalidHandleEvent{ordinal, std::move(staged_target), std::move(staged)}); + } else { + plan.events.emplace_back( + NotWorkingEvent{ordinal, std::move(staged_target), std::move(staged)}); + } + seal_plan(plan); + impl->plan = std::move(plan); + return PreparedReplace(std::move(impl)); } - const auto reason = validate_request(staged_request, quantity_grid); + const auto reason = validate_request(staged, context, staged_target); if (reason) { - Request live_request = live_[live_index].request; - const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); - ReplaceResult result{ReplaceStatus::ReplaceRejected, ordinal, std::nullopt, reason}; - CommandEvent event{ReplaceRejectedEvent{ordinal, std::move(staged_target), - std::move(live_request), std::move(staged_request), - *reason}}; - reserve_one(history_); - history_.push_back(std::move(event)); - ++next_timeline_ordinal; - return result; + impl->result = ReplaceResult{ReplaceStatus::ReplaceRejected, ordinal, std::nullopt, reason}; + ReplaceRejectedEvent rejected; + rejected.ordinal = ordinal; + rejected.live_definition = live_[live_index].definition; + rejected.attempted = std::move(staged); + rejected.reason = *reason; + rejected.surface = context.surface; + plan.events.emplace_back(std::move(rejected)); + seal_plan(plan); + impl->plan = std::move(plan); + return PreparedReplace(std::move(impl)); } - LiveRequest predecessor = live_[live_index]; - const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); const uint64_t incarnation = usable_incarnation(next_order_incarnation); - RequestHandle successor_handle{identity_, incarnation}; - Birth birth{ordinal, decision_time_ms}; - LiveRequest successor{successor_handle, staged_request, birth, predecessor.handle}; - CommandEvent event{ReplacedEvent{ordinal, predecessor.handle, std::move(predecessor.request), - successor_handle, std::move(staged_request), birth}}; - ReplaceResult result{ReplaceStatus::Replaced, ordinal, std::move(successor_handle), - std::nullopt}; - reserve_one(history_); - live_.erase(live_.begin() + static_cast(live_index)); - live_.push_back(std::move(successor)); - history_.push_back(std::move(event)); - ++next_timeline_ordinal; - ++next_order_incarnation; - return result; + RequestHandle successor{identity_, incarnation}; + Birth birth{ordinal, context.decision_time_ms}; + auto definition = std::make_shared( + RequestDefinition{successor, staged, birth, staged_target}); + LiveRequest live = make_live(definition, context, EventId{identity_, ordinal}); + ReplacedEvent replaced; + replaced.ordinal = ordinal; + replaced.predecessor_definition = live_[live_index].definition; + replaced.successor_definition = definition; + replaced.surface = context.surface; + plan.events.emplace_back(std::move(replaced)); + plan.live_change = kLiveErasePush; + plan.live_index = live_index; + plan.live_row = std::move(live); + plan.consume_incarnation = true; + plan.incarnation_used = incarnation; + seal_plan(plan); + impl->plan = std::move(plan); + impl->result = ReplaceResult{ReplaceStatus::Replaced, ordinal, successor, std::nullopt}; + return PreparedReplace(std::move(impl)); } -uint64_t TerminalCommit::usable_ordinal(const WorkingRequestCore& core, uint64_t next) { - return core.usable_ordinal(next); +PreparedCancel WorkingRequestCore::prepare_cancel(const RequestHandle& target, + uint64_t& next_timeline_ordinal) { + require_identity(identity_); + RequestHandle staged_target = target; + std::size_t live_index = 0; + const TargetKind kind = classify(staged_target, &live_index); + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + MutationPlan plan = begin_plan(); + auto impl = std::make_unique(); + if (kind != TargetKind::Live) { + const CancelStatus status = kind == TargetKind::InvalidHandle + ? CancelStatus::InvalidHandle + : CancelStatus::NotWorking; + impl->result = CancelResult{status, ordinal}; + if (kind == TargetKind::InvalidHandle) { + plan.events.emplace_back( + InvalidHandleEvent{ordinal, std::move(staged_target), std::nullopt}); + } else { + plan.events.emplace_back( + NotWorkingEvent{ordinal, std::move(staged_target), std::nullopt}); + } + seal_plan(plan); + impl->plan = std::move(plan); + return PreparedCancel(std::move(impl)); + } + impl->result = CancelResult{CancelStatus::Cancelled, ordinal}; + plan.events.emplace_back(make_cancelled(ordinal, live_[live_index], CancelReason::User, + EventId{identity_, ordinal})); + plan.live_change = kLiveErase; + plan.live_index = live_index; + seal_plan(plan); + impl->plan = std::move(plan); + return PreparedCancel(std::move(impl)); +} + +InstalledCommand WorkingRequestCore::install_submit(PreparedSubmit&& prepared) noexcept { + if (!prepared.impl_) return InstallError::StalePreparation; + auto installed = commit(prepared.impl_->plan); + if (const auto* error = std::get_if(&installed)) return *error; + return CommandInstalled{std::move(prepared.impl_->result), + std::get(installed).events}; +} + +InstalledCommand WorkingRequestCore::install_replace(PreparedReplace&& prepared) noexcept { + if (!prepared.impl_) return InstallError::StalePreparation; + auto installed = commit(prepared.impl_->plan); + if (const auto* error = std::get_if(&installed)) return *error; + return CommandInstalled{std::move(prepared.impl_->result), + std::get(installed).events}; +} + +InstalledCommand WorkingRequestCore::install_cancel(PreparedCancel&& prepared) noexcept { + if (!prepared.impl_) return InstallError::StalePreparation; + auto installed = commit(prepared.impl_->plan); + if (const auto* error = std::get_if(&installed)) return *error; + return CommandInstalled{std::move(prepared.impl_->result), + std::get(installed).events}; } -void TerminalCommit::reserve_history(WorkingRequestCore& core) { - reserve_one(core.history_); +SubmitResult WorkingRequestCore::submit(const Request& request, + int64_t decision_time_ms, + uint64_t& next_order_incarnation, + uint64_t& next_timeline_ordinal, + std::optional quantity_grid) { + auto prepared = prepare_submit(request, CommandContext{decision_time_ms, quantity_grid, std::nullopt}, + next_order_incarnation, next_timeline_ordinal); + auto installed = install_submit(std::move(prepared)); + if (std::holds_alternative(installed)) { + throw std::logic_error("native submit install failed"); + } + auto& ok = std::get>(installed); + ++next_timeline_ordinal; + if (ok.result.status == SubmitStatus::Accepted) ++next_order_incarnation; + return std::move(ok.result); } -void TerminalCommit::install(WorkingRequestCore& core, - std::size_t live_index, - CommandEvent&& event) noexcept { - static_assert(std::is_nothrow_move_constructible_v); - static_assert(std::is_nothrow_move_assignable_v); - core.history_.push_back(std::move(event)); - core.live_.erase(core.live_.begin() + static_cast(live_index)); +ReplaceResult WorkingRequestCore::replace(const RequestHandle& target, + const Request& request, + int64_t decision_time_ms, + uint64_t& next_order_incarnation, + uint64_t& next_timeline_ordinal, + std::optional quantity_grid) { + auto prepared = prepare_replace(target, request, + CommandContext{decision_time_ms, quantity_grid, std::nullopt}, + next_order_incarnation, next_timeline_ordinal); + auto installed = install_replace(std::move(prepared)); + if (std::holds_alternative(installed)) { + throw std::logic_error("native replace install failed"); + } + auto& ok = std::get>(installed); + ++next_timeline_ordinal; + if (ok.result.status == ReplaceStatus::Replaced) ++next_order_incarnation; + return std::move(ok.result); } CancelResult WorkingRequestCore::cancel(const RequestHandle& target, uint64_t& next_timeline_ordinal) { + auto prepared = prepare_cancel(target, next_timeline_ordinal); + auto installed = install_cancel(std::move(prepared)); + if (std::holds_alternative(installed)) { + throw std::logic_error("native cancel install failed"); + } + auto& ok = std::get>(installed); + ++next_timeline_ordinal; + return std::move(ok.result); +} + +InstallResult WorkingRequestCore::install_mutation(PreparedMutation&& prepared) noexcept { + if (!prepared.impl_) return InstallError::StalePreparation; + return commit(prepared.impl_->plan); +} + +namespace { + +Allowance initialize_allowance(const Remaining& remaining, const Capacity& capacity, uint64_t point) { + if (std::holds_alternative(remaining)) return AllowanceAllScope{point}; + if (std::holds_alternative(remaining)) return AllowanceUnset{}; + const double q = working_units(remaining); + double initial = q; + if (const auto* budget = std::get_if(&capacity)) initial = std::min(q, budget->units); + return AllowanceUnits{point, initial, initial}; +} + +bool same_point_allowance(const Allowance& allowance, uint64_t point) noexcept { + if (const auto* units = std::get_if(&allowance)) { + return units->point_ordinal == point; + } + if (const auto* all = std::get_if(&allowance)) { + return all->point_ordinal == point; + } + return false; +} + +} // namespace + +Preparation WorkingRequestCore::prepare_evaluation( + const RequestHandle& target, + const EvaluationContext& context, + const TargetObservation& observation, + uint64_t& next_timeline_ordinal) { require_identity(identity_); - RequestHandle staged_target = target; std::size_t live_index = 0; - const TargetKind kind = classify(staged_target, &live_index); + if (classify(target, &live_index) != TargetKind::Live) { + return NoChange{NoChangeReason::NotWorking}; + } + const LiveRequest& live = live_[live_index]; + if (std::holds_alternative(live.authority)) { + return NoChange{NoChangeReason::StillWaiting}; + } + const EligibilityFacts facts = eligibility_facts(live, context); + if (!facts.birth_ok || !facts.driver_ok) return NoChange{NoChangeReason::NotEligible}; + + if (std::holds_alternative(live.authority)) { + if (std::holds_alternative(observation.current_position)) { + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + MutationPlan plan = begin_plan(); + NoEffectEvent none; + none.ordinal = ordinal; + none.definition = live.definition; + none.remaining = project_remaining(live.remaining); + none.authority = live.authority; + none.cursor = context.cursor; + plan.events.emplace_back(std::move(none)); + plan.live_change = kLiveErase; + plan.live_index = live_index; + return finish_mutation(std::move(plan)); + } + const auto* nonflat = std::get_if(&observation.current_position); + if (!nonflat || nonflat->cycle <= 0) { + return PreparationError{CoreFailure::MissingObservation, EventId{identity_, 0}, target}; + } + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + MutationPlan plan = begin_plan(); + LiveRequest updated = live; + BookClose bound; + bound.cycle = nonflat->cycle; + bound.side = nonflat->side; + bound.binding_event = EventId{identity_, ordinal}; + bound.binding_cursor = context.cursor; + CloseBoundEvent bound_event; + bound_event.ordinal = ordinal; + bound_event.definition = live.definition; + bound_event.cycle = bound.cycle; + bound_event.side = bound.side; + bound_event.cursor = context.cursor; + bound_event.before = live.authority; + bound_event.after = bound; + updated.authority = bound; + updated.allowance = initialize_allowance(live.remaining, live.request().capacity, + context.cursor.point.ordinal); + plan.events.emplace_back(std::move(bound_event)); + plan.live_change = kLiveUpdate; + plan.live_index = live_index; + plan.live_row = std::move(updated); + return finish_mutation(std::move(plan)); + } + + if (const auto* close = std::get_if(&live.authority)) { + if (!book_close_alive(observation, *close)) { + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + MutationPlan plan = begin_plan(); + plan.events.emplace_back(make_cancelled(ordinal, live, CancelReason::OwnerGone, + EventId{identity_, ordinal})); + plan.live_change = kLiveErase; + plan.live_index = live_index; + return finish_mutation(std::move(plan)); + } + } + if (const auto* close = std::get_if(&live.authority)) { + if (!opening_close_alive(observation, *close)) { + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + MutationPlan plan = begin_plan(); + plan.events.emplace_back(make_cancelled(ordinal, live, CancelReason::OwnerGone, + EventId{identity_, ordinal})); + plan.live_change = kLiveErase; + plan.live_index = live_index; + return finish_mutation(std::move(plan)); + } + } + + if (same_point_allowance(live.allowance, context.cursor.point.ordinal)) { + return NoChange{NoChangeReason::NoTransition}; + } + LiveRequest updated = live; + updated.allowance = initialize_allowance(live.remaining, live.request().capacity, + context.cursor.point.ordinal); + MutationPlan plan = begin_plan(); + plan.live_change = kLiveUpdate; + plan.live_index = live_index; + plan.live_row = std::move(updated); + return finish_mutation(std::move(plan)); +} + +Preparation WorkingRequestCore::prepare_trigger( + const RequestHandle& target, + const TriggerTransition& transition, + uint64_t& next_timeline_ordinal) { + require_identity(identity_); + std::size_t live_index = 0; + if (classify(target, &live_index) != TargetKind::Live) { + return NoChange{NoChangeReason::NotWorking}; + } + LiveRequest updated = live_[live_index]; + if (std::holds_alternative(updated.authority) + || std::holds_alternative(updated.authority)) { + return NoChange{NoChangeReason::NotEligible}; + } + const bool is_buy = working_is_buy(updated); + MutationPlan plan = begin_plan(); + + auto emit_activated = [&](ActivationKind kind, TriggerState after, const MatchCursor& cursor, + double price) { + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + ActivatedEvent activated; + activated.ordinal = ordinal; + activated.definition = updated.definition; + activated.kind = kind; + activated.before = updated.trigger_state; + activated.after = after; + activated.reached_price = price; + activated.cursor = cursor; + updated.trigger_state = after; + plan.events.emplace_back(std::move(activated)); + plan.live_change = kLiveUpdate; + plan.live_index = live_index; + plan.live_row = std::move(updated); + return finish_mutation(std::move(plan)); + }; + + if (const auto* begin = std::get_if(&transition)) { + if (!std::holds_alternative(updated.trigger_state)) { + if (std::holds_alternative(updated.trigger_state)) { + return NoChange{NoChangeReason::NoTransition}; + } + return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, + target}; + } + if (!std::isfinite(begin->reached_price)) { + return PreparationError{CoreFailure::NonrepresentableQuantity, EventId{identity_, 0}, + target}; + } + const auto* trail = std::get_if(&updated.request().trigger); + if (!trail || !trail_level_ok(begin->reached_price, trail->offset, is_buy, nullptr)) { + return PreparationError{CoreFailure::NonrepresentableQuantity, EventId{identity_, 0}, + target}; + } + return emit_activated(ActivationKind::TrailArm, TrailTrack{begin->reached_price}, + begin->cursor, begin->reached_price); + } + if (const auto* extremum = std::get_if(&transition)) { + auto* track = std::get_if(&updated.trigger_state); + if (!track) { + return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, + target}; + } + const double next_best = is_buy ? std::min(track->best, extremum->reached_price) + : std::max(track->best, extremum->reached_price); + if (next_best == track->best) return NoChange{NoChangeReason::NoTransition}; + const auto* trail = std::get_if(&updated.request().trigger); + if (!trail || !trail_level_ok(next_best, trail->offset, is_buy, nullptr)) { + return PreparationError{CoreFailure::NonrepresentableQuantity, EventId{identity_, 0}, + target}; + } + track->best = next_best; + plan.live_change = kLiveUpdate; + plan.live_index = live_index; + plan.live_row = std::move(updated); + return finish_mutation(std::move(plan)); + } + if (const auto* stop = std::get_if(&transition)) { + if (!std::holds_alternative(updated.trigger_state)) { + if (std::holds_alternative(updated.trigger_state)) { + return NoChange{NoChangeReason::NoTransition}; + } + return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, + target}; + } + return emit_activated(ActivationKind::Stop, StopActive{}, stop->cursor, stop->reached_price); + } + if (const auto* stop_limit = std::get_if(&transition)) { + if (!std::holds_alternative(updated.trigger_state)) { + if (std::holds_alternative(updated.trigger_state)) { + return NoChange{NoChangeReason::NoTransition}; + } + return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, + target}; + } + return emit_activated(ActivationKind::StopLimit, StopLimitLive{}, stop_limit->cursor, + stop_limit->reached_price); + } + const auto& trail_hit = std::get(transition); + const auto* track = std::get_if(&updated.trigger_state); + if (!track) { + if (std::holds_alternative(updated.trigger_state)) { + return NoChange{NoChangeReason::NoTransition}; + } + return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, target}; + } + return emit_activated(ActivationKind::TrailTrigger, TrailActive{track->best}, trail_hit.cursor, + trail_hit.reached_price); +} + +Preparation WorkingRequestCore::prepare_no_effect( + const RequestHandle& target, + const EvaluationContext& context, + uint64_t& next_timeline_ordinal) { + require_identity(identity_); + std::size_t live_index = 0; + if (classify(target, &live_index) != TargetKind::Live) { + return NoChange{NoChangeReason::NotWorking}; + } + const LiveRequest& live = live_[live_index]; const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); - if (kind != TargetKind::Live) { - const CancelStatus status = kind == TargetKind::InvalidHandle - ? CancelStatus::InvalidHandle - : CancelStatus::NotWorking; - CancelResult result{status, ordinal}; - CommandEvent event = kind == TargetKind::InvalidHandle - ? CommandEvent{InvalidHandleEvent{ordinal, std::move(staged_target), std::nullopt}} - : CommandEvent{NotWorkingEvent{ordinal, std::move(staged_target), std::nullopt}}; - reserve_one(history_); - history_.push_back(std::move(event)); - ++next_timeline_ordinal; - return result; - } - LiveRequest removed = live_[live_index]; - CancelResult result{CancelStatus::Cancelled, ordinal}; - CommandEvent event{CancelledEvent{ordinal, std::move(removed.handle), - std::move(removed.request)}}; - reserve_one(history_); - live_.erase(live_.begin() + static_cast(live_index)); - history_.push_back(std::move(event)); - ++next_timeline_ordinal; - return result; + MutationPlan plan = begin_plan(); + NoEffectEvent none; + none.ordinal = ordinal; + none.definition = live.definition; + none.remaining = project_remaining(live.remaining); + none.authority = live.authority; + none.cursor = context.cursor; + plan.events.emplace_back(std::move(none)); + plan.live_change = kLiveErase; + plan.live_index = live_index; + return finish_mutation(std::move(plan)); +} + +Preparation WorkingRequestCore::prepare_match_rejected( + const RequestHandle& target, + const EvaluationContext& context, + MatchRejectReason reason, + uint64_t& next_timeline_ordinal) { + require_identity(identity_); + std::size_t live_index = 0; + if (classify(target, &live_index) != TargetKind::Live) { + return NoChange{NoChangeReason::NotWorking}; + } + const LiveRequest& live = live_[live_index]; + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + MutationPlan plan = begin_plan(); + MatchRejectedEvent rejected; + rejected.ordinal = ordinal; + rejected.reason = reason; + rejected.definition = live.definition; + rejected.remaining = project_remaining(live.remaining); + rejected.authority = live.authority; + rejected.cursor = context.cursor; + plan.events.emplace_back(std::move(rejected)); + plan.live_change = kLiveErase; + plan.live_index = live_index; + return finish_mutation(std::move(plan)); +} + +Preparation WorkingRequestCore::prepare_execution( + const RequestHandle& target, + const ExecutionProposal& proposal, + uint64_t& next_timeline_ordinal) { + require_identity(identity_); + std::size_t live_index = 0; + if (classify(target, &live_index) != TargetKind::Live) { + return NoChange{NoChangeReason::NotWorking}; + } + const LiveRequest& live = live_[live_index]; + if (std::holds_alternative(live.authority) + || std::holds_alternative(live.authority)) { + return NoChange{NoChangeReason::NotEligible}; + } + if (!point_eligible(live.birth(), proposal.cursor.point.ordinal, + proposal.cursor.point.effective_time_ms)) { + return NoChange{NoChangeReason::NotEligible}; + } + if (!fillable_state(live.trigger_state)) return NoChange{NoChangeReason::NotEligible}; + + const uint64_t point = proposal.cursor.point.ordinal; + double allowance_left = 0.0; + const bool has_units_allowance = std::holds_alternative(live.allowance); + const bool has_all_allowance = std::holds_alternative(live.allowance); + if (has_units_allowance) { + const auto& units = std::get(live.allowance); + if (units.point_ordinal != point) return NoChange{NoChangeReason::NotEligible}; + allowance_left = units.left; + if (!(allowance_left > 0.0) && !std::holds_alternative(live.remaining)) { + return NoChange{NoChangeReason::NotEligible}; + } + } else if (has_all_allowance) { + if (std::get(live.allowance).point_ordinal != point) { + return NoChange{NoChangeReason::NotEligible}; + } + } else { + return NoChange{NoChangeReason::NotEligible}; + } + + const bool flatten = as_flatten(live.request().intent) != nullptr; + const bool reduce = as_reduce(live.request().intent) != nullptr; + const auto* transact = as_transact(live.request().intent); + double cap = flatten ? 0.0 : working_units(live.remaining); + if (has_units_allowance) cap = std::min(cap, allowance_left); + + if (flatten) { + if (!std::holds_alternative(proposal.physical_action)) { + return PreparationError{CoreFailure::InvalidProposal, EventId{identity_, 0}, target}; + } + } else if (reduce) { + const auto* phys = std::get_if(&proposal.physical_action); + if (!phys || !finite_positive(phys->units) || phys->units > cap) { + return PreparationError{CoreFailure::InvalidProposal, EventId{identity_, 0}, target}; + } + } else if (transact) { + const auto* phys = std::get_if(&proposal.physical_action); + if (!phys || !finite_nonzero(phys->signed_units) + || ((phys->signed_units > 0.0) != (transact->signed_units > 0.0)) + || std::abs(phys->signed_units) > cap) { + return PreparationError{CoreFailure::InvalidProposal, EventId{identity_, 0}, target}; + } + } else { + return PreparationError{CoreFailure::InvalidProposal, EventId{identity_, 0}, target}; + } + + const bool opening_auth = std::holds_alternative(live.authority); + const bool book_close = std::holds_alternative(live.authority); + if (opening_auth) { + const auto& close = std::get(live.authority); + const auto* scope = std::get_if(&proposal.scope); + if (!scope || scope->incarnation != close.opening.incarnation || scope->cycle != close.cycle) { + return PreparationError{CoreFailure::InvalidScope, EventId{identity_, 0}, target}; + } + } else if (!std::holds_alternative(proposal.scope)) { + return PreparationError{CoreFailure::InvalidScope, EventId{identity_, 0}, target}; + } + (void)book_close; + + if (!std::isfinite(proposal.inspected_closed_units) || proposal.inspected_closed_units < 0.0 + || !std::isfinite(proposal.inspected_opened_units) + || !std::isfinite(proposal.resolved_price) || proposal.resolved_price <= 0.0) { + return PreparationError{CoreFailure::InvalidProposal, EventId{identity_, 0}, target}; + } + double filled = proposal.inspected_closed_units; + if (transact) { + if (!checked_add_positive(proposal.inspected_closed_units, + std::abs(proposal.inspected_opened_units), &filled)) { + return PreparationError{CoreFailure::NonrepresentableQuantity, EventId{identity_, 0}, + target}; + } + } + if (!(filled > 0.0) || !std::isfinite(filled)) { + return PreparationError{CoreFailure::InvalidProposal, EventId{identity_, 0}, target}; + } + if (!flatten && filled > working_units(live.remaining)) { + return PreparationError{CoreFailure::InvalidProposal, EventId{identity_, 0}, target}; + } + if (has_units_allowance && filled > allowance_left) { + return PreparationError{CoreFailure::InvalidProposal, EventId{identity_, 0}, target}; + } + + RemainingProjection remaining_after = RemainingProjectionFlattenAll{}; + bool units_exhausted = flatten; + if (!flatten) { + double after = 0.0; + if (!checked_sub_cap(working_units(live.remaining), std::min(filled, working_units(live.remaining)), + &after, &units_exhausted)) { + if (filled != working_units(live.remaining)) { + return PreparationError{CoreFailure::NonrepresentableQuantity, EventId{identity_, 0}, + target}; + } + units_exhausted = true; + after = 0.0; + } + remaining_after = RemainingProjectionUnits{after}; + } + + Allowance allowance_after = live.allowance; + if (has_units_allowance) { + auto units = std::get(live.allowance); + bool allow_ex = false; + double left = 0.0; + if (filled == units.left) { + units.left = 0.0; + } else if (!checked_sub_cap(units.left, filled, &left, &allow_ex)) { + return PreparationError{CoreFailure::NonrepresentableQuantity, EventId{identity_, 0}, + target}; + } else { + units.left = left; + } + allowance_after = units; + } + + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + ExecutionAppliedEvent base; + base.ordinal = ordinal; + base.definition = live.definition; + base.raw_price = proposal.raw_price; + base.resolved_price = proposal.resolved_price; + base.current_ticket = proposal.inspected_current_ticket; + base.closed_units = proposal.inspected_closed_units; + base.opened_units = proposal.inspected_opened_units; + base.filled_working = filled; + base.remaining_before = project_remaining(live.remaining); + base.allowance_before = live.allowance; + base.allowance_after = allowance_after; + base.scope = proposal.scope; + fill_applied_cursor(base, proposal.cursor); + + ExecutionAppliedEvent terminal = base; + terminal.terminal = true; + terminal.remaining_after = flatten ? RemainingProjectionFlattenAll{} + : (units_exhausted ? RemainingProjectionUnits{0.0} + : remaining_after); + terminal.terminal_reason = flatten ? AppliedTerminalReason::Flattened + : (units_exhausted ? AppliedTerminalReason::WorkingUnitsSatisfied + : AppliedTerminalReason::TargetExhausted); + + ExecutionAppliedEvent retain = base; + retain.terminal = false; + retain.remaining_after = remaining_after; + retain.terminal_reason = std::nullopt; + + MutationPlan plan = begin_plan(); + plan.events.emplace_back(terminal); + plan.live_change = kLiveErase; + plan.live_index = live_index; + seal_plan(plan); + + auto impl = std::make_unique(); + impl->terminal = std::move(plan); + impl->retain_event = CommandEvent{std::move(retain)}; + impl->can_retain = !flatten && !units_exhausted; + if (impl->can_retain) { + LiveRequest kept = live; + kept.remaining = RemainingUnits{std::get(remaining_after).q}; + kept.allowance = allowance_after; + impl->retain_live = std::move(kept); + } + impl->flatten = flatten; + impl->opening = opening_auth; + if (opening_auth) impl->opening_close = std::get(live.authority); + if (std::holds_alternative(live.authority)) { + impl->book_close = std::get(live.authority); + } + impl->proposal = proposal; + return PreparedExecution(std::move(impl)); } -} // inline namespace native_order_v1 +InstallResult WorkingRequestCore::install_execution(PreparedExecution&& prepared, + const CommittedExecutionFacts& facts) noexcept { + if (!prepared.impl_) return InstallError::StalePreparation; + auto& impl = *prepared.impl_; + if (facts.result.status != execution::Status::Applied) return InstallError::WrongCoreOrRun; + if (facts.result.closed_units != impl.proposal.inspected_closed_units + || facts.result.opened_units != impl.proposal.inspected_opened_units + || facts.result.current_ticket != impl.proposal.inspected_current_ticket) { + return InstallError::WrongCoreOrRun; + } + + auto stamp = [&](ExecutionAppliedEvent& event) { + event.current_ticket = facts.result.current_ticket; + event.first_trade_index = facts.result.first_trade_index; + event.closed_trade_count = facts.result.closed_trade_count; + event.opened_lot_incarnation = facts.result.opened_lot_incarnation; + event.cycle_before = facts.cycle_before; + event.cycle_after = facts.cycle_after; + }; + + bool retain = impl.can_retain; + if (retain && impl.opening && !opening_close_alive(facts.post_target, impl.opening_close)) { + retain = false; + } + if (retain && std::holds_alternative(impl.retain_live.authority) + && !book_close_alive(facts.post_target, impl.book_close)) { + retain = false; + } + + if (retain) { + auto* event = std::get_if(&impl.retain_event); + if (!event) return InstallError::WrongCoreOrRun; + stamp(*event); + impl.terminal.events.clear(); + impl.terminal.events.push_back(std::move(impl.retain_event)); + impl.terminal.live_change = kLiveUpdate; + impl.terminal.live_row = std::move(impl.retain_live); + return commit(impl.terminal); + } + + auto* event = std::get_if(&impl.terminal.events.front()); + if (!event) return InstallError::WrongCoreOrRun; + stamp(*event); + if (impl.can_retain && event->terminal_reason == AppliedTerminalReason::TargetExhausted) { + // already staged + } else if (impl.can_retain) { + event->terminal = true; + event->terminal_reason = AppliedTerminalReason::TargetExhausted; + } + impl.terminal.live_change = kLiveErase; + return commit(impl.terminal); +} + +Preparation WorkingRequestCore::prepare_group_effect( + const EventId& applied, + const RequestHandle& recipient, + uint64_t& next_timeline_ordinal) { + require_identity(identity_); + const CommandEvent* event = event_at(applied); + const auto* payload = event ? as_applied(*event) : nullptr; + if (!payload || !payload->definition) { + return PreparationError{CoreFailure::InvalidCause, applied, recipient}; + } + const auto* member = std::get_if(&payload->definition->request.group); + if (!member) return NoChange{NoChangeReason::NoTransition}; + if (member->effect == GroupEffect::Cancel && !payload->terminal) { + return NoChange{NoChangeReason::NoTransition}; + } + if (member->effect == GroupEffect::Reduce && !(payload->filled_working > 0.0)) { + return NoChange{NoChangeReason::NoTransition}; + } + uint64_t seen = 0; + if (receipt_seen(applied, recipient, member->effect, &seen)) { + return NoChange{NoChangeReason::AlreadyApplied}; + } + if (!receipts_.empty()) { + const auto& last = receipts_.back(); + if (receipt_cmp(applied.ordinal, recipient.incarnation, member->effect, last.cause.ordinal, + last.recipient.incarnation, last.effect) + < 0) { + return PreparationError{CoreFailure::InvalidCause, applied, recipient}; + } + } + std::size_t live_index = 0; + if (classify(recipient, &live_index) != TargetKind::Live) { + return NoChange{NoChangeReason::NotWorking}; + } + const LiveRequest& live = live_[live_index]; + const auto* other = std::get_if(&live.request().group); + if (!other || other->group != member->group || other->cohort == member->cohort + || live.birth().acceptance_ordinal >= payload->ordinal) { + return PreparationError{CoreFailure::InvalidCause, applied, recipient}; + } + + MutationPlan plan = begin_plan(); + plan.add_receipt = true; + plan.receipt_cause = applied; + plan.receipt_recipient = recipient; + plan.receipt_effect = member->effect; + + if (std::holds_alternative(live.remaining)) { + if (member->effect == GroupEffect::Cancel) { + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + plan.events.emplace_back(make_cancelled(ordinal, live, CancelReason::Group, applied)); + plan.live_change = kLiveErase; + plan.live_index = live_index; + plan.receipt_outcome = ordinal; + return finish_mutation(std::move(plan)); + } + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + PendingDeferred after; + PendingAdjustments before = live.pending; + if (const auto* pending = std::get_if(&live.pending)) { + if (!checked_add_positive(pending->total, payload->filled_working, &after.total)) { + return PreparationError{CoreFailure::UnrepresentableReservation, applied, recipient}; + } + after.count = pending->count + 1; + after.tail_receipt = EventId{identity_, ordinal}; + } else { + after.total = payload->filled_working; + after.count = 1; + after.tail_receipt = EventId{identity_, ordinal}; + if (!std::isfinite(after.total) || !(after.total > 0.0)) { + return PreparationError{CoreFailure::UnrepresentableReservation, applied, recipient}; + } + } + DeferredGroupAdjustmentEvent deferred; + deferred.ordinal = ordinal; + deferred.definition = live.definition; + deferred.cause = applied; + deferred.recipient = recipient; + deferred.effect = GroupEffect::Reduce; + deferred.deferred_delta = payload->filled_working; + deferred.pending_before = before; + deferred.pending_after = after; + if (const auto* pending = std::get_if(&before)) { + deferred.previous_pending_receipt = pending->tail_receipt; + } + LiveRequest updated = live; + updated.pending = after; + plan.events.emplace_back(std::move(deferred)); + plan.live_change = kLiveUpdate; + plan.live_index = live_index; + plan.live_row = std::move(updated); + plan.receipt_outcome = ordinal; + return finish_mutation(std::move(plan)); + } + + if (member->effect == GroupEffect::Cancel + || std::holds_alternative(live.remaining)) { + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + plan.events.emplace_back(make_cancelled(ordinal, live, CancelReason::Group, applied)); + plan.live_change = kLiveErase; + plan.live_index = live_index; + plan.receipt_outcome = ordinal; + return finish_mutation(std::move(plan)); + } + + const double before_q = working_units(live.remaining); + const double deduct = std::min(payload->filled_working, before_q); + double after_q = 0.0; + bool exhausted = false; + if (!checked_sub_cap(before_q, deduct, &after_q, &exhausted)) { + return PreparationError{CoreFailure::UnrepresentableReservation, applied, recipient}; + } + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + ReservationReducedEvent reduced; + reduced.ordinal = ordinal; + reduced.definition = live.definition; + reduced.cause = applied; + reduced.recipient = recipient; + reduced.effect = GroupEffect::Reduce; + reduced.requested_delta = payload->filled_working; + reduced.actual_deduction = deduct; + reduced.before = RemainingUnits{before_q}; + reduced.after = RemainingProjectionUnits{after_q}; + plan.events.emplace_back(std::move(reduced)); + plan.receipt_outcome = ordinal; + if (exhausted) { + uint64_t cancel_ord = ordinal + 1; + if (cancel_ord == 0 || cancel_ord == std::numeric_limits::max() + || cancel_ord <= last_ordinal_) { + throw std::invalid_argument("native timeline ordinal reused or regressed"); + } + LiveRequest snapshot = live; + snapshot.remaining = RemainingUnits{before_q}; + plan.events.emplace_back(make_cancelled(cancel_ord, snapshot, CancelReason::Group, applied)); + plan.live_change = kLiveErase; + plan.live_index = live_index; + } else { + LiveRequest updated = live; + updated.remaining = RemainingUnits{after_q}; + plan.live_change = kLiveUpdate; + plan.live_index = live_index; + plan.live_row = std::move(updated); + } + return finish_mutation(std::move(plan)); +} + +Preparation WorkingRequestCore::prepare_owner_applied( + const EventId& applied, + const RequestHandle& child, + const std::optional& observation, + uint64_t& next_timeline_ordinal) { + require_identity(identity_); + const CommandEvent* event = event_at(applied); + const auto* payload = event ? as_applied(*event) : nullptr; + if (!payload) return PreparationError{CoreFailure::InvalidCause, applied, child}; + std::size_t live_index = 0; + if (classify(child, &live_index) != TargetKind::Live) { + return NoChange{NoChangeReason::NotWorking}; + } + const LiveRequest& live = live_[live_index]; + const auto* wait = std::get_if(&live.authority); + if (!wait || wait->parent != payload->handle()) { + return NoChange{NoChangeReason::NoTransition}; + } + const bool closing = as_reduce(live.request().intent) || as_flatten(live.request().intent); + const bool opened = payload->opened_units != 0.0; + if (closing && !opened && !payload->terminal) { + return NoChange{NoChangeReason::StillWaiting}; + } + if (closing && !opened && payload->terminal) { + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + MutationPlan plan = begin_plan(); + plan.events.emplace_back( + make_cancelled(ordinal, live, CancelReason::UnsupportedRelation, applied)); + plan.live_change = kLiveErase; + plan.live_index = live_index; + return finish_mutation(std::move(plan)); + } + + MutationPlan plan = begin_plan(); + LiveRequest updated = live; + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + if (!closing) { + ArmedTransaction armed; + armed.parent = wait->parent; + armed.cause = applied; + armed.cause_cursor = payload->cursor; + ArmedEvent armed_event; + armed_event.ordinal = ordinal; + armed_event.definition = live.definition; + armed_event.before = live.authority; + armed_event.after = armed; + armed_event.enrollment = EnrollmentFromApplied{applied, payload->cursor}; + updated.authority = armed; + plan.events.emplace_back(std::move(armed_event)); + plan.live_change = kLiveUpdate; + plan.live_index = live_index; + plan.live_row = std::move(updated); + return finish_mutation(std::move(plan)); + } + + if (!observation) { + return PreparationError{CoreFailure::MissingObservation, applied, child}; + } + const int64_t cycle = payload->cycle_after; + const Side side = side_from_opened(payload->opened_units); + if (!opening_alive(*observation, payload->handle(), cycle)) { + return PreparationError{CoreFailure::ObservationMismatch, applied, child}; + } + + Remaining remaining = live.remaining; + std::optional quantity_resolution; + const bool owner_opened = + as_reduce(live.request().intent) && is_owner_opened(*as_reduce(live.request().intent)); + if (owner_opened) { + const double source = std::abs(payload->opened_units); + if (!finite_positive(source)) { + return PreparationError{CoreFailure::NonrepresentableQuantity, applied, child}; + } + const auto ids = pending_receipt_ids(live.pending); + double pending_total = 0.0; + if (const auto* deferred = std::get_if(&live.pending)) { + pending_total = deferred->total; + } + const double deduct = std::min(pending_total, source); + double after = source; + bool exhausted = deduct == source; + if (deduct > 0.0 && !exhausted) { + if (!checked_sub_cap(source, deduct, &after, &exhausted)) { + return PreparationError{CoreFailure::UnrepresentableReservation, applied, child}; + } + } else if (exhausted) { + after = 0.0; + } + QuantityBoundEvent bound; + bound.ordinal = ordinal; + bound.definition = live.definition; + bound.source = applied; + bound.source_units = source; + bound.prior_adjustment_ids = ids; + bound.pending_total = pending_total; + bound.effective_deduction = deduct; + bound.remaining = RemainingProjectionUnits{after}; + plan.events.emplace_back(std::move(bound)); + quantity_resolution = EventId{identity_, ordinal}; + updated.pending = PendingNone{}; + if (exhausted) { + uint64_t cancel_ord = ordinal + 1; + if (cancel_ord <= last_ordinal_ || cancel_ord == 0) { + throw std::invalid_argument("native timeline ordinal reused or regressed"); + } + plan.events.emplace_back(make_cancelled(cancel_ord, live, CancelReason::Group, applied)); + plan.live_change = kLiveErase; + plan.live_index = live_index; + return finish_mutation(std::move(plan)); + } + remaining = RemainingUnits{after}; + updated.remaining = remaining; + } + + OpeningClose close; + close.opening = payload->handle(); + close.cycle = cycle; + close.side = side; + close.enrollment = EnrollmentFromApplied{applied, payload->cursor}; + const uint64_t arm_ord = owner_opened ? ordinal + 1 : ordinal; + if (owner_opened) { + if (arm_ord <= last_ordinal_ || arm_ord == 0) { + throw std::invalid_argument("native timeline ordinal reused or regressed"); + } + } + ArmedEvent armed_event; + armed_event.ordinal = arm_ord; + armed_event.definition = live.definition; + armed_event.before = live.authority; + armed_event.after = close; + armed_event.enrollment = close.enrollment; + armed_event.quantity_resolution = quantity_resolution; + updated.authority = close; + if (!owner_opened) { + plan.events.emplace_back(std::move(armed_event)); + } else { + plan.events.emplace_back(std::move(armed_event)); + } + plan.live_change = kLiveUpdate; + plan.live_index = live_index; + plan.live_row = std::move(updated); + return finish_mutation(std::move(plan)); +} + +Preparation WorkingRequestCore::prepare_bound_expiry( + const EventId& physical_cause, + const RequestHandle& child, + const TargetObservation& observation, + uint64_t& next_timeline_ordinal) { + require_identity(identity_); + std::size_t live_index = 0; + if (classify(child, &live_index) != TargetKind::Live) { + return NoChange{NoChangeReason::NotWorking}; + } + const LiveRequest& live = live_[live_index]; + bool gone = false; + if (const auto* close = std::get_if(&live.authority)) { + gone = !book_close_alive(observation, *close); + } else if (const auto* close = std::get_if(&live.authority)) { + gone = !opening_close_alive(observation, *close); + } else { + return NoChange{NoChangeReason::NoTransition}; + } + if (!gone) return NoChange{NoChangeReason::NoTransition}; + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + MutationPlan plan = begin_plan(); + plan.events.emplace_back(make_cancelled(ordinal, live, CancelReason::OwnerGone, physical_cause)); + plan.live_change = kLiveErase; + plan.live_index = live_index; + return finish_mutation(std::move(plan)); +} + +Preparation WorkingRequestCore::prepare_parent_terminal( + const EventId& terminal_or_replaced, + const RequestHandle& child, + uint64_t& next_timeline_ordinal) { + require_identity(identity_); + const CommandEvent* event = event_at(terminal_or_replaced); + if (!event) { + return PreparationError{CoreFailure::InvalidCause, terminal_or_replaced, child}; + } + std::size_t live_index = 0; + if (classify(child, &live_index) != TargetKind::Live) { + return NoChange{NoChangeReason::NotWorking}; + } + const LiveRequest& live = live_[live_index]; + const auto* wait = std::get_if(&live.authority); + if (!wait) return NoChange{NoChangeReason::NoTransition}; + + bool parent_ended = false; + if (const auto* cancelled = std::get_if(event)) { + parent_ended = cancelled->handle() == wait->parent; + } else if (const auto* replaced = std::get_if(event)) { + parent_ended = replaced->predecessor() == wait->parent; + } else if (const auto* none = std::get_if(event)) { + parent_ended = none->handle() == wait->parent; + } else if (const auto* rejected = std::get_if(event)) { + parent_ended = rejected->handle() == wait->parent; + } else if (const auto* applied = as_applied(*event)) { + parent_ended = applied->handle() == wait->parent && applied->terminal; + } + if (!parent_ended) return NoChange{NoChangeReason::NoTransition}; + + const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); + MutationPlan plan = begin_plan(); + plan.events.emplace_back( + make_cancelled(ordinal, live, CancelReason::OwnerGone, terminal_or_replaced)); + plan.live_change = kLiveErase; + plan.live_index = live_index; + return finish_mutation(std::move(plan)); +} + +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_constructible_v); + +} // inline namespace native_order_v2 } // namespace pineforge::native_order diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3fd4e048..ecad2950 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,9 @@ set(TEST_SOURCES test_order_action test_native_order_core test_native_order_terminal + test_native_order_resting_core + test_native_resting_contract + test_native_auto_path test_native_market_vertical test_native_market_vertical_contract test_native_calendar_driver_contract diff --git a/tests/fixtures/native_cpp_abi/README.md b/tests/fixtures/native_cpp_abi/README.md index 32f656ff..f5453c8b 100644 --- a/tests/fixtures/native_cpp_abi/README.md +++ b/tests/fixtures/native_cpp_abi/README.md @@ -15,6 +15,8 @@ current-header changes. | `calendar-262a280` | commit `262a28013ed277990349e845ba0554a63f6fbc79`, tree `fb00ed9ba02cede2fe08ee8113ae32f71fd26b00`, header SHA `c47b3ee15d69587879cca8f491e73880cddae1ce3e7dfd0d1f4db16dfb8fcf4b` | Pre-wrapper unversioned `timezone_identity_descriptor`. Byte-identical to independently reviewed `f041756`. | | `driver-08b5c88` | commit `08b5c8868eadd3022a62b32e7e2cabe0640e75f4`, tree `3482d155df5fe91c5dfd4a05fdfb506641d0ee19` | Unversioned driver values. `NativeCoordinate` is 72 bytes. `src/market_driver.cpp` is the authentic production `native_bar_structurally_valid` definition. | | `run-spec-262a280` | same 262a280 commit/tree, `native_run_spec.hpp` SHA `b263a8bf3a68f58201c968ffa9ec8b54633fe3c752332df164ea11cf4bce3c7c` | Pre-wrapper unversioned `NativeRunSpec`. Matching old/old uses a labeled minimal symbol control, not a historical validator runtime. | +| `host-e7d023d` | commit `e7d023dbdff1c98229155ec5bcdd1e4ac534f5fb`, tree `0201bf052429490fb453bbfd6037e5afd1669626`, `native_host.hpp` SHA `871865715f084a0054c9d8e220cb9b957318bfdc0a0e100765d1cda85d7944b3` | `engine_script_run_v12` host/event return layout. Return-only `native_events()` pairing; not a historical runtime. | +| `order-e7d023d` | same e7d023d commit/tree, `native_order.hpp` SHA `b13006e99554ba9caa3e5b444cca3e4f2b5ebd5e372d3dcbe44bb6a677192a3e` | `native_order_v1`. `CommandEvent` has 10 alternatives. `src/native_order.cpp` is an authentic compile-only old object. | Sources were taken from the pairing-audit capture `tasks/native-abi-audit/snapshot-20260912T064119Z` and, where that capture diff --git a/tests/fixtures/native_cpp_abi/host-e7d023d/README.md b/tests/fixtures/native_cpp_abi/host-e7d023d/README.md new file mode 100644 index 00000000..1beeffea --- /dev/null +++ b/tests/fixtures/native_cpp_abi/host-e7d023d/README.md @@ -0,0 +1,9 @@ +# Frozen e7d023d native host/event header closure + +Exact public include closure of `` from +`e7d023dbdff1c98229155ec5bcdd1e4ac534f5fb` (tree +`0201bf052429490fb453bbfd6037e5afd1669626`). `NativeStrategyHost`, +`NativeMarketEvent`, and `native_events()` live in `engine_script_run_v12`. +Used only as a compile/link control so an old host that only reads returned +events cannot silently decode a later `CommandEvent` layout. No executable is +run. diff --git a/tests/fixtures/native_cpp_abi/host-e7d023d/headers.json.gz b/tests/fixtures/native_cpp_abi/host-e7d023d/headers.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..f6f6a3e767875c91e0be4a07d3da51376f228f17 GIT binary patch literal 172613 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
Pdbg#vM7f1UaHh2Z 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/native_cpp_abi/host-e7d023d/manifest.json b/tests/fixtures/native_cpp_abi/host-e7d023d/manifest.json new file mode 100644 index 00000000..1458eef1 --- /dev/null +++ b/tests/fixtures/native_cpp_abi/host-e7d023d/manifest.json @@ -0,0 +1,181 @@ +{ + "source_commit": "e7d023dbdff1c98229155ec5bcdd1e4ac534f5fb", + "source_tree": "0201bf052429490fb453bbfd6037e5afd1669626", + "surface": "native_host", + "internal_namespace": "engine_script_run_v12", + "note": "e7d023d NativeStrategyHost/event return layout at engine_script_run_v12. CommandEvent is native_order_v1. Authentic header closure for return-only native_events pairing; not a historical runtime.", + "archive": "headers.json.gz", + "archive_sha256": "63ecaa5a951513748c5158bc1e70adf501d6dbe148ab39c51b466364af6cd1e3", + "files": { + "pineforge/bar.hpp": { + "sha256": "665c2528dca64ac9474de622dfd7ffdb540ff91316f70317e39aada158d1e0de", + "git_blob": "836f73daab382e9c8d5a6d03a942a921e39b756f", + "bytes": 655 + }, + "pineforge/broker_events.hpp": { + "sha256": "4f3d9948a4d1604750901fa3982d6ea49bd50dd15a86c4e5d690b83d1f5f23c0", + "git_blob": "2972d53c7b039af266f4da161301c01eec54c16d", + "bytes": 4343 + }, + "pineforge/compat/pine/exit_activation.hpp": { + "sha256": "eccae8a148a6093cd12fdea24d0b72f8128aa528fda0962a49aa75d7612fcf2a", + "git_blob": "4ce38272a9daf7223441926c8b71616e2b08452f", + "bytes": 3017 + }, + "pineforge/compat/pine/frozen_market_instruction.hpp": { + "sha256": "fab882aa689e28c4d0965078c2cc67428c16411033e34d495ffdda11467258bc", + "git_blob": "9d43f4f37f33899c844780adb9107d32df8bdf4c", + "bytes": 3201 + }, + "pineforge/compat/pine/intraday_cap.hpp": { + "sha256": "0ac0eab34625ab9a974037ff8c0217603a01deb81599350a4b24f7bf48ac0cbd", + "git_blob": "0aba76b6a78251df33e92f19290473adecf5a61e", + "bytes": 10469 + }, + "pineforge/compat/pine/intraday_order_budget.hpp": { + "sha256": "43f7700f9c66c630ab8a9d8fd899eecb03c2fd9aabff11d2dc2b7fa85f92981b", + "git_blob": "730d941fffa3939e375489f40a50901ccea9ec87", + "bytes": 3878 + }, + "pineforge/compat/pine/order_birth.hpp": { + "sha256": "15d2c74711c02b4e8af0954e3d82f22329ff1f734d854e536e9a41209d370d33", + "git_blob": "f18ff1e93a2be75de8de49a668c24e4dd07c5a09", + "bytes": 841 + }, + "pineforge/compat/pine/order_priority.hpp": { + "sha256": "16cedc3acaeb6a3052726ef52fed75230a971c731f5294c3acce3987091f8618", + "git_blob": "36e107e65f79db98d7079c249b0a79199b578eff", + "bytes": 1577 + }, + "pineforge/engine.hpp": { + "sha256": "210a43d3166e9788dfa8bb0e670776a243af136ef8fd026f340b52911484c8bf", + "git_blob": "be3574cfb9d920ec0226e9458659b90ed7e6aadc", + "bytes": 312105 + }, + "pineforge/execution.hpp": { + "sha256": "e6e2440118f38a0eba5b75a0fb0cf03675f92939c154aaf8dc0837653a113895", + "git_blob": "f270b7c64544ee9188c22752d5e41eaa78bf01d1", + "bytes": 4294 + }, + "pineforge/execution_close_scope.hpp": { + "sha256": "daba6e221a19d3d3cd1db248a2861414ef91648c8330709917067ab32b2ebdb4", + "git_blob": "65294d9f25468ccf3e4c79861beb96b8037b56d5", + "bytes": 583 + }, + "pineforge/execution_consumer.hpp": { + "sha256": "e52e2e954b061d8720f44a49d9eabeadefa75851924a6068876380dfd8d6f9f3", + "git_blob": "adf6776fa0ce3ccc689f32adf2c68cad41dfe137", + "bytes": 3010 + }, + "pineforge/exit_leg_lifecycle.hpp": { + "sha256": "9d785271511a350114adc7cf5bcb1600cf569f0a10559a8671b7e4b9cb9dc89a", + "git_blob": "91492ec22e05d7b6d9327f60ff5e239ae6906fcd", + "bytes": 22622 + }, + "pineforge/leg_activation.hpp": { + "sha256": "859d75d0ae90ec3cc109765da066b37c3bbbff9c7c840368fbdd05dc7e178f3f", + "git_blob": "4160c841782800853bb574219b3dad2ca8aa0cc6", + "bytes": 1353 + }, + "pineforge/magnifier.hpp": { + "sha256": "c0ed220b6df054b44596a434d4f48b1c3e960ca329bb8644a405a608175e06ff", + "git_blob": "1bbc910a869d162900d7610fa0bd9a88776b7b0f", + "bytes": 3241 + }, + "pineforge/market_admission.hpp": { + "sha256": "22914434a2c82f761b98c1b5a2ff9365c89bb802b584cdda2a3c30c5986ec6fe", + "git_blob": "70be6af6b556bf77cab6c9f823bb3492cf7c8294", + "bytes": 10221 + }, + "pineforge/market_driver.hpp": { + "sha256": "53851a469bb875eca1210225841f8e2b8ed5ca30e2a1f53d2c30a68885a6672a", + "git_blob": "d46beab6ab561f49608bf64dc3c990625d7d32a9", + "bytes": 5374 + }, + "pineforge/na.hpp": { + "sha256": "0e8c256fcdeda94a0855458fa3b8c4ad4f9864dc2aecce969934f6bea82fd0a5", + "git_blob": "e98505aadc68a09cbe5a14aacf591a799a15ecd2", + "bytes": 1064 + }, + "pineforge/native_calendar.hpp": { + "sha256": "5495cb007509937616255a9615dcc92c49e5115692c8375ef93b594a36e55860", + "git_blob": "742ecfabc3764d626617c2762d4b574d46d8ff46", + "bytes": 19589 + }, + "pineforge/native_host.hpp": { + "sha256": "871865715f084a0054c9d8e220cb9b957318bfdc0a0e100765d1cda85d7944b3", + "git_blob": "5fe94d129aedce20f58855b45a1dee11e38eb3f7", + "bytes": 5362 + }, + "pineforge/native_order.hpp": { + "sha256": "b13006e99554ba9caa3e5b444cca3e4f2b5ebd5e372d3dcbe44bb6a677192a3e", + "git_blob": "e042d10bb7b4c092228659804ab5d02d2764cfce", + "bytes": 10797 + }, + "pineforge/native_run_spec.hpp": { + "sha256": "20c62a1c9855461054b6138ee3999651c43bb81429ca8f3eee2ff40feb883051", + "git_blob": "098f5694afac252fdeac630859037dbd246871f3", + "bytes": 5509 + }, + "pineforge/order_action.hpp": { + "sha256": "7d76ddc3f6ba76216d5feb34cdf28908e33a92a56757a707ac6bd94dca315001", + "git_blob": "d992472cbefefdf8e29704a21f04614d68d39a58", + "bytes": 3470 + }, + "pineforge/order_birth.hpp": { + "sha256": "0eadb3e2a92248fbc8e6ba8f94285b15b0e83d7a0396f5904b40c216619604e4", + "git_blob": "ed4a4038e9d079c92b4ebd777a5f22f1f1256b0c", + "bytes": 4833 + }, + "pineforge/order_cancellation.hpp": { + "sha256": "389eb3659f121351ad4b53b936d3a2c79f343ecdb6c0c4bb0bc13777596b90ee", + "git_blob": "a4f11409f19c7a85347879ecfd942bd7bae5c2eb", + "bytes": 7172 + }, + "pineforge/order_priority.hpp": { + "sha256": "25bb7898972153b044675245db230c1171644856ef904f4dd02353b8968d433b", + "git_blob": "3e9fa5ee4121971a7962db8a60859aba6fb9ec95", + "bytes": 694 + }, + "pineforge/pending_order_mirror.hpp": { + "sha256": "7921179f690dbb6f00ee8ab6ca71b3174fcc42a61ebc6786c3727c7f85a576a4", + "git_blob": "fc08bfd7328718de1c658bdcea1826c2fbc9eebe", + "bytes": 21978 + }, + "pineforge/pineforge.h": { + "sha256": "e4c735a1e39411db4156ac13a50cee10c554039b20f33e3c2299cd73be6a6c3e", + "git_blob": "31db9f0b9f8ac5f33041917f7d599daff91013c8", + "bytes": 74362 + }, + "pineforge/position_close_obligation.hpp": { + "sha256": "6f6ad0aaca952dda7a9386cb78e57705b3494364dbd8970b0a051fe99d05b777", + "git_blob": "09576ee0f2a1d49becc46f5701f4369cf8f87bf6", + "bytes": 1060 + }, + "pineforge/quantity_intent.hpp": { + "sha256": "c4cc0f1cd5d6ed1f248825712a5ba4b4f5a808b787c05c58971934b89d770b90", + "git_blob": "41262fee5aa2db4032fc008542c573032563629b", + "bytes": 3054 + }, + "pineforge/reservation_expansion.hpp": { + "sha256": "27e514249f62aa7353ac084a6e59e25e338a867fe4d67ce4646446a1cf9f9dab", + "git_blob": "b4302c9eed97c3409d9911fd6cecdc64fa0aad86", + "bytes": 2434 + }, + "pineforge/series.hpp": { + "sha256": "e7709f67df9046c9eba163c2dbe91f8161084a955265ccf6b163355856c0afde", + "git_blob": "61baaef2c351d37b0d6a5eb179e91ce670eb83c3", + "bytes": 3439 + }, + "pineforge/session_time.hpp": { + "sha256": "f58c94bfae948e08fa21e52f20eae3940b31e398d83b800baf3a31f3a5fde669", + "git_blob": "af3a4472d852e0965947c006d0b6ad85304539e7", + "bytes": 10337 + }, + "pineforge/timeframe.hpp": { + "sha256": "5e4c312da463fdcff55e0887188523c8bbf85a2ae62f3317a2b23f22fe0161e2", + "git_blob": "3d1bc9d495ac7e5b39ea5bae57c9d9afe580192b", + "bytes": 29534 + } + } +} diff --git a/tests/fixtures/native_cpp_abi/order-e7d023d/README.md b/tests/fixtures/native_cpp_abi/order-e7d023d/README.md new file mode 100644 index 00000000..5d180f6b --- /dev/null +++ b/tests/fixtures/native_cpp_abi/order-e7d023d/README.md @@ -0,0 +1,7 @@ +# Frozen e7d023d native_order_v1 closure + +Exact `native_order_v1` header and `src/native_order.cpp` from +`e7d023dbdff1c98229155ec5bcdd1e4ac534f5fb` (tree +`0201bf052429490fb453bbfd6037e5afd1669626`). `CommandEvent` has 10 +alternatives. The object is a compile/link control, not a historical runtime. +No executable is run. diff --git a/tests/fixtures/native_cpp_abi/order-e7d023d/headers.json.gz b/tests/fixtures/native_cpp_abi/order-e7d023d/headers.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..ebdffe2cf2f0b771195b11b198a180b261a67ebc GIT binary patch literal 11950 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 AkN^Mx literal 0 HcmV?d00001 diff --git a/tests/fixtures/native_cpp_abi/order-e7d023d/manifest.json b/tests/fixtures/native_cpp_abi/order-e7d023d/manifest.json new file mode 100644 index 00000000..0cbdec7a --- /dev/null +++ b/tests/fixtures/native_cpp_abi/order-e7d023d/manifest.json @@ -0,0 +1,36 @@ +{ + "source_commit": "e7d023dbdff1c98229155ec5bcdd1e4ac534f5fb", + "source_tree": "0201bf052429490fb453bbfd6037e5afd1669626", + "surface": "native_order", + "internal_namespace": "native_order_v1", + "note": "e7d023d native_order_v1. CommandEvent has 10 alternatives. Authentic src/native_order.cpp is a compile-only old object, not a historical runtime.", + "archive": "headers.json.gz", + "archive_sha256": "a548a9f5c417b197364a304d7edfacfab3c3a6b135edf5437f107e6e423416a1", + "files": { + "pineforge/execution.hpp": { + "sha256": "e6e2440118f38a0eba5b75a0fb0cf03675f92939c154aaf8dc0837653a113895", + "git_blob": "f270b7c64544ee9188c22752d5e41eaa78bf01d1", + "bytes": 4294 + }, + "pineforge/exit_leg_lifecycle.hpp": { + "sha256": "9d785271511a350114adc7cf5bcb1600cf569f0a10559a8671b7e4b9cb9dc89a", + "git_blob": "91492ec22e05d7b6d9327f60ff5e239ae6906fcd", + "bytes": 22622 + }, + "pineforge/native_order.hpp": { + "sha256": "b13006e99554ba9caa3e5b444cca3e4f2b5ebd5e372d3dcbe44bb6a677192a3e", + "git_blob": "e042d10bb7b4c092228659804ab5d02d2764cfce", + "bytes": 10797 + }, + "pineforge/order_action.hpp": { + "sha256": "7d76ddc3f6ba76216d5feb34cdf28908e33a92a56757a707ac6bd94dca315001", + "git_blob": "d992472cbefefdf8e29704a21f04614d68d39a58", + "bytes": 3470 + }, + "src/native_order.cpp": { + "sha256": "84884d9eb8fa30990c8e433d6733dbf780e39f7a4c49d5481bd461485b83badb", + "git_blob": "1d1175d7cdb3ceb1241cb2f18e2e23c8fd980c0d", + "bytes": 13237 + } + } +} diff --git a/tests/fixtures/script_cpp_abi/basev12/README.md b/tests/fixtures/script_cpp_abi/basev12/README.md new file mode 100644 index 00000000..6cbb08be --- /dev/null +++ b/tests/fixtures/script_cpp_abi/basev12/README.md @@ -0,0 +1,8 @@ +# Frozen engine_script_run_v12 header closure + +This fixture is the exact recursive tracked header closure from +`e7d023dbdff1c98229155ec5bcdd1e4ac534f5fb` (tree +`0201bf052429490fb453bbfd6037e5afd1669626`). It is used only by compile and +link controls to prove that the pre-resting-host v12 native/generated surface +is rejected by the v13 runtime. No strategy, executable, feed, or reference +engine is run. diff --git a/tests/fixtures/script_cpp_abi/basev12/headers.json.gz b/tests/fixtures/script_cpp_abi/basev12/headers.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..b6aef49cc4a874b13c88d4ba7add56fa1c1b2cc6 GIT binary patch literal 160017 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
Pdbg#vM7f1UaHh2Z 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%0A-p<%_LSIoHAhNj}%@}VqkYE z!Zc083GqhL12 z1ex1PJ5y)uZISF)z#R;$b|6 z>^1oyE(jI!4bITB=%NkGmx9+Bh}ei$hxaN~5;ybcZ#Yp#3NaH#oFTRxG4j1KEM&lN z@z|fGG<@>1dRkYKr80{jUZ1e>N4c+HBOBL30 zN7q1G`euOd0n9ww^KCS=R#S{~1JKNYIJ>te^7m@0<|tZ7&3M@d{pBZGI!x_EndWH1 zAwCo?!?&{`rGx^!fe6&QN7%>d?_Wp%MS?wDdk_R}*&NR<)4AACpX@w&*4}y6-hHyh z{N`?t8UE45_ZlZa8ZO_c&z^kwN9h=_M@xUwdHOVZd;T&aZ8=_J_9svN@T~LfnS$uW zSJBhXmplJOXZF&WMl6Wc+XLHEBizeFmxJ9g)e&uzRFT!p7u}3=3Vj7Z;(5^pPZVaph~cp&ZnQwPBI(af=dcb67U>i108|7z{RoDi`)HgvUs>*8IdICC->teBFUB)J)*I}t7Z&LUflruR2qb#5o)?9T_;Iv9ny?VvQ?6y0Qn}4w8M`z z2_JUyM0Sm7he-w6A)m}dr@jrX81Km#FqB}!&G#^3Zr_yQ#;ddNht1JEQ z_Rf>$o)-b_s$scn%&SN*$2*EI6FMO;#zl~^HNmQ_ZT;5WykH5mJ{9y@Azhv7oizjK zT6b!;7$I5H;1;nWB|%{Dg;iP%&} zRA`Vzmk8M&LRRjVBqhjk22ZX5+ZV!h;ehiAk3phH{#2TG$y((s#+^;XCKk%9?W)}T#G52 zr0V7h)rGd5S0D^T3E>7~iZW5^5>#pPl>8hO_F)fZrZsDjy0>gy9|By;N(P*`B zTfD(@o(6H1QXWkt6q;}wlSRa6W_0_q&SujdjqhSuR!OG!r;9BZ{G-PK6!i~4qe9uz z_LNdu3L<4eRE5Xf&$}JGumxl}Jv=)&JidSsLXS2#+{kf!EW${B8x=N;abjz%MvC*r zExVkd2Nor}mPvAfX1MGPIqlQE!GAA2T_J~gxx^NUH-mUeFdQi`h`Z9EH8}9O3u>etQR!FU5&}jNr=Pz5l~l!Rg8Q5qxr<9!Cw* zorh^Q9mV&7X5L)(+G<4c-~T%SOON-8=-Xu0jcQh{hrAtZqT}Bb?5z5hf*JBP?p9OX z;0}e9q_Y7baIYu*P%@#s;z|ma5Lc=` z#u7w)3uWM6a!-QQ6|qoIF#ed{E^e3IU>B({j*gCB9wM!dB(lwxdn>PLu>pi_r~!)0 z65FsF<21l11Jm_pFS6vb(99j(W!`Yz{3iptf+$s?!9Q495Jbq`@qaCa3Rhhvni+b~ zQMGd@vU<&mHXBlr9FDDxHk)fs1teRIH+#|kXtdco-aUG)r_FqbT`~v~V;wCfVeslI z;piD*qVt=0tdT_Gp2+qE%PD`A?PB4bTPCl?OETG_-8T zK%Mr6kSb7Y4C}S1Uh|(Qu^S)?=C^u4) zj?TsmBWYW440h30G)mtm#F=XCwGt#>G}yWtIUntT&^ep;0JrB8dgWwsePa>#!`Woo ziX5MJWHb=*3nyOjA}3<-iz*%<+BFh;w0ixga2FGGNr+bt<{R>OGajn#aLEEgH+X2o z1q1*KUdAkiXXA!=w)&_UK1^+KGGg%`Cm%LjrNWor(2)GBET2v$an7_v;;C~q^~_f9 zB^2h0z~%=EkcO{M_Ol9Vmy3JrN&-!?Y@bqlQP1o}TTw$wMZ*!WEk$?Pd)p??(Q=sd z(Cfw%N(O;Ag;P=2xG_Njmu{uOs<4f6L{Q-Y*xD2_|KgQqG+;`y*Ik{h;3(odvBUJhU3&y#Vop$LUFbFKry1|D*v%yfPz#@D`@B}w$;zqap* zwm#V=(HYX!E!NfCR8AX@7aHhwN$dY2G<4NUNSR^)}?XnmUuHcW4ld7hZsaB7TK=EK-*lY`K zdC*TlDBM2mB>uH9k^OP#MI}tQu!D~>(V1I?lh%6o60fe2>>HAMNEeKF_X*AMg2)cx z>uz%!)^DT`A%9^^7>g1PTIFU#DbglF$QK>rKLsl{$Hpqqdu-&oO^^&Yq$vx|O9XFA z`O^1xH(PlfN(amSSAb#=U(dQVvkDR_Q#15HwBpWIqrq`Q${4o`oU8f&HFmf0aMq-g zeJIrj(lKGjHjdd_rY!O_ea%a8l-W5JvaFb6CDuur+8VS~oCf z3d5(A}22#t2nu6Ok(n`fE6MDYt2wXGZJ)SNzd&93(c`G^uK@okr>uXM@u61J<5ib)3v}%{Js^npJUKl)u3UTBmM0)x zD&{{9@AKm**h+c6;LGkZ1V$u#84SOccJ2=s!bZQCh1+QMBgW=oO4{lKYPEzRCbNeo zdz|akFKNd5oDY}Iix=n4hv7(H_i|QOIU%V`nGRj9bSbMCn<=M)AZJ)RsT7jk2q+h? zG@TmM$|`S`=Lp(d$vhGt?HZP_qU%~<1?itzc`g(KVhph*Rn5iGyln_n979o@@7FRI zD{nGTgzJEbNhmngL#-X350hL{Na*Hidis6mLD@uugyUM5rWElzV34i6{H4snrMKPe&q0W|jQk4;UlIcz zxj6BPkT~=kNrOP}BeoB7CS5Vhik6MXD-6@pksu#Qhdgdg?PIlviyKTVk}${6LXdbw z`KAzyeI;1}q;}+zzKMlZB!NGSK%DWL=${~k)H z_`}ilw)z*l%})KXx??J+2CtLxHIV^Y`)3xXQ%Z8xe^p_op{Et%^dQ8N)A4IVO+`MC zac!^!^w)HXBJZ_-oW3l67af!g9V^!Ag6qfy_D_zZJYgtX`f?~dsdhWFS=~lVgbJjx z`hDaIqmVR@D;6YhUEpsk$C49uv{nTGn-ib1&gyqLVaQ$=yvx25<>3zXCO_p>CM&s( z1+UWY)_+qDSA4mXb>QqPLu89fIp!j)&!tpLrd3xmv*58Q(uQ}|c>~+cq-U_bC9G#5 zwOml9YhL>XPm>+ek;WGf<9S?rk>yUzS*}E8e2zxHpU-85!h~3IMpy_!ASH>6QgUI5Up~ zaR~AS`jLAVWJ`|$Djs*kcX@opl+r6mH6f{7RycSzFNfupwsA+CeiyQ%f*!FI&Fn*p=65q;5f0V69%z-4gaRNV+xl_wrN?NzceN{wFhCbs=miL7xkKKh(@MG#q=f*9JH5@l%*Oflesw@aD&wdoT+p9|sB7%t3pT$a;E;b_9@dYOI^e zjIMJj7{{GUEapJFfuab(RZ3d;@yW$u&>b6i?T>lD725WqL64uvpK;!T<_xx?_jRk8 zcNFCx<7-ui6Qv@8EmP=Vi*c*W+f7Q!W=|-V8eG7f>_q(xxuZRP`}#FqwVk#=LkvHD z5^$Bt)m7BcVx&#@WyX^NDyUT-u^vd;;rMu>HeA9_-CaPCbL@HJRzCOiLFx;a%lvGG?6)}il5;MfgdFy-p*6>uSib(1UJNAA<+;*< zzT)4>G$uZ3TKIfY*<=9KM$iE$Z}YmFq(iRW?7-x@{rsoo@V{kQ|MQEp{fonIe~dck zCqze2PtGpRU1X=#k(WACWi0qlKZSqWgXN<+M5G%x4N~K}DreRm!Fma?&1gcr_yhMY zkS0QGvy_%V+CX8HUe02Ywbkm$l`;^dp~X&VmRnb+_a?a)!6#@JQqi0s3yQ|;;tDMm zc+l}gYDtj%M=RXJb6LXkGr=b1W&jykm~A3`zaZ?fFO-Eno(2YEF1Bc&P{HIXCL-KFZa4J>b4CEu2?NU(5YpmJ{%>>Z|+?S!FNZ=*1dKH5_3d z_?J0De2ZIB1Qicz@GftTN4ZJiK@GLWw_p)zO1Pt&YgDr8Uqaibf$bq5+h8s!8yt^n zUA<0EUS`q-OE>!M0tmn>@YQ*bPkKfOdoK?UUY{cn!}r=?Y0!%H`?}TI1q`YLig5T+ zyO9}52@LQsH5op7?Bqwp*-D|qJr(aU3cwo z6S|ms*@W$DDfyHupDSLt)%I+)jhs9XWlblHeD5lVYoTUH?N*CXBZ&54JS(^mqz~1& zJvP4&6nSwvX&)=mnmHtU7~{U6cK3(`YkjWwJ|Sp1%TCsE!ItWrfh?E~NU%zW_Bu^0 zw4$25TQrEcq?6x^c3dT^-ZhPAp`^3aF#gz!FS8z3Ld5Bq+9YktNfM3+659R+Kg(*< zXi^`7q4lS6=>Vr59nUYtR(||&PdobMmHKEa*nihon8bYJ#qFi*9~llY3ss9on*poh zF#%W0egPD=umSqvD==(9jSl`4t!XMS*pj-DLD$0k{2C4GHi4NDDBH8I@*OG*Dr!sc zNBX+AEYYZU^|2-8&3ZwLat}-`j3d_177SOt_bJL~=}YiI*qzW4KkH`tOkf>ia{BX+^yW7PDkJwdM63R3Rlv|&JG6#EVw#7 z6B_zci}IT%T{yF-5@5(z3#~c?@~AHm#q)MC4`K$ZD2uc7+T!nai=?Hc7*n~yE#8Mv zpK@EH5({wPP#Xmxl6Im{2a3k7UF~d(%3k?!2=8B2=of&S z=lqSZ(P*$^Pu<2#lmk*dLpxiH!^V0NZRnfp!AR+x2~uO2O=8|zQo^N- zb^HQe$dW6;K8aMOf#asc;lzPCWjBoB4T;kvz z^e6le76$MtG=HioMnHmy1Z}(){`2^ql>!nBYZ+y8cBJA_g#u}bJ}mJ`TP5x-Nq1cG zB&gDO#Z-oZTY$=P1hIza1>EosmRy&`IuyzME8t5NX5{|inBkMDLV?nQj1T!IO+*Nu zJSsZl-aXotw#i@0k6r!wY)AioiJzTL$B?Z4-yutPDix}`E9_y(*W)sbqssE@b#hDq zvVya(Q~4G(Msl@Vx-_=$5`ew)OyM?}jF33l*g>5Wt)$*`oebEWQ$Y&cltphW9pHJ` ziX)MX3Llr}VvzQtr_H*#pf}0ptJ-;p`{A=CeF!SaLGIs4fg~V~h>?E%JIv8tn_T1# zZ}WOrIx2X_+|h3|$l={u3(DcqS|dtmvJRZ;PPc$4>r7Qc_4wFtT=)?2g!vr3xrbt1 z&WDeA`gSxVm1{C5jPBG509*@pY_)pZ;&TXQl5o9HDCjQ_-m5@HkExB^9_6>@GTXYh z-P9O>jl~s++U$oM>a+=<9(XU}vk*RPQ0;2X%%9sAylMvR1AMDrxwaS#_r$H+js> z$1C1pebcyuMDlTNj*Uuww3G*t`iW;QtaHQXL{ZZ@va`^Cn2rOtKT2IU_X4Gn)kNo{ zW@>7hd}1w5S(W9wYbeEf8=DCsYgMt9OCiqueE;1e!j1$zySMQNttG=>1xY(oXJbAt zmHom4xDX-G65V5W@;rW@Xkv6^=b0@orCkHVFk2#U!r8_k+L#39C=cu%8x4?3_uUHQ zD59A(n-{%=kB69WD9N5Bg9#B5xY&qg;^YNk_sY2zgu_9t6MoEvyDIDL{Y2`pena>A zp5B+jVaV~bs;#>*>fDpAn)G8V+0FbOQOL)0bQb6b9N7n5s*--~gj{UJ%OZ{`wq7dG}%2_QJP@1!G)Vy;D?+!W-Le0MI5 zpsr}qwG>>Q)5t1&w#o;=uaY&IW-2|@*bTnK{Ofw>{>Pj2Z|Sx?dwYCw^yV;nee})Q z{@IVqJeO$bonE?-|Nr4K@M|R^(D3G~<#i!XxQwbhVijO6L)myzJ>Bfgv zs2zdE=CW`!hNF;HRrRl!ThkAcYe)>1HVe&`QV+u6*?s+F(tgh)x+{r0moN}y3QH|4 z*yksxtWZChYYP#0MO^1WL-w}bEq1C;qROM*D;aY4VMdpwyjRUl+@ zy(8TNIg`Qc&GiG6^aTuzfPfRjEX zySLg9!AlCfMr`gWtWOigGWu?C;~Nd$LS&BodZm+>u-yfmyzEvAdiTIM-A2IXd3A~qs97xYf#dTc{9QCfLu@igt} z8XJ(3oB1BXPB7$FR@_l>9?APU){oF-Eohf;mxD}5yC<8uTyVNa*3;&XFKjHqgl+^D z5Ras(FM{Ag5d;YpuF8Wp5!_}{%-7x4HA}d@@*r`BYzT8W2M!NZLcup&XF3;SE>Ckg zAq>Y0C0*Q6Hl_0xRsz~bj3B z=4O$Vp+NahrITwZcT31Bsyvjxg2qK}Ek+5IgcgewssKr@1QDkqD}yo?&37}OU`$kM zo>_`fnIG!aaXN$x#l1vM2lISsb4a)_PDY}57)R_#%6^_Xc{ZUOs#^-T08$j?KxUaz zuR#hy;)87(rc9C3$T(mco+i4h^!Aght2QFEwh^E@xoV4+4LPUf#8EF*u^ri@uyowE z&_!0ZLDM^MF>nRDw#xS$yTK{N8sC_`Pp4DmL{rjL&2k(0;H>TTF0G7c%khtT9zi;~ zvJR+xbl}5nEdr*pyvR^{M$Ndal?fZt9Fc>x1sUjyvaCqA{RSNq5`i_8fI9;)$|xd)vB!+{Gr_5X zzBC5eqZ`8nr4yYiK@dR;-c5`D6lp7%RKcgEzweD0*`Wzvk0S)On%`^{t!ctH;i7Q2;I>&V~Bg!q!pk$>I z0K%+p+xofBeNfk-G>*$mgdmz53s(S317>OGXrw~V(mTW}Ix+ka<7sv?Vbx(UyPv`)7EfiIVg>3-Uh7;hW_B$a33l{Qub3mHTJVLY!sWrw!fxWRQ{ROF6Qbs~ zgf2(PEo2fYbR@b{&d>J$CRtvdt7QK3P#0nddE7pfd_FzZh5}2$% z3F|Q)g!{q*&0T=YJm5x}v8(wb`mIyHtsAhgi03bYsz{6Jbd+l3XonbHn#@C~!HWi$ z#KY-y*drotRI%KlY8$ei^mdVy(G5ls<>X{_r<+JE4N_my@zuoTRY25sLdPn_M7IAR zqxo0TT?em#}SMs&ekAr@CS$EtK=`z;T?feXL3+I1f0Tr$;?hD}own@?Bo z^61{QJUxw@$%2TJNQSW;v!uwH@Oz0Syk_su|E|Hq zlVOvY*bCfEmMTh|l?#5jSFlZrb25mpF0#7U=T3@kc$xAV>~xw8qIZj(o%j!5batDZ z4H~`PKfmBht?&_-$rU@6G#8O6Za(m;(G*E2U)}N&+Jd#R;tZqDvuK8Z6gY!4_s<_C zE4L<9O(FDUh*``_FD73RcC`790=uGiTeU!34qQIhLPO0Nv5BjoBZNZ)=E<1*m2j=- z*nCy&eNzh0evk;^_B3n+GjfP`fs}?kIgLF>n40Ecz;sFIZ-OVUCKG*I-X0LzJ+u9j zs@Vr;VUab>aLinam&50@EKnR8siWvg2L%tAyXXk|mIt{8-Okl1jSPHY_XcV5u<``1 z2M!e=WjINQ&CVG*1XZ&s^9H!4w=)pwob7gy(P=Y%9(vKIqeUjZNikkryrRQOrWkp} zp@8rspw4unJ%w$qF9axJ`swQl)6TkR5uOF@ngmp{$`%Z*-Iy9llzW)YWw$KJE4b>4R%N+b@f(tv~fo58Wz+E;VadOT#jT=@5;f1`$>p> zgD09o|91D0``2fT2Ei2U5&V#5Nmn|a=jv58Txvm3KC6kN&7B!KuEf)c3U{VYg=u<&fVUrsU^kn5*cM9?jKCr> z0bhd`+>?OSSC&wuQx%fdcBF zmp`3}fk;d8^qGkE@eMnnu1S`M;2?8Z8aMb!lMQ}M;Rvn0nKQgleAL0MQb_r$mDt(Q zq;HR39=%=01f29R?A9!haIg6yBv*^7v*)vnJJr!xr6mybPm6tT0yt9oKa{ zm5ZOXq&=^*?B?qwv3sZoWe!{I0g{Bo*XiJMuY~Y5Z1Xi#BGk!Ar4sSxMF0|g|)qz)D7n~+PLpBr&bmAgYaZUjtX-1(y_vBLrI>gg! z;Qfu6)INtRHfi@|Rek1Fnbz`RoYh`8sTQkd+sB@C^cu$Z?E#(g5N;~aBtGtzYJa%u zV(8Y3B(+*K9!?JCp2(n>D&*Uqd<)i%-~&#)Qn$EOLY_>wN9k=kcmDBf6hEK4@au*P zZPy?L4RP-`1KsyF{@8X~;NGTu`Ptq_GZ;GQXN3w+KP+`_pe5L7^qDMW@0%3H1hQ1pCV|C&FEO>PV>nxnL`UcLRLdlX4xkYvXu+v2oTL- zVW2Dt9cHqkTD4w4n@zUC_0$?p{sxOOGLUeB5;o0f5xmDPM6UY)(-9WyIE5>SDx_Id z$YhaK+(GPZ*5c(X0Pu2PsF}Q{r}ZIur)N7kZP3I<1~X`^UORR5VJ54hD8G-LE=H_r zxR^OyUQ=5{yj&F@$(3i%B1hWuuyV4l+oE?pG) z;B{6NKSenv0!o`E;tOk;J>@_ml`IcVPDrhRad5#NBeNA|-nA&YMT8WWR*}d_y`W)J zXeX11a`sGvC0qM@C_@9mbB1G~H|F6*k}UTP#vxvd&O_1yft>ZWyIt`J=|zETBa?Al zh2$EGw`(nFQ7K)uA}06AY%^GolNp1E3@eRo4jvZ!^@7Po1?G$p2u`RpLCGEiRX`%r zOaf9pbbJxWf-zTO%|67V_ZD}PQ$W29xS>&kzE_H@u4RgG96k*_EYm_Dqz5-!3TV2a z>BzBf-L37@al2rBqB2y=pUcp@B!7Al&U94qfBO5_fV4=$qDg-J%HXfQ=tg2HcK#cf zio{h2(dyqv8m*P4m!(Z`CNk4wD^YDtTg6g=O2bUejlPX4?8PFTYAi^iv7x$ggm?&@ z6fRDe8{!2%=^?PChr;(gHcIrp@X2FE*n!7#&L%dd;ei;)p)XUY`W%^xSSvv~!*QiP z!-Yjhw=vqh!dh}@T*~?jYY3oEs083xy*x&VgSTgA=%|L#)gpJRXsqN;yD{PW6-Ra{ z^^sVa?w4Wj)J*zB^Kc$$Q>Kj}xWPkyMH{Iz?-Lnhz1zLG5`7go5P$)C;W>06Y?^LP7?td+)8^7m>*2h5-Cbb_FW_2 z-`wzQI3Tfu4oy=1lIZY4Sn#95ohWA0^SsOM^{upJHC31(Okn zXgWg@Yt&vm3LRIW>=~gmC+pG9%D;z8%%v2a7WP107`vjplL%L$R*rj3ClK*kvNQ z?o9+hFIC{YXx(!k$7{j%zI9U~O(5}N_Gqe?=lJ66M?MPnbD;XYe?-1*iNradELeb< zjGa28@?jtQvMy&dR^ajh@jXz!t=wnRSU@CA)KzM>ulM#|(lp#q?VKh#r8$0(DL#j% zl$4nn^8vBYkLUD_u~-+jRh>zs zF|2~3SzsTp3BIq`@nCGq0RFl1n>LU1r=*8%qKIFKm}{T{i6^6>jRrwZ=P4YslAz_= z(A!l~QW&^M)SttPAu5Zt#CN2nH4FUd9EYD=ZMW@Y%Y%CvKt0(kv#N85o-b%HJ|E8#t|ADHpoP zXF)T^Or7nDEu8O&q@im8c<$9AO@Oef;4~3zlzYpT4<19P%wwcI1UP2HL6OiFr<1TY z#P#mYIh@I5lnDHIsD&t4m+t#Hol!)XMyv*d zLdlPUU6h$?(`P_QfE)WKR@4e%zQMsQbUAix<1Bvt>yw{zbm}uyXJ`ETeDh)2pya!QC%}Exx=C$Eu zT5HaU$|jqZ@|fS&CsP~N>OKui9{f-lsL`tP zmY`mNs|2u4cZ*_|(9)Aiv~F*cVM?FD49uv^_Qg7bjdecvW*TdevZV|e74E|KR`&4d zi1h;=cLAD(wOKjxsjy88?h%6}bAzTa3dxovG#7|5n7YB!3af`}1j2V%5_RVBmEk@{ zE#TVZRE?}g3MbfUyq}k^_3Rz&3zE7<_i<%>rBOMtU6lZwQRy0$BKeS%?n4i?k!wbO zWmjoXAHdYTabVvn^kYE|!Yqxs`Wx4}AdLA26?+bAR>Aw{XiPv!5FoH>v2sa#l6~tg z2rX%7*g?aU$_QcUs?=+=7(14Tkq9bTsa-*}z(t%#F|LRq!ud7*Phvt?@N>7LtVpR- zlEZ2AMY<1Zn5euZXnV8DJh%Mbnv51{kb-N><1uJGWh&I8R99VJnOf3UXq9a*D8o^v zp~|*i9Ubq#R*}u8gw$wOQvY(@<$-BT2Hvi>0@@~LTA3R+PgnWBMw)s#*T=ANC!~3y zS(5JwhmL5l#4~z<%Udj$yLY*)i@JmhU5KVE5zF<}_Sl8Y5lW@>3JXTd9Hm?%*`frV zI+l6NmHjwvM$A#B;p!FxHKF96bLVa5$XsC$oc9vDF9~-j_y30$7ZH!X=sXK8!Rqr< zi=G|r@!_8@L}1Q?MImx#QN>*ogA4a7EnThx0yuUda$~wgHpz>_E7QcVe6DXhOg^^f zHxA5dAm5R?CapZoS?nZ8sAZ5Jh4pJpnB$Y~x?x7IvuUd`!^V$T?cKiRHp`!VDRVuf zi}+Jt2&^yNEd?~Byb6fkLdwCotC-t=?{1fNEb@$Lp13yC)HG%ixj{S8M4^|CTl~x9 zs!=&nv&qMlaR)g`;hu445Yq)fL!<0gZR3(_tTlI72j)gF+*$>V8!YXdg#>!7DAq0x z1HLEs2Ag~o&!9I>xDJm_%nXBc+Qj^UYz-u+`8X{J&#c@O30U;*PA=7&(6%i(T{s5S z0dL~jU!Ohw8<7q`&lc40xfXbR4a#CiS)XtBv#0&2@#^^d_HGF+%be;oZnw+4Oo{6G8BjDA?_itSdrGRq4d@VkQH8?hFEaT_-cc zC%w2WcQLrpoy7ifJZMWFR;-DhM}Jh&Vm_G&0)Rcj&S2IAamNM6-j5#BM>IiS+skyr zFiSn>;2oj2Jr1ym;Bl{Q=cRFuBgYVS+m`oVsJoxX0~xp`O<9%cslDN7Lb*-&UiJ{$ zMy1iElyH*Nj*5#*`@Hz>=-kz`ba=b6Ofw@x>k`u>Pfg_jLbr%i?~yVl zTsDC*%+xj`n%r;`nc&xWdqb2<=5bADZeSw>v+O)-xkSA1%(kMXxY;0pZpkhUiw>C^ zwS5i&IEfWcFbFSUZ6X6$1)N$z;EzQ?z%^&m`UPyOlVSy7NEI5ZqJ6T(wJ9GIuEz;( zq)BmA2SH7zkr|g&tRz+oS7;{wp^-LE;)P1E!HnbvtAc6C&AItKK~RN6Gz@v0R$0JT zkSn4_AA#^B16zgNuB+~$B;OzgZ#5JxR6Wkrk!x?@Z%HI;UnX--C!-@o6fUicVYwBH0q#Zr+8Q%kj{a3a^;fk+Hw&FulC|I5XY=%t>p8Xg!^ z#jOXW0HU3r#tIU(p@9jZ;)hwwYS2!C9X1;5TBW)clneI5&8x~7_CH}paK*eW03Hk2@Gw|EGVLdu%WNit_z{8rU&*Al#w>0)64-x3`i+~;A z^6!&rko*Lk;4sd3IHcW7U~`Y9XjovbRRFr0J#xnb7m-9a$*rkemAv^e_B~#IF&3<@fN_i|=*WmiGXFKUPeSoGUHJyKVa(2-s z^*zA*!DKW_!L8Yg1>_LN7GY_))+~kfHV7!u6601X{I5HI>&WtpU+nT1ebqC8c^phJ z36qP^f(uD8cykZy>$zq1cu_t13MGID7=0p+-@xsvXXTS~*-<$eCL@mDWZSA(8!{_x z4zAmec180Sr};|{py;<1cl7y-XjdyTi^CE%WiW}b z2I%1<;+gFK;!E0a71qT?Y~M1T2gUhk$f0ikA-W<6kM5&CS(aOWK|5PA1tIQzmM6eW zaa8mHFWZY$k5tz$zUPuScF9`4TRXlBLG?vH+P~<%I(q$@V{DanDR(l;qs)%q+YoOD zlbJwva|h%Z-LB8Xlf(WzS8~s#8*wDIURpgL*&!#P0- zxV9^n3`x$|vt%9+CiRuYS48=TvGh;vh4|JGnE+Eb-y{g6ol=;ez-(ZsZp}Vh0(xBg zE;l1H{YEvpk+KP@iE^nzw^p`^!A8KZ<}%<&$ub*)&Cp{P+V5Tj?4RGjAA=N1E&`Fn zKOME7eHrzI=p)+YUGB~{tJvp zX*&C7tBA8NYN^$Sg&+7}@FF+RR6pPXr?x1$fLm!+gs#F;i@?0zzsQ?|0_?q{O%B+? zY{W?y*n|5W6qKZwJ>!faNL1rx9mL_ZuT#gRsVi8G_f^DR?HPz&l-(*ng?)c>lS*AX z!5Y_~m5Tx>{Zqct=-X30BaA}36(1Z4cY=1{a9IKd?T!mVULmaPIU=GW9xD<7#99Rc zvm-_T@h^)<-UT^?hB_mIApx<_Pcfe&B}QS?&4v3Z*H5%)xXc&WoTUhc=s&k;hN9&& zxUDM4=dCn{>$vvAC?hzOx@QyJs}mPF39Ah+ibWyJIKygw*W+J>l|U2{<*CnLE#FcT zwV4Q+109rzR_FKuW3|*~2|1YrSW0Cyy=mda(h>_Oqe721#px8&spr;+1~Zl9NCZA_ z6BE6<+M3mkyfI6W;y6AoAwkQbjZ?JD3}#$Mj}2xAQlUtFYH&TrocioA*(|k1ZsVEE zJk$#&TEmy&Qi9t@3AE%Z)Lag9GA%;M%}77 z7u>@4X1Km-U0k7b3`=cc);!iL`x_NPli^#UaEb}Uowa@tKw9h~fqYSG{ooKg#3jxW z_1}t>EVB?lNkNy2ON_sqE$IK`kn|qIA2A&kkUNGkNDM2Ah+;!Ql%@#JEsbFfgN(VJ zs2CT<+R(Yr*H`p)CGQ;XCe{c9#dGl;smK#PY?(-dL{mCY+AcHGi@97wn&*Stn0c$H zb*(+zb50Kxif$2$D;~0*0kDTf$q{EgA~?(v+-(=TVKPduwH3YPj}W=!O8)R11?3Ui zyXA|aHs`|CP&x$6UKepkBniNY&qp>~g1(`>RxC+8G+SX=`(Txm60Xk?XBB}9~?p<7YQ>MgzJeX0V1Y1$Jc5`45VfC=>iEpTk9P0 z#@%TD_@(oO8D7&Dcx%gxL8m9@M;AvY$IP`p&)A0ILWkqb5p=`S`OTgJG%k*@b;(*Cj;E(DH3O{ZXs3QiR-GTNA{lMo#5(X<$NJSD`F-Kr;quQbTt>M+KWUafP!kk*JY@biI zchYfqD}`=eaS$_zrs7O>JO*D4d~I=O?ya6J=8jS&7Jo#AP1nXaRuH#hC%PQ56FTPv z4wH*Dno{6fv9`HA4LXOSghw~8%V0`*wOlQGvFq|xwhLP?Ie+PWzGtsucJ z=59G+s~~cl47@3q)TqpapLAHEu(;(&i+WX7EMH8DyJr>@->DQ~iZml18&rN%Ed(JP z{P$kI!xAlen{RGg3!hs@XIILvC1geug}vD(Iy*#DId-Iy;!f8y4udY2w|oh>!jMG2 zzO*Zu=SoMRmUJs$4N6`168ne3)1B`Iyrt9R@nXkJs{%hi4h%K%m*XBL{`NV~IS1@OHyodDbB6WfMc z9Zu10Y{xRa#hX`vv|>e?m1CNYjxP?+5I4vSAeH#xQ2ogFWC|RM^n8EY|E&oHsTV8* z*QKUf6h=o{^TUEy*`b;xnhnZ}ard|rPF8qp15Zl7R`2|GhA z5jCuGVkt@*3nXs|%Czvi!U@M!bx#`oS(?4?l=5w&$?U! zsZ0k#qNSu#;AWmN;S`ys zJw&v@IM+nZT%6yW?H|BZQh2ElSzl>kn+4l#(O>VDzbd-~G=~+1R7B9ILYQohj%VF@tD8A+j$uOVR^bWMTs>UI`J{*KIy6T^D zT@;a1U-r(SxCUn-Aiw{OHkOu1uf4q-(GZN`>CB%jyj;I`QXiK|-YeANY=0F!>qdR0 zebI{-GD8(p!Lwg<1Jk4qQvXdqq+Wd&KB#K`yGPuxCOS2cH{Qe_wQW6st#(N{Pkq^! z%#3WPHlxdpcjx|^@TaN6o_0+nf@s34V!Noy6%w86-O00s+L+|BpB<9eQ@ky7AzD^& zjrQN?UWpg?Q#uPc4%9NsI;pr$2$Ypf70JbRKcErb zg@FX1Yo-Map2pWPD)uQYtzH&>8-raFrZ&aeA)(ZTnr$YVfzYuv0H44&}6_P@gXr9qW^>`a8@z9PL__J<#C_{|QG0$){nP(t>R#YB23K zSSbPDb2)gLN|s)ZB@qAl#s1`BZbiLVPCNSyL(08+Ym8mW11 zBT;yVgp#xCMA#%PlhE(js6=_1M8ZjvNuU*A9u@E3N30`XgyK~)M-utkG>^_?(x6%A(bd~JpGKYug`{Z~f z+nKqpn>JH0?=jz^_Vs9TNfe9Mf+M@7?WgVe4Ldv#6fqkooNqpyj6X+dgV}5`mEy30 zz{vzgGc*I(olAG5OKq~}G6X(Dvi+s1Z6F%q-#w)Kb)r+Lr(j-l@knSb?~4B8S`kew z;fiHL^sio;T`mQo?K;%;niQkd{O6!2+K5y$JqNV)MWD<_Z8ujS7fK@}bWOWxb)r{D z$4^MHZtJv(Zyd> z&6-KljVC4U#!5YyupT-onlp6!y+e{B^KKb#X@*iDN1G4cq|=UGEAB3GJq8((0;rW} zvbEwU6|@gHOh@?bC~c|g0%#kdJA=fA($Y41MU+1Y#Y#nUgJy?fL$dSO<9Cr>~A z;oYOA9`D*GYk*L%&Tj?^eZz7{*fL7sKrG6PZJ4MWiI!5dAH`o}9`*Zsv}LUBBS7)) z5pjaSd-+w6kbXDFTrId7v3#qFPK!Su4|T{D|I+Dnx-O93Ped?1XBi;V*kQ}+H0Nu$ zTBlcLdUjgAn<(5gl~mcLL%wZa3g-T|L_>nm3KXO#@=y=*b0lzC%84-&df0Cf66qxZ zJ7kGN{??+-L>F-~kh$nH2Cn>`8mL*M9o)-b$lT9=BFVs0wrlt!6fZqiO*$&-++$w; ztJx{md*C>h_O2L>(Aez#7%q+XiK+1dZZ(Em|IIalo^Kr&;yZ(CIn;FtoAiQ|fxOnG zw?st<%To?3|SOevtyc@!dRNS*_JDn$A@{t^#x3CKOCr9+^&rNPr*LI06b`i9M^rzkiU2OSiRNK<; zmx5PRK0%oP!2B_P8p3$Dv(pUNnr!2LwYgXz2Jmm@`4ypO%E9Wfb=RDzjzqF|a<9Q{ zrXZ_iIFBtL5s z0i5hQR@!Q3H#tKHYCYStu5`Io1RPHUp)#Kkr(GX>)j+oPNJ*tU)~GurO98{Ym zn@;%DO_^1N%XVf3G+1wu$#lUMYOK9=T~QltHy$3T??fjgZINkbU{aD`kkpO$YzMuo zkA08GZ~+HJ6&3s(z$5?_XGfNd601ERAm_-sB1~nKR(x%EjZ7)AAz{X_PYD{<$f|7x zu4!=K7*_61^y)6=f{Yw_FG+9qPIw7#lNl-jx*Q8cD&PW5C3Oom!<++S-}Rx@sxEX2 zb|9HA-%ZS)1Cwd_blW)BmV340u)?^%yF~pmgg{A0^?Rv_*2kL%+Akq7aB{;}Hgo6^ z*`v?^_i456XZds!vx+l!jI4fxAsVkaIfD!taCx3nsfZuO$Q5jEG+|vc_r5r^MuPuo%n@B9Kgb;VU-Qwfl zXac^x4|t^rSavUP`#*yN`l^-+9PeKoeSf%&5JDdjM9gMF~}!Cq@Qb& zg|i4~LoN}uKp=#{>vutCw0Q343y%L}97GfWlX(#bXc!_^9>)5&=i5iLWyHfkdx30} zt1@1_poX(Bsl@_f_e)6MjOI-jkCq3AYS1xiL*QCzFw^drdpkSP!A00`j!^dSeD`PVxw=XRsTPOvO`B#?O*A^*KYzLZKikJYo*!->pC76ddBEkr!`4_Ti#pIS z#U8}2fRWZI>O?Fe5+SQ*wIHL^Wcde4MZ&+12WbHd; z#q0gtDztjBRM#ZkWkiE%+~&QPGMR=OkK)T@q#k>*BesWDTv?iBV?Qsqf7pH_uttKE z=*d?>%fV*Q=QY=jIqBv)gk2oHIXeC}m!*u;wyi^x(mYj7$`V4WWg7xIES?qz2()uo z?3cdC8bGa;nI}Rwd9ECn@_|j6aXAZq-s zJ$~j|1`MjVb4OsxSAfoPi4ul2(?%TsnxSb1sNk5r9!}MOGMH0-@M4Xk09dKTLbO zi~m%+y(3gyTEJb5F*>-8Mp7Rx^#1sK-hS4%xS-TL&|160r&to{D3%2D?Ha_1P?IE- zH!R8iwB$KokSuJtt?(1Ds*w-L^|%IEv!GzOGMu>(!G1O$?(GrFPNtITZo%hwY~`(P zT;V!>YBzub4ks33wHSX$(J(BI4jw;_+{EfC_N(vHZWFp@?;f##p5x$1s6~BWsRN^g zMWWt|?|7X5CI}xC(*xo$Cm%>Xzj$TN7cR%IRe)lyC@;~QxrLZ3hgZO9^fOn_x98PBsjZJ9*JGxr%g$EbD4x$DlCYJY z(=N0Tcru$88wApPO2RCYgp*6obW$Z{u3v+eF-?bEM|t@3e2TXJQWz^_KmCkl0>w$q zjiGOTWn_||liWH^+-YJd(*^xG*przqCzFvic_6`DF!mR7tqm7RuX7V;J@GF#yV0*9 z+2du{VLz|h`d7cF$&>fhGrQTuBd6V_8kHc_^J&ry7#h8hm)}nAl7@Zf=38T3Tvr+S zN>Hj%{#q=Cb`o4(h)xIuU*&Nn`rV7%nwPIZzty?)3xeI0CCg-QeJJuV8s-r- z+K!go<1*E;f@nzDBOI=#H92Z?8_#dnp*2GhY;&J#8>BjNBV-1x5;Sn@e{^+6ytqG+ z)J)j-7T(lW^lD~h0{4lH$o^^f`$5gR_!b&@%jA9`L|hO)r~@YzTA{YPR9J8^nu?@E zl;?(F;7VVHgJaDE6w3GTgw8Td5X&;=zqyq8717CgGFDR>%{SE-sA_`Lo6of_CR^DO zB!3H$Xn;l1H_*Be!my3%v`|;7;9~bf2_VADnj3mP+FR z^k3|=1xw+aYHT-3kNSDlX?@i#&_Ef_=li1K(A}F^5-4n*o$HO((JhFk() zjoXx}i6tOwArgAZHZEaHk)HV-1e_!Q@h`gEV4c6__sH_N&YnrB7(0g&;WS|W-cW($ zP$gq8ii~V;59Diak4Rtsh0ud0Bn!Z`pRsvNdf4Wq1P7SugmCm8v5c^*kP3E-Qv5A9 zl?GJCT!CXDAz?wIi8gw@q*p|*gJPX5xMmTOh&urg2tb;wojdE^+*a$$zbfVOUsb?N zglB(11%6k7t_Hw)!ea+X&mDqs-Iot?*EuFbKlq2*j@ImOcP-jl@0< zH;r`IP&) zJr~ej;LufTDzRziND8`!Gw*2lS71)y#kR`pbR>-Ch)I^ZH+CWuvng&mnX22tzX6M$ zqkLd+m^QSE+b;nyaedoBiR0@Rp4*GH$Sp{WC$+;suzqKMw7;r`dTwgqW6w~gu?l+n z;%+bj+d#lI*Bo2HRmZR8iGlkz-eZt56kuK2QQ+Cq+{Eu5kWtj{Uu@{1!ZfCWJmydzX-R|x{2^P~-E=Bkq#6K~OtW+(U2~bj zA;$z}PF=A@Si(i?f{`z7{1z4V+NeybGpG$_TOE`+U@G`;llcwT>k#?U!#{}p5F;O8 z`<2efe>}D|M=|LYH#8FoIOa}-X(b&1*&6# z*GUFRj@!8lA>VK};Q76tzxAOQBR+HQDzTeo48ZZeoQ~IH8DqJo$B*>7 ztIWfmxmFC8Q(~EIZ5d`08BZe8R|$1PP`A86I2n*Mllv$hT~GKDho8QCG>$PllEs&p zj^Qo>dwUMIHlB(uzOti#c~2jtgEKAGSk2c?v536kP;DF;aYenLfE+2nMTL>$#@&Z97=MO$r1iH_79IaIacW;LA08}zgo>g=67915>& zx;3Dc-p`4LQ;Y5dVG3-I`5F9zazJL;Nap(M&ff?Lka?8<+R$gW@OcsCSkNKWJxhu& zuqPY(3oqjT4REg=?GnPsx9!r4Y<>KF^&0&SpZjS`8+E&R{J1dw*I)}yWD5{=F$LdU zimFQh9H2Qyu7uZ$4Hs8@bEd$S8mqY?qCE>+PRxudSS*F_fM-i$D9N1Zf)x8*4$3|S?T zlwW~Tuy7wFDD?uKkq3DACGa$We2F_0*e8a{(sHv@(#+y|xCDX46`-XmzG~%{8VrZt zliO3u(4!yE-E`gP@#C~~%lsn$n&R#^wqSy3(jV0~PGOykdGK?DYYKUNxbE(HXYZEn zPEn<6iP1s|xKtBSJJ7wa_|FzBbkCCx@*m+MO6pGyUCn0=f{@Tk21ue*Tq6jeA8 zIfNjMPIR1nfU3(bs7woQYOghram%Rq|J%FTt~RbD{e6B#Cm(_|Q40wSj>R~$#Ne16 zV|$GpPi8VlG^jCJgrrqVCO9Yi+xI^8a&O)47GPq}&hE*aGqDZbx9@9J-KwXaBBOD~ zir)+Bw`8VYP}S$srvZ^2=I>CWlCVUcb%`0p1_gtxFiuEIF=j9^0S^a-((qIaxK{xE zBU&o_f|>;or4h0ODG@;jDK~Fatt=NFkeXyc=sQt2DVCJggPE2!W|;E&Z3eE4LbiIv zm=xRg2K$p6ASezC67#gifldReO+e0o54j^HB^XAJ6CxW1)!tSV8^wa&1+9Q=hd|@U*K(@r6Tb^w0&rSBFx_`1zZeZjL4^>1m zC@qpplyznPNdg+$AT&HgrTwIIOa-_qYc}8KeOQh@c61RF#JCsXkLTnUOgQEgg)pW9PST}emp#Q@$}`O|F^;J>+c&sH;-khSw{JwMoyBK{DMOolUoCN zI^=sXhl-mv)JT`3`$?sVQvuw15)A?A=V2I07Z3U*{c(bXLo&4 zw#}Uz{&3ePh3fL|9r1ff?)*ri!UN<9oN@TU~lr@VA05YJfd z3rFqSgZ{IZZ~93ld6U|z7c2`~CX8~~*F9IfKl<)OefRRY`#fb?uOnUC{Xyy4!coxZ zJ_I|#ejhi(m%A_acA2BHm2@8UHt83y`_HH`&qCn26x>4{ybXy;onooz{lba~jvW(nR2ADnB5B}gGj>bN+Ef>l_*a(P%RZ<3u% zk#Ek1TVybRFxDw-5;30W0K|mp#^%-|{^#yx4i&TRZn}dXr}K-SzI1Za8GP>F)F6K5 z?3&>Opx|R1x~!@`PQ}f<%IZ` zD>s3q1LlIZ!SD;D5@zIk)B$BZ91Q|W#`Ov!_Do{@$V z*eSk~GIRJMo8=JGu9c0>##Y*SkaoK5cITn{qq!p_ZSV8cAA8=)Zo+-B=X^Fj<<@z* ziP(d;$zvg9rp|g(YTa`LCq~E#7dmCv!U7pBQjY1KGMUnxqKD0vuciM&Im}T=q_(TY~x=7B?FWtfsr!g9+^9SJ(zdqiBv(A91 zpBI{IM07iuubTm*&@+vyRwf?>%n+U;$~TGC1GNvG)@ZJCpWI)30TK82wo|qZ|AVCa z_{gnU`{CoGY{5&#BtlO4bvIfGs);0^JZT2^|9py(Rz8A;O)X0CC>Says;){n8FtHk zz4!pX;W)cI8D?tefa|*O1uU3+FgtyD#Nz$~VK~J<^jO5^zWWPuP?lL0g?BrzF~vmA*p%noQ=$zsNqkd%PG5&fNE9+E@1&>OH@S z9eIz}LUEkCzbt<5rEs6n`VSVDQg;iWVfh~U> zoJ@z;?NBNLR{=H+#o6cS?OY$+?qV!3_X68+G*R(CkQVF2kNw!)$d$AaLa{F{zw#rm znG~MHq`~gwqA8OTIDE?WGc+lH>n|=o=(yNh>j%X5oW{MB_&xNvfrIjh{7|2cr(ika zVLAaza{s|Trh-|iAta9s)O2&i?$-I`)a~GaP(QCkmC8Mvw6U6 z=rqu1VTu-Ml%ySuWc?5%EZ@xe2woN+DZM_eF<2T5m;XsepW{=OzfDb|uy)GyE=+^u zc9Wk;_Yz{BY<&K`OBOc(knB^EkH7+Q{wd)Gh4Y?=knkoUE2? zpUwO~s#xb#nVz8NzM7FhUEXge1T}T1y$Fto=Ld%e{oTI1Mfd2eN~HFj8PQxrUMGy5Y;qgAeXr<+WjSQZR^6^lT zPe5x)0~L#coY}tAv3KdRwZZa!VPdMU=EQ%r(2^r&37x84Qn@tbVa&nKi+NN_LFaIW zbQF5EmvkP$zf9lUM4w0~B}v2om2O5uUXyqko7$Y~3%A#!7T8HHO>V${&H9!|I+X8T zxI!nX?<+dW^*Np&?I)Qe$itBfMS&uFmvZvmZ3X2EX?t)g#Bh2_E;Fhm@?Zbk#!X$%{F*oJFQM+!zmMsn-$%ctkN524Dl3tg>(hvD`?3(z zV>yltVG@{LP}0RXdGc)cbw5#ga)!EjSM3OLBSiFUTwQwjpi;-PFD2vnQaYWd%mB_V zWFkcd?HZRU4?%glV`Sp=1vgm2v9Ys++%ps_7piHr&mlI7-cWWIAEqKH*R>2ZL`Iwxpn&+zp|zUXWJEu_;8cp`@dhT?9E~u1*CM(7bwoo2YO4WD=5IKoR7?jiL1t^Un^E`?n zW?4_R9xCCuiA4!B6W=`5dnsWEjx9*a3H#Df4b(&*%nlFPTkfQhb$Q_3FFC>BQ^_h5xcL!fafzg|gmo==AbzsbDrXC6~&hL&nJj_=$7r%`8+&ei9B|L;ftcj9>}Xkr2A_Z#uzC_jCb&)wuFkCShHj;Y$A4dnfA+E~HD9z0g6hU!1AVFT`zWuwVLC0AC&Y12 z)eLNUcg=$LpX&WrBW=Gg#OGSvbrTiLB6~ZS@*zklKd_&@uUAvfbe}A;6`2mNm}rOZ}a>jJSkOR zzU;o`2e&&9zR@QS5ohs_Quz@)dB{(~ve z>&ZR`HkoH)N!GS4tSZ6Iu-CQZCT1>4zfHPZQtOBQ(F{C6(v#5Eik?`eb$53%xnY#z z1`{ATO?viGpS+DcthLS>C{j+52fFrXttpM*zkf`+o#bmgm*O#-&q9bVx*Ws|Dw0Nm zx4(5p`k(1rI@oh!ovQ|KLu$OGg+h`H?+&yP_wnKO!@T?lm(JiAPDMs}w-0np};5uB9!ChW(wMMz#OT`E+d?6;j z>z}YSdoQv3MEa~N5?C56@GPtw|nhata3 zX=iTz4PuW72z_t9`rD3k& z2cS%vrNlITb>L@PcJg;9%)o`bxME?V*{9MyaAI^iH|^dh8s1hhVY>Od<#^LcFkFc` ziLVG{pZt=%c==+#mF(^Q*h=2+@BgKh9KL+ff97e{`3TJ~M-$Ix&Wpe>eJKPcGR^N| zwrtfqiz-w-d9i0sj)Gw%FQpLj>(&pj@&CU^!izX(g8PSyp=aAH((Huyd|Dgem#4ob%v>OUge3Y4?Yspy( zMHOuCu`!Zr(*I$t;P$j6H*IXnh~Z?3f|E8*r}Npepz2kZx!X1x>XwJNjfO8*`nvzD z#VI*Ru0=Wl{S6o)+@8l;9m6YtyP3Hz!$hZ8vVtrhZ6$ zO{sLM=Zgs{xMl>$O5D>S2!aY&I#7MZ9ngYvc^a7iIDGKamk*qBT-PfsroSmkJ2pTf zH^4qm7d^mLX{SqDFK$ZJ-F-p|pe{6u0gqK7wcJ{imO>c+kp#ow8f{8gqm*i)gF;!V zbw$~Ud7)Vr^%)k}f3#aE3Pyk|rfVb`NX^bzPnMI6N`X@;$4`31Rywxp-R3tSN^n>| zjg4RJ)4e*g%Ib(x0XZFyt7hZ0{q6<}W`-a<;=>v09ZPDMB(HER7fv1B*o}!`FiGlY zwj*u9CZ6s|Ty_7=Yh+S2%E8iB0`WmIq$#Hu8FbVEA8~d9!ADDdjvWx^?iWWm8rS~! zJCe`X+ueQjJZW?uZMw#{I~_)j@CV#mJ`wV8ir3rir%wK4*E==&j^Q26i)3sES8pei zMzR_dvj>~wr!Pwxbuyt&nrISS)_#AL&DIJ{^^>eqY|xe!P<+hTMr!(uHvD7V=)Pk8;)`Yiz9G z%UAm^jzqBAv{8h5-3%Zn>Zrea_{wGoTFnEUD1@)_Q;->o8lnchDr$+|+0*^2ad)$Y z%Ibpy-8?`uze2oFNv9Q*Kr4we|8{J*{eGjwK3J>yTqqT?S+1dVTxM4&oRHhcGS27s zDY^ifP-E*{3JFWjmHpVJ0n5ZYpi-Tg+ghCh=oM6AY?&FzAp!u5)k(H+68K~(5JwG`EHDnybzdH&K#p+vH9LAqb}E8s$9ChJ|JQ-b?(h ztC@Kj@F!b=yRAeCiJ^<(wWB%3RGY-=}-f8FH&cw!*Mx5n_6VPq6~O1+H_8BcOh9j)h&o87jx=6;gsq zH`{ovxG9<#mDxA&8{q!g^H=V4@qxWZB#^kJ_*(76kH0FryF3zmkP*TPRl4K)xHu2w z>HTN>rK9Fa6~yW(n__DPM=SK^E`Ef*+J*`wn9h5>n0f(rqTbLHZn>_exlW|O@9tR9p^4$BELqGLnu4Qa)_LNCR9?PA~Z#x;Pgo$YX$JXb0_8x{G zO&FQDHb?Y!WkPd6wRLDSd?JN%*X_6;@0I&%aS4;Sn`ioMq^N0^eC;P(K#+~@c2M7e zEQw=>KQCmtjEWb^CHykt5W-40a~e?_x5GGW<9fh+l$3iGgb%&E+NQXJQLx$+;Mz%_ zXz6~7F>sqTaO-+M-#zLl{nxvD`+M<1@~>$Zh#P-}L5E#1Dpt)_?~Wis?m=?u)`NGxqu3Ke>xtTi!^qgS|~okAu$F;E1X$TGlnkA>?_hs1Jm|Sx7{<`4$Z(}Hl-*6x>Cc3QTpZIWmvV(HxLsRDw zf5G~iHIcus`5byq8w#RRBgOw>o@w7Na?aEo2XEW5Ju)hkMUM0M^wJ!>*y{f<2Nu;8 zk|&4zgTsRt2fY{n(fwk5|ET+gvt%#51Jk$(Tkz$Io7Viex!Q}%M@AjMN`FUuZ`?zP z3bKzajs}vTOZRsB`(ZSNx{cTsN@wj!-|c%kVQj@@$(y58-2-_xcW<&hW&fmMS=jS( z(mOFJ%kgX~9!zbe!Te@h>Be?)m|sp&zXiBcxN70r=_%oFl>g);T!MGDq@(lYgq+p= zWlleT$Wm%2>1?OnN6Ei0C;1|Gruq4fdg!P*@TqOf*bbc02}ejzMK4u;@#-I8&(T1I}t{|`X=daxZBE( z^1W@l*U|06=YMgHZKU08WX~3KxhU4qWYY`G@2AsYDzeZ`S>!ryP~RUoW4 zQi9eKlD&0viU+slL9gUwoYuFA2RAypF`O;wE_hg6?zkKJ7Q({g>?B#dPp0XqyZO1! z@or3!(9V4ugMDHSeuWfob{=)M7Tt}_tm?goTmHNjtONrgp1T{G zDRV#Uwn9laf>0xx?qsAS#HD9>4zu^l-L~>^*`PcQ?K z6>RF=!)J?f+{@Es5MKrD&MWUhQ1#*!3uca^vW>+DV`jVl`kS}ZiIB0Z70sT=C6WCw zi#Vy@-$zH0LlQ@TBtQ^6`ULnmKqf+U%>7MqhqMGKeRk|g6OPqeYR%p-5;l4yF zo?PU)j#`F@uNUmlAqG=_`ata%oq~qDyH2bFlR-~$YOXtE{)fq|(FqcjI}=F}H%qt|h3c2La2*62@zV1;;lunwsk|77BrJRxH$aCQsV~P96t2-!yfw{H{y$5g+cF z(=Cy3mb;50AD=P9mpiuMt5kc|I`iZX)Aqn-&es>MGtM&<3O;tC;aDQ$?l+i6!~uCi zIQ@K`SU$pW(A5#>5-C^JZP@F6RT|av{1rI;@86D^q={DNy{qT18)ml_uL;*=$zlD5KY%bcN@6s7ppwvGe(kh}G=$0=C>f+04COWUiCX zK3+3r6qTq`WnU0sCo|9rZbB{rYs{cJo=#^dRJ>wa{gFD(+ofK*tbTKbO}P5kH#N%J zm$L*`jp>(F6~$J(aAQwxd0WDKHf|gDtjQ(G@Xmj)`lNg0)(ND~HGSE~kL^AE_N%5d z28Ovws$TV=FIe{YV_J+$K;JNt7>iBcR+jbIe0m~GgBDyksdwKA0G_DRE&KSvi~XlB zUw_Zk<1cpm2S4l+h5D=A*Zl)9s11JTG7b5P%<<}-@ORMcC(Z8RBU?Am<>SifJR~@K z#BHk7$Z&hpUC2N-n^MNLreTp4+T`ny2-y=sd`xhIV{1rCRVIRpN_itrvWx^t-PfR= zslE8KPoiAQUMTmFpq$DW*9qcuEo1PIO%4JH!vAu%z+mgYW(!2$2Uv*6k!~=I9*%!Oc82p zDcqC8!9ua5&EXDA<+Mf%&Qzc+5%LwLa_gn8plY5kb7f=2|GY)|ywv+T>5nOX9&d=J zIjwje&Plkl*xi9jkGafAO!@(TMM$yyDp=m}hv>1UuXdQc*#B!ke00sym|jfaS!;A$ zhnExjM=KO|r?xug%T)bwsb*|xNf~V@xa$E7R0Pf?SF9BJ(!^b!r->5C6l%;)q8=$y zO^Y+q^*>zE1tmQG0b90CD$;V=`zI&uPX z?gDNmX8mR}a#}E!KaL1^sVx*p&%;Vsdx^Ug_kGBnNV7a6{6Fom^OQ* zuSdcm3?s>67*+iQlfL8*Xp393+k5f(TxR!9m zXi?LW`LtVC>0gXm-P7E62ERm-d@Dr&`T>WMD4^*!i0ZGV!1XzxX*1!taMh}%X*eNv zkrTF6vgwg)EGx<=o}_X5Hrd?V+-`4oOKQ)`R+~lQv#^v(!a{&J!l;!BwW+X9;s{>w zren+E5P&#RW)vxJbRPiuYd^me9SGL3mu5D3qSh0BGaMzeC|pLOaEAKA>SA7Z-c(l^ zOdoa1vlHB7DLKGE%fmX_bO~%B-C_F6e=f(Zv;3qrnZCblIcw?iS9jb!qJkcyj?YGu zR;0c|TSrp~4C4$*8GtN6+bhahTxdbV$LBo~J)^TTb!`SOV%<3jj(xv?47$66ThiTK zNc?uNxoAe+#;_L*&O!2EuH<3_m{_A$I5f+aL@(Y~)@Ec@Vr%G#Q#v0}4Dc$u{vaqJt~!+kMHYbcr}<#EkO9`64fLj+>7e&$<7&p zai2dHnOJV-i{(j5m;A}ps&yofYDv9o*Mcakg_DqMO4I3!RJ?@|hE_FOi0UDQF2NE3 zc_BH`$0emVFDWq!WNmbNTMz%1Y+mZ1TsN$MbuC(ePD#ANvfmCh$7txJv$W|)e-r*Z zYfZ1TaI;Bq1==|%uN^jRNn6pFQfinT1wuFBZqf&UFk$zQdoLT; z?t-UswUTX4FC_AU&W@4Ta)UG%vWoGq01{m`cX)Y@c3afPNZiszA*%u(JTAr< z)%pBA2S3$7BFpKNlnIGfDJEI?pphR@b!vpYM9*Y6V4(Z$1@Z3VIqtmT=4Yo_OS{hV zl-?%ZDUlchY0nGLrm}uh0UBLhbI{HLY)E=V5SGG!MkNC3=`5H8yhUW{3@4miSL_8Z zLJWwbvPXf-kjbL545En5l!=rS^o%dJGn)8)RwvB$m(^1gouB9AWRPcv!TUHYNn%oP z{<+$t_*R6ib@_~Z#E{A_8E(U6}7HiK@3)7YIn z^2)w3fMK=N&PJ(1%c5eR|GO0T3z!dGxTo@Guny)@Wv zf^}!*tZBhM2N%*(?_L{%tY$^%sX_Qo=H)55^E`yOWe>JR5tmWR#DZ{!io&FJtS%*< z%V=wnD>$rjMA{;TM@UW9Lh9;c%!?u2MCv?>y?_<|Cf)4TBBu$e?H=9`hbNJ;81vF2 z9s}uu>yBToaG7Puz!Den2}PK6Mx(LvOozH8i;R7Qas)GjoAeGsL5U>03b^#E`I}Lf zW{ZVW*08{MPf}9a^3d?%9R}?i`%rmpoEZv+Z?{Gp=UB6>K>a+LBDY7W!xM(Rgp7w zaMc>TsS{DHSW~JoPaJ+Eac;)8MjJYEdDTOF+}feT^7ED-f$>=|Ga~9e`Oxp~kCso7 zUci|mu8b>hX9eV*H?iEk?|okK$I$#6m84F9v*jGd%#JtAMVYZe_P%7?3?l7&P4R&m zl$K9|XIW%E3HtNpN#Ry?>1{bm^dnFoBiRJO7~_S+`DF3JvD1F1d|O@_n}EsQP7YBr zWjsL{DfP9@PX=0t&HWcSt{WtH{Ruh;$|kuQseC{M_cNhjbVw* z1VtXVsz;6O6ZNROMq*F>$eMt(_?7qoOD*-IH{120>x$pG>~SPd4~I}Jr-6^Zon~`l z=3M1j@6`kVkx2yikTcM!)FYstj4E{#&k<|8nc|o>=pPUFLCdLTs`(m~IO{)2{%bj1 z6zPVft&zwikm_kMjiy-0~4V~arng@gBg1g%otCn z7uk894Z-ngolP0*E^By@HB0$|-ES~8;__PQ=Xbwn=cR@m(AQ{N1;?MzX4xIX%iWt>39etaHPTg|BAv5?*CW68c_uoO{dk)YHQNyoP$&Avu{h7HN-1-0eXoS$l#Se%D0=}9655}UZ$>{VCZFshW( zAEMi9?c^RGQ%By91x`L~z6^rT>++jNB z4eta34XQ+62zU|4XUW63f{u&$^;{3lKH_9c8&;@~HB~}PEsT(Ia@d>LPF^kNcAk~m zPZq{)wzJW@C6|cqLHZ>}2t zS#Xj&4B!#l2@f7_HrruO#)CIX!@8T}4ahDs6}glaw-CqAXqXC3su5%y5R#h(=vw*6 zbuWdWmhM{IOYv|%fVIYOlxx%$FS%m)X!;v~+rU;H$k~aVSmqiN$EE`>sAurec+imN z{h9;-n`~9?A9HBQQ7mR{D*UD-S6J6Zu1aRhWMcrECBjoogs(;>&int_-rT97b+lQK z)@t`Mti(jDbHwDpu>{a_Y|NwYvsrpJ&-ow~a?hbYMh?RYF?~-kII+NGSD6g%;K<%- zg-onmF;)GOE6!$|31fygg%L?+;i2wM zojZh;iX1Vtw<;8-Dl1u0SwmZjaJTX72(n$OY7A`paSQ{HCrr<(RdV8{`DR?#auVhA z4n>B;&g9x2;u|1KV0g~?#?VL*^~qEXaT?i(@|IIDhTbE;d-=(RD=0SJ>E^2%G~ zk0}<=4DwyMiz*SvIvctdw!TF2W&sZ@N5YV;NpkwSCX};&@^1T;;W%2dz1<(dF+zSz zaf|q11o=(FeE_A_+c)I9#~!ww+n=P~vWD1+w}?Ct(%=0ysra8NsqRh=@kGx-&e|`4os;=@9YdOyWcX3$2_qL zPhlT6*D!)<3>3Cyuw;_U;U6S$1#HaeEl+<$nsuWlxtf=;8N{`2hcfkY5Y@llR=WBR Qzy9HW0MUOTNWPm00M^9W_5c6? literal 0 HcmV?d00001 diff --git a/tests/fixtures/script_cpp_abi/basev12/manifest.json b/tests/fixtures/script_cpp_abi/basev12/manifest.json new file mode 100644 index 00000000..5a3536c5 --- /dev/null +++ b/tests/fixtures/script_cpp_abi/basev12/manifest.json @@ -0,0 +1,155 @@ +{ + "source_commit": "e7d023dbdff1c98229155ec5bcdd1e4ac534f5fb", + "source_tree": "0201bf052429490fb453bbfd6037e5afd1669626", + "internal_namespace": "engine_script_run_v12", + "generated_dependency": "pineforge/version.h comes from current configure; its value is not a frozen layout field.", + "archive": "headers.json.gz", + "archive_sha256": "eb941fffad6b3ebe1f9e54bc3777d21b4e593edc80498328f27061cb12af5374", + "files": { + "pineforge/bar.hpp": { + "sha256": "665c2528dca64ac9474de622dfd7ffdb540ff91316f70317e39aada158d1e0de", + "git_blob": "836f73daab382e9c8d5a6d03a942a921e39b756f", + "bytes": 655 + }, + "pineforge/broker_events.hpp": { + "sha256": "4f3d9948a4d1604750901fa3982d6ea49bd50dd15a86c4e5d690b83d1f5f23c0", + "git_blob": "2972d53c7b039af266f4da161301c01eec54c16d", + "bytes": 4343 + }, + "pineforge/compat/pine/exit_activation.hpp": { + "sha256": "eccae8a148a6093cd12fdea24d0b72f8128aa528fda0962a49aa75d7612fcf2a", + "git_blob": "4ce38272a9daf7223441926c8b71616e2b08452f", + "bytes": 3017 + }, + "pineforge/compat/pine/frozen_market_instruction.hpp": { + "sha256": "fab882aa689e28c4d0965078c2cc67428c16411033e34d495ffdda11467258bc", + "git_blob": "9d43f4f37f33899c844780adb9107d32df8bdf4c", + "bytes": 3201 + }, + "pineforge/compat/pine/intraday_cap.hpp": { + "sha256": "0ac0eab34625ab9a974037ff8c0217603a01deb81599350a4b24f7bf48ac0cbd", + "git_blob": "0aba76b6a78251df33e92f19290473adecf5a61e", + "bytes": 10469 + }, + "pineforge/compat/pine/intraday_order_budget.hpp": { + "sha256": "43f7700f9c66c630ab8a9d8fd899eecb03c2fd9aabff11d2dc2b7fa85f92981b", + "git_blob": "730d941fffa3939e375489f40a50901ccea9ec87", + "bytes": 3878 + }, + "pineforge/compat/pine/order_birth.hpp": { + "sha256": "15d2c74711c02b4e8af0954e3d82f22329ff1f734d854e536e9a41209d370d33", + "git_blob": "f18ff1e93a2be75de8de49a668c24e4dd07c5a09", + "bytes": 841 + }, + "pineforge/compat/pine/order_priority.hpp": { + "sha256": "16cedc3acaeb6a3052726ef52fed75230a971c731f5294c3acce3987091f8618", + "git_blob": "36e107e65f79db98d7079c249b0a79199b578eff", + "bytes": 1577 + }, + "pineforge/engine.hpp": { + "sha256": "210a43d3166e9788dfa8bb0e670776a243af136ef8fd026f340b52911484c8bf", + "git_blob": "be3574cfb9d920ec0226e9458659b90ed7e6aadc", + "bytes": 312105 + }, + "pineforge/execution.hpp": { + "sha256": "e6e2440118f38a0eba5b75a0fb0cf03675f92939c154aaf8dc0837653a113895", + "git_blob": "f270b7c64544ee9188c22752d5e41eaa78bf01d1", + "bytes": 4294 + }, + "pineforge/execution_close_scope.hpp": { + "sha256": "daba6e221a19d3d3cd1db248a2861414ef91648c8330709917067ab32b2ebdb4", + "git_blob": "65294d9f25468ccf3e4c79861beb96b8037b56d5", + "bytes": 583 + }, + "pineforge/execution_consumer.hpp": { + "sha256": "e52e2e954b061d8720f44a49d9eabeadefa75851924a6068876380dfd8d6f9f3", + "git_blob": "adf6776fa0ce3ccc689f32adf2c68cad41dfe137", + "bytes": 3010 + }, + "pineforge/exit_leg_lifecycle.hpp": { + "sha256": "9d785271511a350114adc7cf5bcb1600cf569f0a10559a8671b7e4b9cb9dc89a", + "git_blob": "91492ec22e05d7b6d9327f60ff5e239ae6906fcd", + "bytes": 22622 + }, + "pineforge/leg_activation.hpp": { + "sha256": "859d75d0ae90ec3cc109765da066b37c3bbbff9c7c840368fbdd05dc7e178f3f", + "git_blob": "4160c841782800853bb574219b3dad2ca8aa0cc6", + "bytes": 1353 + }, + "pineforge/magnifier.hpp": { + "sha256": "c0ed220b6df054b44596a434d4f48b1c3e960ca329bb8644a405a608175e06ff", + "git_blob": "1bbc910a869d162900d7610fa0bd9a88776b7b0f", + "bytes": 3241 + }, + "pineforge/market_admission.hpp": { + "sha256": "22914434a2c82f761b98c1b5a2ff9365c89bb802b584cdda2a3c30c5986ec6fe", + "git_blob": "70be6af6b556bf77cab6c9f823bb3492cf7c8294", + "bytes": 10221 + }, + "pineforge/na.hpp": { + "sha256": "0e8c256fcdeda94a0855458fa3b8c4ad4f9864dc2aecce969934f6bea82fd0a5", + "git_blob": "e98505aadc68a09cbe5a14aacf591a799a15ecd2", + "bytes": 1064 + }, + "pineforge/order_action.hpp": { + "sha256": "7d76ddc3f6ba76216d5feb34cdf28908e33a92a56757a707ac6bd94dca315001", + "git_blob": "d992472cbefefdf8e29704a21f04614d68d39a58", + "bytes": 3470 + }, + "pineforge/order_birth.hpp": { + "sha256": "0eadb3e2a92248fbc8e6ba8f94285b15b0e83d7a0396f5904b40c216619604e4", + "git_blob": "ed4a4038e9d079c92b4ebd777a5f22f1f1256b0c", + "bytes": 4833 + }, + "pineforge/order_cancellation.hpp": { + "sha256": "389eb3659f121351ad4b53b936d3a2c79f343ecdb6c0c4bb0bc13777596b90ee", + "git_blob": "a4f11409f19c7a85347879ecfd942bd7bae5c2eb", + "bytes": 7172 + }, + "pineforge/order_priority.hpp": { + "sha256": "25bb7898972153b044675245db230c1171644856ef904f4dd02353b8968d433b", + "git_blob": "3e9fa5ee4121971a7962db8a60859aba6fb9ec95", + "bytes": 694 + }, + "pineforge/pending_order_mirror.hpp": { + "sha256": "7921179f690dbb6f00ee8ab6ca71b3174fcc42a61ebc6786c3727c7f85a576a4", + "git_blob": "fc08bfd7328718de1c658bdcea1826c2fbc9eebe", + "bytes": 21978 + }, + "pineforge/pineforge.h": { + "sha256": "e4c735a1e39411db4156ac13a50cee10c554039b20f33e3c2299cd73be6a6c3e", + "git_blob": "31db9f0b9f8ac5f33041917f7d599daff91013c8", + "bytes": 74362 + }, + "pineforge/position_close_obligation.hpp": { + "sha256": "6f6ad0aaca952dda7a9386cb78e57705b3494364dbd8970b0a051fe99d05b777", + "git_blob": "09576ee0f2a1d49becc46f5701f4369cf8f87bf6", + "bytes": 1060 + }, + "pineforge/quantity_intent.hpp": { + "sha256": "c4cc0f1cd5d6ed1f248825712a5ba4b4f5a808b787c05c58971934b89d770b90", + "git_blob": "41262fee5aa2db4032fc008542c573032563629b", + "bytes": 3054 + }, + "pineforge/reservation_expansion.hpp": { + "sha256": "27e514249f62aa7353ac084a6e59e25e338a867fe4d67ce4646446a1cf9f9dab", + "git_blob": "b4302c9eed97c3409d9911fd6cecdc64fa0aad86", + "bytes": 2434 + }, + "pineforge/series.hpp": { + "sha256": "e7709f67df9046c9eba163c2dbe91f8161084a955265ccf6b163355856c0afde", + "git_blob": "61baaef2c351d37b0d6a5eb179e91ce670eb83c3", + "bytes": 3439 + }, + "pineforge/session_time.hpp": { + "sha256": "f58c94bfae948e08fa21e52f20eae3940b31e398d83b800baf3a31f3a5fde669", + "git_blob": "af3a4472d852e0965947c006d0b6ad85304539e7", + "bytes": 10337 + }, + "pineforge/timeframe.hpp": { + "sha256": "5e4c312da463fdcff55e0887188523c8bbf85a2ae62f3317a2b23f22fe0161e2", + "git_blob": "3d1bc9d495ac7e5b39ea5bae57c9d9afe580192b", + "bytes": 29534 + } + } +} diff --git a/tests/test_native_calendar_driver_contract.cpp b/tests/test_native_calendar_driver_contract.cpp index f0591e61..3af6e8f1 100644 --- a/tests/test_native_calendar_driver_contract.cpp +++ b/tests/test_native_calendar_driver_contract.cpp @@ -322,15 +322,16 @@ std::string action_json(const Request& request) { out << "{\"type\":\"Transact\","; append_f64(out, "signedUnits", action.signed_units); out << "}"; - } else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { + const auto* units = std::get_if(&action.size); out << "{\"type\":\"Reduce\","; - append_f64(out, "units", action.units); + append_f64(out, "units", units ? units->units : 0.0); out << "}"; } else { out << "{\"type\":\"Flatten\"}"; } return out.str(); - }, request.action); + }, request.intent); } const char* command_kind_name(const CommandEvent& event) { @@ -346,10 +347,34 @@ const char* command_kind_name(const CommandEvent& event) { if constexpr (std::is_same_v) return "NoEffect"; if constexpr (std::is_same_v) return "MatchRejected"; if constexpr (std::is_same_v) return "ExecutionApplied"; + if constexpr (std::is_same_v) return "CloseBound"; + if constexpr (std::is_same_v) return "Activated"; + if constexpr (std::is_same_v) { + return "ReservationReduced"; + } + if constexpr (std::is_same_v) { + return "DeferredGroupAdjustment"; + } + if constexpr (std::is_same_v) return "QuantityBound"; + if constexpr (std::is_same_v) return "Armed"; return "Unknown"; }, event); } +bool r1_command_event(const CommandEvent& event) { + const char* kind = command_kind_name(event); + return std::strcmp(kind, "Accepted") == 0 + || std::strcmp(kind, "Rejected") == 0 + || std::strcmp(kind, "Replaced") == 0 + || std::strcmp(kind, "ReplaceRejected") == 0 + || std::strcmp(kind, "Cancelled") == 0 + || std::strcmp(kind, "NotWorking") == 0 + || std::strcmp(kind, "InvalidHandle") == 0 + || std::strcmp(kind, "NoEffect") == 0 + || std::strcmp(kind, "MatchRejected") == 0 + || std::strcmp(kind, "ExecutionApplied") == 0; +} + uint64_t command_ordinal(const CommandEvent& event) { return std::visit([](const auto& payload) { return payload.ordinal; }, event); } @@ -358,9 +383,14 @@ double request_quantity(const Request& request) { return std::visit([](const auto& action) -> double { using T = std::decay_t; if constexpr (std::is_same_v) return action.signed_units; - if constexpr (std::is_same_v) return action.units; + if constexpr (std::is_same_v) { + if (const auto* units = std::get_if(&action.size)) { + return units->units; + } + return 0.0; + } return 0.0; - }, request.action); + }, request.intent); } NativeRunSpec spec(const char* input = "1", const char* script = "1") { @@ -612,6 +642,7 @@ class TraceHost final : public NativeStrategyHost { for (const auto& event : events) { if (event.command) { const auto& command = *event.command; + if (!r1_command_event(command)) continue; const char* kind = command_kind_name(command); std::ostringstream out; out << "{\"kind\":" << json_escape(kind) @@ -636,10 +667,10 @@ class TraceHost final : public NativeStrategyHost { effect << "{\"kind\":\"ExecutionApplied\",\"ordinal\":" << json_u64(applied->ordinal) << ","; append_f64(effect, "price", applied->raw_price); - effect << ",\"timestampMs\":" << json_i64(applied->effective_time_ms) - << ",\"provenance\":" << json_u64(applied->provenance) + effect << ",\"timestampMs\":" << json_i64(applied->effective_time_ms()) + << ",\"provenance\":" << json_u64(applied->provenance()) << ","; - append_f64(effect, "quantity", request_quantity(applied->request)); + append_f64(effect, "quantity", request_quantity(applied->request())); effect << ",\"observations\":{\"hostOrdinal\":" << host_ordinal_ << ",\"resolvedPrice\":"; if (std::isfinite(applied->resolved_price)) @@ -1092,8 +1123,8 @@ void lunch_real_off_session_and_replay() { const auto executed = fills(*host); CHECK(executed.size() == 1); if (!executed.empty()) { - CHECK(executed[0].effective_time_ms == 1749473100000LL); - CHECK(executed[0].provenance == static_cast(NativePriceProvenance::ObservedPrint)); + CHECK(executed[0].effective_time_ms() == 1749473100000LL); + CHECK(executed[0].provenance() == static_cast(NativePriceProvenance::ObservedPrint)); near(executed[0].raw_price, 123); } CHECK(host->stream_end(false)); @@ -1369,8 +1400,8 @@ void ticks_same_timestamp_and_atomic_refusal() { CHECK(executed.size() == 1); if (!executed.empty()) { near(executed[0].raw_price, 100.50); - CHECK(executed[0].effective_time_ms == 60000); - CHECK(executed[0].provenance == static_cast(NativePriceProvenance::ObservedPrint)); + CHECK(executed[0].effective_time_ms() == 60000); + CHECK(executed[0].provenance() == static_cast(NativePriceProvenance::ObservedPrint)); } const auto before = host.native_continuation_hash(); CHECK(!host.stream_push_tick(TradeTick{60000, 2, 200, 2})); @@ -1427,8 +1458,8 @@ void quiet_birth_floor() { const auto executed = fills(host); CHECK(executed.size() == 1); if (!executed.empty()) { - CHECK(executed[0].effective_time_ms == (advance_before_submit ? 120000 : 60000)); - CHECK(executed[0].provenance == static_cast(NativePriceProvenance::CarriedOpen)); + CHECK(executed[0].effective_time_ms() == (advance_before_submit ? 120000 : 60000)); + CHECK(executed[0].provenance() == static_cast(NativePriceProvenance::CarriedOpen)); } CHECK(host.native_decision_floor() == 180000); CHECK(host.stream_end(false)); @@ -1450,9 +1481,9 @@ void confirmed_close_policy() { const auto executed = fills(host); CHECK(executed.size() == (policy == NativeCloseExecution::AfterCalculation ? 1U : 0U)); if (!executed.empty()) { - CHECK(executed[0].effective_time_ms == 120000); + CHECK(executed[0].effective_time_ms() == 120000); CHECK(executed[0].ordinal > host.contexts[0].coordinate.ordinal); - CHECK(executed[0].provenance == static_cast(NativePriceProvenance::AfterCalculationClose)); + CHECK(executed[0].provenance() == static_cast(NativePriceProvenance::AfterCalculationClose)); near(executed[0].raw_price, 101); near(host.runup(), 0); near(host.drawdown(), 0); @@ -1477,10 +1508,10 @@ void delayed_open_after_external_birth() { const auto executed = fills(host); CHECK(executed.size() == (policy == NativeCloseExecution::AfterCalculation ? 1U : 0U)); if (!executed.empty()) { - CHECK(executed[0].effective_time_ms == 600000); + CHECK(executed[0].effective_time_ms() == 600000); near(executed[0].raw_price, 109); } - for (const auto& fill : executed) CHECK(fill.effective_time_ms >= 420000); + for (const auto& fill : executed) CHECK(fill.effective_time_ms() >= 420000); CHECK(host.stream_end(false)); ordered(host); } @@ -1549,7 +1580,7 @@ void partial_end_equal_clock_and_policy() { const auto executed = fills(host); CHECK(executed.size() == (should_fill ? 1U : 0U)); if (should_fill && !executed.empty()) { - CHECK(executed[0].effective_time_ms == 90000); + CHECK(executed[0].effective_time_ms() == 90000); near(executed[0].raw_price, 107); const auto modeled = points(host, NativePriceProvenance::AfterCalculationClose); // Warmup has one after-calculation point even with no request. diff --git a/tests/test_native_host_repairs.cpp b/tests/test_native_host_repairs.cpp index 08250c87..76665b5e 100644 --- a/tests/test_native_host_repairs.cpp +++ b/tests/test_native_host_repairs.cpp @@ -16,7 +16,7 @@ #include #include -#ifndef PINEFORGE_HAS_NATIVE_STRATEGY_HOST_V12 +#ifndef PINEFORGE_HAS_NATIVE_STRATEGY_HOST_V13 #error "native strategy host must fail closed against an unversioned epoch" #endif @@ -264,7 +264,7 @@ class BeginSubmitHost final : public NativeStrategyHost { if (!event.command) continue; if (const auto* accepted = std::get_if(&*event.command)) { - birth_floor = accepted->birth.decision_time_lower_bound; + birth_floor = accepted->birth().decision_time_lower_bound; } } } @@ -300,7 +300,7 @@ const native_order::ExecutionAppliedEvent* first_applied( int main() { { const std::string name = typeid(NativeStrategyHost).name(); - CHECK(name.find("engine_script_run_v12") != std::string::npos); + CHECK(name.find("engine_script_run_v13") != std::string::npos); CHECK(name.find("NativeStrategyHost") != std::string::npos); } { @@ -568,8 +568,8 @@ int main() { if (!event.command) continue; if (const auto* applied = std::get_if(&*event.command)) { - if (applied->effective_time_ms == 0 - && applied->provenance + if (applied->effective_time_ms() == 0 + && applied->provenance() == static_cast(NativePriceProvenance::ModeledOHLCOpen)) { opening_fill = true; } @@ -636,8 +636,8 @@ int main() { if (event.command) { if (const auto* applied = std::get_if(&*event.command)) { - if (applied->effective_time_ms == 60000) filled_60000 = true; - if (applied->effective_time_ms == 120000) filled_120000 = true; + if (applied->effective_time_ms() == 60000) filled_60000 = true; + if (applied->effective_time_ms() == 120000) filled_120000 = true; } } } @@ -843,7 +843,7 @@ int main() { if (!event.command) continue; if (const auto* applied = std::get_if(&*event.command)) { - if (applied->provenance + if (applied->provenance() == static_cast(NativePriceProvenance::AfterCalculationClose)) { after_fill = true; } @@ -1536,9 +1536,9 @@ int main() { if (!event.command) continue; if (const auto* applied = std::get_if(&*event.command)) { - CHECK(applied->effective_time_ms == -60000); + CHECK(applied->effective_time_ms() == -60000); near(applied->raw_price, 100.0); - CHECK(applied->birth.decision_time_lower_bound == -60000); + CHECK(applied->birth().decision_time_lower_bound == -60000); saw_open_fill = true; } } @@ -1613,8 +1613,8 @@ int main() { if (!event.command) continue; if (const auto* applied = std::get_if(&*event.command)) { - CHECK(applied->effective_time_ms == 60000); - CHECK(applied->birth.decision_time_lower_bound >= 60000); + CHECK(applied->effective_time_ms() == 60000); + CHECK(applied->birth().decision_time_lower_bound >= 60000); near(applied->raw_price, 102.0); } } diff --git a/tests/test_native_market_vertical_contract.cpp b/tests/test_native_market_vertical_contract.cpp index 690a58a8..a0229101 100644 --- a/tests/test_native_market_vertical_contract.cpp +++ b/tests/test_native_market_vertical_contract.cpp @@ -113,7 +113,7 @@ Request tx(double q, const char* label = "", const char* comment = "") { return Request{Transact{q}, label, comment}; } Request rd(double q, const char* label = "", const char* comment = "") { - return Request{Reduce{q}, label, comment}; + return pineforge::native_order::market_request(Reduce{q}, label, comment); } Request flat(const char* label = "", const char* comment = "") { return Request{Flatten{}, label, comment}; @@ -259,10 +259,38 @@ const char* event_kind_name(const CommandEvent& event) { if constexpr (std::is_same_v) return "NoEffect"; if constexpr (std::is_same_v) return "MatchRejected"; if constexpr (std::is_same_v) return "ExecutionApplied"; + if constexpr (std::is_same_v) { + return "CloseBound"; + } + if constexpr (std::is_same_v) return "Activated"; + if constexpr (std::is_same_v) { + return "ReservationReduced"; + } + if constexpr (std::is_same_v) { + return "DeferredGroupAdjustment"; + } + if constexpr (std::is_same_v) { + return "QuantityBound"; + } + if constexpr (std::is_same_v) return "Armed"; return "Unknown"; }, event); } +bool r1_command_event(const CommandEvent& event) { + const char* kind = event_kind_name(event); + return std::strcmp(kind, "Accepted") == 0 + || std::strcmp(kind, "Rejected") == 0 + || std::strcmp(kind, "Replaced") == 0 + || std::strcmp(kind, "ReplaceRejected") == 0 + || std::strcmp(kind, "Cancelled") == 0 + || std::strcmp(kind, "NotWorking") == 0 + || std::strcmp(kind, "InvalidHandle") == 0 + || std::strcmp(kind, "NoEffect") == 0 + || std::strcmp(kind, "MatchRejected") == 0 + || std::strcmp(kind, "ExecutionApplied") == 0; +} + uint64_t event_ordinal(const CommandEvent& event) { return std::visit([](const auto& payload) { return payload.ordinal; }, event); } @@ -318,9 +346,11 @@ const ExecutionAppliedEvent* applied_at(const std::vector& events, std::string lifecycle_json(const std::vector& events) { std::ostringstream out; out << "["; - for (std::size_t i = 0; i < events.size(); ++i) { - if (i) out << ","; - const auto& event = events[i]; + bool first = true; + for (const auto& event : events) { + if (!r1_command_event(event)) continue; + if (!first) out << ","; + first = false; out << "{\"kind\":" << json_escape(event_kind_name(event)) << ",\"ordinal\":" << json_u64(event_ordinal(event)); if (as_event(event)) { @@ -357,8 +387,8 @@ std::string physical_json(const FixtureHost& host, const std::vectorordinal) << ",\"price\":" << json_num(applied->resolved_price) - << ",\"timestampMs\":" << json_i64(applied->effective_time_ms) - << ",\"provenance\":" << json_u64(applied->provenance); + << ",\"timestampMs\":" << json_i64(applied->effective_time_ms()) + << ",\"provenance\":" << json_u64(applied->provenance()); if (idx >= 0 && idx < host.trade_count()) { out << ",\"quantity\":" << json_num(host.get_trade(idx).qty); } @@ -368,15 +398,15 @@ std::string physical_json(const FixtureHost& host, const std::vectorordinal) << ",\"price\":" << json_num(applied->resolved_price) - << ",\"timestampMs\":" << json_i64(applied->effective_time_ms) - << ",\"provenance\":" << json_u64(applied->provenance) + << ",\"timestampMs\":" << json_i64(applied->effective_time_ms()) + << ",\"provenance\":" << json_u64(applied->provenance()) << "}"; } if (applied->closed_trade_count == 0 && applied->opened_lot_incarnation == 0) { comma(); out << "{\"kind\":\"ExecutionApplied\",\"ordinal\":" << json_u64(applied->ordinal) << ",\"price\":" << json_num(applied->resolved_price) - << ",\"timestampMs\":" << json_i64(applied->effective_time_ms) + << ",\"timestampMs\":" << json_i64(applied->effective_time_ms()) << "}"; } } @@ -473,8 +503,8 @@ void expect_modeled_open_fill(const ExecutionAppliedEvent* applied, if (!applied) return; near(applied->raw_price, raw); near(applied->resolved_price, resolved); - CHECK(applied->effective_time_ms == time_ms); - CHECK(applied->provenance + CHECK(applied->effective_time_ms() == time_ms); + CHECK(applied->provenance() == static_cast(NativePriceProvenance::ModeledOHLCOpen)); near(applied->current_ticket, kFee); } diff --git a/tests/test_native_order_core.cpp b/tests/test_native_order_core.cpp index 453961b9..2b9ddc9c 100644 --- a/tests/test_native_order_core.cpp +++ b/tests/test_native_order_core.cpp @@ -14,7 +14,6 @@ #include #include -using pineforge::execution::Action; using pineforge::execution::Flatten; using pineforge::native_order::AcceptedEvent; using pineforge::native_order::Birth; @@ -36,7 +35,6 @@ using pineforge::native_order::RunIdentity; using pineforge::native_order::SubmitStatus; using pineforge::native_order::WorkingRequestCore; using pineforge::native_order::point_eligible; -using pineforge::order_action::Reduce; using pineforge::order_action::Transact; namespace { @@ -69,19 +67,23 @@ double step_n(double x, double toward, int n) { } Request tx(double q, std::string label = {}, std::string comment = {}) { - return Request{Action{Transact{q}}, std::move(label), std::move(comment)}; + return Request{Transact{q}, std::move(label), std::move(comment)}; } Request rd(double q, std::string label = {}, std::string comment = {}) { - return Request{Action{Reduce{q}}, std::move(label), std::move(comment)}; + return Request{pineforge::native_order::Reduce{pineforge::native_order::ExplicitUnits{q}}, + std::move(label), std::move(comment)}; } Request fl(std::string label = {}, std::string comment = {}) { - return Request{Action{Flatten{}}, std::move(label), std::move(comment)}; + return Request{Flatten{}, std::move(label), std::move(comment)}; } -double transact_qty(const Action& action) { - return std::get(action).signed_units; +double transact_qty(const pineforge::native_order::OrderIntent& intent) { + return std::get(intent).signed_units; +} +double reduce_qty(const pineforge::native_order::OrderIntent& intent) { + const auto& reduce = std::get(intent); + return std::get(reduce.size).units; } -double reduce_qty(const Action& action) { return std::get(action).units; } template const T* as(const CommandEvent& event) { @@ -120,21 +122,21 @@ void submit_accept_and_reject() { CHECK(core.live().size() == 1); CHECK(core.history().size() == 1); const LiveRequest& live = core.live().front(); - CHECK(live.handle == *accepted.handle); - CHECK(live.birth.acceptance_ordinal == 1); - CHECK(live.birth.decision_time_lower_bound == 1000); - CHECK(!live.predecessor); - CHECK(live.request.label == "entry"); - CHECK(live.request.comment == "first"); - CHECK(bits_eq(transact_qty(live.request.action), 2.5)); + CHECK(live.handle() == *accepted.handle); + CHECK(live.birth().acceptance_ordinal == 1); + CHECK(live.birth().decision_time_lower_bound == 1000); + CHECK(!live.predecessor()); + CHECK(live.request().label == "entry"); + CHECK(live.request().comment == "first"); + CHECK(bits_eq(transact_qty(live.request().intent), 2.5)); const auto* event = as(core.history().back()); CHECK(event); if (event) { CHECK(event->ordinal == 1); - CHECK(event->handle == live.handle); - CHECK(event->birth == live.birth); - CHECK(event->request.label == "entry"); - CHECK(bits_eq(transact_qty(event->request.action), 2.5)); + CHECK(event->handle() == live.handle()); + CHECK(event->birth() == live.birth()); + CHECK(event->request().label == "entry"); + CHECK(bits_eq(transact_qty(event->request().intent), 2.5)); } CHECK(core.find_live(*accepted.handle) == &core.live().front()); @@ -146,7 +148,7 @@ void submit_accept_and_reject() { CHECK(inc == 2); CHECK(ord == 3); CHECK(core.live().size() == 1); - CHECK(core.live().front().handle.incarnation == 1); + CHECK(core.live().front().handle().incarnation == 1); CHECK(core.history().size() == 2); const auto* rej = as(core.history().back()); CHECK(rej); @@ -155,7 +157,7 @@ void submit_accept_and_reject() { CHECK(rej->reason == RequestRejectReason::InvalidQuantity); CHECK(rej->request.label == "zero"); CHECK(rej->request.comment == "nope"); - CHECK(bits_eq(transact_qty(rej->request.action), 0.0)); + CHECK(bits_eq(transact_qty(rej->request.intent), 0.0)); } } @@ -199,12 +201,12 @@ void quantity_rejections_preserve_bits() { CHECK(rej->ordinal == ord_before); CHECK(rej->reason == cases[i].reason); CHECK(rej->request.label == cases[i].request.label); - if (std::holds_alternative(cases[i].request.action)) { - CHECK(bits_eq(transact_qty(rej->request.action), - transact_qty(cases[i].request.action))); + if (std::holds_alternative(cases[i].request.intent)) { + CHECK(bits_eq(transact_qty(rej->request.intent), + transact_qty(cases[i].request.intent))); } else { - CHECK(bits_eq(reduce_qty(rej->request.action), - reduce_qty(cases[i].request.action))); + CHECK(bits_eq(reduce_qty(rej->request.intent), + reduce_qty(cases[i].request.intent))); } } CHECK(core.history().size() == n); @@ -217,19 +219,19 @@ void flatten_and_signed_reduce() { uint64_t ord = 1; const auto flat = core.submit(fl("flat", "all"), 8, inc, ord, 0.25); CHECK(flat.status == SubmitStatus::Accepted); - CHECK(std::holds_alternative(core.live().back().request.action)); - CHECK(core.live().back().request.label == "flat"); + CHECK(std::holds_alternative(core.live().back().request().intent)); + CHECK(core.live().back().request().label == "flat"); CHECK(inc == 2); CHECK(ord == 2); const double denorm = std::numeric_limits::denorm_min(); const auto reduce = core.submit(rd(denorm, "dust"), 8, inc, ord); CHECK(reduce.status == SubmitStatus::Accepted); - CHECK(bits_eq(reduce_qty(core.live().back().request.action), denorm)); + CHECK(bits_eq(reduce_qty(core.live().back().request().intent), denorm)); const auto sell = core.submit(tx(-4.0, "sell"), 9, inc, ord); CHECK(sell.status == SubmitStatus::Accepted); - CHECK(bits_eq(transact_qty(core.live().back().request.action), -4.0)); + CHECK(bits_eq(transact_qty(core.live().back().request().intent), -4.0)); CHECK(core.live().size() == 3); } @@ -242,32 +244,32 @@ void grid_accepts_and_preserves_bits() { CHECK(quantity_on_grid(q, step)); const auto result = core.submit(tx(q, "sum"), 1, inc, ord, step); CHECK(result.status == SubmitStatus::Accepted); - CHECK(bits_eq(transact_qty(core.live().back().request.action), q)); + CHECK(bits_eq(transact_qty(core.live().back().request().intent), q)); CHECK(!bits_eq(q, 0.3)); const auto sell = core.submit(tx(-2.0, "sell-grid"), 1, inc, ord, 1.0); CHECK(sell.status == SubmitStatus::Accepted); - CHECK(bits_eq(transact_qty(core.live().back().request.action), -2.0)); + CHECK(bits_eq(transact_qty(core.live().back().request().intent), -2.0)); const double exact = 2.0; const double one_ulp = step_n(exact, Inf, 1); CHECK(quantity_on_grid(one_ulp, 1.0)); const auto near = core.submit(tx(one_ulp, "ulp1"), 1, inc, ord, 1.0); CHECK(near.status == SubmitStatus::Accepted); - CHECK(bits_eq(transact_qty(core.live().back().request.action), one_ulp)); + CHECK(bits_eq(transact_qty(core.live().back().request().intent), one_ulp)); CHECK(!bits_eq(one_ulp, exact)); const double below = step_n(exact, 0.0, 1); CHECK(quantity_on_grid(below, 1.0)); const auto near_lo = core.submit(rd(below, "ulp-1"), 1, inc, ord, 1.0); CHECK(near_lo.status == SubmitStatus::Accepted); - CHECK(bits_eq(reduce_qty(core.live().back().request.action), below)); + CHECK(bits_eq(reduce_qty(core.live().back().request().intent), below)); const double pow2 = 0x1p53; CHECK(quantity_on_grid(pow2, 1.0)); const auto edge = core.submit(tx(pow2, "n53"), 1, inc, ord, 1.0); CHECK(edge.status == SubmitStatus::Accepted); - CHECK(bits_eq(transact_qty(core.live().back().request.action), pow2)); + CHECK(bits_eq(transact_qty(core.live().back().request().intent), pow2)); } void grid_rejects_off_grid_and_half() { @@ -286,7 +288,7 @@ void grid_rejects_off_grid_and_half() { CHECK(rej); if (rej) { CHECK(rej->reason == RequestRejectReason::OffGrid); - CHECK(bits_eq(transact_qty(rej->request.action), five_ulp)); + CHECK(bits_eq(transact_qty(rej->request.intent), five_ulp)); CHECK(rej->request.label == "five"); } CHECK(core.live().empty()); @@ -297,7 +299,7 @@ void grid_rejects_off_grid_and_half() { CHECK(inc == inc_before); const auto* half_e = as(core.history().back()); CHECK(half_e); - if (half_e) CHECK(bits_eq(transact_qty(half_e->request.action), 1.5)); + if (half_e) CHECK(bits_eq(transact_qty(half_e->request.intent), 1.5)); const auto dust = core.submit(rd(0.4, "dust"), 2, inc, ord, 1.0); CHECK(dust.status == SubmitStatus::Rejected); @@ -320,8 +322,8 @@ void submit_before_point_eligibility() { CHECK(second.event_ordinal == 6); const LiveRequest& a = core.live()[0]; const LiveRequest& b = core.live()[1]; - CHECK(a.birth.acceptance_ordinal == 5); - CHECK(b.birth.acceptance_ordinal == 6); + CHECK(a.birth().acceptance_ordinal == 5); + CHECK(b.birth().acceptance_ordinal == 6); CHECK(!point_eligible(a, 5, 1000)); CHECK(!point_eligible(a, 6, 999)); CHECK(point_eligible(a, 6, 1000)); @@ -329,7 +331,7 @@ void submit_before_point_eligibility() { CHECK(!point_eligible(b, 6, 1000)); CHECK(!point_eligible(b, 7, 999)); CHECK(point_eligible(b, 7, 1000)); - CHECK(!point_eligible(a.birth, a.birth.acceptance_ordinal, a.birth.decision_time_lower_bound)); + CHECK(!point_eligible(a.birth(), a.birth().acceptance_ordinal, a.birth().decision_time_lower_bound)); } void replace_valid_invalid_and_stale_cancel() { @@ -351,16 +353,16 @@ void replace_valid_invalid_and_stale_cancel() { CHECK(ord == 4); CHECK(core.live().size() == 2); CHECK(core.find_live(h1)); - CHECK(core.find_live(h1)->request.label == "old"); - CHECK(bits_eq(transact_qty(core.find_live(h1)->request.action), 1.0)); + CHECK(core.find_live(h1)->request().label == "old"); + CHECK(bits_eq(transact_qty(core.find_live(h1)->request().intent), 1.0)); const auto* rr = as(core.history().back()); CHECK(rr); if (rr) { - CHECK(rr->target == h1); - CHECK(rr->live_request.label == "old"); + CHECK(rr->target() == h1); + CHECK(rr->live_request().label == "old"); CHECK(rr->attempted.label == "bad"); CHECK(rr->attempted.comment == "no"); - CHECK(bits_eq(transact_qty(rr->attempted.action), 0.0)); + CHECK(bits_eq(transact_qty(rr->attempted.intent), 0.0)); CHECK(rr->reason == RequestRejectReason::InvalidQuantity); } @@ -381,27 +383,27 @@ void replace_valid_invalid_and_stale_cancel() { CHECK(!core.find_live(h1)); CHECK(core.find_live(h2)); CHECK(core.live().size() == 2); - CHECK(core.live()[0].handle == h2); + CHECK(core.live()[0].handle() == h2); const LiveRequest& successor = core.live()[1]; - CHECK(successor.handle == *ok.successor); - CHECK(successor.birth.acceptance_ordinal == 5); - CHECK(successor.birth.decision_time_lower_bound == 30); - CHECK(successor.predecessor == h1); - CHECK(successor.request.label == "new"); - CHECK(successor.request.comment == "keep-new"); - CHECK(bits_eq(transact_qty(successor.request.action), -3.0)); + CHECK(successor.handle() == *ok.successor); + CHECK(successor.birth().acceptance_ordinal == 5); + CHECK(successor.birth().decision_time_lower_bound == 30); + CHECK(successor.predecessor() == h1); + CHECK(successor.request().label == "new"); + CHECK(successor.request().comment == "keep-new"); + CHECK(bits_eq(transact_qty(successor.request().intent), -3.0)); CHECK(point_eligible(successor, 6, 30)); CHECK(!point_eligible(successor, 5, 30)); const auto* replaced = as(core.history().back()); CHECK(replaced); if (replaced) { CHECK(replaced->ordinal == 5); - CHECK(replaced->predecessor == h1); - CHECK(replaced->predecessor_request.label == "old"); - CHECK(replaced->successor == successor.handle); - CHECK(replaced->successor_request.comment == "keep-new"); - CHECK(replaced->successor_birth == successor.birth); - CHECK(bits_eq(transact_qty(replaced->successor_request.action), -3.0)); + CHECK(replaced->predecessor() == h1); + CHECK(replaced->predecessor_request().label == "old"); + CHECK(replaced->successor() == successor.handle()); + CHECK(replaced->successor_request().comment == "keep-new"); + CHECK(replaced->successor_birth() == successor.birth()); + CHECK(bits_eq(transact_qty(replaced->successor_request().intent), -3.0)); } const auto stale = core.cancel(h1, ord); @@ -409,7 +411,7 @@ void replace_valid_invalid_and_stale_cancel() { CHECK(stale.event_ordinal == 6); CHECK(ord == 7); CHECK(core.find_live(h2)); - CHECK(core.find_live(successor.handle)); + CHECK(core.find_live(successor.handle())); const auto* nw = as(core.history().back()); CHECK(nw); if (nw) { @@ -421,7 +423,7 @@ void replace_valid_invalid_and_stale_cancel() { CHECK(stale_rep.status == ReplaceStatus::NotWorking); CHECK(!stale_rep.successor); CHECK(inc == 4); - CHECK(core.find_live(successor.handle)->request.label == "new"); + CHECK(core.find_live(successor.handle())->request().label == "new"); const auto* nw2 = as(core.history().back()); CHECK(nw2); if (nw2) { @@ -471,7 +473,7 @@ void foreign_zero_and_absent_handles() { CHECK(core.live().size() == 1); CHECK(core.find_live(*live.handle)); - CHECK(core.live().front().request.label == "live"); + CHECK(core.live().front().request().label == "live"); } void metadata_and_const_views() { @@ -484,14 +486,14 @@ void metadata_and_const_views() { const WorkingRequestCore& view = core; CHECK(view.live().size() == 1); CHECK(view.history().size() == 1); - CHECK(view.live().front().request.label == label); - CHECK(view.live().front().request.comment == comment); - CHECK(view.find_live(view.live().front().handle)); + CHECK(view.live().front().request().label == label); + CHECK(view.live().front().request().comment == comment); + CHECK(view.find_live(view.live().front().handle())); const auto* accepted = as(view.history().front()); CHECK(accepted); if (accepted) { - CHECK(accepted->request.label == label); - CHECK(accepted->request.comment == comment); + CHECK(accepted->request().label == label); + CHECK(accepted->request().comment == comment); } } @@ -511,7 +513,7 @@ void counters_do_not_partially_mutate() { CHECK(core.live().size() == live_n); CHECK(core.history().size() == hist_n); CHECK(core.find_live(kept)); - CHECK(core.find_live(kept)->request.label == "keep"); + CHECK(core.find_live(kept)->request().label == "keep"); }; inc = 0; @@ -561,8 +563,8 @@ void counters_do_not_partially_mutate() { } catch (const std::overflow_error&) { CHECK(true); } CHECK(inc == std::numeric_limits::max()); CHECK(core.find_live(kept)); - CHECK(core.find_live(kept)->request.label == "keep"); - CHECK(bits_eq(transact_qty(core.find_live(kept)->request.action), 1.0)); + CHECK(core.find_live(kept)->request().label == "keep"); + CHECK(bits_eq(transact_qty(core.find_live(kept)->request().intent), 1.0)); inc = 8; const uint64_t ord_now = ord; @@ -582,7 +584,7 @@ void counters_do_not_partially_mutate() { } catch (const std::invalid_argument&) { CHECK(true); } CHECK(ord == 0); CHECK(core.find_live(kept)); - CHECK(core.find_live(kept)->request.label == "keep"); + CHECK(core.find_live(kept)->request().label == "keep"); ord = std::numeric_limits::max(); try { @@ -597,7 +599,7 @@ void counters_do_not_partially_mutate() { const auto still = core.replace(kept, tx(0.0, "bad"), 2, inc, ord); CHECK(still.status == ReplaceStatus::ReplaceRejected); CHECK(inc == 8); - CHECK(core.find_live(kept)->request.label == "keep"); + CHECK(core.find_live(kept)->request().label == "keep"); } void reset_makes_old_handles_invalid() { @@ -612,7 +614,7 @@ void reset_makes_old_handles_invalid() { } catch (const std::invalid_argument&) { CHECK(true); } CHECK(core.identity() == kRun); CHECK(core.find_live(old)); - CHECK(core.find_live(old)->request.label == "old-run"); + CHECK(core.find_live(old)->request().label == "old-run"); try { core.reset(RunIdentity{"session-a", 0}); @@ -639,7 +641,7 @@ void reset_makes_old_handles_invalid() { CHECK(fresh.status == SubmitStatus::Accepted); CHECK(fresh.handle->run == next); CHECK(fresh.handle->incarnation == 2); - CHECK(core.live().front().request.label == "new-run"); + CHECK(core.live().front().request().label == "new-run"); CHECK(old != *fresh.handle); } @@ -703,24 +705,24 @@ void move_construction_transfers_and_reset_rebinds() { const auto* transferred = destination.find_live(*successor.successor); CHECK(transferred); if (transferred) { - CHECK(transferred->predecessor == original.handle); - CHECK(transferred->birth == (Birth{3, 12})); - CHECK(transferred->request.label == "successor"); - CHECK(transferred->request.comment == "kept"); - CHECK(bits_eq(transact_qty(transferred->request.action), -2.5)); + CHECK(transferred->predecessor() == original.handle); + CHECK(transferred->birth() == (Birth{3, 12})); + CHECK(transferred->request().label == "successor"); + CHECK(transferred->request().comment == "kept"); + CHECK(bits_eq(transact_qty(transferred->request().intent), -2.5)); } const auto* accepted_event = as(destination.history().front()); CHECK(accepted_event); if (accepted_event) { - CHECK(accepted_event->handle == *original.handle); - CHECK(accepted_event->request.comment == "move me"); + CHECK(accepted_event->handle() == *original.handle); + CHECK(accepted_event->request().comment == "move me"); } CHECK(as(destination.history()[1])); const auto* replaced_event = as(destination.history().back()); CHECK(replaced_event); if (replaced_event) { - CHECK(replaced_event->predecessor == *original.handle); - CHECK(replaced_event->successor == *successor.successor); + CHECK(replaced_event->predecessor() == *original.handle); + CHECK(replaced_event->successor() == *successor.successor); CHECK(replaced_event->ordinal == 3); } const auto cancelled = destination.cancel(*successor.successor, ord); @@ -766,10 +768,10 @@ void move_assignment_replaces_destination_and_preserves_self_move() { const auto* transferred = destination.find_live(*request.handle); CHECK(transferred); if (transferred) { - CHECK(transferred->birth == (Birth{11, 80})); - CHECK(transferred->request.label == "transferred"); - CHECK(transferred->request.comment == "assign me"); - CHECK(bits_eq(reduce_qty(transferred->request.action), 2.0)); + CHECK(transferred->birth() == (Birth{11, 80})); + CHECK(transferred->request().label == "transferred"); + CHECK(transferred->request().comment == "assign me"); + CHECK(bits_eq(reduce_qty(transferred->request().intent), 2.0)); } auto* self = &destination; destination = std::move(*self); @@ -817,18 +819,18 @@ void cancel_live_erases_only_that_request() { CHECK(core.find_live(*b.handle)); CHECK(core.find_live(*c.handle)); CHECK(core.live().size() == 2); - CHECK(core.live()[0].request.label == "b"); - CHECK(core.live()[1].request.label == "c"); + CHECK(core.live()[0].request().label == "b"); + CHECK(core.live()[1].request().label == "c"); const auto* cancelled = as(core.history().back()); CHECK(cancelled); if (cancelled) { - CHECK(cancelled->handle == *a.handle); - CHECK(cancelled->request.label == "a"); - CHECK(bits_eq(transact_qty(cancelled->request.action), 1.0)); + CHECK(cancelled->handle() == *a.handle); + CHECK(cancelled->request().label == "a"); + CHECK(bits_eq(transact_qty(cancelled->request().intent), 1.0)); } const auto again = core.cancel(*a.handle, ord); CHECK(again.status == CancelStatus::NotWorking); - CHECK(core.find_live(*b.handle)->request.comment == "stay"); + CHECK(core.find_live(*b.handle)->request().comment == "stay"); } void same_counter_alias() { @@ -861,7 +863,7 @@ void same_counter_alias() { CHECK(inc == 2); CHECK(ord == 2); CHECK(core.find_live(*first.handle)); - CHECK(core.find_live(*first.handle)->request.label == "kept"); + CHECK(core.find_live(*first.handle)->request().label == "kept"); CHECK(core.history().size() == 1); } @@ -885,7 +887,7 @@ void reused_and_gapped_counters() { const auto after = core.submit(tx(2.0, "after-cancel"), 1, inc, ord); CHECK(after.status == SubmitStatus::Accepted); CHECK(after.handle->incarnation == 2); - CHECK(core.live().front().request.label == "after-cancel"); + CHECK(core.live().front().request().label == "after-cancel"); } WorkingRequestCore core(kRun); @@ -934,7 +936,7 @@ void reused_and_gapped_counters() { } catch (const std::invalid_argument&) { CHECK(true); } CHECK(inc == 1); CHECK(core.live().size() == 1); - CHECK(core.live().front().handle.incarnation == 10); + CHECK(core.live().front().handle().incarnation == 10); inc = 10; try { @@ -968,7 +970,7 @@ void reused_and_gapped_counters() { core.replace(*replaced.successor, tx(6.0, "reuse-pred"), 1, inc, ord); CHECK(false); } catch (const std::invalid_argument&) { CHECK(true); } - CHECK(core.find_live(*replaced.successor)->request.label == "succ"); + CHECK(core.find_live(*replaced.successor)->request().label == "succ"); inc = 20; const auto later = core.submit(tx(7.0, "later"), 3, inc, ord); @@ -983,38 +985,38 @@ void aliased_const_arguments() { CHECK(core.submit(tx(1.0, "orig", "c1"), 10, inc, ord).status == SubmitStatus::Accepted); CHECK(core.submit(tx(2.0, "peer", "c2"), 11, inc, ord).status == SubmitStatus::Accepted); - const auto copied = core.submit(core.live().front().request, 12, inc, ord); + const auto copied = core.submit(core.live().front().request(), 12, inc, ord); CHECK(copied.status == SubmitStatus::Accepted); CHECK(copied.handle->incarnation == 3); CHECK(core.live().size() == 3); - CHECK(core.live()[0].request.label == "orig"); - CHECK(core.live()[0].request.comment == "c1"); - CHECK(core.live()[2].request.label == "orig"); - CHECK(core.live()[2].request.comment == "c1"); - CHECK(bits_eq(transact_qty(core.live()[2].request.action), 1.0)); + CHECK(core.live()[0].request().label == "orig"); + CHECK(core.live()[0].request().comment == "c1"); + CHECK(core.live()[2].request().label == "orig"); + CHECK(core.live()[2].request().comment == "c1"); + CHECK(bits_eq(transact_qty(core.live()[2].request().intent), 1.0)); - const auto repl = core.replace(core.live().front().handle, core.live().front().request, 13, inc, + const auto repl = core.replace(core.live().front().handle(), core.live().front().request(), 13, inc, ord); CHECK(repl.status == ReplaceStatus::Replaced); CHECK(repl.successor->incarnation == 4); CHECK(!core.find_live(RequestHandle{kRun, 1})); CHECK(core.live().size() == 3); - CHECK(core.live()[0].request.label == "peer"); - CHECK(core.live().back().request.label == "orig"); - CHECK(core.live().back().request.comment == "c1"); - CHECK(core.live().back().predecessor->incarnation == 1); + CHECK(core.live()[0].request().label == "peer"); + CHECK(core.live().back().request().label == "orig"); + CHECK(core.live().back().request().comment == "c1"); + CHECK(core.live().back().predecessor()->incarnation == 1); - const auto cancelled = core.cancel(core.live().front().handle, ord); + const auto cancelled = core.cancel(core.live().front().handle(), ord); CHECK(cancelled.status == CancelStatus::Cancelled); CHECK(core.live().size() == 2); - CHECK(core.live().front().request.label == "orig"); + CHECK(core.live().front().request().label == "orig"); const auto* first_accepted = as(core.history().front()); CHECK(first_accepted); - const auto from_history = core.submit(first_accepted->request, 14, inc, ord); + const auto from_history = core.submit(first_accepted->request(), 14, inc, ord); CHECK(from_history.status == SubmitStatus::Accepted); - CHECK(core.live().back().request.label == "orig"); - CHECK(core.live().back().request.comment == "c1"); + CHECK(core.live().back().request().label == "orig"); + CHECK(core.live().back().request().comment == "c1"); } void history_prefix_append_stable() { @@ -1027,24 +1029,24 @@ void history_prefix_append_stable() { CHECK(result.handle->incarnation == static_cast(i + 1)); } const CommandEvent* prefix = &core.history().front(); - CHECK(as(*prefix)->request.label == "p0"); + CHECK(as(*prefix)->request().label == "p0"); CHECK(as(*prefix)->ordinal == 1); CHECK(core.submit(tx(1.0, "p32"), 1, inc, ord).status == SubmitStatus::Accepted); if (&core.history().front() != prefix) prefix = &core.history().front(); - CHECK(as(*prefix)->request.label == "p0"); + CHECK(as(*prefix)->request().label == "p0"); const auto rejected = core.submit(tx(0.0, "rej"), 1, inc, ord); CHECK(rejected.status == SubmitStatus::Rejected); CHECK(&core.history().front() == prefix); - CHECK(as(core.history().front())->request.label == "p0"); + CHECK(as(core.history().front())->request().label == "p0"); CHECK(as(core.history().front())->ordinal == 1); const auto extra = core.submit(tx(2.0, "more"), 1, inc, ord); CHECK(extra.status == SubmitStatus::Accepted); CHECK(&core.history().front() == prefix); - CHECK(as(core.history()[1])->request.label == "p1"); - CHECK(core.live().front().request.label == "p0"); + CHECK(as(core.history()[1])->request().label == "p1"); + CHECK(core.live().front().request().label == "p0"); } void bounded_many_commands() { @@ -1060,26 +1062,26 @@ void bounded_many_commands() { } CHECK(core.live().size() == 64); CHECK(core.history().size() == 64); - CHECK(as(core.history().front())->handle == first); - CHECK(as(core.history().front())->request.label == "m0"); - CHECK(core.live().back().request.label == "m63"); + CHECK(as(core.history().front())->handle() == first); + CHECK(as(core.history().front())->request().label == "m0"); + CHECK(core.live().back().request().label == "m63"); for (int i = 0; i < 32; ++i) { - const RequestHandle handle = core.live().front().handle; + const RequestHandle handle = core.live().front().handle(); const auto cancelled = core.cancel(handle, ord); CHECK(cancelled.status == CancelStatus::Cancelled); } CHECK(core.live().size() == 32); CHECK(core.history().size() == 96); - CHECK(core.live().front().request.label == "m32"); - CHECK(as(core.history().front())->request.label == "m0"); + CHECK(core.live().front().request().label == "m32"); + CHECK(as(core.history().front())->request().label == "m0"); - const auto replaced = core.replace(core.live().front().handle, tx(2.0, "rep"), 100, inc, ord); + const auto replaced = core.replace(core.live().front().handle(), tx(2.0, "rep"), 100, inc, ord); CHECK(replaced.status == ReplaceStatus::Replaced); CHECK(core.live().size() == 32); - CHECK(core.live().back().request.label == "rep"); - CHECK(core.live().back().predecessor->incarnation == 33); - CHECK(as(core.history().front())->request.label == "m0"); + CHECK(core.live().back().request().label == "rep"); + CHECK(core.live().back().predecessor()->incarnation == 33); + CHECK(as(core.history().front())->request().label == "m0"); CHECK(core.history().size() == 97); } } // namespace diff --git a/tests/test_native_order_resting_core.cpp b/tests/test_native_order_resting_core.cpp new file mode 100644 index 00000000..6335929f --- /dev/null +++ b/tests/test_native_order_resting_core.cpp @@ -0,0 +1,707 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using pineforge::execution::Flatten; +using pineforge::native_order::ActivateStop; +using pineforge::native_order::AllowanceUnits; +using pineforge::native_order::AppliedTerminalReason; +using pineforge::native_order::ArmedEvent; +using pineforge::native_order::ArmedTransaction; +using pineforge::native_order::BindOpening; +using pineforge::native_order::BookClose; +using pineforge::native_order::CancelReason; +using pineforge::native_order::CancelledEvent; +using pineforge::native_order::CloseBoundEvent; +using pineforge::native_order::CommandContext; +using pineforge::native_order::CommandEvent; +using pineforge::native_order::CommandInstalled; +using pineforge::native_order::CommandSurface; +using pineforge::native_order::CommittedExecutionFacts; +using pineforge::native_order::DeferredGroupAdjustmentEvent; +using pineforge::native_order::DriverEligibilityClass; +using pineforge::native_order::EvaluationContext; +using pineforge::native_order::EventId; +using pineforge::native_order::ExecutionAppliedEvent; +using pineforge::native_order::ExecutionProposal; +using pineforge::native_order::ExplicitUnits; +using pineforge::native_order::GroupEffect; +using pineforge::native_order::Independent; +using pineforge::native_order::Installed; +using pineforge::native_order::InstallError; +using pineforge::native_order::Limit; +using pineforge::native_order::Market; +using pineforge::native_order::Member; +using pineforge::native_order::NoChange; +using pineforge::native_order::NoChangeReason; +using pineforge::native_order::NoEffectEvent; +using pineforge::native_order::OpeningClose; +using pineforge::native_order::OpeningObservation; +using pineforge::native_order::OwnerOpenedUnits; +using pineforge::native_order::PointBudget; +using pineforge::native_order::Preparation; +using pineforge::native_order::PositionFlat; +using pineforge::native_order::PositionNonflat; +using pineforge::native_order::PreparedExecution; +using pineforge::native_order::PreparedMutation; +using pineforge::native_order::PreparedSubmit; +using pineforge::native_order::RejectedEvent; +using pineforge::native_order::ReplaceRejectedEvent; +using pineforge::native_order::ReplaceStatus; +using pineforge::native_order::PreparationError; +using pineforge::native_order::QuantityBoundEvent; +using pineforge::native_order::Reduce; +using pineforge::native_order::RemainingUnbound; +using pineforge::native_order::RemainingUnits; +using pineforge::native_order::Request; +using pineforge::native_order::RequestHandle; +using pineforge::native_order::RequestRejectReason; +using pineforge::native_order::ReservationReducedEvent; +using pineforge::native_order::RunIdentity; +using pineforge::native_order::Side; +using pineforge::native_order::Stop; +using pineforge::native_order::SubmitStatus; +using pineforge::native_order::TargetObservation; +using pineforge::native_order::Transact; +using pineforge::native_order::Wait; +using pineforge::native_order::WaitForApplied; +using pineforge::native_order::WorkingRequestCore; +using pineforge::native_order::has_market_defaults; +using pineforge::native_order::market_request; + +std::atomic g_allocs{0}; + +void* operator new(std::size_t n) { + g_allocs.fetch_add(1, std::memory_order_relaxed); + void* p = std::malloc(n ? n : 1); + if (!p) throw std::bad_alloc(); + return p; +} +void* operator new(std::size_t n, const std::nothrow_t&) noexcept { + g_allocs.fetch_add(1, std::memory_order_relaxed); + return std::malloc(n ? n : 1); +} +void* operator new(std::size_t n, std::align_val_t a) { + g_allocs.fetch_add(1, std::memory_order_relaxed); + void* p = nullptr; + if (posix_memalign(&p, static_cast(a), n ? n : 1) != 0) throw std::bad_alloc(); + return p; +} +void* operator new[](std::size_t n) { return ::operator new(n); } +void operator delete(void* p) noexcept { std::free(p); } +void operator delete(void* p, std::size_t) noexcept { std::free(p); } +void operator delete(void* p, std::align_val_t) noexcept { std::free(p); } +void operator delete(void* p, std::size_t, std::align_val_t) noexcept { std::free(p); } +void operator delete[](void* p) noexcept { ::operator delete(p); } +void operator delete[](void* p, std::size_t) noexcept { ::operator delete(p); } + +namespace { +int checks = 0; +int failures = 0; +#define CHECK(x) \ + do { \ + ++checks; \ + if (!(x)) { \ + ++failures; \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); \ + } \ + } while (0) + +const RunIdentity kRun{"resting", 1}; + +EvaluationContext print_ctx(uint64_t point, int64_t time_ms = 1000, bool matching = true) { + EvaluationContext context; + context.cursor.point.ordinal = point; + context.cursor.point.effective_time_ms = time_ms; + context.driver_class = DriverEligibilityClass::ObservedPrint; + context.existing_matching_bit = matching; + return context; +} + +TargetObservation long_book(int64_t cycle = 7) { + TargetObservation observation; + observation.current_position = PositionNonflat{cycle, Side::Long}; + return observation; +} + +TargetObservation flat_book() { + TargetObservation observation; + observation.current_position = PositionFlat{}; + return observation; +} + +Request market_tx(double q, std::string label = {}) { + return Request{Transact{q}, std::move(label), {}}; +} + +uint64_t commit_mutation(WorkingRequestCore& core, Preparation prep, + uint64_t& ordinal) { + CHECK(std::holds_alternative(prep)); + auto installed = core.install_mutation(std::get(std::move(prep))); + CHECK(std::holds_alternative(installed)); + const auto count = std::get(installed).events.count; + ordinal += count; + return count; +} + +ExecutionAppliedEvent fill_transact(WorkingRequestCore& core, const RequestHandle& handle, + double filled, uint64_t& ordinal, double remaining_after, + bool terminal, int64_t cycle_before, int64_t cycle_after, + double opened = 0.0) { + const auto* live = core.find_live(handle); + CHECK(live); + uint64_t point = 0; + if (const auto* units = std::get_if(&live->allowance)) { + point = units->point_ordinal; + } + ExecutionProposal proposal; + proposal.cursor.point.ordinal = point; + proposal.cursor.point.effective_time_ms = 1000; + proposal.raw_price = 100; + proposal.resolved_price = 100; + const auto* tx = std::get_if(&live->request().intent); + const double signed_fill = (tx && tx->signed_units < 0.0) ? -filled : filled; + proposal.physical_action = Transact{signed_fill}; + proposal.scope = pineforge::execution::Book{}; + if (cycle_before == 0) proposal.pre_fill = PositionFlat{}; + else proposal.pre_fill = PositionNonflat{cycle_before, Side::Long}; + proposal.inspected_closed_units = opened != 0.0 ? std::abs(filled - std::abs(opened)) : 0.0; + if (opened == 0.0 && cycle_after == 0 && cycle_before != 0) { + proposal.inspected_closed_units = filled; + } else if (opened != 0.0) { + proposal.inspected_closed_units = std::max(0.0, filled - std::abs(opened)); + proposal.inspected_opened_units = opened; + } else { + proposal.inspected_opened_units = filled; + proposal.inspected_closed_units = 0.0; + } + proposal.inspected_current_ticket = 1; + auto prep = core.prepare_execution(handle, proposal, ordinal); + CHECK(std::holds_alternative(prep)); + CommittedExecutionFacts facts; + facts.result.status = pineforge::execution::Status::Applied; + facts.result.closed_units = proposal.inspected_closed_units; + facts.result.opened_units = proposal.inspected_opened_units; + facts.result.current_ticket = 1; + facts.cycle_before = cycle_before; + facts.cycle_after = cycle_after; + facts.post_target = cycle_after == 0 ? flat_book() : long_book(cycle_after); + facts.committed_action = proposal.physical_action; + auto installed = core.install_execution(std::get(std::move(prep)), facts); + CHECK(std::holds_alternative(installed)); + ordinal += std::get(installed).events.count; + CHECK(std::holds_alternative(core.history().back())); + const auto& applied = std::get(core.history().back()); + CHECK(applied.terminal == terminal); + (void)remaining_after; + return applied; +} + +void validation_matrix_and_market_defaults() { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + CHECK(has_market_defaults(market_tx(1.0, "m"))); + CHECK(core.submit(market_request(pineforge::order_action::Transact{2.0}, "phys"), 1, inc, ord) + .status + == SubmitStatus::Accepted); + + Request bad_basis{Reduce{OwnerOpenedUnits{}}, "oo", "", Market{}, + pineforge::native_order::ImmediateRemaining{}, Independent{}}; + const auto rejected = core.submit(bad_basis, 1, inc, ord); + CHECK(rejected.status == SubmitStatus::Rejected); + CHECK(rejected.reason == RequestRejectReason::InvalidQuantityBasis); + + Request wait_tx{Transact{1.0}, "wtx", "", Market{}, pineforge::native_order::ImmediateRemaining{}, + WaitForApplied{core.live().front().handle()}}; + wait_tx.intent = Transact{1.0}; + wait_tx.owner = WaitForApplied{core.live().front().handle()}; + Request wait_oo = wait_tx; + wait_oo.intent = Reduce{OwnerOpenedUnits{}}; + wait_oo.owner = WaitForApplied{core.live().front().handle()}; + const auto child = core.submit(wait_oo, 1, inc, ord); + CHECK(child.status == SubmitStatus::Accepted); + CHECK(std::holds_alternative(core.find_live(*child.handle)->remaining)); + CHECK(std::holds_alternative(core.find_live(*child.handle)->authority)); + + Request wait_tx_oo{Transact{1.0}, "bad", ""}; + wait_tx_oo.owner = WaitForApplied{*child.handle}; + wait_tx_oo.intent = Transact{1.0}; + // OwnerOpened is only on Reduce; Transact cannot carry it. InvalidQuantityBasis via + // Independent+OwnerOpened already covered. Wait+Transact is accepted. + Request flatten_budget{Flatten{}, "fb", ""}; + flatten_budget.capacity = PointBudget{1.0}; + CHECK(core.submit(flatten_budget, 1, inc, ord).reason == RequestRejectReason::InvalidCapacity); + + Request bad_trig{Transact{1.0}, "bt", ""}; + bad_trig.trigger = Stop{0.0}; + CHECK(core.submit(bad_trig, 1, inc, ord).reason == RequestRejectReason::InvalidTrigger); + + Request bad_group{Transact{1.0}, "bg", ""}; + bad_group.group = Member{0, 1, GroupEffect::Cancel}; + CHECK(core.submit(bad_group, 1, inc, ord).reason == RequestRejectReason::InvalidGroup); + + Request bind_tx{Transact{1.0}, "bx", ""}; + bind_tx.owner = BindOpening{core.live().front().handle(), 7}; + CHECK(core.submit(bind_tx, 1, inc, ord).reason == RequestRejectReason::InvalidOwner); +} + +void bind_opening_and_replace_predecessor_wait() { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + const auto parent = core.submit(market_tx(1.0, "p"), 1, inc, ord); + OpeningObservation opening; + opening.queried_opening = *parent.handle; + opening.queried_cycle = 7; + opening.current_position = PositionNonflat{7, Side::Long}; + opening.has_live_matching_lot = true; + Request child{Reduce{ExplicitUnits{1.0}}, "c", ""}; + child.owner = BindOpening{*parent.handle, 7}; + auto prepared = core.prepare_submit(child, pineforge::native_order::CommandContext{10, std::nullopt, opening}, + inc, ord); + CHECK(prepared.predicted().status == SubmitStatus::Accepted); + auto installed = core.install_submit(std::move(prepared)); + CHECK(std::holds_alternative>(installed)); + ++ord; + ++inc; + CHECK(std::holds_alternative(core.live().back().authority)); + + Request wait_on_self{Transact{-1.0}, "ws", ""}; + wait_on_self.owner = WaitForApplied{*parent.handle}; + const auto replaced = core.replace(*parent.handle, wait_on_self, 20, inc, ord); + CHECK(replaced.status == pineforge::native_order::ReplaceStatus::ReplaceRejected); + CHECK(replaced.reason == RequestRejectReason::InvalidOwner); + CHECK(core.find_live(*parent.handle)); +} + +void c9_eligibility_does_not_mutate() { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + const auto tx = core.submit(market_tx(1.0, "t"), 1, inc, ord); + const auto rd = core.submit(Request{Reduce{ExplicitUnits{1.0}}, "r", ""}, 1, inc, ord); + const std::size_t live_n = core.live().size(); + const std::size_t hist_n = core.history().size(); + const auto ctx = print_ctx(3, 1, true); + CHECK(core.evaluation_eligible(*core.find_live(*tx.handle), ctx)); + CHECK(core.evaluation_eligible(*core.find_live(*rd.handle), ctx)); + CHECK(core.live().size() == live_n); + CHECK(core.history().size() == hist_n); + CHECK(std::holds_alternative( + core.find_live(*rd.handle)->authority)); +} + +void flat_close_noeffect_and_bind() { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + const auto close = core.submit(Request{Reduce{ExplicitUnits{1.0}}, "c", ""}, 1, inc, ord); + auto none = core.prepare_evaluation(*close.handle, print_ctx(2), flat_book(), ord); + commit_mutation(core, std::move(none), ord); + CHECK(core.live().empty()); + CHECK(std::holds_alternative(core.history().back())); + + const auto close2 = core.submit(Request{Reduce{ExplicitUnits{2.0}}, "c2", ""}, 1, inc, ord); + auto bound = core.prepare_evaluation(*close2.handle, print_ctx(ord + 1), long_book(), ord); + commit_mutation(core, std::move(bound), ord); + CHECK(std::holds_alternative(core.history().back())); + CHECK(std::holds_alternative(core.find_live(*close2.handle)->authority)); +} + +void partial_retain_and_target_exhaust() { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + Request req{Transact{5.0}, "p", ""}; + req.capacity = PointBudget{2.0}; + const auto accepted = core.submit(req, 1, inc, ord); + auto eval = core.prepare_evaluation(*accepted.handle, print_ctx(ord + 1), flat_book(), ord); + commit_mutation(core, std::move(eval), ord); + const auto first = fill_transact(core, *accepted.handle, 2.0, ord, 3.0, false, 0, 7, 2.0); + CHECK(!first.terminal); + CHECK(core.find_live(*accepted.handle)); + CHECK(std::holds_alternative(core.find_live(*accepted.handle)->remaining)); + CHECK(std::get(core.find_live(*accepted.handle)->remaining).q == 3.0); + + Request reduce{Reduce{ExplicitUnits{5.0}}, "red", ""}; + const auto closer = core.submit(reduce, 1, inc, ord); + auto bind = core.prepare_evaluation(*closer.handle, print_ctx(ord + 1), long_book(7), ord); + commit_mutation(core, std::move(bind), ord); + ExecutionProposal proposal; + const auto* live = core.find_live(*closer.handle); + proposal.cursor.point.ordinal = std::get(live->allowance).point_ordinal; + proposal.cursor.point.effective_time_ms = 1000; + proposal.raw_price = 100; + proposal.resolved_price = 100; + proposal.physical_action = pineforge::order_action::Reduce{5.0}; + proposal.scope = pineforge::execution::Book{}; + proposal.pre_fill = PositionNonflat{7, Side::Long}; + proposal.inspected_closed_units = 1.0; + proposal.inspected_opened_units = 0.0; + proposal.inspected_current_ticket = 2; + auto prep = core.prepare_execution(*closer.handle, proposal, ord); + CHECK(std::holds_alternative(prep)); + CommittedExecutionFacts facts; + facts.result.status = pineforge::execution::Status::Applied; + facts.result.closed_units = 1.0; + facts.result.current_ticket = 2; + facts.cycle_before = 7; + facts.cycle_after = 0; + facts.post_target = flat_book(); + facts.committed_action = proposal.physical_action; + auto installed = core.install_execution(std::get(std::move(prep)), facts); + CHECK(std::holds_alternative(installed)); + CHECK(!core.find_live(*closer.handle)); + const auto& applied = std::get(core.history().back()); + CHECK(applied.terminal); + CHECK(applied.terminal_reason == AppliedTerminalReason::TargetExhausted); +} + +void group_reduce_defer_and_quantity_bound() { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + Request filler{Transact{2.0}, "f", ""}; + filler.group = Member{7, 1, GroupEffect::Reduce}; + const auto parent = core.submit(filler, 1, inc, ord); + Request child{Reduce{OwnerOpenedUnits{}}, "ch", ""}; + child.owner = WaitForApplied{*parent.handle}; + child.group = Member{7, 2, GroupEffect::Cancel}; + const auto waiting = core.submit(child, 1, inc, ord); + CHECK(std::holds_alternative(core.find_live(*waiting.handle)->remaining)); + + auto eval = core.prepare_evaluation(*parent.handle, print_ctx(ord + 1), flat_book(), ord); + commit_mutation(core, std::move(eval), ord); + const auto applied = fill_transact(core, *parent.handle, 2.0, ord, 0.0, true, 0, 8, 2.0); + CHECK(applied.terminal); + const EventId cause{kRun, applied.ordinal}; + auto recipients = core.group_recipients(cause); + CHECK(recipients.size() == 1); + CHECK(recipients[0] == *waiting.handle); + auto deferred = core.prepare_group_effect(cause, *waiting.handle, ord); + commit_mutation(core, std::move(deferred), ord); + CHECK(std::holds_alternative(core.history().back())); + CHECK(core.find_live(*waiting.handle)); + CHECK(std::holds_alternative(core.find_live(*waiting.handle)->remaining)); + auto replay = core.prepare_group_effect(cause, *waiting.handle, ord); + CHECK(std::holds_alternative(replay)); + CHECK(std::get(replay).reason == NoChangeReason::AlreadyApplied); + + OpeningObservation opening; + opening.queried_opening = *parent.handle; + opening.queried_cycle = 8; + opening.current_position = PositionNonflat{8, Side::Long}; + opening.has_live_matching_lot = true; + auto armed = core.prepare_owner_applied(cause, *waiting.handle, opening, ord); + CHECK(std::holds_alternative(armed)); + commit_mutation(core, std::move(armed), ord); + CHECK(std::holds_alternative(core.history()[core.history().size() - 2])); + const auto& bound = std::get(core.history()[core.history().size() - 2]); + CHECK(bound.source_units == 2.0); + CHECK(bound.effective_deduction == 2.0); + CHECK(std::holds_alternative(core.history().back())); + CHECK(!core.find_live(*waiting.handle)); +} + +void owner_applied_wait_through_close_only() { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + Request parent_req{Transact{3.0}, "p", ""}; + parent_req.capacity = PointBudget{1.0}; + const auto parent = core.submit(parent_req, 1, inc, ord); + Request child{Reduce{OwnerOpenedUnits{}}, "c", ""}; + child.owner = WaitForApplied{*parent.handle}; + child.trigger = Limit{100.0}; + const auto waiting = core.submit(child, 1, inc, ord); + auto eval = core.prepare_evaluation(*parent.handle, print_ctx(ord + 1), + TargetObservation{PositionNonflat{7, Side::Short}, std::nullopt}, + ord); + commit_mutation(core, std::move(eval), ord); + + ExecutionProposal proposal; + proposal.cursor.point.ordinal = + std::get(core.find_live(*parent.handle)->allowance).point_ordinal; + proposal.cursor.point.effective_time_ms = 1000; + proposal.raw_price = 100; + proposal.resolved_price = 100; + proposal.physical_action = Transact{1.0}; + proposal.scope = pineforge::execution::Book{}; + proposal.pre_fill = PositionNonflat{7, Side::Short}; + proposal.inspected_closed_units = 1.0; + proposal.inspected_opened_units = 0.0; + proposal.inspected_current_ticket = 1; + auto prep = core.prepare_execution(*parent.handle, proposal, ord); + CHECK(std::holds_alternative(prep)); + CommittedExecutionFacts facts; + facts.result.status = pineforge::execution::Status::Applied; + facts.result.closed_units = 1.0; + facts.result.current_ticket = 1; + facts.cycle_before = 7; + facts.cycle_after = 7; + facts.post_target.current_position = PositionNonflat{7, Side::Short}; + facts.committed_action = proposal.physical_action; + auto installed = core.install_execution(std::get(std::move(prep)), facts); + CHECK(std::holds_alternative(installed)); + ord += std::get(installed).events.count; + const EventId f1{kRun, std::get(core.history().back()).ordinal}; + auto still = core.prepare_owner_applied(f1, *waiting.handle, std::nullopt, ord); + CHECK(std::holds_alternative(still)); + CHECK(std::get(still).reason == NoChangeReason::StillWaiting); + CHECK(std::holds_alternative(core.find_live(*waiting.handle)->authority)); + CHECK(std::holds_alternative(core.find_live(*waiting.handle)->remaining)); +} + +void checked_reservation_failure() { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + Request filler{Transact{std::numeric_limits::max()}, "f", ""}; + filler.group = Member{3, 1, GroupEffect::Reduce}; + const auto parent = core.submit(filler, 1, inc, ord); + Request child{Reduce{OwnerOpenedUnits{}}, "c", ""}; + child.owner = WaitForApplied{*parent.handle}; + child.group = Member{3, 2, GroupEffect::Reduce}; + const auto waiting = core.submit(child, 1, inc, ord); + auto eval = core.prepare_evaluation(*parent.handle, print_ctx(ord + 1), flat_book(), ord); + commit_mutation(core, std::move(eval), ord); + ExecutionProposal proposal; + proposal.cursor.point.ordinal = + std::get(core.find_live(*parent.handle)->allowance).point_ordinal; + proposal.cursor.point.effective_time_ms = 1000; + proposal.raw_price = 100; + proposal.resolved_price = 100; + proposal.physical_action = Transact{std::numeric_limits::max()}; + proposal.scope = pineforge::execution::Book{}; + proposal.inspected_closed_units = 0.0; + proposal.inspected_opened_units = std::numeric_limits::max(); + proposal.inspected_current_ticket = 1; + auto prep = core.prepare_execution(*parent.handle, proposal, ord); + if (std::holds_alternative(prep)) { + CHECK(std::get(prep).code + == pineforge::native_order::CoreFailure::NonrepresentableQuantity); + return; + } + CHECK(std::holds_alternative(prep)); + CommittedExecutionFacts facts; + facts.result.status = pineforge::execution::Status::Applied; + facts.result.opened_units = std::numeric_limits::max(); + facts.result.current_ticket = 1; + facts.cycle_after = 1; + facts.post_target = long_book(1); + facts.committed_action = proposal.physical_action; + auto installed = core.install_execution(std::get(std::move(prep)), facts); + CHECK(std::holds_alternative(installed)); + ord += std::get(installed).events.count; + const EventId cause{kRun, std::get(core.history().back()).ordinal}; + auto first = core.prepare_group_effect(cause, *waiting.handle, ord); + commit_mutation(core, std::move(first), ord); + Request filler2{Transact{std::numeric_limits::max()}, "f2", ""}; + filler2.group = Member{3, 1, GroupEffect::Reduce}; + const auto parent2 = core.submit(filler2, 1, inc, ord); + (void)parent2; +} + +void stale_prepare_and_parent_terminal() { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + const auto parent = core.submit(market_tx(1.0, "p"), 1, inc, ord); + Request child{Reduce{ExplicitUnits{1.0}}, "c", ""}; + child.owner = WaitForApplied{*parent.handle}; + const auto waiting = core.submit(child, 1, inc, ord); + auto first = core.prepare_cancel(*parent.handle, ord); + auto second = core.prepare_cancel(*parent.handle, ord); + auto stale = core.install_cancel(std::move(first)); + CHECK(std::holds_alternative(stale)); + auto ok = core.install_cancel(std::move(second)); + CHECK(std::holds_alternative>(ok)); + ++ord; + CHECK(!core.find_live(*parent.handle)); + const EventId cause{kRun, std::get(core.history().back()).ordinal}; + auto kids = core.waiting_children(*parent.handle); + CHECK(kids.size() == 1); + auto term = core.prepare_parent_terminal(cause, kids.front(), ord); + commit_mutation(core, std::move(term), ord); + CHECK(!core.find_live(*waiting.handle)); + CHECK(std::get(core.history().back()).reason == CancelReason::OwnerGone); +} + +void stop_activation_and_receipt_dedup() { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + Request req{Transact{1.0}, "s", ""}; + req.trigger = Stop{101.0}; + const auto accepted = core.submit(req, 1, inc, ord); + auto eval = core.prepare_evaluation(*accepted.handle, print_ctx(ord + 1), flat_book(), ord); + commit_mutation(core, std::move(eval), ord); + ActivateStop hit; + hit.cursor = print_ctx(ord).cursor; + hit.reached_price = 101; + auto trig = core.prepare_trigger(*accepted.handle, hit, ord); + commit_mutation(core, std::move(trig), ord); + CHECK(std::holds_alternative( + core.find_live(*accepted.handle)->trigger_state)); + auto again = core.prepare_trigger(*accepted.handle, hit, ord); + CHECK(std::holds_alternative(again)); +} + +void wrong_core_token_and_move_invalidation() { + const RunIdentity same{"same-logical-run-with-a-long-key", 1}; + WorkingRequestCore a(same), b(same); + uint64_t ai = 1, ao = 1, bi = 1, bo = 1; + auto left = a.prepare_submit(Request{Transact{1}, "A", ""}, CommandContext{}, ai, ao); + auto right = b.prepare_submit(Request{Transact{9}, "B", ""}, CommandContext{}, bi, bo); + auto cross = b.install_submit(std::move(left)); + CHECK(std::holds_alternative(cross)); + CHECK(std::get(cross) == InstallError::WrongCoreOrRun); + CHECK(b.live().empty()); + CHECK(b.history().empty()); + auto ok = b.install_submit(std::move(right)); + CHECK(std::holds_alternative>(ok)); + ++bo; + ++bi; + + WorkingRequestCore src(same); + uint64_t si = 1, so = 1; + auto token = src.prepare_submit(Request{Transact{1}, "m", ""}, CommandContext{}, si, so); + WorkingRequestCore dst = std::move(src); + auto moved = dst.install_submit(std::move(token)); + CHECK(std::holds_alternative(moved)); + CHECK(std::get(moved) == InstallError::WrongCoreOrRun); + CHECK(dst.live().empty()); +} + +void market_only_committed_rejection() { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + const auto live = core.submit(market_tx(1.0, "keep"), 1, inc, ord); + Request extra{Transact{1.0}, "x", ""}; + extra.trigger = Limit{100.0}; + extra.owner = WaitForApplied{*live.handle}; + CommandContext market; + market.surface = CommandSurface::MarketOnly; + auto prepared = core.prepare_submit(extra, market, inc, ord); + CHECK(prepared.predicted().status == SubmitStatus::Rejected); + CHECK(prepared.predicted().reason == RequestRejectReason::InvalidTrigger); + CHECK(prepared.predicted().event_ordinal != 0); + auto installed = core.install_submit(std::move(prepared)); + CHECK(std::holds_alternative>(installed)); + ++ord; + const auto* rejected = std::get_if(&core.history().back()); + CHECK(rejected); + CHECK(rejected->reason == RequestRejectReason::InvalidTrigger); + CHECK(rejected->surface == CommandSurface::MarketOnly); + CHECK(rejected->ordinal != 0); + CHECK(std::holds_alternative(rejected->request.trigger)); + CHECK(std::holds_alternative(rejected->request.owner)); + + Request group_extra{Transact{1.0}, "g", ""}; + group_extra.group = Member{3, 1, GroupEffect::Cancel}; + auto gprep = core.prepare_submit(group_extra, market, inc, ord); + CHECK(gprep.predicted().reason == RequestRejectReason::InvalidGroup); + core.install_submit(std::move(gprep)); + ++ord; + + Request cap_extra{Transact{1.0}, "c", ""}; + cap_extra.capacity = PointBudget{1.0}; + auto cprep = core.prepare_submit(cap_extra, market, inc, ord); + CHECK(cprep.predicted().reason == RequestRejectReason::InvalidCapacity); + core.install_submit(std::move(cprep)); + ++ord; + + Request own_extra{Transact{1.0}, "o", ""}; + own_extra.owner = WaitForApplied{*live.handle}; + auto oprep = core.prepare_submit(own_extra, market, inc, ord); + CHECK(oprep.predicted().reason == RequestRejectReason::InvalidOwner); + core.install_submit(std::move(oprep)); + ++ord; + + Request ghost{Transact{1.0}, "ghost", ""}; + ghost.trigger = Stop{50.0}; + RequestHandle absent{kRun, 99}; + auto rprep = core.prepare_replace(absent, ghost, market, inc, ord); + CHECK(rprep.predicted().status == ReplaceStatus::NotWorking); + core.install_replace(std::move(rprep)); + ++ord; + CHECK(std::holds_alternative(core.history().back())); + + auto live_replace = core.prepare_replace(*live.handle, ghost, market, inc, ord); + CHECK(live_replace.predicted().status == ReplaceStatus::ReplaceRejected); + CHECK(live_replace.predicted().reason == RequestRejectReason::InvalidTrigger); + core.install_replace(std::move(live_replace)); + ++ord; + const auto* rr = std::get_if(&core.history().back()); + CHECK(rr); + CHECK(rr->surface == CommandSurface::MarketOnly); + CHECK(rr->reason == RequestRejectReason::InvalidTrigger); + CHECK(core.find_live(*live.handle)); +} + +void long_key_install_and_eligibility_no_alloc() { + const RunIdentity long_run{std::string(80, 'K'), 1}; + WorkingRequestCore core(long_run); + uint64_t inc = 1; + uint64_t ord = 1; + auto prepared = core.prepare_submit(Request{Transact{1.0}, "long", ""}, CommandContext{}, inc, ord); + g_allocs.store(0); + auto installed = core.install_submit(std::move(prepared)); + CHECK(g_allocs.load() == 0); + CHECK(std::holds_alternative>(installed)); + auto result = std::move(std::get>(installed).result); + CHECK(result.handle->run.session_key.size() == 80); + ++inc; + ++ord; + + Request child{Reduce{pineforge::native_order::ExplicitUnits{1.0}}, "w", ""}; + child.owner = WaitForApplied{*result.handle}; + auto child_prep = core.prepare_submit(child, CommandContext{}, inc, ord); + g_allocs.store(0); + auto child_inst = core.install_submit(std::move(child_prep)); + CHECK(g_allocs.load() == 0); + ++inc; + ++ord; + const auto& waiting = core.live().back(); + g_allocs.store(0); + auto facts = core.eligibility_facts(waiting, print_ctx(3)); + CHECK(g_allocs.load() == 0); + CHECK(facts.waiting); + CHECK(facts.authority != nullptr); + + g_allocs.store(0); + const auto conv = core.submit(Request{Transact{2.0}, "conv", ""}, 1, inc, ord); + CHECK(conv.status == SubmitStatus::Accepted); + CHECK(conv.handle->run.session_key.size() == 80); +} +} // namespace + +int main() { + validation_matrix_and_market_defaults(); + bind_opening_and_replace_predecessor_wait(); + c9_eligibility_does_not_mutate(); + flat_close_noeffect_and_bind(); + partial_retain_and_target_exhaust(); + group_reduce_defer_and_quantity_bound(); + owner_applied_wait_through_close_only(); + checked_reservation_failure(); + stale_prepare_and_parent_terminal(); + stop_activation_and_receipt_dedup(); + wrong_core_token_and_move_invalidation(); + market_only_committed_rejection(); + long_key_install_and_eligibility_no_alloc(); + std::printf("native order resting core: %d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} diff --git a/tests/test_native_order_terminal.cpp b/tests/test_native_order_terminal.cpp index 6fa45230..5e90e149 100644 --- a/tests/test_native_order_terminal.cpp +++ b/tests/test_native_order_terminal.cpp @@ -2,15 +2,28 @@ #include #include +#include using pineforge::execution::Flatten; using pineforge::native_order::CommandEvent; +using pineforge::native_order::CommittedExecutionFacts; +using pineforge::native_order::DriverEligibilityClass; +using pineforge::native_order::EvaluationContext; using pineforge::native_order::ExecutionAppliedEvent; +using pineforge::native_order::ExecutionProposal; +using pineforge::native_order::Installed; using pineforge::native_order::MatchRejectedEvent; +using pineforge::native_order::MatchRejectReason; +using pineforge::native_order::NoChange; using pineforge::native_order::NoEffectEvent; +using pineforge::native_order::PositionFlat; +using pineforge::native_order::PositionNonflat; +using pineforge::native_order::PreparedExecution; +using pineforge::native_order::PreparedMutation; using pineforge::native_order::Request; using pineforge::native_order::RunIdentity; -using pineforge::native_order::TerminalCommit; +using pineforge::native_order::Side; +using pineforge::native_order::TargetObservation; using pineforge::native_order::WorkingRequestCore; using pineforge::order_action::Transact; @@ -25,6 +38,20 @@ int failures = 0; std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); \ } \ } while (0) + +EvaluationContext ctx_at(uint64_t ordinal, int64_t time_ms, bool matching = true) { + EvaluationContext context; + context.cursor.point.ordinal = ordinal; + context.cursor.point.effective_time_ms = time_ms; + context.driver_class = DriverEligibilityClass::ObservedPrint; + context.existing_matching_bit = matching; + return context; +} + +void advance(uint64_t& ordinal, const Installed& installed) { + ordinal += installed.events.count == 0 ? 0 : installed.events.count; + if (installed.events.count == 0) ++ordinal; // unused; callers pass next +} } // namespace int main() { @@ -37,52 +64,81 @@ int main() { CHECK(core.live().size() == 1); CHECK(core.history().size() == 1); - const uint64_t next = TerminalCommit::usable_ordinal(core, ordinal); - CHECK(next == ordinal); - TerminalCommit::reserve_history(core); - NoEffectEvent none; - none.ordinal = next; - none.handle = *accepted.handle; - none.request = Request{Transact{1.0}, "buy", ""}; - none.birth = core.live()[0].birth; - TerminalCommit::install(core, 0, CommandEvent{std::move(none)}); - ++ordinal; + auto none_prep = core.prepare_no_effect(*accepted.handle, ctx_at(2, 60000), ordinal); + CHECK(std::holds_alternative(none_prep)); + auto none_installed = core.install_mutation( + std::get(std::move(none_prep))); + CHECK(std::holds_alternative(none_installed)); + ordinal += std::get(none_installed).events.count; CHECK(core.live().empty()); CHECK(core.history().size() == 2); CHECK(std::holds_alternative(core.history().back())); auto second = core.submit(Request{Transact{-1.0}, "sell", ""}, 120000, incarnation, ordinal); CHECK(second.status == pineforge::native_order::SubmitStatus::Accepted); - TerminalCommit::usable_ordinal(core, ordinal); - TerminalCommit::reserve_history(core); - MatchRejectedEvent rejected; - rejected.ordinal = ordinal; - rejected.handle = *second.handle; - rejected.request = Request{Transact{-1.0}, "sell", ""}; - rejected.birth = core.live()[0].birth; - rejected.reason = pineforge::native_order::MatchRejectReason::MaxAbsUnits; - TerminalCommit::install(core, 0, CommandEvent{std::move(rejected)}); - ++ordinal; + auto reject_prep = core.prepare_match_rejected( + *second.handle, ctx_at(ordinal + 1, 120000), MatchRejectReason::MaxAbsUnits, ordinal); + CHECK(std::holds_alternative(reject_prep)); + auto reject_installed = core.install_mutation( + std::get(std::move(reject_prep))); + CHECK(std::holds_alternative(reject_installed)); + ordinal += std::get(reject_installed).events.count; CHECK(core.live().empty()); CHECK(std::holds_alternative(core.history().back())); + CHECK(std::get(core.history().back()).reason + == MatchRejectReason::MaxAbsUnits); auto third = core.submit(Request{Flatten{}, "flat", ""}, 180000, incarnation, ordinal); CHECK(third.status == pineforge::native_order::SubmitStatus::Accepted); - TerminalCommit::usable_ordinal(core, ordinal); - TerminalCommit::reserve_history(core); - ExecutionAppliedEvent applied; - applied.ordinal = ordinal; - applied.handle = *third.handle; - applied.request = Request{Flatten{}, "flat", ""}; - applied.birth = core.live()[0].birth; - applied.resolved_price = 104; - applied.current_ticket = 6; - applied.first_trade_index = 0; - applied.closed_trade_count = 1; - TerminalCommit::install(core, 0, CommandEvent{std::move(applied)}); + TargetObservation long_book; + long_book.current_position = PositionNonflat{1, Side::Long}; + auto eval = core.prepare_evaluation(*third.handle, ctx_at(ordinal + 1, 180000), long_book, + ordinal); + CHECK(std::holds_alternative(eval)); + auto eval_installed = core.install_mutation(std::get(std::move(eval))); + CHECK(std::holds_alternative(eval_installed)); + ordinal += std::get(eval_installed).events.count; + + ExecutionProposal proposal; + proposal.cursor.point.ordinal = ordinal; // overwritten below from live allowance point + proposal.cursor = ctx_at(core.live()[0].allowance.index() == 0 ? 1 : 1, 180000).cursor; + // Use the same point as evaluation initialized. + proposal.cursor.point.ordinal = 5; // evaluation used usable ordinal then next point + proposal.cursor.point.effective_time_ms = 180000; + // Re-read live allowance point. + if (const auto* units = std::get_if( + &core.live()[0].allowance)) { + proposal.cursor.point.ordinal = units->point_ordinal; + } + proposal.raw_price = 104; + proposal.resolved_price = 104; + proposal.physical_action = Flatten{}; + proposal.scope = pineforge::execution::Book{}; + proposal.pre_fill = PositionNonflat{1, Side::Long}; + proposal.inspected_closed_units = 1.0; + proposal.inspected_opened_units = 0.0; + proposal.inspected_current_ticket = 6; + auto exec_prep = core.prepare_execution(*third.handle, proposal, ordinal); + CHECK(std::holds_alternative(exec_prep)); + CommittedExecutionFacts facts; + facts.result.status = pineforge::execution::Status::Applied; + facts.result.closed_units = 1.0; + facts.result.opened_units = 0.0; + facts.result.current_ticket = 6; + facts.result.first_trade_index = 0; + facts.result.closed_trade_count = 1; + facts.cycle_before = 1; + facts.cycle_after = 0; + facts.post_target.current_position = PositionFlat{}; + facts.committed_action = Flatten{}; + auto exec_installed = core.install_execution( + std::get(std::move(exec_prep)), facts); + CHECK(std::holds_alternative(exec_installed)); CHECK(core.live().empty()); CHECK(std::holds_alternative(core.history().back())); CHECK(std::get(core.history().back()).current_ticket == 6); + CHECK(std::get(core.history().back()).terminal); + (void)advance; std::printf("%s %d checks %d failures\n", failures ? "FAIL" : "PASS", checks, failures); return failures ? 1 : 0; diff --git a/tests/test_native_resting_contract.cpp b/tests/test_native_resting_contract.cpp new file mode 100644 index 00000000..54251569 --- /dev/null +++ b/tests/test_native_resting_contract.cpp @@ -0,0 +1,281 @@ +// Native R2 integration witnesses: real host, driver, working core and physical book. +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; +namespace { +int checks=0, failures=0; +const char* scenario="setup"; +#define CHECK(x) do { ++checks; if(!(x)) { ++failures; std::printf("FAIL %s:%d %s\n",scenario,__LINE__,#x); } } while(0) +void near(double actual,double expected) { + CHECK(std::isfinite(actual) && std::abs(actual-expected)<=1e-11*std::max(1.0,std::abs(expected))); +} +constexpr int64_t T=1736121600000LL; +struct Host final : NativeStrategyHost { + std::function begin; + std::function calculation; + uint64_t sequence=0; + void on_native_run_begin() override { if(begin) begin(*this); } + void on_native_bar(const Bar&,const NativeDecisionContext&) override { if(calculation) calculation(*this); } + void tick(int64_t offset,double price,double volume=1) { + CHECK(stream_push_tick(TradeTick{T+offset+1,++sequence,price,volume})); + CHECK(native_state().kind==NativeLifecycleKind::Running); + } +}; +NativeRunSpec spec(const char* key,double fee=0) { + NativeRunSpec s; + s.identity={key,1};s.input_tf="1";s.script_tf="1";s.ticker="N";s.tickerid="TEST:N"; + s.type="crypto";s.currency="USD";s.basecurrency="USD";s.description="native R2"; + s.volumetype="base";s.timezone="UTC";s.session="24x7";s.initial_capital=10000; + s.point_value=1;s.account_fx=1;s.price_tick=.01;s.fee_kind=NativeFeeKind::CashPerExecution; + s.fee_value=fee;return s; +} +void start_stream(Host& h) { + // Preserve the existing nonempty warmup contract. Seed the realtime + // interval before submitting so these print cases do not also consume a + // CarriedOpen (whose separate limit-only behavior has its own witness). + auto begin=std::exchange(h.begin,{}); + const Bar warmup{100,100,100,100,1,T-60000}; + CHECK(h.stream_begin(&warmup,1,"1","1")); + CHECK(h.stream_push_tick(TradeTick{T,++h.sequence,100,0})); + h.begin=std::move(begin); + if(h.begin)h.begin(h); +} +void start(Host& h,const char* key,double fee=0) { + CHECK(h.configure_native(spec(key,fee)).status==NativeSetupStatus::Applied); + start_stream(h); +} +no::Request tx(double q,const char* label="") { return no::Request{no::Transact{q},label,""}; } +no::Request rd(double q,const char* label="") { return no::Request{no::Reduce{no::ExplicitUnits{q}},label,""}; } +no::Request event_rd(const no::RequestHandle& parent) { + no::Request r{no::Reduce{no::OwnerOpenedUnits{}},"child",""}; + r.owner=no::WaitForApplied{parent};return r; +} +no::RequestHandle put(Host& h,const no::Request& r) { + auto result=h.submit(r);CHECK(result.status==no::SubmitStatus::Accepted);CHECK(result.handle.has_value()); + return result.handle.value_or(no::RequestHandle{}); +} +template std::vector events(const Host& h) { + std::vector out; + for(const auto& e:h.native_events(0)) if(e.command) + if(const auto* value=std::get_if(&*e.command)) out.push_back(*value); + return out; +} +std::vector fills(const Host& h,const no::RequestHandle& target) { + std::vector out; + for(const auto& e:events(h)) { + CHECK(bool(e.definition)); + if(e.definition && e.definition->handle==target) out.push_back(e); + } + return out; +} +std::vector cancellations(const Host& h,const no::RequestHandle& target) { + std::vector out; + for(const auto& e:events(h)) { + CHECK(bool(e.definition)); + if(e.definition && e.definition->handle==target) out.push_back(e); + } + return out; +} +void finish(Host& h) { CHECK(h.stream_end(false));CHECK(h.native_state().kind==NativeLifecycleKind::Completed); } + +void capacity_per_print() { + scenario="C1 point capacity"; Host h;no::RequestHandle p; + h.begin=[&](Host& x){auto r=tx(5);r.trigger=no::Limit{100};r.capacity=no::PointBudget{2};p=put(x,r);}; + start(h,"R2-C1");h.tick(0,100,999);CHECK(fills(h,p).size()==1);near(h.physical_position().signed_units,2); + h.tick(1,100,0);CHECK(fills(h,p).size()==2);near(h.physical_position().signed_units,4); + h.tick(2,100,0.001);auto f=fills(h,p);CHECK(f.size()==3); + if(f.size()==3) { + CHECK(f[0].opened_units==2 && f[1].opened_units==2 && f[2].opened_units==1); + CHECK(!f[0].terminal && !f[1].terminal && f[2].terminal); + CHECK(f[0].cursor.point.ordinal!=f[1].cursor.point.ordinal); + CHECK(f[1].cursor.point.ordinal!=f[2].cursor.point.ordinal); + } + near(h.physical_position().signed_units,5);CHECK(h.physical_position().lot_count==3); + CHECK(h.cancel(p).status==no::CancelStatus::NotWorking);finish(h); +} +void persistent_stop_and_limit() { + scenario="T5 active stop remainder";Host h;no::RequestHandle p; + h.begin=[&](Host& x){auto r=tx(2);r.trigger=no::Stop{100};r.capacity=no::PointBudget{1};p=put(x,r);}; + start(h,"R2-T5");h.tick(0,99);CHECK(fills(h,p).empty());h.tick(1,101);CHECK(fills(h,p).size()==1); + h.tick(2,99);auto f=fills(h,p);CHECK(f.size()==2); + if(f.size()==2){CHECK(f[0].raw_price==101 && f[1].raw_price==99);CHECK(f[1].terminal);} + near(h.physical_position().average_price,100);finish(h); + scenario="T7 limit remains conditional";Host l;no::RequestHandle q; + l.begin=[&](Host& x){auto r=tx(2);r.trigger=no::Limit{100};r.capacity=no::PointBudget{1};q=put(x,r);}; + start(l,"R2-T7");l.tick(0,100);l.tick(1,101);CHECK(fills(l,q).size()==1);l.tick(2,100); + CHECK(fills(l,q).size()==2);finish(l); +} +void stop_limit_models() { + scenario="T3 continuous stop limit";Host h;no::RequestHandle p; + h.begin=[&](Host& x){auto r=tx(1);r.trigger=no::StopLimit{101,102};p=put(x,r);}; + auto s=spec("R2-T3");s.slippage_ticks=200;CHECK(h.configure_native(s).status==NativeSetupStatus::Applied); + const Bar b{100,103,99,102,1,T};h.run(&b,1);CHECK(h.native_state().kind==NativeLifecycleKind::Completed); + auto f=fills(h,p);CHECK(f.size()==1);if(f.size()==1){CHECK(f[0].raw_price==101);CHECK(f[0].resolved_price==102);} + scenario="T8 discrete stop limit gap";Host d;no::RequestHandle q; + d.begin=[&](Host& x){auto r=tx(1);r.trigger=no::StopLimit{101,102};q=put(x,r);}; + auto ds=spec("R2-T8");ds.slippage_ticks=200;CHECK(d.configure_native(ds).status==NativeSetupStatus::Applied); + start_stream(d);d.tick(0,100);d.tick(1,103);CHECK(fills(d,q).empty()); + d.tick(2,100);auto df=fills(d,q);CHECK(df.size()==1);if(df.size()==1){CHECK(df[0].raw_price==100);CHECK(df[0].resolved_price==102);} + finish(d); +} +void partial_reversal_child(double sign,bool derived) { + scenario=derived?"O10/O12 event-sized reversal child":"O10 explicit reversal child"; + Host h;no::RequestHandle seed; + h.begin=[&](Host& x){seed=put(x,tx(-sign*2,"seed"));}; + start(h,sign>0?(derived?"R2-O12-L":"R2-O10-L"):(derived?"R2-O12-S":"R2-O10-S"),2); + h.tick(0,100);auto old=fills(h,seed);CHECK(old.size()==1); + auto pr=tx(sign*3,"parent");pr.capacity=no::PointBudget{1};auto parent=put(h,pr); + auto cr=derived?event_rd(parent):rd(1,"child");cr.owner=no::WaitForApplied{parent};cr.trigger=no::Limit{100};auto child=put(h,cr); + h.tick(1,100);CHECK(fills(h,parent).size()==1);CHECK(fills(h,child).empty());CHECK(cancellations(h,child).empty()); + h.tick(2,100);CHECK(fills(h,parent).size()==2);CHECK(fills(h,child).empty());CHECK(cancellations(h,child).empty()); + h.tick(3,100);auto pf=fills(h,parent),cf=fills(h,child);CHECK(pf.size()==3);CHECK(cf.size()==1); + if(pf.size()==3 && cf.size()==1 && old.size()==1) { + CHECK(pf[0].closed_units==1 && pf[0].opened_units==0); + CHECK(pf[1].closed_units==1 && pf[1].opened_units==0); + CHECK(pf[2].closed_units==0 && pf[2].opened_units==sign); + CHECK(cf[0].closed_units==1 && cf[0].opened_units==0 && cf[0].terminal); + CHECK(cf[0].ordinal>pf[2].ordinal && cf[0].cursor.point.ordinal==pf[2].cursor.point.ordinal); + CHECK(pf[2].cycle_after!=old[0].cycle_after); + const auto* scope=std::get_if(&cf[0].scope); + CHECK(scope && scope->incarnation==parent.incarnation && scope->cycle==pf[2].cycle_after); + for(const auto& x:pf) CHECK(x.current_ticket==2); + CHECK(cf[0].current_ticket==2); + if(derived) { + auto binds=events(h);CHECK(binds.size()==1); + if(binds.size()==1){CHECK(binds[0].source.ordinal==pf[2].ordinal);CHECK(binds[0].source_units==1);} + } + } + CHECK(cancellations(h,child).empty());near(h.physical_position().signed_units,0);near(h.native_marked_equity(100),9990); + finish(h); +} +void no_opening_parent(double sign) { + scenario="O11 no opening";Host h;h.begin=[&](Host& x){put(x,tx(-sign*2));};start(h,sign>0?"R2-O11-L":"R2-O11-S");h.tick(0,100); + auto p=tx(sign*2);p.capacity=no::PointBudget{1};auto parent=put(h,p);auto c=event_rd(parent);auto child=put(h,c); + h.tick(1,100);CHECK(cancellations(h,child).empty());h.tick(2,100); + CHECK(fills(h,child).empty());auto ce=cancellations(h,child);CHECK(ce.size()==1); + if(ce.size()==1) CHECK(ce[0].reason==no::CancelReason::UnsupportedRelation); + put(h,tx(sign));h.tick(3,100);near(h.physical_position().signed_units,sign);CHECK(fills(h,child).empty());finish(h); +} +void close_lifetime() { + scenario="C2 clipped default close";Host h;h.begin=[](Host& x){put(x,tx(1));};start(h,"R2-C2");h.tick(0,100); + auto close=put(h,rd(5));h.tick(1,100);auto f=fills(h,close);CHECK(f.size()==1); + if(f.size()==1){CHECK(f[0].closed_units==1 && f[0].terminal);CHECK(f[0].terminal_reason==no::AppliedTerminalReason::TargetExhausted);} + put(h,tx(1));h.tick(2,100);near(h.physical_position().signed_units,1);CHECK(fills(h,close).size()==1);finish(h); + scenario="C5 close cannot cross cycle";Host c;c.begin=[](Host& x){put(x,tx(5));};start(c,"R2-C5");c.tick(0,100); + auto reverse=tx(-6);reverse.trigger=no::Limit{105};auto rev=put(c,reverse); + auto closing=rd(5);closing.capacity=no::PointBudget{2};auto old=put(c,closing); + c.tick(1,100);near(c.physical_position().signed_units,3);c.tick(2,105);near(c.physical_position().signed_units,-3); + CHECK(fills(c,old).size()==1);auto ce=cancellations(c,old);CHECK(ce.size()==1); + if(ce.size()==1){CHECK(ce[0].reason==no::CancelReason::OwnerGone);auto rf=fills(c,rev);CHECK(rf.size()==1);if(rf.size()==1)CHECK(ce[0].cause && ce[0].cause->ordinal==rf[0].ordinal);} + finish(c); +} +void cancel_cohorts() { + scenario="G1 opaque cohorts";Host h;no::RequestHandle a1,a2,b; + h.begin=[&](Host& x){auto a=tx(1,"same");a.trigger=no::Limit{100};a.group=no::Member{7,1,no::GroupEffect::Cancel};a1=put(x,a); + a.label="different";a2=put(x,a);a.label="same";a.group=no::Member{7,2,no::GroupEffect::Cancel};b=put(x,a);}; + start(h,"R2-G1");h.tick(0,100);CHECK(fills(h,a1).size()==1 && fills(h,a2).size()==1);CHECK(fills(h,b).empty()); + auto ce=cancellations(h,b);CHECK(ce.size()==1);if(ce.size()==1)CHECK(ce[0].reason==no::CancelReason::Group); + near(h.physical_position().signed_units,2);finish(h); +} +void deferred_group_receipts() { + scenario="G6 deferred causes";Host h;no::RequestHandle parent,child,a,b; + h.begin=[&](Host& x){auto p=tx(5,"parent");p.trigger=no::Stop{105};parent=put(x,p); + auto c=event_rd(parent);c.trigger=no::Limit{200};c.group=no::Member{7,0,no::GroupEffect::Reduce};child=put(x,c); + auto r=tx(2,"A");r.group=no::Member{7,1,no::GroupEffect::Reduce};a=put(x,r);r.label="B";b=put(x,r);}; + start(h,"R2-G6");h.tick(0,100);CHECK(fills(h,parent).empty());auto af=fills(h,a),bf=fills(h,b);CHECK(af.size()==1 && bf.size()==1); + auto deferred=events(h);CHECK(deferred.size()==2); + h.tick(1,105);auto pf=fills(h,parent);CHECK(pf.size()==1);auto bound=events(h);CHECK(bound.size()==1); + if(bound.size()==1 && pf.size()==1 && deferred.size()==2 && af.size()==1 && bf.size()==1) { + CHECK(bound[0].source.ordinal==pf[0].ordinal && bound[0].source_units==5); + CHECK(bound[0].pending_total==4 && bound[0].effective_deduction==4 && bound[0].prior_adjustment_ids.size()==2); + CHECK(deferred[0].cause.ordinal==af[0].ordinal && deferred[1].cause.ordinal==bf[0].ordinal); + CHECK(deferred[0].recipient==child && deferred[1].recipient==child); + CHECK(bound[0].prior_adjustment_ids[0].ordinal==deferred[0].ordinal && bound[0].prior_adjustment_ids[1].ordinal==deferred[1].ordinal); + } + CHECK(fills(h,child).empty());h.tick(2,200);auto cf=fills(h,child);CHECK(cf.size()==1); + if(cf.size()==1)CHECK(cf[0].closed_units==1 && cf[0].terminal); + near(h.physical_position().signed_units,8);finish(h); +} + +void persistent_trail(double sign) { + scenario="T6 active trail remainder";Host h;h.begin=[&](Host& x){put(x,tx(sign*2));}; + start(h,sign>0?"R2-T6-L":"R2-T6-S");h.tick(0,100); + auto r=rd(2);r.capacity=no::PointBudget{1};r.trigger=no::Trail{2,100+sign*5};auto close=put(h,r); + h.tick(1,100);CHECK(fills(h,close).empty());h.tick(2,100+sign*6);CHECK(fills(h,close).empty()); + h.tick(3,100+sign*10);CHECK(fills(h,close).empty());h.tick(4,100+sign*8);CHECK(fills(h,close).size()==1); + h.tick(5,100+sign*20);auto f=fills(h,close);CHECK(f.size()==2); + if(f.size()==2){CHECK(f[0].raw_price==100+sign*8 && f[1].raw_price==100+sign*20);CHECK(!f[0].terminal && f[1].terminal);} + near(h.physical_position().signed_units,0);finish(h); +} +void group_actual_quantity() { + scenario="G2 physical group quantity";Host h;h.begin=[](Host& x){put(x,tx(-1));};start(h,"R2-G2");h.tick(0,100); + auto a=tx(2);a.group=no::Member{7,1,no::GroupEffect::Reduce};auto parent=put(h,a); + auto b=tx(5);b.trigger=no::Limit{95};b.group=no::Member{7,2,no::GroupEffect::Cancel};auto sibling=put(h,b); + h.tick(1,100);auto f=fills(h,parent);CHECK(f.size()==1);CHECK(fills(h,sibling).empty()); + auto reductions=events(h);CHECK(reductions.size()==1); + if(reductions.size()==1 && f.size()==1){CHECK(reductions[0].cause.ordinal==f[0].ordinal);CHECK(reductions[0].recipient==sibling);CHECK(reductions[0].requested_delta==2 && reductions[0].actual_deduction==2);} + h.tick(2,95);auto sf=fills(h,sibling);CHECK(sf.size()==1);if(sf.size()==1)CHECK(sf[0].opened_units==3); + near(h.physical_position().signed_units,4);near(h.physical_position().average_price,96.25);finish(h); +} +void group_before_dependencies() { + scenario="G8 group before dependent cleanup";Host h;no::RequestHandle parent,child,grandchild,filler; + h.begin=[&](Host& x){auto p=tx(1,"P");p.trigger=no::Stop{200};p.group=no::Member{7,2,no::GroupEffect::Cancel};parent=put(x,p); + auto c=tx(1,"C");c.owner=no::WaitForApplied{parent};c.group=no::Member{7,3,no::GroupEffect::Cancel};child=put(x,c); + auto g=tx(1,"G");g.owner=no::WaitForApplied{child};grandchild=put(x,g); + auto f=tx(1,"F");f.group=no::Member{7,1,no::GroupEffect::Cancel};filler=put(x,f);}; + start(h,"R2-G8");h.tick(0,100); + auto f=fills(h,filler); + auto pc=cancellations(h,parent),cc=cancellations(h,child),gc=cancellations(h,grandchild); + CHECK(f.size()==1 && pc.size()==1 && cc.size()==1 && gc.size()==1); + if(f.size()==1 && pc.size()==1 && cc.size()==1 && gc.size()==1){ + CHECK(pc[0].reason==no::CancelReason::Group && cc[0].reason==no::CancelReason::Group); + CHECK(pc[0].cause && pc[0].cause->ordinal==f[0].ordinal);CHECK(cc[0].cause && cc[0].cause->ordinal==f[0].ordinal); + CHECK(gc[0].reason==no::CancelReason::OwnerGone && gc[0].cause && gc[0].cause->ordinal==cc[0].ordinal); + CHECK(pc[0].ordinal0 && !rejected.handle); + auto history=events(h);CHECK(history.size()==1); + if(history.size()==1)CHECK(history[0].ordinal==rejected.event_ordinal); + auto original=tx(2);original.trigger=no::Limit{90};auto handle=put(h,original); + auto rr=h.replace_market(handle,bad);CHECK(rr.status==no::ReplaceStatus::ReplaceRejected && rr.event_ordinal>rejected.event_ordinal); + CHECK(events(h).size()==1);h.tick(0,90);auto f=fills(h,handle);CHECK(f.size()==1);if(f.size()==1)CHECK(f[0].opened_units==2); + CHECK(h.replace_market(handle,bad).status==no::ReplaceStatus::NotWorking);finish(h); +} +void nonfinite_slippage() { + scenario="limit cannot hide nonfinite slippage"; + for(double sign:{1.0,-1.0}) { + Host h;no::RequestHandle handle; + h.begin=[&](Host& x){auto r=tx(sign);r.trigger=no::Limit{100};handle=put(x,r);}; + auto s=spec(sign>0?"R2-slip-overflow-L":"R2-slip-overflow-S"); + s.price_tick=std::numeric_limits::max();s.slippage_ticks=2; + CHECK(h.configure_native(s).status==NativeSetupStatus::Applied); + start_stream(h);h.tick(0,100); + CHECK(fills(h,handle).empty()); + CHECK(events(h).size()==1); + near(h.physical_position().signed_units,0);finish(h); + } +} +} +int main() { + capacity_per_print();persistent_stop_and_limit();stop_limit_models(); + for(double sign:{1.0,-1.0}){partial_reversal_child(sign,false);partial_reversal_child(sign,true);no_opening_parent(sign);} + close_lifetime();cancel_cohorts();deferred_group_receipts(); + persistent_trail(1);persistent_trail(-1);group_actual_quantity();group_before_dependencies();market_surface_rejections(); + nonfinite_slippage(); + std::printf("%s native resting contract: %d checks, %d failures\n",failures?"FAIL":"PASS",checks,failures); + return failures?1:0; +} From a2492ea6fb83d1f5c9c66ad5ccaf7794ac32a3db Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 12 Sep 2026 22:56:17 +0800 Subject: [PATCH 2/5] Harden native lifecycle receipts and complete resting driver integration --- docs/native-refactor-progress.md | 94 ++-- docs/pages/native-engine.md | 44 +- include/pineforge/native_order.hpp | 16 +- src/native_execution_consumer.cpp | 46 +- src/native_execution_consumer.hpp | 3 +- src/native_matching.hpp | 14 +- src/native_order.cpp | 293 ++++++++++-- tests/CMakeLists.txt | 3 +- tests/test_native_order_resting_core.cpp | 434 ++++++++++++++++-- tests/test_native_resting_acceptance.cpp | 434 ++++++++++++++++++ tests/test_native_resting_contract.cpp | 33 +- tests/test_native_resting_driver_contract.cpp | 377 +++++++++++++++ 12 files changed, 1630 insertions(+), 161 deletions(-) create mode 100644 tests/test_native_resting_acceptance.cpp create mode 100644 tests/test_native_resting_driver_contract.cpp diff --git a/docs/native-refactor-progress.md b/docs/native-refactor-progress.md index 46d0e037..c90807fb 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -14,7 +14,7 @@ improvement resumes after the final native audit. | --- | --- | --- | | R0 | Native settlement and CI convergence | Complete, [PR #241](https://github.com/pineforge-4pass/pineforge-engine/pull/241) | | R1 | Native market orders, host, drivers, calendar and forward runner | Complete, [PR #243](https://github.com/pineforge-4pass/pineforge-engine/pull/243) | -| R2 | Resting orders, partial execution, owner-bound children and group effects | Design under review; next implementation slice | +| R2 | Resting orders, partial execution, owner-bound children and group effects | R2a prerequisite complete ([PR #244](https://github.com/pineforge-4pass/pineforge-engine/pull/244)); resting lifecycle implemented, validation and acceptance pending | | R3 | Accounting, admission, risk and observation ownership | Open | | R3a | Reversal execution and lifecycle settlement | Complete, [PR #242](https://github.com/pineforge-4pass/pineforge-engine/pull/242) | | R3b | Migrate remaining physical fill paths to shared settlement | Open | @@ -26,56 +26,74 @@ These are the nine phases in the campaign ledger, including the queued return to parity improvement. No R2 design document or partial implementation marks R2 complete. -## Last accepted implementation +## Accepted scoped-settlement prerequisite -PR #243 was squash merged as -`5f3299d90a02e3161659720fb8c2e82f7543ef7a`. Its tree -`770f7bd2913c7c9c579d38f5c3ffc8950676bb40` is identical to the independently -reviewed and measured candidate `ce995a2db1c3bd771369f105fcce2d1234d107b8`. +PR #244 completed **R2a scoped native settlement**, the internal prerequisite +for owner-bound closes. It was squash merged as +`e7d023dbdff1c98229155ec5bcdd1e4ac534f5fb`. Its tree +`0201bf052429490fb453bbfd6037e5afd1669626` is identical to the independently +reviewed and measured candidate `794d1f27bf46f23be238f8cdf9173d9a132d3b6b`. +The scope selects all surviving fragments of an opening request in its exact +position cycle, preserving siblings, historical entry costs and one current +execution fee. Existing whole-book settlement remains available. -- Linux Debug with ASan/UBSan: 317/317 CTests, no skips; direct WebSocket - check passed. The native proof includes 46 structured scenarios and actual - runner delivery failure, retry and replay. -- Fixed-population Cloud sweep: 72/72 cases and 4190/4190 scored probes, - with zero differences in full grades, raw trades, substantive verifier - results or non-product inputs against the pinned baseline. +- Fresh local Release build: 309/309 CTests. The scoped-close suite has + 12 numerical cases and 3319 checks; additive and existing ABI checks passed. +- Completed fixed-population Cloud retry: 72/72 cases and 4190/4190 scored + probes, with zero full-grade, raw-trade, substantive-verifier, coverage or + configuration differences against accepted R1. The first 71/72 attempt + failed during one Cloud task's startup and was abandoned; attempt 2 + completed without a code change. - All nine PR checks and four post-merge workflows passed. Independent final candidate review was GREEN. -- The actual gate execution `pineforge-pr-gate-7zlr7` returned +- The actual gate execution `pineforge-pr-gate-m6j9h` returned **FAIL: `target.not-positive` only**, because results did not improve. This is retained as a FAIL under the authorized neutral-refactor exception. The campaign baseline remains unchanged; promotion was skipped. -These results apply to the R1 candidate, not to future R2 edits. The final -receipt is content-addressed in the campaign evidence store as -`7c4487fa35366a2a839f8663223f5f2085e8228ccfae7e772c52acf7f9ff996c`. - -## Next increments - -1. **Scoped physical closes.** Extend the existing inspection/settlement - path to close only surviving exposure from a particular opening request - in an exact position cycle. Whole-book FIFO remains the default. - This preparatory seam needs its own contract, native checks and review. -2. **Resting-order chronology and partial execution.** Add typed triggers - and live remainders to the existing driver/consumer path. Distinguish - actual tick prints from modeled OHLC crossings; protect limit prices. -3. **Causal relationships.** Activate owner-bound children and apply typed - group cancellation/reduction from committed events, with replacement, - exhaustion and replay behavior explicit. -4. **Integration proof.** Exercise the real native producer/consumer and - review the Pine mapping independently before accepting R2. - -The first relationship proposal is not accepted. Its review found ambiguous -tick interpolation, remaining-path ordering, same-point child activation, -and missing nonterminal partial executions. These decisions must be resolved -before relationship implementation. Opening-request identity may cover -several physical fragments; it is not a unique lot id. Canceling a working -remainder does not undo its already committed exposure. +These results apply to R2a. They do not establish acceptance of the later +resting-order implementation, and R2a is not an additional completed roadmap +phase. + +## Current R2 implementation + +This branch implements the pinned native contract. Validation and review +repairs are still in progress: + +1. Limit, stop, stop-limit and trailing requests retain trigger state and + partial remainders. Matching distinguishes actual tick prints from + continuous modeled OHLC crossings, protects limit prices, and applies + explicit request capacity at each original input point. +2. Owner-bound children use committed opening events and cycle identity. + Typed group cancellation/reduction follows committed execution effects; + replacement and exhaustion finish their dependency cleanup before a + command returns. One opening request may own several physical fragments. + Canceling its working remainder does not undo committed exposure. +3. Request/core/event values use `native_order_v2`; identity values remain + `native_order_v1`. Engine, pending, host and consumer C++ boundaries move + together to epoch 13, with broker/stream hash version 13. Existing native + run-spec, calendar/driver value domains and C ABI 4 prefixes remain. + +The latest integrated Release build passed 322 CTests with zero failures. +One WebSocket transport test was skipped because the selected macOS libcurl +lacks WebSocket support. The native proof includes 831 resting-contract +checks, 20 scoped-ownership/chronology/failure cases (782 checks), 13 driver +cases (882 checks), and 343 resting-core checks. Independent review findings +have corresponding regression witnesses, including token ownership, event-sized +partial exits, exact deferred-receipt arithmetic and same-cursor execution prices. + +Final review of the publication candidate, remaining R2 acceptance coverage, +a new fixed-population 4190 comparison, the actual gate and published-head CI +remain required. R2 is not complete. The earlier R2a and separate AUTO-isolation +compatibility results do not substitute for this implementation's proof. R2 preserves all currently supported calendar, session, timezone, DST and timeframe behavior. Source labels remain inert in the native kernel; `from_entry`, source call grouping, Pine OCA policy and dormant revival belong to codegen/adapters. Range-end reporting remains nonphysical. +The native R2 boundary is pinned; complete Pine lowering remains mandatory +R4 work. The parity-improvement campaign remains paused through the final +native audit. ## Publication and acceptance diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 0687f0c6..932413ab 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1,4 +1,4 @@ -# Native market engine {#native_engine} +# Native engine {#native_engine} @tableofcontents @@ -10,6 +10,11 @@ Codegen and source adapters select those policies separately. Resting requests use the general host commands; request-value members live in `` and are not restated here. +The resting-order lifecycle is implemented in the current R2 candidate; +final integration, compatibility and independent-review acceptance are still +pending. [Refactor progress](../native-refactor-progress.md) separates this +candidate from the completed R2a scoped-settlement prerequisite. + Subclass `pineforge::NativeStrategyHost`. Configure with `configure_native`, then `run` or `stream_*`. Submit from native begin/bar callbacks, or between realtime inputs on the same thread. Do not override @@ -125,7 +130,7 @@ byte-identical** to the spec. Conflicting values are a preflight refusal: an unsupported source mutation (`Failed`). Magnifier/source-feed arguments are not native spec fields. -## Market requests +## Native requests From a native callback in `Batch` / `Warmup` / `Realtime`: @@ -149,9 +154,17 @@ slice. still constructs from: - `order_action::Transact{signed_units}` — finite nonzero -- `order_action::Reduce{units}` — finite positive +- `native_order::Reduce{native_order::ExplicitUnits{units}}` — finite positive - `execution::Flatten{}` — quantity-free whole-book close +General requests also select typed `Market`, `Limit`, `Stop`, `StopLimit` or +`Trail` triggers; `ImmediateRemaining` or per-point `PointBudget` capacity; +and explicit owner/group relationships. Partial executions retain live +remaining quantity. Owner-bound closes use committed opening exposure and +cycle identity through scoped settlement. Group cancellation/reduction is +caused by committed execution events. See the request header for the exact +value types; source-specific Pine lowering remains codegen/adapter work. + A `quantity_grid`, when present, admits Transact/Reduce quantities on the exact binary64 grid in `native_order.hpp`. Flatten is not gridded. Rejection does not rewrite the attempted bits. @@ -164,8 +177,8 @@ does not rewrite the attempted bits. - `Rejected` — rejection ordinal, no handle (`InvalidQuantity` / `OffGrid`). Fills appear later as `ExecutionAppliedEvent` on the same command history -(`native_events(0)`). Matching walks live requests in acceptance/incarnation -order at a matching driver point. Eligibility is +(`native_events(0)`). Default market requests retain acceptance/incarnation +priority at an eligible matching driver point. Their birth eligibility is `point_ordinal > birth.acceptance_ordinal` and `effective_time_ms >= birth.decision_time_lower_bound` (`point_eligible` in the header). A request accepted on bar *N* cannot fill on that bar’s already @@ -178,8 +191,8 @@ absent, replaced, or already terminal → `NotWorking`. Foreign or malformed handle → `InvalidHandle`. Every outcome is an event. Commands never move lots or cash. -At a matching point, Reduce/Flatten use **current** exposure, not the -acceptance-cycle book: a flatten accepted while flat can still close a +For independent market requests, Reduce/Flatten use **current** exposure, +not the acceptance-cycle book: a flatten accepted while flat can still close a same-point opening that already filled. If the book is flat at execution, Reduce/Flatten terminalize `NoEffectEvent`: no execution identity, no fill, no fee, no physical action. `MatchRejectedEvent` is an event ordinal without @@ -276,14 +289,20 @@ completed run with no events. Two driver models only: confirmed OHLCV and observed ticks. Mixing them on one stream is refused. Callbacks stay **close-only** (script-bar calculation). -Confirmed script OHLC: modeled **Opening** at the first contributor’s actual -open (**match**), high/low in the existing AUTO order (**excursion only**), -close (**excursion only**), then calculation, then optional -AfterCalculation close (**match**). +Confirmed OHLC retains the modeled **Opening**, high/low in the existing +AUTO order, close, calculation and optional AfterCalculation close sequence. +Default market requests match at the modeled opening and eligible +AfterCalculation close; high/low/close traversal updates their excursions. +Resting requests additionally match along the remaining continuous modeled +price segments. The earliest eligible hit is resolved before later hits; +trigger activation, partial fills and newly eligible relationships retain +their causal cursor. Geometric fractions keep the original point's time +and identity rather than inventing intermediate timestamps. Observed tick: the real price at its timestamp/sequence (**match** / excursion). Sequence, when nonzero, must increase. Off-session **observed** -prints are still delivered and may fill an eligible live request. +prints are still delivered and may fill an eligible live request. Resting +triggers evaluate each actual print without interpolating between prints. Quiet **tradable** interval with no prints: explicit **carried open** at that interval’s tradable open, last known price (**match** if eligible), then @@ -314,7 +333,6 @@ These are existing refusals, not implied future features: entry/exit/cancel commands — native hosts latch `Failed` (`UnsupportedSource`) before mutation - C-level native request submit/replace/cancel -- Limit, stop, trail, or bracket **relationships** Rebuild strategy libraries against this engine. An ABI-v4 module without the native contract is legacy and cannot take `--native-config`. diff --git a/include/pineforge/native_order.hpp b/include/pineforge/native_order.hpp index 8f8ece5e..238475d3 100644 --- a/include/pineforge/native_order.hpp +++ b/include/pineforge/native_order.hpp @@ -449,12 +449,14 @@ struct NotWorkingEvent { uint64_t ordinal = 0; RequestHandle target; std::optional attempted; + CommandSurface surface = CommandSurface::General; }; struct InvalidHandleEvent { uint64_t ordinal = 0; RequestHandle target; std::optional attempted; + CommandSurface surface = CommandSurface::General; }; struct NoEffectEvent { @@ -782,6 +784,7 @@ class WorkingRequestCore { uint64_t& next_timeline_ordinal); Preparation prepare_trigger(const RequestHandle& target, const TriggerTransition& transition, + DriverEligibilityClass driver_class, uint64_t& next_timeline_ordinal); InstallResult install_mutation(PreparedMutation&& prepared) noexcept; @@ -847,9 +850,15 @@ class WorkingRequestCore { LiveRequest make_live(DefinitionRef definition, const CommandContext& context, EventId accepted) const; - std::vector pending_receipt_ids(const PendingAdjustments& pending) const; - bool receipt_seen(const EventId& cause, const RequestHandle& recipient, GroupEffect effect, - uint64_t* outcome) const; + enum class ReceiptLookup : std::uint8_t { Absent = 0, Present = 1, Conflict = 2 }; + bool collect_pending_chain(const PendingAdjustments& pending, + const RequestHandle& recipient, + std::vector* ids, + double* total) const; + ReceiptLookup receipt_lookup(const EventId& cause, const RequestHandle& recipient, + GroupEffect effect, uint64_t* outcome) const; + bool authenticate_receipt_outcome(const CommandEvent& event, const EventId& cause, + const RequestHandle& recipient, GroupEffect effect) const; bool trail_level_ok(double best, double offset, bool is_buy, double* stop) const noexcept; uint64_t usable_ordinal(uint64_t next) const; @@ -883,6 +892,7 @@ class WorkingRequestCore { GroupEffect receipt_effect = GroupEffect::Reduce; uint64_t receipt_outcome = 0; }; + std::optional validate_plan(const MutationPlan& plan) const noexcept; InstallResult commit(MutationPlan& plan) noexcept; MutationPlan begin_plan() const; void reserve_plan(const MutationPlan& plan); diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index ed9d1420..995ec3f8 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -318,10 +318,12 @@ void hash_command(Fnv& f, const native_order::CommandEvent& event) noexcept { f.u(6); hash_handle(f, payload.target); hash_optional_request(f, payload.attempted); + hash_surface(f, payload.surface); } else if constexpr (std::is_same_v) { f.u(7); hash_handle(f, payload.target); hash_optional_request(f, payload.attempted); + hash_surface(f, payload.surface); } else if constexpr (std::is_same_v) { f.u(8); hash_definition(f, payload.definition); @@ -1419,8 +1421,13 @@ void NativeExecutionConsumer::drain_after_applied( } void NativeExecutionConsumer::observe_trails( - BacktestEngine& engine, const native_order::MatchCursor& cursor, double price) { + BacktestEngine& engine, const NativeDriverPoint& point, + const native_order::MatchCursor& cursor, bool continuous, double price) { if (!std::isfinite(price) || failed()) return; + native_order::EvaluationContext evaluation; + evaluation.cursor = cursor; + evaluation.driver_class = classify_driver(point, continuous); + evaluation.existing_matching_bit = point.matching; std::vector handles; handles.reserve(requests_.live().size()); for (const auto& live : requests_.live()) handles.push_back(live.handle()); @@ -1428,6 +1435,7 @@ void NativeExecutionConsumer::observe_trails( if (failed()) return; const auto* live = requests_.find_live(handle); if (!live) continue; + if (!requests_.evaluation_eligible(*live, evaluation)) continue; const auto* track = std::get_if(&live->trigger_state); if (!track) continue; const bool buy = requests_.working_is_buy(*live); @@ -1435,7 +1443,8 @@ void NativeExecutionConsumer::observe_trails( native_order::Preparation prep; try { prep = requests_.prepare_trigger( - handle, native_order::ObserveTrailExtremum{cursor, price}, next_timeline_ordinal_); + handle, native_order::ObserveTrailExtremum{cursor, price}, + evaluation.driver_class, next_timeline_ordinal_); } catch (const std::exception& e) { fail(engine, NativeFailure{NativeFailureCode::Allocation, NativeFailureOperation::Settlement, cursor.point.ordinal}); @@ -1478,8 +1487,10 @@ void NativeExecutionConsumer::match_path( const auto driver_class = classify_driver(point, continuous); const uint64_t P = point.coordinate.ordinal; double t_cursor = 0.0; + double cursor_price = from_price; native_order::MatchCursor path_cursor = make_cursor(point, t_cursor); - observe_trails(engine, path_cursor, native_matching::price_at(from_price, to_price, t_cursor)); + observe_trails(engine, point, path_cursor, continuous, + native_matching::price_at(from_price, to_price, t_cursor)); enum class Kind : std::uint8_t { Evaluate = 0, @@ -1549,12 +1560,15 @@ void NativeExecutionConsumer::match_path( if (!facts.birth_ok || facts.waiting || !facts.driver_ok) continue; const double t_min = cause_floor(*live); if (t_min > 1.0) continue; + const native_matching::GeometricHit start{ + t_min, t_min == t_cursor ? cursor_price + : native_matching::price_at(from_price, to_price, t_min)}; Candidate row; row.handle = handle; row.incarnation = handle.incarnation; if (needs_evaluation(*live, facts)) { row.t = t_min; - row.price = native_matching::price_at(from_price, to_price, t_min); + row.price = start.price; row.kind = Kind::Evaluate; } else { const bool buy = requests_.working_is_buy(*live); @@ -1566,23 +1580,22 @@ void NativeExecutionConsumer::match_path( const auto* stop = std::get_if(&trigger); if (!stop) continue; hit = native_matching::first_region_entry( - from_price, to_price, t_min, stop->price, !buy, true); + from_price, to_price, start, stop->price, !buy, true); kind = Kind::ActivateStop; } else if (std::holds_alternative(state)) { const auto* sl = std::get_if(&trigger); if (!sl) continue; hit = native_matching::first_region_entry( - from_price, to_price, t_min, sl->stop, !buy, true); + from_price, to_price, start, sl->stop, !buy, true); kind = Kind::ActivateStopLimit; } else if (std::holds_alternative(state)) { const auto* trail = std::get_if(&trigger); if (!trail) continue; if (!trail->arm_price) { - hit = native_matching::GeometricHit{t_min, - native_matching::price_at(from_price, to_price, t_min)}; + hit = start; } else { hit = native_matching::first_region_entry( - from_price, to_price, t_min, *trail->arm_price, buy, true); + from_price, to_price, start, *trail->arm_price, buy, true); } kind = Kind::BeginTrail; } else if (const auto* track = std::get_if(&state)) { @@ -1596,7 +1609,7 @@ void NativeExecutionConsumer::match_path( return; } hit = native_matching::trail_stop_hit( - from_price, to_price, t_min, track->best, trail->offset, buy); + from_price, to_price, start, track->best, trail->offset, buy); kind = Kind::ActivateTrail; } else if (std::holds_alternative(state) || std::holds_alternative(state)) { @@ -1609,7 +1622,7 @@ void NativeExecutionConsumer::match_path( continue; } hit = native_matching::first_region_entry( - from_price, to_price, t_min, level, buy, true); + from_price, to_price, start, level, buy, true); kind = Kind::Fill; } else if (std::holds_alternative(state) || std::holds_alternative(state) @@ -1618,8 +1631,7 @@ void NativeExecutionConsumer::match_path( if (std::holds_alternative(state) && continuous) { continue; } - hit = native_matching::GeometricHit{ - t_min, native_matching::price_at(from_price, to_price, t_min)}; + hit = start; kind = Kind::Fill; } else { continue; @@ -1651,9 +1663,10 @@ void NativeExecutionConsumer::match_path( if (continuous && winner->t > t_cursor) { apply_excursion(engine, winner->price); path_cursor = make_cursor(point, winner->t); - observe_trails(engine, path_cursor, winner->price); + observe_trails(engine, point, path_cursor, continuous, winner->price); } t_cursor = winner->t; + cursor_price = winner->price; path_cursor = make_cursor(point, t_cursor); eval.cursor = path_cursor; const auto* live = requests_.find_live(winner->handle); @@ -1711,7 +1724,8 @@ void NativeExecutionConsumer::match_path( } native_order::Preparation prep; try { - prep = requests_.prepare_trigger(winner->handle, transition, next_timeline_ordinal_); + prep = requests_.prepare_trigger(winner->handle, transition, + driver_class, next_timeline_ordinal_); } catch (const std::exception& e) { fail(engine, NativeFailure{NativeFailureCode::Allocation, NativeFailureOperation::Settlement, P}); @@ -1995,7 +2009,7 @@ void NativeExecutionConsumer::match_path( } if (continuous && t_cursor < 1.0 && !failed()) { apply_excursion(engine, to_price); - observe_trails(engine, make_cursor(point, 1.0), to_price); + observe_trails(engine, point, make_cursor(point, 1.0), continuous, to_price); } } diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 4149322d..f6066ad7 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -182,7 +182,8 @@ class NativeExecutionConsumer final : public IExecutionConsumer { BacktestEngine& engine, std::vector> seeds, NativeFailureOperation operation); - void observe_trails(BacktestEngine& engine, const native_order::MatchCursor& cursor, + void observe_trails(BacktestEngine& engine, const NativeDriverPoint& point, + const native_order::MatchCursor& cursor, bool continuous, double price); void record_driver(const NativeDriverPoint& point); NativeCoordinate coordinate_from(const native_calendar::NativeInterval& interval, diff --git a/src/native_matching.hpp b/src/native_matching.hpp index e4b9d7f3..a8fd3f22 100644 --- a/src/native_matching.hpp +++ b/src/native_matching.hpp @@ -22,6 +22,8 @@ inline bool finite_positive(double v) noexcept { } inline double price_at(double from, double to, double t) noexcept { + if (t == 0.0) return from; + if (t == 1.0) return to; return from + t * (to - from); } @@ -45,13 +47,16 @@ inline bool in_region(double price, double level, bool le) noexcept { // reports t_start when the cursor is already inside; otherwise the first // later crossing uses the threshold as the modeled raw price. inline std::optional first_region_entry( - double from, double to, double t_start, double level, bool le, + double from, double to, const GeometricHit& start, double level, bool le, bool include_current) noexcept { + const double t_start = start.t; if (!std::isfinite(from) || !std::isfinite(to) || !std::isfinite(level) || !std::isfinite(t_start) || t_start < 0.0 || t_start > 1.0) { return std::nullopt; } - const double current = price_at(from, to, t_start); + // A reached threshold is the authoritative price of this cursor. Do not + // reconstruct it from its rounded fraction on an activation/fill rescan. + const double current = start.price; if (!std::isfinite(current)) return std::nullopt; if (include_current && in_region(current, level, le)) { return GeometricHit{t_start, current}; @@ -98,7 +103,8 @@ inline bool trail_best_improves(double best, double price, bool buy) noexcept { // Adverse remaining suffix against a frozen best. Favorable monotonic // motion cannot hit the trailing stop on one linear segment. inline std::optional trail_stop_hit( - double from, double to, double t_start, double best, double offset, bool buy) noexcept { + double from, double to, const GeometricHit& start, double best, double offset, bool buy) noexcept { + const double t_start = start.t; double stop = 0.0; if (!checked_trail_stop(best, offset, buy, &stop)) return std::nullopt; if (!std::isfinite(from) || !std::isfinite(to) || !std::isfinite(t_start) @@ -106,7 +112,7 @@ inline std::optional trail_stop_hit( return std::nullopt; } const bool le = !buy; - return first_region_entry(from, to, t_start, stop, le, true); + return first_region_entry(from, to, start, stop, le, true); } } // namespace native_matching diff --git a/src/native_order.cpp b/src/native_order.cpp index 45387527..3c9cead0 100644 --- a/src/native_order.cpp +++ b/src/native_order.cpp @@ -131,9 +131,14 @@ bool position_matches(const PositionIdentity& position, int64_t cycle, Side side bool opening_alive(const OpeningObservation& observation, const RequestHandle& opening, - int64_t cycle) noexcept { - return observation.queried_opening == opening && observation.queried_cycle == cycle - && observation.has_live_matching_lot && cycle > 0; + int64_t cycle, + const Side* required_side = nullptr) noexcept { + if (observation.queried_opening != opening || observation.queried_cycle != cycle) return false; + if (!observation.has_live_matching_lot || cycle <= 0) return false; + const auto* nonflat = std::get_if(&observation.current_position); + if (!nonflat || nonflat->cycle != cycle) return false; + if (required_side && nonflat->side != *required_side) return false; + return true; } bool book_close_alive(const TargetObservation& observation, const BookClose& close) noexcept { @@ -141,7 +146,79 @@ bool book_close_alive(const TargetObservation& observation, const BookClose& clo } bool opening_close_alive(const TargetObservation& observation, const OpeningClose& close) noexcept { - return observation.opening && opening_alive(*observation.opening, close.opening, close.cycle); + if (!observation.opening) return false; + if (!opening_alive(*observation.opening, close.opening, close.cycle, &close.side)) return false; + return position_matches(observation.current_position, close.cycle, close.side); +} + +bool driver_class_matches_cursor(DriverEligibilityClass driver, + const MatchCursor& cursor) noexcept { + using pineforge::NativePriceProvenance; + using pineforge::NativePathPhase; + switch (cursor.point.provenance) { + case NativePriceProvenance::ObservedPrint: + return driver == DriverEligibilityClass::ObservedPrint; + case NativePriceProvenance::CarriedOpen: + return driver == DriverEligibilityClass::CarriedOpen; + case NativePriceProvenance::AfterCalculationClose: + // Completion describes the slot, not whether the input was ticks + // or a confirmed bar. The consumer supplies that transient fact. + return driver == DriverEligibilityClass::TickAfterCalculation + || driver == DriverEligibilityClass::ConfirmedAfterCalculationClose; + case NativePriceProvenance::Confirmed: + if (cursor.point.path_phase == NativePathPhase::High + || cursor.point.path_phase == NativePathPhase::Low + || cursor.point.path_phase == NativePathPhase::Close) { + return driver == DriverEligibilityClass::ConfirmedExcursion; + } + return driver == DriverEligibilityClass::ConfirmedOpen; + case NativePriceProvenance::ModeledOHLCOpen: + return driver == DriverEligibilityClass::ConfirmedOpen; + case NativePriceProvenance::ModeledOHLCClose: + return driver == DriverEligibilityClass::ConfirmedExcursion + || driver == DriverEligibilityClass::ConfirmedAfterCalculationClose; + case NativePriceProvenance::PartialFinalized: + case NativePriceProvenance::Calculation: + return driver == DriverEligibilityClass::ConfirmedAfterCalculationClose; + } + return false; +} + +const MatchCursor& transition_cursor(const TriggerTransition& transition) noexcept { + return std::visit([](const auto& payload) -> const MatchCursor& { return payload.cursor; }, + transition); +} + +bool stop_price_reached(bool is_buy, double level, double reached) noexcept { + if (!std::isfinite(reached) || !finite_positive(level)) return false; + return is_buy ? reached >= level : reached <= level; +} + +bool same_p_continuation(const LiveRequest& live, const MatchCursor& cursor) noexcept { + if (cursor.point.ordinal == 0) return false; + if (cursor.point.effective_time_ms < live.birth().decision_time_lower_bound) return false; + auto same = [&](const MatchCursor& cause) noexcept { + return cause.point.ordinal == cursor.point.ordinal; + }; + if (const auto* armed = std::get_if(&live.authority)) { + return same(armed->cause_cursor); + } + if (const auto* close = std::get_if(&live.authority)) { + if (const auto* from = std::get_if(&close->enrollment)) { + return same(from->cursor); + } + } + if (const auto* close = std::get_if(&live.authority)) { + return same(close->binding_cursor); + } + return false; +} + +bool trigger_cursor_eligible(const LiveRequest& live, const MatchCursor& cursor) noexcept { + if (point_eligible(live.birth(), cursor.point.ordinal, cursor.point.effective_time_ms)) { + return true; + } + return same_p_continuation(live, cursor); } const ExecutionAppliedEvent* as_applied(const CommandEvent& event) noexcept { @@ -447,35 +524,124 @@ const CommandEvent* WorkingRequestCore::event_at(const EventId& id) const { return event; } -std::vector WorkingRequestCore::pending_receipt_ids( - const PendingAdjustments& pending) const { - std::vector ids; +bool WorkingRequestCore::authenticate_receipt_outcome(const CommandEvent& event, + const EventId& cause, + const RequestHandle& recipient, + GroupEffect effect) const { + if (const auto* reduced = std::get_if(&event)) { + return effect == GroupEffect::Reduce && reduced->cause == cause + && reduced->recipient == recipient && reduced->effect == GroupEffect::Reduce + && reduced->definition && reduced->definition->handle == recipient; + } + if (const auto* deferred = std::get_if(&event)) { + return effect == GroupEffect::Reduce && deferred->cause == cause + && deferred->recipient == recipient && deferred->effect == GroupEffect::Reduce + && deferred->definition && deferred->definition->handle == recipient; + } + if (const auto* cancelled = std::get_if(&event)) { + const bool allowed_effect = effect == GroupEffect::Cancel + || (effect == GroupEffect::Reduce + && std::holds_alternative(cancelled->unexecuted) + && cancelled->definition + && as_flatten(cancelled->definition->request.intent)); + return allowed_effect && cancelled->reason == CancelReason::Group + && cancelled->cause == cause && cancelled->definition + && cancelled->definition->handle == recipient; + } + return false; +} + +bool WorkingRequestCore::collect_pending_chain(const PendingAdjustments& pending, + const RequestHandle& recipient, + std::vector* ids, + double* total) const { + ids->clear(); + *total = 0.0; const auto* deferred = std::get_if(&pending); - if (!deferred) return ids; + if (!deferred) return true; + if (deferred->count == 0 || deferred->tail_receipt.ordinal == 0 + || deferred->tail_receipt.run != identity_) { + return false; + } EventId cursor = deferred->tail_receipt; for (uint64_t i = 0; i < deferred->count; ++i) { const CommandEvent* event = event_at(cursor); const auto* payload = event ? std::get_if(event) : nullptr; - if (!payload) break; - ids.push_back(cursor); - if (!payload->previous_pending_receipt) break; - cursor = *payload->previous_pending_receipt; - } - std::reverse(ids.begin(), ids.end()); - return ids; -} - -bool WorkingRequestCore::receipt_seen(const EventId& cause, const RequestHandle& recipient, - GroupEffect effect, uint64_t* outcome) const { - for (const auto& row : receipts_) { - if (row.cause == cause && row.recipient == recipient && row.effect == effect) { - const CommandEvent* event = event_at(EventId{identity_, row.outcome_ordinal}); - if (!event) return false; - if (outcome) *outcome = row.outcome_ordinal; - return true; + if (!payload || payload->recipient != recipient || payload->effect != GroupEffect::Reduce + || !payload->definition || payload->definition->handle != recipient) { + return false; + } + ids->push_back(cursor); + if (i + 1 < deferred->count) { + if (!payload->previous_pending_receipt + || payload->previous_pending_receipt->ordinal >= cursor.ordinal) return false; + cursor = *payload->previous_pending_receipt; + } else if (payload->previous_pending_receipt) { + return false; } } - return false; + if (ids->size() != deferred->count) return false; + std::reverse(ids->begin(), ids->end()); + // Reproduce the exact accumulation order used when each receipt was + // committed. Reassociating these additions tail-first changes binary64 + // totals for valid chains such as 0.1, 0.2, 0.3. + double sum = 0.0; + for (std::size_t i = 0; i < ids->size(); ++i) { + const CommandEvent* event = event_at((*ids)[i]); + const auto* payload = event ? std::get_if(event) : nullptr; + if (!payload || !finite_positive(payload->deferred_delta)) return false; + if (i == 0) { + if (!std::holds_alternative(payload->pending_before)) return false; + } else { + const auto* before = std::get_if(&payload->pending_before); + if (!before || before->total != sum || before->count != i + || before->tail_receipt != (*ids)[i - 1]) return false; + } + if (!checked_add_positive(sum, payload->deferred_delta, &sum)) return false; + if (payload->pending_after.total != sum || payload->pending_after.count != i + 1 + || payload->pending_after.tail_receipt != (*ids)[i]) return false; + } + if (sum != deferred->total) return false; + *total = sum; + return true; +} + +WorkingRequestCore::ReceiptLookup WorkingRequestCore::receipt_lookup( + const EventId& cause, const RequestHandle& recipient, GroupEffect effect, + uint64_t* outcome) const { + ReceiptKey needle{cause, recipient, effect, 0}; + const auto it = std::lower_bound( + receipts_.begin(), receipts_.end(), needle, + [](const ReceiptKey& a, const ReceiptKey& b) { + return receipt_cmp(a.cause.ordinal, a.recipient.incarnation, a.effect, + b.cause.ordinal, b.recipient.incarnation, b.effect) + < 0; + }); + if (it == receipts_.end()) return ReceiptLookup::Absent; + if (it->cause != cause || it->recipient != recipient || it->effect != effect) { + return ReceiptLookup::Absent; + } + const CommandEvent* event = event_at(EventId{identity_, it->outcome_ordinal}); + if (!event || !authenticate_receipt_outcome(*event, cause, recipient, effect)) { + return ReceiptLookup::Conflict; + } + if (outcome) *outcome = it->outcome_ordinal; + return ReceiptLookup::Present; +} + +std::optional WorkingRequestCore::validate_plan(const MutationPlan& plan) const noexcept { + if (plan.consumed) return InstallError::AlreadyConsumed; + const auto locked = plan.instance.lock(); + if (!locked || locked != instance_ || !instance_ || instance_->expired + || plan.generation != instance_->generation || plan.run != identity_) { + return InstallError::WrongCoreOrRun; + } + if (plan.epoch != epoch_ || plan.history_size != history_.size() + || plan.last_ordinal != last_ordinal_ + || epoch_ == std::numeric_limits::max()) { + return InstallError::StalePreparation; + } + return std::nullopt; } bool WorkingRequestCore::trail_level_ok(double best, double offset, bool is_buy, @@ -490,17 +656,7 @@ bool WorkingRequestCore::trail_level_ok(double best, double offset, bool is_buy, } InstallResult WorkingRequestCore::commit(MutationPlan& plan) noexcept { - if (plan.consumed) return InstallError::AlreadyConsumed; - const auto locked = plan.instance.lock(); - if (!locked || locked != instance_ || !instance_ || instance_->expired - || plan.generation != instance_->generation || plan.run != identity_) { - return InstallError::WrongCoreOrRun; - } - if (plan.epoch != epoch_ || plan.history_size != history_.size() - || plan.last_ordinal != last_ordinal_) { - return InstallError::StalePreparation; - } - if (epoch_ == std::numeric_limits::max()) return InstallError::StalePreparation; + if (const auto error = validate_plan(plan)) return *error; const std::size_t first = history_.size(); const std::size_t count = plan.events.size(); for (auto& event : plan.events) { @@ -611,7 +767,7 @@ std::optional WorkingRequestCore::validate_request( return RequestRejectReason::InvalidCapacity; } if (const auto* budget = std::get_if(&request.capacity)) { - if (flatten || owner_opened) return RequestRejectReason::InvalidCapacity; + if (flatten) return RequestRejectReason::InvalidCapacity; if (!finite_positive(budget->units)) return RequestRejectReason::InvalidCapacity; if (!on_optional_grid(budget->units, context.quantity_grid)) { return RequestRejectReason::OffGrid; @@ -655,6 +811,9 @@ std::optional WorkingRequestCore::validate_request( } if (const auto* member = std::get_if(&request.group)) { if (member->group == 0) return RequestRejectReason::InvalidGroup; + if (member->effect != GroupEffect::Cancel && member->effect != GroupEffect::Reduce) { + return RequestRejectReason::InvalidGroup; + } } return std::nullopt; } @@ -896,11 +1055,11 @@ PreparedReplace WorkingRequestCore::prepare_replace(const RequestHandle& target, : ReplaceStatus::NotWorking; impl->result = ReplaceResult{status, ordinal, std::nullopt, std::nullopt}; if (kind == TargetKind::InvalidHandle) { - plan.events.emplace_back( - InvalidHandleEvent{ordinal, std::move(staged_target), std::move(staged)}); + plan.events.emplace_back(InvalidHandleEvent{ordinal, std::move(staged_target), + std::move(staged), context.surface}); } else { - plan.events.emplace_back( - NotWorkingEvent{ordinal, std::move(staged_target), std::move(staged)}); + plan.events.emplace_back(NotWorkingEvent{ordinal, std::move(staged_target), + std::move(staged), context.surface}); } seal_plan(plan); impl->plan = std::move(plan); @@ -1179,6 +1338,7 @@ Preparation WorkingRequestCore::prepare_evaluation( Preparation WorkingRequestCore::prepare_trigger( const RequestHandle& target, const TriggerTransition& transition, + DriverEligibilityClass driver_class, uint64_t& next_timeline_ordinal) { require_identity(identity_); std::size_t live_index = 0; @@ -1190,6 +1350,15 @@ Preparation WorkingRequestCore::prepare_trigger( || std::holds_alternative(updated.authority)) { return NoChange{NoChangeReason::NotEligible}; } + const MatchCursor& cursor = transition_cursor(transition); + if (!trigger_cursor_eligible(updated, cursor)) { + return NoChange{NoChangeReason::NotEligible}; + } + if (!driver_class_matches_cursor(driver_class, cursor) + || !trigger_permits_driver(updated.request().trigger, updated.trigger_state, + driver_class, false)) { + return NoChange{NoChangeReason::NotEligible}; + } const bool is_buy = working_is_buy(updated); MutationPlan plan = begin_plan(); @@ -1229,6 +1398,11 @@ Preparation WorkingRequestCore::prepare_trigger( return PreparationError{CoreFailure::NonrepresentableQuantity, EventId{identity_, 0}, target}; } + if (trail->arm_price + && !stop_price_reached(!is_buy, *trail->arm_price, begin->reached_price)) { + return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, + target}; + } return emit_activated(ActivationKind::TrailArm, TrailTrack{begin->reached_price}, begin->cursor, begin->reached_price); } @@ -1260,6 +1434,11 @@ Preparation WorkingRequestCore::prepare_trigger( return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, target}; } + const auto* stop_px = std::get_if(&updated.request().trigger); + if (!stop_px || !stop_price_reached(is_buy, stop_px->price, stop->reached_price)) { + return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, + target}; + } return emit_activated(ActivationKind::Stop, StopActive{}, stop->cursor, stop->reached_price); } if (const auto* stop_limit = std::get_if(&transition)) { @@ -1270,6 +1449,12 @@ Preparation WorkingRequestCore::prepare_trigger( return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, target}; } + const auto* stop_limit_px = std::get_if(&updated.request().trigger); + if (!stop_limit_px + || !stop_price_reached(is_buy, stop_limit_px->stop, stop_limit->reached_price)) { + return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, + target}; + } return emit_activated(ActivationKind::StopLimit, StopLimitLive{}, stop_limit->cursor, stop_limit->reached_price); } @@ -1281,6 +1466,12 @@ Preparation WorkingRequestCore::prepare_trigger( } return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, target}; } + const auto* trail = std::get_if(&updated.request().trigger); + double level = 0.0; + if (!trail || !trail_level_ok(track->best, trail->offset, is_buy, &level) + || !stop_price_reached(is_buy, level, trail_hit.reached_price)) { + return PreparationError{CoreFailure::UnsupportedTransition, EventId{identity_, 0}, target}; + } return emit_activated(ActivationKind::TrailTrigger, TrailActive{track->best}, trail_hit.cursor, trail_hit.reached_price); } @@ -1528,6 +1719,7 @@ InstallResult WorkingRequestCore::install_execution(PreparedExecution&& prepared const CommittedExecutionFacts& facts) noexcept { if (!prepared.impl_) return InstallError::StalePreparation; auto& impl = *prepared.impl_; + if (const auto error = validate_plan(impl.terminal)) return *error; if (facts.result.status != execution::Status::Applied) return InstallError::WrongCoreOrRun; if (facts.result.closed_units != impl.proposal.inspected_closed_units || facts.result.opened_units != impl.proposal.inspected_opened_units @@ -1596,8 +1788,10 @@ Preparation WorkingRequestCore::prepare_group_effect( return NoChange{NoChangeReason::NoTransition}; } uint64_t seen = 0; - if (receipt_seen(applied, recipient, member->effect, &seen)) { - return NoChange{NoChangeReason::AlreadyApplied}; + const ReceiptLookup lookup = receipt_lookup(applied, recipient, member->effect, &seen); + if (lookup == ReceiptLookup::Present) return NoChange{NoChangeReason::AlreadyApplied}; + if (lookup == ReceiptLookup::Conflict) { + return PreparationError{CoreFailure::ConflictingReceipt, applied, recipient}; } if (!receipts_.empty()) { const auto& last = receipts_.back(); @@ -1741,6 +1935,9 @@ Preparation WorkingRequestCore::prepare_owner_applied( if (!wait || wait->parent != payload->handle()) { return NoChange{NoChangeReason::NoTransition}; } + if (payload->ordinal <= live.birth().acceptance_ordinal) { + return NoChange{NoChangeReason::NoTransition}; + } const bool closing = as_reduce(live.request().intent) || as_flatten(live.request().intent); const bool opened = payload->opened_units != 0.0; if (closing && !opened && !payload->terminal) { @@ -1783,7 +1980,7 @@ Preparation WorkingRequestCore::prepare_owner_applied( } const int64_t cycle = payload->cycle_after; const Side side = side_from_opened(payload->opened_units); - if (!opening_alive(*observation, payload->handle(), cycle)) { + if (!opening_alive(*observation, payload->handle(), cycle, &side)) { return PreparationError{CoreFailure::ObservationMismatch, applied, child}; } @@ -1796,10 +1993,10 @@ Preparation WorkingRequestCore::prepare_owner_applied( if (!finite_positive(source)) { return PreparationError{CoreFailure::NonrepresentableQuantity, applied, child}; } - const auto ids = pending_receipt_ids(live.pending); + std::vector ids; double pending_total = 0.0; - if (const auto* deferred = std::get_if(&live.pending)) { - pending_total = deferred->total; + if (!collect_pending_chain(live.pending, child, &ids, &pending_total)) { + return PreparationError{CoreFailure::ConflictingReceipt, applied, child}; } const double deduct = std::min(pending_total, source); double after = source; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ecad2950..d0f173e8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,11 +8,12 @@ set(TEST_SOURCES test_native_order_terminal test_native_order_resting_core test_native_resting_contract + test_native_resting_acceptance + test_native_resting_driver_contract test_native_auto_path test_native_market_vertical test_native_market_vertical_contract test_native_calendar_driver_contract - test_native_auto_path test_native_host_repairs test_order_action_integration test_placement_facts diff --git a/tests/test_native_order_resting_core.cpp b/tests/test_native_order_resting_core.cpp index 6335929f..eee64051 100644 --- a/tests/test_native_order_resting_core.cpp +++ b/tests/test_native_order_resting_core.cpp @@ -121,6 +121,7 @@ EvaluationContext print_ctx(uint64_t point, int64_t time_ms = 1000, bool matchin EvaluationContext context; context.cursor.point.ordinal = point; context.cursor.point.effective_time_ms = time_ms; + context.cursor.point.provenance = pineforge::NativePriceProvenance::ObservedPrint; context.driver_class = DriverEligibilityClass::ObservedPrint; context.existing_matching_bit = matching; return context; @@ -155,7 +156,7 @@ uint64_t commit_mutation(WorkingRequestCore& core, Preparation ExecutionAppliedEvent fill_transact(WorkingRequestCore& core, const RequestHandle& handle, double filled, uint64_t& ordinal, double remaining_after, bool terminal, int64_t cycle_before, int64_t cycle_after, - double opened = 0.0) { + double opened = 0.0, double price = 100.0) { const auto* live = core.find_live(handle); CHECK(live); uint64_t point = 0; @@ -165,8 +166,8 @@ ExecutionAppliedEvent fill_transact(WorkingRequestCore& core, const RequestHandl ExecutionProposal proposal; proposal.cursor.point.ordinal = point; proposal.cursor.point.effective_time_ms = 1000; - proposal.raw_price = 100; - proposal.resolved_price = 100; + proposal.raw_price = price; + proposal.resolved_price = price; const auto* tx = std::get_if(&live->request().intent); const double signed_fill = (tx && tx->signed_units < 0.0) ? -filled : filled; proposal.physical_action = Transact{signed_fill}; @@ -201,7 +202,17 @@ ExecutionAppliedEvent fill_transact(WorkingRequestCore& core, const RequestHandl CHECK(std::holds_alternative(core.history().back())); const auto& applied = std::get(core.history().back()); CHECK(applied.terminal == terminal); - (void)remaining_after; + const auto* remaining_units = + std::get_if(&applied.remaining_after); + CHECK(remaining_units); + CHECK(remaining_units->q == remaining_after); + if (!terminal) { + const auto* live_now = core.find_live(handle); + CHECK(live_now); + CHECK(std::get(live_now->remaining).q == remaining_after); + } else { + CHECK(!core.find_live(handle)); + } return applied; } @@ -462,40 +473,27 @@ void owner_applied_wait_through_close_only() { CHECK(std::holds_alternative(core.find_live(*waiting.handle)->remaining)); } -void checked_reservation_failure() { - WorkingRequestCore core(kRun); - uint64_t inc = 1; - uint64_t ord = 1; - Request filler{Transact{std::numeric_limits::max()}, "f", ""}; - filler.group = Member{3, 1, GroupEffect::Reduce}; - const auto parent = core.submit(filler, 1, inc, ord); - Request child{Reduce{OwnerOpenedUnits{}}, "c", ""}; - child.owner = WaitForApplied{*parent.handle}; - child.group = Member{3, 2, GroupEffect::Reduce}; - const auto waiting = core.submit(child, 1, inc, ord); - auto eval = core.prepare_evaluation(*parent.handle, print_ctx(ord + 1), flat_book(), ord); +EventId fill_group_source(WorkingRequestCore& core, RequestHandle handle, double opened, + uint64_t& inc, uint64_t& ord) { + (void)inc; + auto eval = core.prepare_evaluation(handle, print_ctx(ord + 1), flat_book(), ord); commit_mutation(core, std::move(eval), ord); ExecutionProposal proposal; proposal.cursor.point.ordinal = - std::get(core.find_live(*parent.handle)->allowance).point_ordinal; + std::get(core.find_live(handle)->allowance).point_ordinal; proposal.cursor.point.effective_time_ms = 1000; proposal.raw_price = 100; proposal.resolved_price = 100; - proposal.physical_action = Transact{std::numeric_limits::max()}; + proposal.physical_action = Transact{opened}; proposal.scope = pineforge::execution::Book{}; proposal.inspected_closed_units = 0.0; - proposal.inspected_opened_units = std::numeric_limits::max(); + proposal.inspected_opened_units = opened; proposal.inspected_current_ticket = 1; - auto prep = core.prepare_execution(*parent.handle, proposal, ord); - if (std::holds_alternative(prep)) { - CHECK(std::get(prep).code - == pineforge::native_order::CoreFailure::NonrepresentableQuantity); - return; - } + auto prep = core.prepare_execution(handle, proposal, ord); CHECK(std::holds_alternative(prep)); CommittedExecutionFacts facts; facts.result.status = pineforge::execution::Status::Applied; - facts.result.opened_units = std::numeric_limits::max(); + facts.result.opened_units = opened; facts.result.current_ticket = 1; facts.cycle_after = 1; facts.post_target = long_book(1); @@ -503,13 +501,94 @@ void checked_reservation_failure() { auto installed = core.install_execution(std::get(std::move(prep)), facts); CHECK(std::holds_alternative(installed)); ord += std::get(installed).events.count; - const EventId cause{kRun, std::get(core.history().back()).ordinal}; - auto first = core.prepare_group_effect(cause, *waiting.handle, ord); - commit_mutation(core, std::move(first), ord); - Request filler2{Transact{std::numeric_limits::max()}, "f2", ""}; - filler2.group = Member{3, 1, GroupEffect::Reduce}; - const auto parent2 = core.submit(filler2, 1, inc, ord); - (void)parent2; + return EventId{kRun, std::get(core.history().back()).ordinal}; +} + +void checked_reservation_failure() { + const double maxv = std::numeric_limits::max(); + const double p60 = 0x1p60; + { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + Request filler{Transact{maxv}, "f", ""}; + filler.group = Member{3, 1, GroupEffect::Reduce}; + const auto parent = core.submit(filler, 1, inc, ord); + Request child{Reduce{OwnerOpenedUnits{}}, "c", ""}; + child.owner = WaitForApplied{*parent.handle}; + child.group = Member{3, 2, GroupEffect::Reduce}; + const auto waiting = core.submit(child, 1, inc, ord); + const EventId cause = fill_group_source(core, *parent.handle, maxv, inc, ord); + auto first = core.prepare_group_effect(cause, *waiting.handle, ord); + commit_mutation(core, std::move(first), ord); + CHECK(std::holds_alternative(core.find_live(*waiting.handle)->remaining)); + const auto* pending = + std::get_if(&core.find_live(*waiting.handle)->pending); + CHECK(pending); + CHECK(pending->total == maxv); + const std::size_t hist = core.history().size(); + Request filler2{Transact{maxv}, "f2", ""}; + filler2.group = Member{3, 1, GroupEffect::Reduce}; + const auto parent2 = core.submit(filler2, 1, inc, ord); + const EventId cause2 = fill_group_source(core, *parent2.handle, maxv, inc, ord); + auto second = core.prepare_group_effect(cause2, *waiting.handle, ord); + CHECK(std::holds_alternative(second)); + CHECK(std::get(second).code + == pineforge::native_order::CoreFailure::UnrepresentableReservation); + CHECK(core.history().size() == hist + 2); + CHECK(std::get( + core.find_live(*waiting.handle)->pending) + .total + == maxv); + } + { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + Request filler{Transact{p60}, "f", ""}; + filler.group = Member{4, 1, GroupEffect::Reduce}; + const auto parent = core.submit(filler, 1, inc, ord); + Request child{Reduce{OwnerOpenedUnits{}}, "c", ""}; + child.owner = WaitForApplied{*parent.handle}; + child.group = Member{4, 2, GroupEffect::Reduce}; + const auto waiting = core.submit(child, 1, inc, ord); + const EventId cause = fill_group_source(core, *parent.handle, p60, inc, ord); + commit_mutation(core, core.prepare_group_effect(cause, *waiting.handle, ord), ord); + Request filler2{Transact{1.0}, "f2", ""}; + filler2.group = Member{4, 1, GroupEffect::Reduce}; + const auto parent2 = core.submit(filler2, 1, inc, ord); + const EventId cause2 = fill_group_source(core, *parent2.handle, 1.0, inc, ord); + auto second = core.prepare_group_effect(cause2, *waiting.handle, ord); + CHECK(std::holds_alternative(second)); + CHECK(std::get(second).code + == pineforge::native_order::CoreFailure::UnrepresentableReservation); + CHECK(std::get( + core.find_live(*waiting.handle)->pending) + .total + == p60); + } + { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + Request recipient{Reduce{ExplicitUnits{p60}}, "r", ""}; + recipient.group = Member{5, 2, GroupEffect::Reduce}; + const auto dest = core.submit(recipient, 1, inc, ord); + auto bind = core.prepare_evaluation(*dest.handle, print_ctx(ord + 1), long_book(1), ord); + commit_mutation(core, std::move(bind), ord); + CHECK(std::get(core.find_live(*dest.handle)->remaining).q == p60); + Request filler{Transact{1.0}, "f", ""}; + filler.group = Member{5, 1, GroupEffect::Reduce}; + const auto parent = core.submit(filler, 1, inc, ord); + const EventId cause = fill_group_source(core, *parent.handle, 1.0, inc, ord); + const std::size_t hist = core.history().size(); + auto reduce = core.prepare_group_effect(cause, *dest.handle, ord); + CHECK(std::holds_alternative(reduce)); + CHECK(std::get(reduce).code + == pineforge::native_order::CoreFailure::UnrepresentableReservation); + CHECK(core.history().size() == hist); + CHECK(std::get(core.find_live(*dest.handle)->remaining).q == p60); + } } void stale_prepare_and_parent_terminal() { @@ -550,11 +629,11 @@ void stop_activation_and_receipt_dedup() { ActivateStop hit; hit.cursor = print_ctx(ord).cursor; hit.reached_price = 101; - auto trig = core.prepare_trigger(*accepted.handle, hit, ord); + auto trig = core.prepare_trigger(*accepted.handle, hit, DriverEligibilityClass::ObservedPrint, ord); commit_mutation(core, std::move(trig), ord); CHECK(std::holds_alternative( core.find_live(*accepted.handle)->trigger_state)); - auto again = core.prepare_trigger(*accepted.handle, hit, ord); + auto again = core.prepare_trigger(*accepted.handle, hit, DriverEligibilityClass::ObservedPrint, ord); CHECK(std::holds_alternative(again)); } @@ -686,6 +765,287 @@ void long_key_install_and_eligibility_no_alloc() { CHECK(conv.status == SubmitStatus::Accepted); CHECK(conv.handle->run.session_key.size() == 80); } + +void review_failure_before_witnesses() { + { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + const auto parent = core.submit(market_tx(2.0, "p"), 1, inc, ord); + Request child{Reduce{OwnerOpenedUnits{}}, "c", ""}; + child.owner = WaitForApplied{*parent.handle}; + child.capacity = PointBudget{1}; + const auto accepted = core.submit(child, 1, inc, ord); + CHECK(accepted.status == SubmitStatus::Accepted); + CHECK(std::holds_alternative(core.find_live(*accepted.handle)->remaining)); + } + { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + Request r{Transact{1.0}, "bad-group", ""}; + r.group = Member{1, 0, static_cast(9)}; + const auto rejected = core.submit(r, 1, inc, ord); + CHECK(rejected.status == SubmitStatus::Rejected); + CHECK(rejected.reason == RequestRejectReason::InvalidGroup); + CHECK(rejected.event_ordinal != 0); + } + { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + RequestHandle owner{kRun, 44}; + Request r{Reduce{ExplicitUnits{1}}, "bind", ""}; + r.owner = BindOpening{owner, 7}; + CommandContext ctx; + ctx.opening = OpeningObservation{owner, 7, PositionNonflat{8, Side::Long}, true}; + auto prepared = core.prepare_submit(r, ctx, inc, ord); + CHECK(prepared.predicted().status == SubmitStatus::Rejected); + CHECK(prepared.predicted().reason == RequestRejectReason::InvalidOwner); + core.install_submit(std::move(prepared)); + CHECK(core.live().empty()); + } + { + WorkingRequestCore core(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + Request parent_req{Transact{2.0}, "p", ""}; + parent_req.capacity = PointBudget{1.0}; + const auto parent = core.submit(parent_req, 1, inc, ord); + auto eval = core.prepare_evaluation(*parent.handle, print_ctx(ord + 1), flat_book(), ord); + commit_mutation(core, std::move(eval), ord); + fill_transact(core, *parent.handle, 1.0, ord, 1.0, false, 0, 7, 1.0); + const EventId cause{kRun, std::get(core.history().back()).ordinal}; + Request child{Transact{1.0}, "late", ""}; + child.owner = WaitForApplied{*parent.handle}; + const auto h = core.submit(child, 1, inc, ord); + CHECK(h.handle->incarnation > 0); + auto arm = core.prepare_owner_applied(cause, *h.handle, std::nullopt, ord); + CHECK(std::holds_alternative(arm)); + CHECK(std::get(arm).reason == NoChangeReason::NoTransition); + CHECK(std::holds_alternative(core.find_live(*h.handle)->authority)); + CHECK(core.find_live(*h.handle)->birth().acceptance_ordinal > cause.ordinal); + } + { + WorkingRequestCore a(kRun), b(kRun); + uint64_t ai = 1, ao = 1, bi = 1, bo = 1; + Request r{Transact{2.0}, "partial", ""}; + r.capacity = PointBudget{1.0}; + const auto ha = a.submit(r, 0, ai, ao); + auto eval = a.prepare_evaluation(*ha.handle, print_ctx(ao + 1), flat_book(), ao); + commit_mutation(a, std::move(eval), ao); + ExecutionProposal proposal; + proposal.cursor.point.ordinal = + std::get(a.find_live(*ha.handle)->allowance).point_ordinal; + proposal.cursor.point.effective_time_ms = 1000; + proposal.raw_price = proposal.resolved_price = 100; + proposal.physical_action = Transact{1.0}; + proposal.inspected_opened_units = 1.0; + auto prep = a.prepare_execution(*ha.handle, proposal, ao); + CHECK(std::holds_alternative(prep)); + auto token = std::get(std::move(prep)); + CommittedExecutionFacts facts; + facts.result.status = pineforge::execution::Status::Applied; + facts.result.opened_units = 1.0; + facts.cycle_after = 7; + facts.post_target = long_book(7); + facts.committed_action = Transact{1.0}; + g_allocs.store(0); + auto wrong = b.install_execution(std::move(token), facts); + CHECK(g_allocs.load() == 0); + CHECK(std::holds_alternative(wrong)); + CHECK(std::get(wrong) == InstallError::WrongCoreOrRun); + CHECK(a.live().size() == 1); + CHECK(a.find_live(*ha.handle)->definition); + g_allocs.store(0); + auto retry = a.install_execution(std::move(token), facts); + CHECK(g_allocs.load() == 0); + CHECK(std::holds_alternative(retry)); + CHECK(std::get_if(&a.history().back())->definition); + auto consumed = a.install_execution(std::move(token), facts); + CHECK(std::holds_alternative(consumed)); + CHECK(std::get(consumed) == InstallError::AlreadyConsumed); + (void)bi; + (void)bo; + } + { + WorkingRequestCore src(kRun); + uint64_t inc = 1; + uint64_t ord = 1; + Request r{Transact{2.0}, "m", ""}; + r.capacity = PointBudget{1.0}; + const auto h = src.submit(r, 0, inc, ord); + auto eval = src.prepare_evaluation(*h.handle, print_ctx(ord + 1), flat_book(), ord); + commit_mutation(src, std::move(eval), ord); + ExecutionProposal proposal; + proposal.cursor.point.ordinal = + std::get(src.find_live(*h.handle)->allowance).point_ordinal; + proposal.cursor.point.effective_time_ms = 1000; + proposal.raw_price = proposal.resolved_price = 100; + proposal.physical_action = Transact{1.0}; + proposal.inspected_opened_units = 1.0; + auto prep = src.prepare_execution(*h.handle, proposal, ord); + auto token = std::get(std::move(prep)); + WorkingRequestCore dst = std::move(src); + CommittedExecutionFacts facts; + facts.result.status = pineforge::execution::Status::Applied; + facts.result.opened_units = 1.0; + facts.cycle_after = 7; + facts.post_target = long_book(7); + facts.committed_action = Transact{1.0}; + auto moved = dst.install_execution(std::move(token), facts); + CHECK(std::holds_alternative(moved)); + CHECK(std::get(moved) == InstallError::WrongCoreOrRun); + CHECK(dst.find_live(*h.handle)); + } +} +void chronological_pending_receipts() { + WorkingRequestCore core(kRun); + uint64_t inc = 1, ord = 1; + Request parent_request{Transact{1.0}, "late-opening", ""}; + parent_request.trigger = Limit{50.0}; + const auto parent = core.submit(parent_request, 0, inc, ord); + Request child_request{Reduce{OwnerOpenedUnits{}}, "decimal-pending", ""}; + child_request.owner = WaitForApplied{*parent.handle}; + child_request.group = Member{7, 2, GroupEffect::Reduce}; + const auto child = core.submit(child_request, 0, inc, ord); + std::vector expected_receipts; + for (double quantity : {0.1, 0.2, 0.3}) { + Request source_request{Transact{quantity}, "adjustment", ""}; + source_request.group = Member{7, 1, GroupEffect::Reduce}; + const auto source = core.submit(source_request, 0, inc, ord); + const uint64_t point = ord++; + commit_mutation(core, core.prepare_evaluation(*source.handle, print_ctx(point), + long_book(7), ord), ord); + const auto applied = fill_transact(core, *source.handle, quantity, ord, 0.0, + true, 7, 7, quantity); + const EventId cause{kRun, applied.ordinal}; + commit_mutation(core, core.prepare_group_effect(cause, *child.handle, ord), ord); + const auto& receipt = std::get(core.history().back()); + CHECK(receipt.cause == cause); + CHECK(receipt.deferred_delta == quantity); + expected_receipts.push_back(EventId{kRun, receipt.ordinal}); + } + const auto& pending = std::get( + core.find_live(*child.handle)->pending); + CHECK(pending.total == 0x1.3333333333334p-1); + CHECK(pending.count == 3); + const uint64_t point = ord++; + commit_mutation(core, core.prepare_evaluation(*parent.handle, print_ctx(point), + long_book(7), ord), ord); + const auto applied = fill_transact(core, *parent.handle, 1.0, ord, 0.0, true, 7, 7, 1.0, 50.0); + const OpeningObservation opening{*parent.handle, 7, PositionNonflat{7, Side::Long}, true}; + commit_mutation(core, core.prepare_owner_applied(EventId{kRun, applied.ordinal}, + *child.handle, opening, ord), ord); + const auto& bound = std::get(core.history()[core.history().size() - 2]); + CHECK(bound.prior_adjustment_ids == expected_receipts); + CHECK(bound.pending_total == 0x1.3333333333334p-1); + CHECK(bound.source_units == 1.0); + CHECK(bound.effective_deduction == 0x1.3333333333334p-1); + CHECK(std::get(core.find_live(*child.handle)->remaining).q + == 0x1.9999999999998p-2); +} + +void reduce_to_flatten_receipt_replay() { + WorkingRequestCore core(kRun); + uint64_t inc = 1, ord = 1; + Request source_request{Transact{1.0}, "reduce-source", ""}; + source_request.group = Member{7, 1, GroupEffect::Reduce}; + const auto source = core.submit(source_request, 0, inc, ord); + Request target_request{Flatten{}, "all-scope", ""}; + target_request.group = Member{7, 2, GroupEffect::Cancel}; + const auto target = core.submit(target_request, 0, inc, ord); + const uint64_t point = ord++; + commit_mutation(core, core.prepare_evaluation(*source.handle, print_ctx(point), + long_book(7), ord), ord); + const auto applied = fill_transact(core, *source.handle, 1.0, ord, 0.0, true, 7, 7, 1.0); + const EventId cause{kRun, applied.ordinal}; + commit_mutation(core, core.prepare_group_effect(cause, *target.handle, ord), ord); + const auto& cancelled = std::get(core.history().back()); + CHECK(cancelled.reason == CancelReason::Group); + CHECK(cancelled.cause == cause); + CHECK(std::holds_alternative( + cancelled.unexecuted)); + CHECK(!core.find_live(*target.handle)); + const std::size_t history_size = core.history().size(); + const uint64_t next = ord; + auto replay = core.prepare_group_effect(cause, *target.handle, ord); + CHECK(std::holds_alternative(replay)); + CHECK(std::get(replay).reason == NoChangeReason::AlreadyApplied); + CHECK(core.history().size() == history_size); + CHECK(ord == next); +} + +void explicit_trigger_driver_and_favorable_arm() { + using pineforge::NativeCompletionKind; + using pineforge::NativePriceProvenance; + using pineforge::NativePathPhase; + for (auto completion : {NativeCompletionKind::Confirmed, NativeCompletionKind::PartialFinalized}) { + WorkingRequestCore core(kRun); + uint64_t inc = 1, ord = 1; + Request request{Transact{1.0}, "tick-aftercalc", ""}; + request.trigger = Stop{100.0}; + const auto accepted = core.submit(request, 0, inc, ord); + auto context = print_ctx(ord++); + context.cursor.point.provenance = NativePriceProvenance::AfterCalculationClose; + context.cursor.point.path_phase = NativePathPhase::Close; + context.cursor.point.completion = completion; + context.driver_class = DriverEligibilityClass::TickAfterCalculation; + commit_mutation(core, core.prepare_evaluation(*accepted.handle, context, flat_book(), ord), ord); + const auto size = core.history().size(); + const ActivateStop valid_hit{context.cursor, 101.0}; + auto bar_aftercalc = core.prepare_trigger(*accepted.handle, valid_hit, + DriverEligibilityClass::ConfirmedAfterCalculationClose, ord); + CHECK(std::holds_alternative(bar_aftercalc)); + CHECK(std::get(bar_aftercalc).reason == NoChangeReason::NotEligible); + auto contradictory = core.prepare_trigger(*accepted.handle, valid_hit, + DriverEligibilityClass::ObservedPrint, ord); + CHECK(std::holds_alternative(contradictory)); + auto carried_hit = valid_hit; + carried_hit.cursor.point.provenance = NativePriceProvenance::CarriedOpen; + auto carried = core.prepare_trigger(*accepted.handle, carried_hit, + DriverEligibilityClass::CarriedOpen, ord); + CHECK(std::holds_alternative(carried)); + auto disguised_carried = core.prepare_trigger(*accepted.handle, carried_hit, + DriverEligibilityClass::TickAfterCalculation, ord); + CHECK(std::holds_alternative(disguised_carried)); + auto before_birth = valid_hit; + before_birth.cursor.point.ordinal = accepted.event_ordinal; + auto early = core.prepare_trigger(*accepted.handle, before_birth, + DriverEligibilityClass::TickAfterCalculation, ord); + CHECK(std::holds_alternative(early)); + auto below_stop = valid_hit; + below_stop.reached_price = 99.0; + auto not_reached = core.prepare_trigger(*accepted.handle, below_stop, + DriverEligibilityClass::TickAfterCalculation, ord); + CHECK(std::holds_alternative(not_reached)); + CHECK(core.history().size() == size); + commit_mutation(core, core.prepare_trigger(*accepted.handle, valid_hit, + DriverEligibilityClass::TickAfterCalculation, ord), ord); + CHECK(std::holds_alternative( + core.find_live(*accepted.handle)->trigger_state)); + } + for (double sign : {-1.0, 1.0}) { + WorkingRequestCore core(kRun); + uint64_t inc = 1, ord = 1; + Request request{Transact{sign}, "trail-arm", ""}; + request.trigger = pineforge::native_order::Trail{2.0, sign < 0.0 ? 105.0 : 95.0}; + const auto accepted = core.submit(request, 0, inc, ord); + auto context = print_ctx(ord++); + commit_mutation(core, core.prepare_evaluation(*accepted.handle, context, flat_book(), ord), ord); + const double wrong_side = sign < 0.0 ? 104.0 : 96.0; + auto refused = core.prepare_trigger(*accepted.handle, + pineforge::native_order::BeginTrailTracking{context.cursor, wrong_side}, + DriverEligibilityClass::ObservedPrint, ord); + CHECK(std::holds_alternative(refused)); + const double favorable = sign < 0.0 ? 106.0 : 94.0; + commit_mutation(core, core.prepare_trigger(*accepted.handle, + pineforge::native_order::BeginTrailTracking{context.cursor, favorable}, + DriverEligibilityClass::ObservedPrint, ord), ord); + CHECK(std::get( + core.find_live(*accepted.handle)->trigger_state).best == favorable); + } +} } // namespace int main() { @@ -702,6 +1062,10 @@ int main() { wrong_core_token_and_move_invalidation(); market_only_committed_rejection(); long_key_install_and_eligibility_no_alloc(); + review_failure_before_witnesses(); + chronological_pending_receipts(); + reduce_to_flatten_receipt_replay(); + explicit_trigger_driver_and_favorable_arm(); std::printf("native order resting core: %d checks, %d failures\n", checks, failures); return failures == 0 ? 0 : 1; } diff --git a/tests/test_native_resting_acceptance.cpp b/tests/test_native_resting_acceptance.cpp new file mode 100644 index 00000000..f94bb081 --- /dev/null +++ b/tests/test_native_resting_acceptance.cpp @@ -0,0 +1,434 @@ +// Bounded R2 v5 acceptance: real native commands, drivers, book and observations. +// No direct settlement, injected lots, synthetic committed facts or source commands. +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; +namespace { +int checks = 0, failures = 0, cases = 0; +const char* scenario = "setup"; +struct StopCase {}; +#define CHECK(x) do { ++checks; if (!(x)) { ++failures; \ + std::printf("FAIL %s:%d %s\n", scenario, __LINE__, #x); } } while (0) +#define REQUIRE(x) do { const bool ok_ = bool(x); CHECK(ok_); if (!ok_) throw StopCase{}; } while (0) + +constexpr int64_t T = 1736121600000LL; + +void near(double actual, double expected) { + const bool ok = std::isfinite(actual) && std::isfinite(expected) + && std::abs(actual - expected) <= 1e-12 * std::max(1.0, std::abs(expected)); + if (!ok) std::printf(" actual=%.17g expected=%.17g\n", actual, expected); + CHECK(ok); +} + +struct Host final : NativeStrategyHost { + std::function beginning; + std::function calculation; + int calculations = 0; + uint64_t sequence = 0; + + void on_native_run_begin() override { if (beginning) beginning(*this); } + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + ++calculations; + if (calculation) calculation(*this); + } + bool input(int64_t offset, double price) { + return stream_push_tick(TradeTick{T + offset + 1, ++sequence, price, 1}); + } + void tick(int64_t offset, double price) { + REQUIRE(input(offset, price)); + REQUIRE(native_state().kind == NativeLifecycleKind::Running); + } + // Read-only physical observations; setup always uses the native producer. + const std::vector& lots() const { return pyramid_entries_; } +}; + +NativeRunSpec configuration(const char* key, uint64_t run = 1, double fee = 0) { + NativeRunSpec s; + s.identity = {key, run}; s.input_tf = "1"; s.script_tf = "1"; + s.ticker = "N"; s.tickerid = "TEST:N"; s.type = "crypto"; + s.currency = "USD"; s.basecurrency = "USD"; s.description = "R2 acceptance"; + s.volumetype = "base"; s.timezone = "UTC"; s.session = "24x7"; + s.initial_capital = 10000; s.point_value = 1; s.account_fx = 1; + s.price_tick = .01; s.fee_kind = NativeFeeKind::CashPerExecution; s.fee_value = fee; + return s; +} + +void start(Host& h, const char* key, uint64_t run = 1, double fee = 0, + double first_price = 100) { + REQUIRE(h.configure_native(configuration(key, run, fee)).status == NativeSetupStatus::Applied); + auto beginning = std::exchange(h.beginning, {}); + const Bar warmup{first_price, first_price, first_price, first_price, 1, T - 60000}; + h.sequence = 0; + REQUIRE(h.stream_begin(&warmup, 1, "1", "1")); + REQUIRE(h.stream_push_tick(TradeTick{T, ++h.sequence, first_price, 0})); + h.beginning = std::move(beginning); + if (h.beginning) h.beginning(h); + REQUIRE(h.native_state().kind == NativeLifecycleKind::Running); +} + +void finish(Host& h) { + REQUIRE(h.stream_end(false)); + CHECK(h.native_state().kind == NativeLifecycleKind::Completed); +} + +no::Request tx(double q, const char* label = "") { return {no::Transact{q}, label, ""}; } +no::Request reduce(double q, const char* label = "") { + return {no::Reduce{no::ExplicitUnits{q}}, label, ""}; +} +no::RequestHandle put(Host& h, const no::Request& request) { + auto result = h.submit(request); + REQUIRE(result.status == no::SubmitStatus::Accepted); + REQUIRE(result.handle.has_value()); + return *result.handle; +} + +template std::vector events(const Host& h) { + std::vector result; + for (const auto& event : h.native_events(0)) { + if (event.command) if (const auto* value = std::get_if(&*event.command)) + result.push_back(*value); + } + return result; +} +std::vector fills(const Host& h, const no::RequestHandle& handle) { + std::vector result; + for (const auto& event : events(h)) { + REQUIRE(event.definition); + if (event.definition->handle == handle) result.push_back(event); + } + return result; +} +const PyramidEntry& physical_lot(const Host& h, const no::RequestHandle& handle) { + const auto found = std::find_if(h.lots().begin(), h.lots().end(), [&](const auto& value) { + return value.entry_incarnation == handle.incarnation; + }); + REQUIRE(found != h.lots().end()); + return *found; +} +int64_t opening_cycle(const Host& h, const no::RequestHandle& handle) { + const auto f = fills(h, handle); + REQUIRE(!f.empty()); + REQUIRE(f.front().opened_units != 0); + REQUIRE(f.front().cycle_after > 0); + return f.front().cycle_after; +} +void check_scope(const no::ExecutionAppliedEvent& event, + const no::RequestHandle& opening, int64_t cycle) { + const auto* scope = std::get_if(&event.scope); + REQUIRE(scope); + CHECK(scope->incarnation == opening.incarnation); + CHECK(scope->cycle == cycle); +} + +void scoped_cost_and_terminal_enrollment(double sign) { + Host h; + start(h, sign > 0 ? "S1-O6-long" : "S1-O6-short", 1, 6); + // A3/cost6 -> reduce A1 -> A2/cost4. Then B3/cost6, behind A. + const auto a = put(h, tx(sign * 3, "A")); h.tick(0, 100); + put(h, reduce(1, "prepare-A2")); h.tick(1, 100); + near(physical_lot(h, a).qty, 2); near(physical_lot(h, a).entry_commission_account, 4); + const auto b = put(h, tx(sign * 3, "B")); h.tick(2, 100 + sign * 10); + const auto cycle = opening_cycle(h, b); + CHECK(cycle == opening_cycle(h, a)); + REQUIRE(h.lots().size() == 2); + const PyramidEntry a_before = physical_lot(h, a); + + auto wait = reduce(1, "late-Wait"); wait.owner = no::WaitForApplied{b}; + const auto refused = h.submit(wait); + CHECK(refused.status == no::SubmitStatus::Rejected && !refused.handle); + CHECK(refused.reason == no::RequestRejectReason::InvalidOwner); + auto bound = reduce(1, "B-only"); bound.owner = no::BindOpening{b, cycle}; + const auto child = put(h, bound); + const int first_row = h.trade_count(); + h.tick(3, 100 + sign * 20); + const auto f = fills(h, child); + REQUIRE(f.size() == 1); check_scope(f.front(), b, cycle); + CHECK(f.front().closed_units == 1 && f.front().opened_units == 0 && f.front().terminal); + CHECK(f.front().current_ticket == 6 && f.front().closed_trade_count == 1); + CHECK(f.front().first_trade_index == static_cast(first_row)); + REQUIRE(h.trade_count() == first_row + 1); + const Trade& row = h.get_trade(first_row); + CHECK(row.entry_incarnation == b.incarnation && row.is_long == (sign > 0)); + near(row.entry_price, 100 + sign * 10); near(row.exit_price, 100 + sign * 20); + near(row.qty, 1); near(row.commission, 8); near(row.pnl, 2); + near(physical_lot(h, b).qty, 2); near(physical_lot(h, b).entry_commission_account, 4); + const auto& a_after = physical_lot(h, a); + CHECK(a_after.entry_incarnation == a_before.entry_incarnation); + CHECK(a_after.entry_id == a_before.entry_id && a_after.time == a_before.time); + CHECK(a_after.price == a_before.price && a_after.qty == a_before.qty); + CHECK(a_after.entry_commission_account == a_before.entry_commission_account); + near(h.physical_position().signed_units, sign * 4); + near(h.physical_position().average_price, 100 + sign * 5); + finish(h); +} + +void foreign_run_bind(double sign) { + Host h; + const char* key = sign > 0 ? "V2-long" : "V2-short"; + start(h, key, 1); + const auto saved = put(h, tx(sign * 2)); h.tick(0, 100); + const auto saved_cycle = opening_cycle(h, saved); + finish(h); + start(h, key, 2); + const auto current = put(h, tx(sign * 2)); h.tick(0, 100); + const auto cycle = opening_cycle(h, current); + REQUIRE(saved.incarnation == current.incarnation && saved_cycle == cycle); + CHECK(saved.run.run_number == 1 && current.run.run_number == 2); + auto bad = reduce(1); bad.owner = no::BindOpening{saved, cycle}; + const auto refusal = h.submit(bad); + CHECK(refusal.status == no::SubmitStatus::Rejected && !refusal.handle); + CHECK(refusal.reason == no::RequestRejectReason::InvalidOwner); + near(h.physical_position().signed_units, sign * 2); + CHECK(h.trade_count() == 0); + bad.owner = no::BindOpening{current, cycle}; + const auto child = put(h, bad); h.tick(1, 100); + const auto f = fills(h, child); REQUIRE(f.size() == 1); + check_scope(f.front(), current, cycle); + near(h.physical_position().signed_units, sign); finish(h); +} + +void owner_remaining_ohlc(double sign) { + Host h; no::RequestHandle a, parent, child; + h.beginning = [&](Host& self) { a = put(self, tx(sign * 2, "A")); }; + h.calculation = [&](Host& self) { + if (self.calculations != 1) return; + parent = put(self, tx(sign, "B-parent")); + auto r = reduce(1, "B-stop"); r.owner = no::WaitForApplied{parent}; + r.trigger = no::Stop{100 - sign}; child = put(self, r); + }; + REQUIRE(h.configure_native(configuration(sign > 0 ? "O1-long" : "O1-short")).status + == NativeSetupStatus::Applied); + const Bar tape[] = {{100,100,100,100,1,T}, + sign > 0 ? Bar{100,105,90,100,1,T+60000} : Bar{100,110,95,100,1,T+60000}}; + h.run(tape, 2); + REQUIRE(h.native_state().kind == NativeLifecycleKind::Completed); + const auto pf = fills(h, parent), cf = fills(h, child); + REQUIRE(pf.size() == 1 && cf.size() == 1); + CHECK(child.incarnation > parent.incarnation); + CHECK(pf[0].cycle_after == opening_cycle(h, a)); + check_scope(cf[0], parent, pf[0].cycle_after); + CHECK(cf[0].ordinal > pf[0].ordinal); + CHECK(cf[0].cursor.point.ordinal > pf[0].cursor.point.ordinal); + CHECK(cf[0].cursor.point.path_phase == (sign > 0 ? NativePathPhase::Low : NativePathPhase::High)); + CHECK(cf[0].cursor.point.provenance == NativePriceProvenance::Confirmed); + near(cf[0].cursor.t, .4); near(cf[0].raw_price, 100 - sign); + near(cf[0].closed_units, 1); CHECK(cf[0].terminal); + REQUIRE(h.trade_count() == 1); + CHECK(h.get_trade(0).entry_incarnation == parent.incarnation); + near(h.get_trade(0).pnl, -1); + REQUIRE(h.lots().size() == 1); + CHECK(h.lots()[0].entry_incarnation == a.incarnation); + near(h.physical_position().signed_units, sign * 2); +} + +void chronological_crossings(double sign, bool unrelated) { + Host h; no::RequestHandle later, earlier, other; + h.beginning = [&](Host& self) { + if (unrelated) { + auto idle = tx(sign); idle.trigger = no::Limit{sign > 0 ? 80.0 : 120.0}; + other = put(self, idle); + } + auto late = tx(sign, "lower-incarnation-later-hit"); late.trigger = no::Stop{100 + sign * 4}; + later = put(self, late); + auto early = tx(sign, "higher-incarnation-earlier-hit"); early.trigger = no::Stop{100 + sign * 2}; + earlier = put(self, early); + }; + REQUIRE(h.configure_native(configuration(sign > 0 ? "chronology-long" : "chronology-short")).status + == NativeSetupStatus::Applied); + const Bar bar = sign > 0 ? Bar{100,106,90,100,1,T} : Bar{100,110,94,100,1,T}; + h.run(&bar, 1); REQUIRE(h.native_state().kind == NativeLifecycleKind::Completed); + const auto first = fills(h, earlier), second = fills(h, later); + REQUIRE(first.size() == 1 && second.size() == 1); + CHECK(later.incarnation < earlier.incarnation && first[0].ordinal < second[0].ordinal); + CHECK(first[0].cursor.point.ordinal == second[0].cursor.point.ordinal); + CHECK(first[0].cursor.point.path_phase == (sign > 0 ? NativePathPhase::High : NativePathPhase::Low)); + CHECK(first[0].cursor.point.effective_time_ms == second[0].cursor.point.effective_time_ms); + CHECK(first[0].cursor.point.provenance == NativePriceProvenance::Confirmed); + near(first[0].cursor.t, 1.0/3.0); near(second[0].cursor.t, 2.0/3.0); + near(first[0].raw_price, 100 + sign * 2); near(second[0].raw_price, 100 + sign * 4); + near(h.physical_position().signed_units, sign * 2); + near(h.physical_position().average_price, 100 + sign * 3); + if (unrelated) CHECK(fills(h, other).empty()); +} + +void flat_incarnation_order(double sign, bool transaction_first) { + Host h; no::RequestHandle opening, closing; + h.beginning = [&](Host& self) { + if (transaction_first) { opening = put(self, tx(sign)); closing = put(self, reduce(1)); } + else { closing = put(self, reduce(1)); opening = put(self, tx(sign)); } + }; + REQUIRE(h.configure_native(configuration(sign > 0 ? "C9-long" : "C9-short")).status + == NativeSetupStatus::Applied); + const Bar bar{100,100,100,100,1,T}; h.run(&bar,1); + REQUIRE(h.native_state().kind == NativeLifecycleKind::Completed); + const auto opened = fills(h, opening), closed = fills(h, closing); + REQUIRE(opened.size() == 1); + const auto no_effect = events(h); + if (transaction_first) { + REQUIRE(closed.size() == 1); CHECK(no_effect.empty()); + CHECK(opened[0].ordinal < closed[0].ordinal); + CHECK(opened[0].cursor.point.ordinal == closed[0].cursor.point.ordinal); + CHECK(closed[0].closed_units == 1 && closed[0].terminal); + CHECK(h.trade_count() == 1); near(h.physical_position().signed_units,0); + } else { + CHECK(closed.empty()); REQUIRE(no_effect.size() == 1); + REQUIRE(no_effect[0].definition); CHECK(no_effect[0].definition->handle == closing); + CHECK(no_effect[0].ordinal < opened[0].ordinal); + CHECK(h.trade_count() == 0); near(h.physical_position().signed_units,sign); + } +} + +void failed_is_permanent(Host& h, int64_t next_offset, double price) { + REQUIRE(h.native_state().kind == NativeLifecycleKind::Failed); + const auto before = h.native_state().failure; + const auto hash = h.native_continuation_hash(); + const auto count = h.native_events(0).size(); + const auto position = h.physical_position(); + CHECK(!h.input(next_offset, price)); + CHECK(!h.stream_advance_time(T + next_offset + 100)); + CHECK(!h.stream_end(false)); + bool threw = false; + try { (void)h.submit(tx(1)); } catch (const std::exception&) { threw = true; } + CHECK(threw); + const auto after = h.native_state(); + CHECK(after.kind == NativeLifecycleKind::Failed); + CHECK(after.failure.code == before.code && after.failure.operation == before.operation); + CHECK(after.failure.ordinal == before.ordinal && after.failure.discriminator == before.discriminator); + CHECK(after.failure.context.kind == before.context.kind); + CHECK(after.failure.context.cause.ordinal == before.context.cause.ordinal); + CHECK(after.failure.context.recipient.incarnation == before.context.recipient.incarnation); + CHECK(h.native_events(0).size() == count); + CHECK(h.native_continuation_hash() == hash); + CHECK(h.physical_position().signed_units == position.signed_units); + CHECK(h.physical_position().lot_count == position.lot_count); +} + +void capacity_progress_failure(double sign) { + Host h; start(h, sign > 0 ? "C8-long" : "C8-short"); + auto r = tx(sign * 0x1p60); r.capacity = no::PointBudget{1}; + const auto handle = put(h,r); + CHECK(!h.input(0,100)); REQUIRE(h.native_state().kind == NativeLifecycleKind::Failed); + const auto failure = h.native_state().failure; + CHECK(failure.code == NativeFailureCode::SettlementFailure); + CHECK(failure.operation == NativeFailureOperation::Settlement); + CHECK(failure.discriminator == static_cast(no::CoreFailure::NonrepresentableQuantity)); + CHECK(native_failure_has_recipient(failure.context)); + CHECK(failure.context.recipient.incarnation == handle.incarnation); + CHECK(fills(h,handle).empty()); CHECK(events(h).empty()); + CHECK(h.trade_count() == 0); near(h.physical_position().signed_units,0); + near(h.native_marked_equity(100),10000); failed_is_permanent(h,1,100); +} + +void check_group_failure(Host& h, const no::RequestHandle& filler, + const no::RequestHandle& recipient) { + REQUIRE(h.native_state().kind == NativeLifecycleKind::Failed); + const auto f = fills(h,filler); REQUIRE(f.size() == 1); + CHECK(f[0].terminal); + const auto failure = h.native_state().failure; + CHECK(failure.code == NativeFailureCode::SettlementFailure); + CHECK(failure.operation == NativeFailureOperation::Settlement); + CHECK(failure.discriminator == static_cast(no::CoreFailure::UnrepresentableReservation)); + CHECK(native_failure_has_cause(failure.context) && native_failure_has_recipient(failure.context)); + CHECK(failure.context.cause.ordinal == f[0].ordinal); + CHECK(failure.context.recipient.incarnation == recipient.incarnation); + CHECK(fills(h,recipient).empty()); + bool account_seen = false; + for (const auto& event : h.native_events(0)) { + if (event.account && event.account->ordinal == f[0].ordinal) account_seen = true; + } + CHECK(account_seen); +} + +void group_subtraction_failure(double sign) { + Host h; start(h,sign > 0 ? "G5-subtract-long" : "G5-subtract-short",1,2); + auto waiting = tx(sign * 0x1p60,"large-recipient"); + waiting.trigger = no::Limit{sign > 0 ? 1.0 : 200.0}; + waiting.group = no::Member{7,2,no::GroupEffect::Cancel}; + const auto recipient = put(h,waiting); + auto emit = tx(sign,"committed-filler"); emit.group = no::Member{7,1,no::GroupEffect::Reduce}; + const auto filler = put(h,emit); + CHECK(!h.input(0,100)); check_group_failure(h,filler,recipient); + REQUIRE(fills(h,filler).size() == 1); + CHECK(fills(h,filler)[0].filled_working == 1 && fills(h,filler)[0].current_ticket == 2); + CHECK(events(h).empty()); + CHECK(events(h).empty()); + near(h.physical_position().signed_units,sign); near(h.native_marked_equity(100),9998); + failed_is_permanent(h,1,100); +} + +void pending_addition_failure(bool overflow) { + Host h; + const double first = overflow ? std::numeric_limits::max() : 0x1p60; + const double second = overflow ? std::numeric_limits::max() : 1.0; + const double price = overflow ? .01 : 100.0; + start(h,overflow ? "G5-pending-overflow" : "G5-pending-absorption",1,0,price); + auto parent_request = tx(1,"unfilled-owner"); parent_request.trigger = no::Stop{price*2}; + const auto parent = put(h,parent_request); + no::Request child{no::Reduce{no::OwnerOpenedUnits{}},"unbound-recipient",""}; + child.owner = no::WaitForApplied{parent}; child.group = no::Member{7,2,no::GroupEffect::Reduce}; + const auto recipient = put(h,child); + auto emit = tx(first,"first-delta"); emit.group = no::Member{7,1,no::GroupEffect::Reduce}; + const auto first_filler = put(h,emit); h.tick(0,price); + auto prior = events(h); REQUIRE(prior.size() == 1); + CHECK(prior[0].recipient == recipient); + REQUIRE(fills(h,first_filler).size() == 1); + CHECK(prior[0].cause.ordinal == fills(h,first_filler)[0].ordinal); + // Remove the first physical exposure without emitting a group effect. + // The still-Wait recipient retains its first deferred adjustment. + put(h,no::Request{execution::Flatten{},"clear-book",""}); h.tick(1,price); + near(h.physical_position().signed_units,0); + emit = tx(second,"second-delta"); emit.group = no::Member{7,1,no::GroupEffect::Reduce}; + const auto second_filler = put(h,emit); + CHECK(!h.input(2,price)); check_group_failure(h,second_filler,recipient); + const auto after = events(h); REQUIRE(after.size() == 1); + CHECK(after[0].ordinal == prior[0].ordinal && after[0].cause == prior[0].cause); + CHECK(events(h).empty()); + CHECK(events(h).empty()); + CHECK(events(h).empty()); + CHECK(fills(h,parent).empty()); + CHECK(h.physical_position().signed_units == second); + CHECK(fills(h,second_filler)[0].filled_working == second); + failed_is_permanent(h,3,price); +} + +void run_case(const char* name, const std::function& body) { + scenario = name; ++cases; + const int previous = failures; + try { body(); } + catch (const StopCase&) {} + catch (const std::exception& e) { std::printf("FAIL %s unexpected exception: %s\n",name,e.what()); ++failures; } + catch (...) { std::printf("FAIL %s unexpected nonstandard exception\n",name); ++failures; } + std::printf("%s %s\n", failures == previous ? "PASS" : "FAIL",name); +} +} // namespace + +int main() { + for (double sign : {1.0,-1.0}) { + run_case(sign>0?"S1/O6 long scoped costs":"S1/O6 short scoped costs",[&]{scoped_cost_and_terminal_enrollment(sign);}); + run_case(sign>0?"V2 long full run identity":"V2 short full run identity",[&]{foreign_run_bind(sign);}); + run_case(sign>0?"O1 long remaining OHLC":"O1 short remaining OHLC",[&]{owner_remaining_ohlc(sign);}); + run_case(sign>0?"OHLC long chronological competitors":"OHLC short chronological competitors",[&]{chronological_crossings(sign,false);}); + run_case(sign>0?"OHLC long unrelated insertion":"OHLC short unrelated insertion",[&]{chronological_crossings(sign,true);}); + run_case(sign>0?"C9 long transaction first":"C9 short transaction first",[&]{flat_incarnation_order(sign,true);}); + run_case(sign>0?"C9 long close first":"C9 short close first",[&]{flat_incarnation_order(sign,false);}); + run_case(sign>0?"C8 long precommit arithmetic failure":"C8 short precommit arithmetic failure",[&]{capacity_progress_failure(sign);}); + run_case(sign>0?"G5 long postcommit subtraction failure":"G5 short postcommit subtraction failure",[&]{group_subtraction_failure(sign);}); + } + run_case("G5 pending 2^60 plus 1",[]{pending_addition_failure(false);}); + run_case("G5 pending DBL_MAX plus DBL_MAX",[]{pending_addition_failure(true);}); + std::printf("%s native resting acceptance: %d cases, %d checks, %d failures\n", + failures?"FAIL":"PASS",cases,checks,failures); + return failures?1:0; +} diff --git a/tests/test_native_resting_contract.cpp b/tests/test_native_resting_contract.cpp index 54251569..940bc8f0 100644 --- a/tests/test_native_resting_contract.cpp +++ b/tests/test_native_resting_contract.cpp @@ -96,6 +96,12 @@ void capacity_per_print() { if(f.size()==3) { CHECK(f[0].opened_units==2 && f[1].opened_units==2 && f[2].opened_units==1); CHECK(!f[0].terminal && !f[1].terminal && f[2].terminal); + for(size_t i=0;i<3;++i) { + const auto* before=std::get_if(&f[i].remaining_before); + const auto* after=std::get_if(&f[i].remaining_after); + CHECK(before && before->q==5.0-2.0*i); + CHECK(after && after->q==(i<2?3.0-2.0*i:0.0)); + } CHECK(f[0].cursor.point.ordinal!=f[1].cursor.point.ordinal); CHECK(f[1].cursor.point.ordinal!=f[2].cursor.point.ordinal); } @@ -120,10 +126,12 @@ void stop_limit_models() { auto s=spec("R2-T3");s.slippage_ticks=200;CHECK(h.configure_native(s).status==NativeSetupStatus::Applied); const Bar b{100,103,99,102,1,T};h.run(&b,1);CHECK(h.native_state().kind==NativeLifecycleKind::Completed); auto f=fills(h,p);CHECK(f.size()==1);if(f.size()==1){CHECK(f[0].raw_price==101);CHECK(f[0].resolved_price==102);} - scenario="T8 discrete stop limit gap";Host d;no::RequestHandle q; + scenario="T4 discrete stop limit gap";Host d;no::RequestHandle q; d.begin=[&](Host& x){auto r=tx(1);r.trigger=no::StopLimit{101,102};q=put(x,r);}; auto ds=spec("R2-T8");ds.slippage_ticks=200;CHECK(d.configure_native(ds).status==NativeSetupStatus::Applied); start_stream(d);d.tick(0,100);d.tick(1,103);CHECK(fills(d,q).empty()); + auto activated=events(d);CHECK(activated.size()==1); + if(activated.size()==1){CHECK(activated[0].kind==no::ActivationKind::StopLimit);CHECK(activated[0].reached_price==103 && activated[0].cursor.t==0);} d.tick(2,100);auto df=fills(d,q);CHECK(df.size()==1);if(df.size()==1){CHECK(df[0].raw_price==100);CHECK(df[0].resolved_price==102);} finish(d); } @@ -210,10 +218,15 @@ void persistent_trail(double sign) { scenario="T6 active trail remainder";Host h;h.begin=[&](Host& x){put(x,tx(sign*2));}; start(h,sign>0?"R2-T6-L":"R2-T6-S");h.tick(0,100); auto r=rd(2);r.capacity=no::PointBudget{1};r.trigger=no::Trail{2,100+sign*5};auto close=put(h,r); - h.tick(1,100);CHECK(fills(h,close).empty());h.tick(2,100+sign*6);CHECK(fills(h,close).empty()); + h.tick(1,100);CHECK(fills(h,close).empty());CHECK(events(h).empty()); + h.tick(2,100+sign*6);CHECK(fills(h,close).empty()); + auto arm=events(h);CHECK(arm.size()==1); + if(arm.size()==1){CHECK(arm[0].kind==no::ActivationKind::TrailArm);CHECK(arm[0].reached_price==100+sign*6);} h.tick(3,100+sign*10);CHECK(fills(h,close).empty());h.tick(4,100+sign*8);CHECK(fills(h,close).size()==1); h.tick(5,100+sign*20);auto f=fills(h,close);CHECK(f.size()==2); if(f.size()==2){CHECK(f[0].raw_price==100+sign*8 && f[1].raw_price==100+sign*20);CHECK(!f[0].terminal && f[1].terminal);} + auto activation=events(h);CHECK(activation.size()==2); + if(activation.size()==2){const auto* active=std::get_if(&activation[1].after);CHECK(active && active->best_at_trigger==100+sign*10);} near(h.physical_position().signed_units,0);finish(h); } void group_actual_quantity() { @@ -269,6 +282,21 @@ void nonfinite_slippage() { near(h.physical_position().signed_units,0);finish(h); } } +void event_sized_child_budget(double sign) { + scenario="event-sized partial child";Host h;no::RequestHandle parent,child; + h.begin=[&](Host& x){parent=put(x,tx(sign*3));auto c=event_rd(parent);c.capacity=no::PointBudget{1};child=put(x,c);}; + start(h,sign>0?"R2-event-budget-L":"R2-event-budget-S"); + h.tick(0,100);near(h.physical_position().signed_units,sign*2); + CHECK(fills(h,parent).size()==1 && fills(h,child).size()==1); + h.tick(1,100);near(h.physical_position().signed_units,sign); + h.tick(2,100);auto f=fills(h,child);CHECK(f.size()==3); + if(f.size()==3){ + for(const auto& e:f)CHECK(e.closed_units==1); + CHECK(!f[0].terminal && !f[1].terminal && f[2].terminal); + CHECK(f[0].cursor.point.ordinal!=f[1].cursor.point.ordinal && f[1].cursor.point.ordinal!=f[2].cursor.point.ordinal); + } + CHECK(events(h).size()==1);near(h.physical_position().signed_units,0);finish(h); +} } int main() { capacity_per_print();persistent_stop_and_limit();stop_limit_models(); @@ -276,6 +304,7 @@ int main() { close_lifetime();cancel_cohorts();deferred_group_receipts(); persistent_trail(1);persistent_trail(-1);group_actual_quantity();group_before_dependencies();market_surface_rejections(); nonfinite_slippage(); + event_sized_child_budget(1);event_sized_child_budget(-1); std::printf("%s native resting contract: %d checks, %d failures\n",failures?"FAIL":"PASS",checks,failures); return failures?1:0; } diff --git a/tests/test_native_resting_driver_contract.cpp b/tests/test_native_resting_driver_contract.cpp new file mode 100644 index 00000000..025041e4 --- /dev/null +++ b/tests/test_native_resting_driver_contract.cpp @@ -0,0 +1,377 @@ +// Native R2 driver/geometry acceptance. Real commands and market delivery only. +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; +namespace { +int checks = 0, failures = 0, cases = 0; +const char* scenario = "setup"; +struct StopCase {}; +#define CHECK(x) do { ++checks; if (!(x)) { ++failures; \ + std::printf("FAIL %s:%d %s\n",scenario,__LINE__,#x); } } while (0) +#define REQUIRE(x) do { const bool ok_ = bool(x); CHECK(ok_); if (!ok_) throw StopCase{}; } while (0) +constexpr int64_t T = 1736121600000LL; + +void near(double a, double b) { + const bool ok = std::isfinite(a) && std::isfinite(b) + && std::abs(a-b) <= 1e-12 * std::max(1.0,std::abs(b)); + if (!ok) std::printf(" actual=%.17g expected=%.17g\n",a,b); + CHECK(ok); +} +void exact_price(double actual, double expected) { + if (actual != expected) + std::printf(" exact price actual=%.17g (%a) expected=%.17g (%a)\n", + actual,actual,expected,expected); + CHECK(actual == expected); +} + +struct Host final : NativeStrategyHost { + std::function beginning; + std::function calculation; + uint64_t sequence = 0; + int calculations = 0; + void on_native_run_begin() override { if (beginning) beginning(*this); } + void on_native_bar(const Bar&,const NativeDecisionContext&) override { + ++calculations; + if (calculation) calculation(*this); + } + void tick(int64_t offset, double price) { + REQUIRE(stream_push_tick(TradeTick{T+offset,++sequence,price,1})); + REQUIRE(native_state().kind == NativeLifecycleKind::Running); + } + void advance(int64_t offset) { + REQUIRE(stream_advance_time(T+offset)); + REQUIRE(native_state().kind == NativeLifecycleKind::Running); + } +}; +NativeRunSpec specification(const char* key) { + NativeRunSpec s; + s.identity={key,1}; s.input_tf="1"; s.script_tf="1"; + s.ticker="N"; s.tickerid="TEST:N"; s.type="crypto"; + s.currency="USD"; s.basecurrency="USD"; s.description="R2 driver acceptance"; + s.volumetype="base"; s.timezone="UTC"; s.session="24x7"; + s.initial_capital=10000; s.point_value=1; s.account_fx=1; s.price_tick=.01; + s.fee_kind=NativeFeeKind::CashPerExecution; s.fee_value=0; + return s; +} +void configure(Host& h,const char* key) { + REQUIRE(h.configure_native(specification(key)).status == NativeSetupStatus::Applied); +} +void start_stream(Host& h,const char* key,double first=100) { + configure(h,key); + auto beginning=std::exchange(h.beginning,{}); + const Bar warmup{first,first,first,first,1,T-60000}; + REQUIRE(h.stream_begin(&warmup,1,"1","1")); + REQUIRE(h.stream_push_tick(TradeTick{T,++h.sequence,first,0})); + h.beginning=std::move(beginning); + if(h.beginning) h.beginning(h); +} +void finish(Host& h) { + REQUIRE(h.stream_end(false)); + CHECK(h.native_state().kind == NativeLifecycleKind::Completed); +} +no::Request tx(double q,const char* text="") { return {no::Transact{q},text,""}; } +no::Request reduce(double q,const char* text="") { return {no::Reduce{no::ExplicitUnits{q}},text,""}; } +no::RequestHandle put(Host& h,const no::Request& request) { + auto result=h.submit(request); + REQUIRE(result.status==no::SubmitStatus::Accepted && result.handle.has_value()); + return *result.handle; +} +template std::vector events(const Host& h,const no::RequestHandle& handle) { + std::vector out; + for(const auto& row:h.native_events(0)) if(row.command) { + if(const auto* event=std::get_if(&*row.command)) { + REQUIRE(event->definition); + if(event->definition->handle==handle) out.push_back(*event); + } + } + return out; +} +auto fills(const Host& h,const no::RequestHandle& handle) { + return events(h,handle); +} +std::vector carried(const Host& h) { + std::vector out; + for(const auto& row:h.native_events(0)) + if(row.driver && row.driver->coordinate.provenance==NativePriceProvenance::CarriedOpen) + out.push_back(*row.driver); + return out; +} +void observed(const no::MatchCursor& cursor,int64_t time) { + CHECK(cursor.point.provenance==NativePriceProvenance::ObservedPrint); + CHECK(cursor.point.path_phase==NativePathPhase::None); + CHECK(cursor.point.effective_time_ms==time && cursor.point.source_price_time_ms==time); + CHECK(cursor.t==0); +} + +void pending_kinds_on_carried(double sign) { + Host h; + const double price=100-sign*3; + start_stream(h,sign>0?"T2-pending-long":"T2-pending-short",price); + put(h,tx(sign*5,"seed")); h.tick(1,price); + auto l=reduce(1,"limit"); l.trigger=no::Limit{price-sign}; const auto limit=put(h,l); + auto s=reduce(1,"stop"); s.trigger=no::Stop{price+sign}; const auto stop=put(h,s); + auto sl=reduce(1,"stop-limit"); sl.trigger=no::StopLimit{price+sign,price-sign}; + const auto stop_limit=put(h,sl); + auto t=reduce(1,"trail"); t.trigger=no::Trail{1,price-sign}; const auto trail=put(h,t); + h.advance(120000); + const auto points=carried(h); REQUIRE(points.size()==1); + CHECK(points[0].coordinate.effective_time_ms==T+60000); + exact_price(points[0].raw_price,price); + const auto lf=fills(h,limit); REQUIRE(lf.size()==1); + CHECK(lf[0].cursor.point.ordinal==points[0].coordinate.ordinal); + CHECK(lf[0].cursor.point.provenance==NativePriceProvenance::CarriedOpen && lf[0].cursor.t==0); + exact_price(lf[0].raw_price,price); + for(const auto& handle:{stop,stop_limit,trail}) { + CHECK(fills(h,handle).empty()); + CHECK(events(h,handle).empty()); + CHECK(events(h,handle).empty()); + } + near(h.physical_position().signed_units,sign*4); + h.tick(120001,price); + for(const auto& handle:{stop,stop_limit}) { + const auto f=fills(h,handle); REQUIRE(f.size()==1); + observed(f[0].cursor,T+120001); exact_price(f[0].raw_price,price); + const auto a=events(h,handle); REQUIRE(a.size()==1); + observed(a[0].cursor,T+120001); exact_price(a[0].reached_price,price); + CHECK(a[0].ordinal(a[0].after)); + else CHECK(std::holds_alternative(a[0].after)); + } + CHECK(fills(h,trail).empty()); + auto ta=events(h,trail); REQUIRE(ta.size()==1); + CHECK(ta[0].kind==no::ActivationKind::TrailArm); + observed(ta[0].cursor,T+120001); exact_price(ta[0].reached_price,price); + REQUIRE(std::holds_alternative(ta[0].after)); + exact_price(std::get(ta[0].after).best,price); + h.tick(120002,price-sign); + const auto tf=fills(h,trail); REQUIRE(tf.size()==1); + observed(tf[0].cursor,T+120002); exact_price(tf[0].raw_price,price-sign); + ta=events(h,trail); REQUIRE(ta.size()==2); + CHECK(ta[1].kind==no::ActivationKind::TrailTrigger); + REQUIRE(std::holds_alternative(ta[1].after)); + exact_price(std::get(ta[1].after).best_at_trigger,price); + near(h.physical_position().signed_units,sign); finish(h); +} + +void active_remainders_on_carried(double sign) { + Host h; start_stream(h,sign>0?"T2-active-long":"T2-active-short"); + put(h,tx(sign*7)); h.tick(1,100); + auto s=reduce(2,"active-stop"); s.trigger=no::Stop{100-sign}; s.capacity=no::PointBudget{1}; + const auto stop=put(h,s); + auto sl=reduce(2,"live-stop-limit"); sl.trigger=no::StopLimit{100-sign,100-sign*2}; + sl.capacity=no::PointBudget{1}; const auto stop_limit=put(h,sl); + auto t=reduce(2,"active-trail"); t.trigger=no::Trail{1,std::nullopt}; + t.capacity=no::PointBudget{1}; const auto trail=put(h,t); + h.tick(2,100+sign); h.tick(3,100-sign); + for(const auto& handle:{stop,stop_limit,trail}) { + const auto f=fills(h,handle); REQUIRE(f.size()==1); CHECK(!f[0].terminal); + } + const auto before_stop=events(h,stop).size(); + const auto before_sl=events(h,stop_limit).size(); + const auto before_trail=events(h,trail).size(); + h.advance(120000); const auto points=carried(h); REQUIRE(points.size()==1); + exact_price(points[0].raw_price,100-sign); + for(const auto& handle:{stop,stop_limit,trail}) CHECK(fills(h,handle).size()==1); + CHECK(events(h,stop).size()==before_stop); + CHECK(events(h,stop_limit).size()==before_sl); + CHECK(events(h,trail).size()==before_trail); + h.tick(120001,100-sign); + for(const auto& handle:{stop,stop_limit,trail}) { + const auto f=fills(h,handle); REQUIRE(f.size()==2); + CHECK(f[1].terminal); observed(f[1].cursor,T+120001); + exact_price(f[1].raw_price,100-sign); + CHECK(f[0].cursor.point.ordinal!=f[1].cursor.point.ordinal); + CHECK(f[1].cursor.point.ordinal!=points[0].coordinate.ordinal); + } + CHECK(events(h,stop).size()==before_stop); + CHECK(events(h,stop_limit).size()==before_sl); + CHECK(events(h,trail).size()==before_trail); + near(h.physical_position().signed_units,sign); finish(h); +} + +void trail_arm_and_competing_hit(double sign) { + Host h; no::RequestHandle trail,competitor; + h.beginning=[&](Host& self){put(self,tx(sign*2));}; + h.calculation=[&](Host& self){ + if(self.calculations!=1) return; + auto t=reduce(1,"trail"); t.trigger=no::Trail{2,100+sign*5}; trail=put(self,t); + auto c=reduce(1,"earlier-limit"); c.trigger=no::Limit{100+sign*6}; competitor=put(self,c); + }; + configure(h,sign>0?"T8-long":"T8-short"); + const Bar tape[]={{100,100,100,100,1,T}, + sign>0?Bar{100,110,85,100,1,T+60000}:Bar{100,115,90,100,1,T+60000}}; + h.run(tape,2); REQUIRE(h.native_state().kind==NativeLifecycleKind::Completed); + const auto a=events(h,trail); REQUIRE(a.size()==2); + const auto cf=fills(h,competitor),tf=fills(h,trail); REQUIRE(cf.size()==1 && tf.size()==1); + CHECK(a[0].kind==no::ActivationKind::TrailArm); + CHECK(a[1].kind==no::ActivationKind::TrailTrigger); + exact_price(a[0].reached_price,100+sign*5); near(a[0].cursor.t,.5); + REQUIRE(std::holds_alternative(a[0].after)); + exact_price(std::get(a[0].after).best,100+sign*5); + CHECK(a[0].cursor.point.path_phase==(sign>0?NativePathPhase::High:NativePathPhase::Low)); + CHECK(a[0].cursor.point.provenance==NativePriceProvenance::Confirmed); + exact_price(cf[0].raw_price,100+sign*6); near(cf[0].cursor.t,.6); + CHECK(a[0].cursor.point.ordinal==cf[0].cursor.point.ordinal); + CHECK(a[0].ordinal(a[1].after)); + exact_price(std::get(a[1].after).best_at_trigger,100+sign*10); + exact_price(tf[0].raw_price,100+sign*8); near(tf[0].cursor.t,.08); + CHECK(tf[0].cursor.point.path_phase==(sign>0?NativePathPhase::Low:NativePathPhase::High)); + REQUIRE(h.trade_count()==2); + CHECK(cf[0].first_trade_index==0 && tf[0].first_trade_index==1); + // At the earlier close, only price106/94 has been traversed: not endpoint110/90. + near(h.get_trade(0).max_runup,6); near(h.get_trade(0).pnl,6); + near(h.get_trade(1).max_runup,10); near(h.get_trade(1).pnl,8); + near(h.physical_position().signed_units,0); +} + +void wait_child_does_not_replay_suffix(double sign) { + Host h; no::RequestHandle parent,child; + h.beginning=[&](Host& self){ + auto p=tx(sign,"parent-after-extreme"); p.trigger=no::Stop{100+sign*5}; parent=put(self,p); + auto c=reduce(1,"child-no-prefix"); c.owner=no::WaitForApplied{parent}; + c.trigger=no::Stop{100-sign}; child=put(self,c); + }; + h.calculation=[&](Host& self){if(self.calculations==1){ + CHECK(fills(self,parent).size()==1); CHECK(fills(self,child).empty()); + CHECK(events(self,child).empty()); + }}; + configure(h,sign>0?"T9-wait-long":"T9-wait-short"); + const Bar tape[]={ + sign>0?Bar{100,111,90,108,1,T}:Bar{100,110,89,92,1,T}, + sign>0?Bar{108,109,98,100,1,T+60000}:Bar{92,102,91,100,1,T+60000}}; + h.run(tape,2); REQUIRE(h.native_state().kind==NativeLifecycleKind::Completed); + const auto p=fills(h,parent),c=fills(h,child); REQUIRE(p.size()==1 && c.size()==1); + exact_price(p[0].raw_price,100+sign*5); exact_price(c[0].raw_price,100-sign); + CHECK(c[0].cursor.point.interval_index==1 && c[0].cursor.point.open_ms==T+60000); + CHECK(c[0].cursor.point.ordinal>p[0].cursor.point.ordinal); + CHECK(c[0].cursor.point.path_phase==(sign>0?NativePathPhase::Low:NativePathPhase::High)); + CHECK(c[0].cursor.point.provenance==NativePriceProvenance::Confirmed); + const auto* scope=std::get_if(&c[0].scope); + REQUIRE(scope); CHECK(scope->incarnation==parent.incarnation && scope->cycle==p[0].cycle_after); + CHECK(events(h,child).size()==1); near(h.physical_position().signed_units,0); +} + +void late_bind_cannot_replay_old_bar(double sign) { + Host h; no::RequestHandle opening,child; + h.beginning=[&](Host& self){opening=put(self,tx(sign));}; + h.calculation=[&](Host& self){ + if(self.calculations==1){ + const auto f=fills(self,opening); REQUIRE(f.size()==1); + auto c=reduce(1); c.owner=no::BindOpening{opening,f[0].cycle_after}; + c.trigger=no::Stop{100-sign}; child=put(self,c); + } + if(self.calculations<=2){ + CHECK(fills(self,child).empty()); CHECK(events(self,child).empty()); + } + }; + configure(h,sign>0?"T9-bind-long":"T9-bind-short"); + const Bar tape[]={ + sign>0?Bar{100,110,90,101,1,T}:Bar{100,110,90,99,1,T}, + sign>0?Bar{101,104,100,102,1,T+60000}:Bar{99,100,96,98,1,T+60000}, + sign>0?Bar{102,103,98,100,1,T+120000}:Bar{98,102,97,100,1,T+120000}}; + h.run(tape,3); REQUIRE(h.native_state().kind==NativeLifecycleKind::Completed); + const auto f=fills(h,child); REQUIRE(f.size()==1); + exact_price(f[0].raw_price,100-sign); near(f[0].cursor.t,.8); + CHECK(f[0].cursor.point.interval_index==2 && f[0].cursor.point.open_ms==T+120000); + CHECK(f[0].cursor.point.ordinal>f[0].definition->birth.acceptance_ordinal); + CHECK(f[0].cursor.point.effective_time_ms>=f[0].definition->birth.decision_time_lower_bound); + CHECK(events(h,child).size()==1); near(h.physical_position().signed_units,0); +} + +void budget_across_same_segment_rescans(double sign) { + Host h; no::RequestHandle budget,auxiliary,stop; + h.beginning=[&](Host& self){ + auto r=tx(sign*5,"budget"); r.trigger=no::Limit{100-sign}; r.capacity=no::PointBudget{2}; budget=put(self,r); + auto a=tx(sign*2,"rescan-fill"); a.trigger=no::Limit{100-sign*2}; a.capacity=no::PointBudget{1}; auxiliary=put(self,a); + auto s=tx(-sign,"rescan-activation"); s.trigger=no::Stop{100-sign*3}; stop=put(self,s); + }; + configure(h,sign>0?"budget-segments-long":"budget-segments-short"); + const Bar bar=sign>0?Bar{100,111,90,98,1,T}:Bar{100,110,89,102,1,T}; + h.run(&bar,1); REQUIRE(h.native_state().kind==NativeLifecycleKind::Completed); + const auto f=fills(h,budget),a=fills(h,auxiliary),s=fills(h,stop); + REQUIRE(f.size()==3 && a.size()==2 && s.size()==1); + const double before[]={5,3,1},after[]={3,1,0},amount[]={2,2,1}; + for(size_t i=0;i<3;++i){ + CHECK(f[i].filled_working==amount[i]); + REQUIRE(std::holds_alternative(f[i].remaining_before)); + REQUIRE(std::holds_alternative(f[i].remaining_after)); + CHECK(std::get(f[i].remaining_before).q==before[i]); + CHECK(std::get(f[i].remaining_after).q==after[i]); + REQUIRE(std::holds_alternative(f[i].allowance_before)); + REQUIRE(std::holds_alternative(f[i].allowance_after)); + const auto& initial=std::get(f[i].allowance_before); + const auto& exhausted=std::get(f[i].allowance_after); + CHECK(initial.point_ordinal==f[i].cursor.point.ordinal && initial.left==amount[i]); + CHECK(exhausted.point_ordinal==initial.point_ordinal && exhausted.left==0); + CHECK(f[i].cursor.point.provenance==NativePriceProvenance::Confirmed); + CHECK(f[i].terminal==(i==2)); + } + CHECK(f[0].cursor.point.path_phase==(sign>0?NativePathPhase::Low:NativePathPhase::High)); + CHECK(f[1].cursor.point.path_phase==(sign>0?NativePathPhase::High:NativePathPhase::Low)); + CHECK(f[2].cursor.point.path_phase==NativePathPhase::Close); + CHECK(f[0].cursor.point.ordinal(h,stop).size()==1); + near(h.physical_position().signed_units,sign*6); +} + +void exact_stop_activation_price() { + Host h; no::RequestHandle order; + constexpr double level=.2055; + h.beginning=[&](Host& self){auto r=tx(1,"exact-threshold");r.trigger=no::Stop{level};order=put(self,r);}; + configure(h,"exact-stop-threshold"); + const Bar bar{.1,.3,.09,.2,1,T}; h.run(&bar,1); + REQUIRE(h.native_state().kind==NativeLifecycleKind::Completed); + const auto a=events(h,order); const auto f=fills(h,order); + REQUIRE(a.size()==1 && f.size()==1); + CHECK(a[0].kind==no::ActivationKind::Stop); + CHECK(std::holds_alternative(a[0].after)); + CHECK(a[0].cursor.point.path_phase==NativePathPhase::High); + CHECK(a[0].cursor.point.ordinal==f[0].cursor.point.ordinal && a[0].cursor.t==f[0].cursor.t); + CHECK(a[0].cursor.point.provenance==NativePriceProvenance::Confirmed); + exact_price(a[0].reached_price,level); + exact_price(f[0].raw_price,level); + exact_price(f[0].resolved_price,level); + std::printf("threshold witness activation=%.17g raw=%.17g resolved=%.17g t=%.17g\n", + a[0].reached_price,f[0].raw_price,f[0].resolved_price,f[0].cursor.t); +} + +void run_case(const char* name,const std::function& body){ + scenario=name; ++cases; const int before=failures; + try{body();}catch(const StopCase&){} + catch(const std::exception& e){++failures;std::printf("FAIL %s exception: %s\n",name,e.what());} + catch(...){++failures;std::printf("FAIL %s unknown exception\n",name);} + std::printf("%s %s\n",before==failures?"PASS":"FAIL",name); +} +} // namespace +int main(){ + for(double sign:{1.0,-1.0}){ + run_case(sign>0?"T2 pending carried long":"T2 pending carried short",[&]{pending_kinds_on_carried(sign);}); + run_case(sign>0?"T2 active carried long":"T2 active carried short",[&]{active_remainders_on_carried(sign);}); + run_case(sign>0?"T8 trail chronology long":"T8 trail chronology short",[&]{trail_arm_and_competing_hit(sign);}); + run_case(sign>0?"T9 causative suffix long":"T9 causative suffix short",[&]{wait_child_does_not_replay_suffix(sign);}); + run_case(sign>0?"T9 late Bind long":"T9 late Bind short",[&]{late_bind_cannot_replay_old_bar(sign);}); + run_case(sign>0?"same-P budget long":"same-P budget short",[&]{budget_across_same_segment_rescans(sign);}); + } + run_case("exact .2055 Stop threshold",[]{exact_stop_activation_price();}); + std::printf("%s native resting driver: %d cases, %d checks, %d failures\n",failures?"FAIL":"PASS",cases,checks,failures); + return failures?1:0; +} From f6b1c5649189ad4b6fe727e699037dca4dc1b06d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 12 Sep 2026 23:40:15 +0800 Subject: [PATCH 3/5] Provide v12 RTTI in sanitized ABI link controls --- scripts/check_script_cpp_abi.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/check_script_cpp_abi.py b/scripts/check_script_cpp_abi.py index 3ab18368..4e2aa8ed 100644 --- a/scripts/check_script_cpp_abi.py +++ b/scripts/check_script_cpp_abi.py @@ -326,8 +326,17 @@ def compile_object(name, source, include): v10_priority_symbols = compile_object("basev10_pending_priority_symbols", priority_symbols, v10_include) v12_native = compile_object("basev12_native", caller("engine_script_run_v12"), v12_include) v12_generated = compile_object("basev12_generated", caller("engine_script_run_v12", True), v12_include) + # V12 moved its virtual destructor out of line. Its positive linker + # control must define that key function so UBSan vptr instrumentation + # can resolve the class RTTI. This remains a never-executed symbol + # control compiled against the unchanged historical header. + v12_symbol_control = BASE_SYMBOL_CONTROL.replace("engine_script_run_v2", "engine_script_run_v12") + ''' +namespace pineforge { namespace engine_script_run_v12 { +BacktestEngine::~BacktestEngine() = default; +}} +''' v12_symbols = compile_object("basev12_symbol_control", - BASE_SYMBOL_CONTROL.replace("engine_script_run_v2", "engine_script_run_v12"), v12_include) + v12_symbol_control, v12_include) v12_priority = compile_object("basev12_pending_priority", priority_caller, v12_include) v12_priority_symbols = compile_object("basev12_pending_priority_symbols", priority_symbols, v12_include) From 3f28b61b3c34b5f44da51eef354452f49f4a1c22 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 13 Sep 2026 00:07:49 +0800 Subject: [PATCH 4/5] Exercise native ownership, groups, replay and admission contracts --- docs/native-refactor-progress.md | 14 +- docs/pages/abi-stability.md | 5 +- docs/pages/native-engine.md | 2 +- tests/CMakeLists.txt | 4 + tests/test_native_resting_contract.cpp | 7 +- tests/test_native_resting_group_contract.cpp | 829 ++++++++++ .../test_native_resting_matching_contract.cpp | 157 ++ tests/test_native_resting_owner_contract.cpp | 754 +++++++++ tests/test_native_resting_replay_contract.cpp | 1348 +++++++++++++++++ 9 files changed, 3108 insertions(+), 12 deletions(-) create mode 100644 tests/test_native_resting_group_contract.cpp create mode 100644 tests/test_native_resting_matching_contract.cpp create mode 100644 tests/test_native_resting_owner_contract.cpp create mode 100644 tests/test_native_resting_replay_contract.cpp diff --git a/docs/native-refactor-progress.md b/docs/native-refactor-progress.md index c90807fb..20f6dae8 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -74,13 +74,15 @@ repairs are still in progress: together to epoch 13, with broker/stream hash version 13. Existing native run-spec, calendar/driver value domains and C ABI 4 prefixes remain. -The latest integrated Release build passed 322 CTests with zero failures. -One WebSocket transport test was skipped because the selected macOS libcurl -lacks WebSocket support. The native proof includes 831 resting-contract -checks, 20 scoped-ownership/chronology/failure cases (782 checks), 13 driver -cases (882 checks), and 343 resting-core checks. Independent review findings -have corresponding regression witnesses, including token ownership, event-sized +The local Release suite and separate native acceptance checks pass. One +WebSocket transport test is skipped when the selected macOS libcurl lacks +WebSocket support. Native coverage exercises working-order quantities, +scoped ownership and costs, group effects, driver chronology, admission, +replay/reset, and failure prefixes. Independent review findings have +corresponding regression witnesses, including token ownership, event-sized partial exits, exact deferred-receipt arithmetic and same-cursor execution prices. +The sanitizer ABI control also supplies the historical v12 destructor needed +for its RTTI; this changes only the link-test stub, not runtime behavior. Final review of the publication candidate, remaining R2 acceptance coverage, a new fixed-population 4190 comparison, the actual gate and published-head CI diff --git a/docs/pages/abi-stability.md b/docs/pages/abi-stability.md index c1ef6eec..f2ebad26 100644 --- a/docs/pages/abi-stability.md +++ b/docs/pages/abi-stability.md @@ -184,8 +184,9 @@ are separate contracts and do not change here. Determinism is conditional on identical externally supplied market, intent and fill-report sequences, configuration, code and version. It is not a claim that live execution prices, quantities or callback arrival are predictable. The -version bump changes linkage and fingerprint bytes; it changes no financial -rule, fill price, fee, quota policy or economic test expectation. +epoch change itself changes linkage and fingerprint identity. The accompanying +native execution behavior is described in [Native engine](native-engine.md); +the ABI boundary alone is not a claim that every execution path is unchanged. If you find yourself reaching for any of these from outside the closed PineForge transpiler, you're holding it wrong — file an issue and we'll diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 932413ab..4a7c2ae7 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -11,7 +11,7 @@ use the general host commands; request-value members live in `` and are not restated here. The resting-order lifecycle is implemented in the current R2 candidate; -final integration, compatibility and independent-review acceptance are still +final merge acceptance, compatibility and CI verification are still pending. [Refactor progress](../native-refactor-progress.md) separates this candidate from the completed R2a scoped-settlement prerequisite. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d0f173e8..632e43be 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,6 +10,10 @@ set(TEST_SOURCES test_native_resting_contract test_native_resting_acceptance test_native_resting_driver_contract + test_native_resting_owner_contract + test_native_resting_group_contract + test_native_resting_replay_contract + test_native_resting_matching_contract test_native_auto_path test_native_market_vertical test_native_market_vertical_contract diff --git a/tests/test_native_resting_contract.cpp b/tests/test_native_resting_contract.cpp index 940bc8f0..2c5e1d1d 100644 --- a/tests/test_native_resting_contract.cpp +++ b/tests/test_native_resting_contract.cpp @@ -219,11 +219,12 @@ void persistent_trail(double sign) { start(h,sign>0?"R2-T6-L":"R2-T6-S");h.tick(0,100); auto r=rd(2);r.capacity=no::PointBudget{1};r.trigger=no::Trail{2,100+sign*5};auto close=put(h,r); h.tick(1,100);CHECK(fills(h,close).empty());CHECK(events(h).empty()); - h.tick(2,100+sign*6);CHECK(fills(h,close).empty()); + h.tick(2,100-sign*3);CHECK(fills(h,close).empty());CHECK(events(h).empty()); + h.tick(3,100+sign*6);CHECK(fills(h,close).empty()); auto arm=events(h);CHECK(arm.size()==1); if(arm.size()==1){CHECK(arm[0].kind==no::ActivationKind::TrailArm);CHECK(arm[0].reached_price==100+sign*6);} - h.tick(3,100+sign*10);CHECK(fills(h,close).empty());h.tick(4,100+sign*8);CHECK(fills(h,close).size()==1); - h.tick(5,100+sign*20);auto f=fills(h,close);CHECK(f.size()==2); + h.tick(4,100+sign*10);CHECK(fills(h,close).empty());h.tick(5,100+sign*8);CHECK(fills(h,close).size()==1); + h.tick(6,100+sign*20);auto f=fills(h,close);CHECK(f.size()==2); if(f.size()==2){CHECK(f[0].raw_price==100+sign*8 && f[1].raw_price==100+sign*20);CHECK(!f[0].terminal && f[1].terminal);} auto activation=events(h);CHECK(activation.size()==2); if(activation.size()==2){const auto* active=std::get_if(&activation[1].after);CHECK(active && active->best_at_trigger==100+sign*10);} diff --git a/tests/test_native_resting_group_contract.cpp b/tests/test_native_resting_group_contract.cpp new file mode 100644 index 00000000..7d8cae06 --- /dev/null +++ b/tests/test_native_resting_group_contract.cpp @@ -0,0 +1,829 @@ +// Bounded R2 group/validity acceptance. Real host commands and observations. +// Core section is only internal duplicate delivery, which is not a public host +// operation. No injected lots, history mutation, or test-only production hooks. +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; + +namespace { +int checks = 0, failures = 0, cases = 0; +const char* scenario = "setup"; +struct StopCase {}; +#define CHECK(x) \ + do { \ + ++checks; \ + if (!(x)) { \ + ++failures; \ + std::printf("FAIL %s:%d %s\n", scenario, __LINE__, #x); \ + } \ + } while (0) +#define REQUIRE(x) \ + do { \ + const bool ok_ = bool(x); \ + CHECK(ok_); \ + if (!ok_) throw StopCase{}; \ + } while (0) + +constexpr int64_t T = 1736121600000LL; + +void near(double actual, double expected) { + const bool ok = std::isfinite(actual) && std::isfinite(expected) + && std::abs(actual - expected) <= 1e-12 * std::max(1.0, std::abs(expected)); + if (!ok) std::printf(" actual=%.17g expected=%.17g\n", actual, expected); + CHECK(ok); +} + +struct Host final : NativeStrategyHost { + std::function beginning; + std::function calculation; + uint64_t sequence = 0; + void on_native_run_begin() override { + if (beginning) beginning(*this); + } + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + if (calculation) calculation(*this); + } + void tick(int64_t offset, double price) { + REQUIRE(stream_push_tick(TradeTick{T + offset + 1, ++sequence, price, 1})); + REQUIRE(native_state().kind == NativeLifecycleKind::Running); + } +}; + +NativeRunSpec configuration(const char* key, uint64_t run = 1, double fee = 0) { + NativeRunSpec s; + s.identity = {key, run}; + s.input_tf = "1"; + s.script_tf = "1"; + s.ticker = "N"; + s.tickerid = "TEST:N"; + s.type = "crypto"; + s.currency = "USD"; + s.basecurrency = "USD"; + s.description = "R2 group acceptance"; + s.volumetype = "base"; + s.timezone = "UTC"; + s.session = "24x7"; + s.initial_capital = 10000; + s.point_value = 1; + s.account_fx = 1; + s.price_tick = .01; + s.fee_kind = NativeFeeKind::CashPerExecution; + s.fee_value = fee; + return s; +} + +void start(Host& h, const char* key, uint64_t run = 1, double fee = 0, double first = 100) { + REQUIRE(h.configure_native(configuration(key, run, fee)).status == NativeSetupStatus::Applied); + auto beginning = std::exchange(h.beginning, {}); + const Bar warmup{first, first, first, first, 1, T - 60000}; + h.sequence = 0; + REQUIRE(h.stream_begin(&warmup, 1, "1", "1")); + REQUIRE(h.stream_push_tick(TradeTick{T, ++h.sequence, first, 0})); + h.beginning = std::move(beginning); + if (h.beginning) h.beginning(h); + REQUIRE(h.native_state().kind == NativeLifecycleKind::Running); +} + +void finish(Host& h) { + REQUIRE(h.stream_end(false)); + CHECK(h.native_state().kind == NativeLifecycleKind::Completed); +} + +no::Request tx(double q, const char* label = "") { return {no::Transact{q}, label, ""}; } +no::Request reduce(double q, const char* label = "") { + return {no::Reduce{no::ExplicitUnits{q}}, label, ""}; +} +no::Request event_reduce(const no::RequestHandle& parent, const char* label = "child") { + no::Request r{no::Reduce{no::OwnerOpenedUnits{}}, label, ""}; + r.owner = no::WaitForApplied{parent}; + return r; +} +double far_open(double sign) { return sign > 0 ? 1.0 : 200.0; } +double far_close(double sign) { return sign > 0 ? 200.0 : 1.0; } + +no::RequestHandle put(Host& h, const no::Request& request) { + auto result = h.submit(request); + REQUIRE(result.status == no::SubmitStatus::Accepted); + REQUIRE(result.handle.has_value()); + return *result.handle; +} + +template +std::vector events(const Host& h) { + std::vector out; + for (const auto& row : h.native_events(0)) { + if (row.command) { + if (const auto* value = std::get_if(&*row.command)) out.push_back(*value); + } + } + return out; +} + +template +std::vector events(const Host& h, const no::RequestHandle& handle) { + std::vector out; + for (const auto& event : events(h)) { + REQUIRE(event.definition); + if (event.definition->handle == handle) out.push_back(event); + } + return out; +} + +std::vector fills(const Host& h, const no::RequestHandle& handle) { + return events(h, handle); +} +std::vector cancellations(const Host& h, const no::RequestHandle& handle) { + return events(h, handle); +} + +void expect_reject(Host& h, const no::Request& request, no::RequestRejectReason reason) { + const auto result = h.submit(request); + CHECK(result.status == no::SubmitStatus::Rejected && !result.handle); + CHECK(result.reason == reason); + CHECK(result.event_ordinal > 0); + const auto rejected = events(h); + REQUIRE(!rejected.empty()); + CHECK(rejected.back().reason == reason); + CHECK(rejected.back().ordinal == result.event_ordinal); +} + +static_assert(std::is_constructible_v); +static_assert(std::is_constructible_v); +static_assert(!std::is_constructible_v); +static_assert(!std::is_constructible_v); +static_assert(std::variant_size_v == 2); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_empty_v); +static_assert(std::is_empty_v); + +// G1: partial Cancel Applied has no group effect; terminal Applied cancels the +// other cohort only. Labels/comments and cohort 0 stay inert/opaque. +void g1_partial_cancel_until_terminal(double sign) { + Host h; + start(h, sign > 0 ? "G1-partial-L" : "G1-partial-S"); + auto same = tx(sign, "alpha"); + same.comment = "same-cohort"; + same.trigger = no::Limit{far_open(sign)}; + same.group = no::Member{7, 1, no::GroupEffect::Cancel}; + const auto same_cohort = put(h, same); + auto other = tx(sign, "alpha"); + other.comment = "other-cohort-zero"; + other.trigger = no::Limit{far_open(sign)}; + other.group = no::Member{7, 0, no::GroupEffect::Cancel}; + const auto other_cohort = put(h, other); + auto filler = tx(sign * 3, "alpha"); + filler.comment = "emitter"; + filler.capacity = no::PointBudget{1}; + filler.group = no::Member{7, 1, no::GroupEffect::Cancel}; + const auto emit = put(h, filler); + + h.tick(0, 100); + REQUIRE(fills(h, emit).size() == 1); + CHECK(!fills(h, emit)[0].terminal); + CHECK(cancellations(h, same_cohort).empty()); + CHECK(cancellations(h, other_cohort).empty()); + CHECK(fills(h, same_cohort).empty() && fills(h, other_cohort).empty()); + + h.tick(1, 100); + REQUIRE(fills(h, emit).size() == 2); + CHECK(!fills(h, emit)[1].terminal); + CHECK(cancellations(h, same_cohort).empty()); + CHECK(cancellations(h, other_cohort).empty()); + + h.tick(2, 100); + const auto ff = fills(h, emit); + REQUIRE(ff.size() == 3); + CHECK(ff[2].terminal); + CHECK(ff[2].terminal_reason == no::AppliedTerminalReason::WorkingUnitsSatisfied); + CHECK(cancellations(h, same_cohort).empty()); + const auto ce = cancellations(h, other_cohort); + REQUIRE(ce.size() == 1); + CHECK(ce[0].reason == no::CancelReason::Group); + REQUIRE(ce[0].cause); + CHECK(ce[0].cause->ordinal == ff[2].ordinal); + CHECK(ce[0].cause->run == emit.run); + + h.tick(3, far_open(sign)); + CHECK(fills(h, same_cohort).size() == 1); + CHECK(fills(h, other_cohort).empty()); + CHECK(cancellations(h, same_cohort).empty()); + near(h.physical_position().signed_units, sign * 4); + finish(h); +} + +// G1: clipped TargetExhausted terminal Cancel still emits. +void g1_target_exhausted_cancel_emits(double sign) { + Host h; + start(h, sign > 0 ? "G1-exhausted-L" : "G1-exhausted-S", 1, 2); + put(h, tx(sign, "seed")); + h.tick(0, 100); + auto recipient = tx(sign, "resting"); + recipient.trigger = no::Limit{far_open(sign)}; + recipient.group = no::Member{7, 2, no::GroupEffect::Reduce}; + const auto other = put(h, recipient); + auto closer = reduce(5, "clip"); + closer.group = no::Member{7, 1, no::GroupEffect::Cancel}; + const auto emit = put(h, closer); + const int trades_before = h.trade_count(); + h.tick(1, 100); + const auto ff = fills(h, emit); + REQUIRE(ff.size() == 1); + CHECK(ff[0].closed_units == 1 && ff[0].opened_units == 0 && ff[0].terminal); + CHECK(ff[0].terminal_reason == no::AppliedTerminalReason::TargetExhausted); + const auto* unused = std::get_if(&ff[0].remaining_after); + REQUIRE(unused); + CHECK(unused->q == 4); + CHECK(ff[0].current_ticket == 2); + CHECK(h.trade_count() == trades_before + 1); + const auto ce = cancellations(h, other); + REQUIRE(ce.size() == 1); + CHECK(ce[0].reason == no::CancelReason::Group); + REQUIRE(ce[0].cause); + CHECK(ce[0].cause->ordinal == ff[0].ordinal); + CHECK(fills(h, other).empty()); + CHECK(h.cancel(other).status == no::CancelStatus::NotWorking); + near(h.physical_position().signed_units, 0); + finish(h); +} + +// G3: parent's own Reduce adjusts the unbound child before quantity binding. +void g3_parent_reduce_unbinds_child(double sign) { + Host h; + start(h, sign > 0 ? "G3-L" : "G3-S", 1, 2); + auto parent_req = tx(sign * 2, "parent"); + parent_req.group = no::Member{7, 1, no::GroupEffect::Reduce}; + const auto parent = put(h, parent_req); + auto child_req = event_reduce(parent); + child_req.trigger = no::Limit{far_close(sign)}; + child_req.group = no::Member{7, 2, no::GroupEffect::Cancel}; + const auto child = put(h, child_req); + h.tick(0, 100); + const auto pf = fills(h, parent); + REQUIRE(pf.size() == 1); + CHECK(pf[0].opened_units == sign * 2 && pf[0].closed_units == 0 && pf[0].terminal); + CHECK(pf[0].filled_working == 2); + CHECK(fills(h, child).empty()); + CHECK(events(h).empty()); + CHECK(events(h, child).empty()); + const auto deferred = events(h); + REQUIRE(deferred.size() == 1); + CHECK(deferred[0].recipient == child); + CHECK(deferred[0].cause.ordinal == pf[0].ordinal); + CHECK(deferred[0].cause.run == parent.run); + CHECK(deferred[0].effect == no::GroupEffect::Reduce); + CHECK(deferred[0].deferred_delta == 2); + CHECK(std::holds_alternative(deferred[0].pending_before)); + CHECK(deferred[0].pending_after.total == 2 && deferred[0].pending_after.count == 1); + const auto bound = events(h, child); + REQUIRE(bound.size() == 1); + CHECK(bound[0].source.ordinal == pf[0].ordinal); + CHECK(bound[0].source_units == 2); + CHECK(bound[0].pending_total == 2 && bound[0].effective_deduction == 2); + REQUIRE(bound[0].prior_adjustment_ids.size() == 1); + CHECK(bound[0].prior_adjustment_ids[0].ordinal == deferred[0].ordinal); + const auto* remaining = std::get_if(&bound[0].remaining); + REQUIRE(remaining); + CHECK(remaining->q == 0); + const auto ce = cancellations(h, child); + REQUIRE(ce.size() == 1); + CHECK(ce[0].reason == no::CancelReason::Group); + REQUIRE(ce[0].cause); + CHECK(ce[0].cause->ordinal == pf[0].ordinal); + CHECK(ce[0].ordinal == bound[0].ordinal + 1); + CHECK(std::holds_alternative(ce[0].unexecuted)); + const auto* pending = std::get_if(&ce[0].pending); + REQUIRE(pending); + CHECK(pending->total == 2 && pending->count == 1); + CHECK(deferred[0].ordinal < bound[0].ordinal && bound[0].ordinal < ce[0].ordinal); + CHECK(h.trade_count() == 0); + CHECK(fills(h, child).empty()); + near(h.physical_position().signed_units, sign * 2); + near(h.native_marked_equity(100), 9998); + finish(h); +} + +// G4: delta 5 against Units 2 stages ReservationReduced actual 2 then Cancelled. +void g4_delta_clips_units_and_cancels(double sign) { + Host h; + start(h, sign > 0 ? "G4-L" : "G4-S", 1, 2); + auto waiting = tx(sign * 2, "units2"); + waiting.trigger = no::Limit{far_open(sign)}; + waiting.group = no::Member{7, 2, no::GroupEffect::Cancel}; + const auto recipient = put(h, waiting); + auto emit = tx(sign * 5, "delta5"); + emit.group = no::Member{7, 1, no::GroupEffect::Reduce}; + const auto filler = put(h, emit); + h.tick(0, 100); + const auto ff = fills(h, filler); + REQUIRE(ff.size() == 1); + CHECK(ff[0].filled_working == 5 && ff[0].opened_units == sign * 5); + CHECK(ff[0].current_ticket == 2); + CHECK(fills(h, recipient).empty()); + const auto reduced = events(h); + REQUIRE(reduced.size() == 1); + CHECK(reduced[0].cause.ordinal == ff[0].ordinal); + CHECK(reduced[0].recipient == recipient); + CHECK(reduced[0].effect == no::GroupEffect::Reduce); + CHECK(reduced[0].requested_delta == 5 && reduced[0].actual_deduction == 2); + CHECK(reduced[0].before.q == 2); + const auto* after = std::get_if(&reduced[0].after); + REQUIRE(after); + CHECK(after->q == 0); + const auto ce = cancellations(h, recipient); + REQUIRE(ce.size() == 1); + CHECK(ce[0].reason == no::CancelReason::Group); + REQUIRE(ce[0].cause); + CHECK(ce[0].cause->ordinal == ff[0].ordinal); + CHECK(ce[0].ordinal == reduced[0].ordinal + 1); + const auto* unexecuted = std::get_if(&ce[0].unexecuted); + REQUIRE(unexecuted); + CHECK(unexecuted->q == 2); + CHECK(h.trade_count() == 0); + CHECK(h.cancel(recipient).status == no::CancelStatus::NotWorking); + h.tick(1, far_open(sign)); + CHECK(fills(h, recipient).empty()); + CHECK(events(h).size() == 1); + CHECK(cancellations(h, recipient).size() == 1); + near(h.physical_position().signed_units, sign * 5); + near(h.native_marked_equity(100), 9998); + finish(h); +} + +// G7 host: one cause, two recipients exactly once; later successor excluded. +void g7_two_recipients_and_later_successor(double sign) { + Host h; + start(h, sign > 0 ? "G7-L" : "G7-S"); + auto parent_req = tx(sign * 5, "parent"); + parent_req.trigger = no::Stop{100 + sign * 5}; + const auto parent = put(h, parent_req); + auto c1 = event_reduce(parent, "first"); + c1.trigger = no::Limit{far_close(sign)}; + c1.group = no::Member{7, 2, no::GroupEffect::Reduce}; + const auto first = put(h, c1); + auto c2 = event_reduce(parent, "second"); + c2.trigger = no::Limit{far_close(sign)}; + c2.group = no::Member{7, 3, no::GroupEffect::Reduce}; + const auto second = put(h, c2); + auto f10 = tx(sign * 2, "F10"); + f10.group = no::Member{7, 1, no::GroupEffect::Reduce}; + const auto filler = put(h, f10); + + h.tick(0, 100); + const auto ff = fills(h, filler); + REQUIRE(ff.size() == 1); + CHECK(fills(h, parent).empty()); + auto deferred = events(h); + REQUIRE(deferred.size() == 2); + CHECK(deferred[0].cause.ordinal == ff[0].ordinal && deferred[1].cause.ordinal == ff[0].ordinal); + CHECK(deferred[0].recipient == first && deferred[1].recipient == second); + CHECK(deferred[0].deferred_delta == 2 && deferred[1].deferred_delta == 2); + CHECK(deferred[0].ordinal != deferred[1].ordinal); + + auto successor_req = event_reduce(parent, "second"); + successor_req.comment = "reused-text"; + successor_req.trigger = no::Limit{far_close(sign)}; + successor_req.group = no::Member{7, 3, no::GroupEffect::Reduce}; + const auto replaced = h.replace(second, successor_req); + REQUIRE(replaced.status == no::ReplaceStatus::Replaced); + REQUIRE(replaced.successor); + const auto successor = *replaced.successor; + CHECK(successor.incarnation != second.incarnation); + CHECK(events(h).size() == 2); + CHECK(cancellations(h, first).empty()); + + h.tick(1, 100 + sign * 5); + const auto pf = fills(h, parent); + REQUIRE(pf.size() == 1); + CHECK(pf[0].opened_units == sign * 5); + CHECK(events(h).size() == 2); + const auto first_bound = events(h, first); + const auto successor_bound = events(h, successor); + REQUIRE(first_bound.size() == 1); + REQUIRE(successor_bound.size() == 1); + CHECK(first_bound[0].source.ordinal == pf[0].ordinal); + CHECK(first_bound[0].source_units == 5); + CHECK(first_bound[0].pending_total == 2 && first_bound[0].effective_deduction == 2); + REQUIRE(first_bound[0].prior_adjustment_ids.size() == 1); + CHECK(first_bound[0].prior_adjustment_ids[0].ordinal == deferred[0].ordinal); + const auto* first_left = std::get_if(&first_bound[0].remaining); + REQUIRE(first_left); + CHECK(first_left->q == 3); + CHECK(successor_bound[0].source.ordinal == pf[0].ordinal); + CHECK(successor_bound[0].source_units == 5); + CHECK(successor_bound[0].pending_total == 0 && successor_bound[0].effective_deduction == 0); + CHECK(successor_bound[0].prior_adjustment_ids.empty()); + const auto* successor_left = + std::get_if(&successor_bound[0].remaining); + REQUIRE(successor_left); + CHECK(successor_left->q == 5); + CHECK(events(h, second).empty()); + CHECK(fills(h, first).empty() && fills(h, successor).empty()); + CHECK(cancellations(h, first).empty() && cancellations(h, successor).empty()); + near(h.physical_position().signed_units, sign * 7); + finish(h); +} + +void v1_representable_owner_action_size() { + Host h; + start(h, "V1-matrix"); + auto independent_tx = tx(1, "I-Tx"); + independent_tx.trigger = no::Limit{far_open(1)}; + CHECK(put(h, independent_tx).incarnation != 0); + auto independent_rd = reduce(1, "I-Rd"); + independent_rd.trigger = no::Limit{far_close(1)}; + CHECK(put(h, independent_rd).incarnation != 0); + auto independent_fl = no::Request{no::Flatten{}, "I-Fl", ""}; + independent_fl.trigger = no::Limit{far_close(1)}; + CHECK(put(h, independent_fl).incarnation != 0); + no::Request independent_oo{no::Reduce{no::OwnerOpenedUnits{}}, "I-Rd-OO", ""}; + expect_reject(h, independent_oo, no::RequestRejectReason::InvalidQuantityBasis); + + auto live_parent = tx(1, "wait-parent"); + live_parent.trigger = no::Limit{far_open(1)}; + const auto parent = put(h, live_parent); + auto wait_tx = tx(1, "W-Tx"); + wait_tx.owner = no::WaitForApplied{parent}; + CHECK(put(h, wait_tx).incarnation != 0); + auto wait_rd = reduce(1, "W-Rd"); + wait_rd.owner = no::WaitForApplied{parent}; + CHECK(put(h, wait_rd).incarnation != 0); + CHECK(put(h, event_reduce(parent, "W-Rd-OO")).incarnation != 0); + auto wait_fl = no::Request{no::Flatten{}, "W-Fl", ""}; + wait_fl.owner = no::WaitForApplied{parent}; + CHECK(put(h, wait_fl).incarnation != 0); + + auto flatten_budget = no::Request{no::Flatten{}, "budget-flat", ""}; + flatten_budget.capacity = no::PointBudget{1}; + expect_reject(h, flatten_budget, no::RequestRejectReason::InvalidCapacity); + auto bad_group = tx(1, "g0"); + bad_group.group = no::Member{0, 1, no::GroupEffect::Cancel}; + expect_reject(h, bad_group, no::RequestRejectReason::InvalidGroup); + auto bad_effect = tx(1, "ge"); + bad_effect.group = no::Member{3, 0, static_cast(9)}; + expect_reject(h, bad_effect, no::RequestRejectReason::InvalidGroup); + auto zero_rd = reduce(0, "zero"); + expect_reject(h, zero_rd, no::RequestRejectReason::InvalidQuantity); + auto zero_tx = tx(0, "zero-tx"); + expect_reject(h, zero_tx, no::RequestRejectReason::InvalidQuantity); + finish(h); + + Host bind_host; + start(bind_host, "V1-bind"); + const auto opener = put(bind_host, tx(1, "opener")); + bind_host.tick(0, 100); + const auto opened = fills(bind_host, opener); + REQUIRE(opened.size() == 1); + const int64_t cycle = opened[0].cycle_after; + REQUIRE(cycle > 0); + auto bind_rd = reduce(1, "B-Rd"); + bind_rd.owner = no::BindOpening{opener, cycle}; + CHECK(put(bind_host, bind_rd).incarnation != 0); + auto bind_fl = no::Request{no::Flatten{}, "B-Fl", ""}; + bind_fl.owner = no::BindOpening{opener, cycle}; + CHECK(put(bind_host, bind_fl).incarnation != 0); + auto bind_oo = no::Request{no::Reduce{no::OwnerOpenedUnits{}}, "B-Rd-OO", ""}; + bind_oo.owner = no::BindOpening{opener, cycle}; + expect_reject(bind_host, bind_oo, no::RequestRejectReason::InvalidQuantityBasis); + auto bind_tx = tx(1, "B-Tx"); + bind_tx.owner = no::BindOpening{opener, cycle}; + expect_reject(bind_host, bind_tx, no::RequestRejectReason::InvalidOwner); + finish(bind_host); + + Host grid; + auto spec = configuration("V1-grid"); + spec.quantity_grid = 1.0; + REQUIRE(grid.configure_native(spec).status == NativeSetupStatus::Applied); + const Bar warmup{100, 100, 100, 100, 1, T - 60000}; + REQUIRE(grid.stream_begin(&warmup, 1, "1", "1")); + REQUIRE(grid.stream_push_tick(TradeTick{T, ++grid.sequence, 100, 0})); + expect_reject(grid, tx(1.5, "off-grid"), no::RequestRejectReason::OffGrid); + finish(grid); +} + +void v3_rejection_precedence() { + Host h; + start(h, "V3-precedence"); + const auto opener = put(h, tx(1, "lot")); + h.tick(0, 100); + const auto opened = fills(h, opener); + REQUIRE(opened.size() == 1); + const int64_t cycle = opened[0].cycle_after; + + auto qty_then_trig = reduce(0, "qty-first"); + qty_then_trig.trigger = no::Stop{0}; + expect_reject(h, qty_then_trig, no::RequestRejectReason::InvalidQuantity); + + auto trig_then_owner = tx(1, "trig-first"); + trig_then_owner.trigger = no::Stop{0}; + trig_then_owner.owner = no::WaitForApplied{no::RequestHandle{{"V3-precedence", 1}, 0}}; + expect_reject(h, trig_then_owner, no::RequestRejectReason::InvalidTrigger); + + auto owner_then_group = tx(1, "owner-first"); + owner_then_group.owner = no::WaitForApplied{no::RequestHandle{{"V3-precedence", 1}, 99}}; + owner_then_group.group = no::Member{0, 0, no::GroupEffect::Cancel}; + expect_reject(h, owner_then_group, no::RequestRejectReason::InvalidOwner); + + auto bind_tx = tx(1, "bind-tx"); + bind_tx.owner = no::BindOpening{opener, cycle}; + expect_reject(h, bind_tx, no::RequestRejectReason::InvalidOwner); + + auto bad_owner_oo = no::Request{no::Reduce{no::OwnerOpenedUnits{}}, "bind-oo-dead", ""}; + bad_owner_oo.owner = no::BindOpening{opener, 0}; + expect_reject(h, bad_owner_oo, no::RequestRejectReason::InvalidOwner); + + auto valid_owner_oo = no::Request{no::Reduce{no::OwnerOpenedUnits{}}, "bind-oo-live", ""}; + valid_owner_oo.owner = no::BindOpening{opener, cycle}; + expect_reject(h, valid_owner_oo, no::RequestRejectReason::InvalidQuantityBasis); + + auto nan_stop = tx(1, "nan"); + nan_stop.trigger = no::Stop{std::numeric_limits::quiet_NaN()}; + expect_reject(h, nan_stop, no::RequestRejectReason::InvalidTrigger); + const auto rejected = events(h); + REQUIRE(!rejected.empty()); + const auto* stop = std::get_if(&rejected.back().request.trigger); + REQUIRE(stop); + CHECK(std::isnan(stop->price)); + finish(h); +} + +void v4_self_wait_and_invalid_trigger_preserve_dependents() { + Host h; + start(h, "V4-self-wait"); + auto parent_req = tx(1, "parent"); + parent_req.trigger = no::Limit{far_open(1)}; + const auto parent = put(h, parent_req); + auto child_req = tx(-1, "child"); + child_req.owner = no::WaitForApplied{parent}; + child_req.trigger = no::Limit{far_close(1)}; + const auto child = put(h, child_req); + auto grandchild_req = reduce(1, "grandchild"); + grandchild_req.owner = no::WaitForApplied{child}; + const auto grandchild = put(h, grandchild_req); + + auto self_wait = tx(1, "self"); + self_wait.owner = no::WaitForApplied{parent}; + const auto self = h.replace(parent, self_wait); + CHECK(self.status == no::ReplaceStatus::ReplaceRejected); + CHECK(self.reason == no::RequestRejectReason::InvalidOwner); + CHECK(!self.successor); + const auto owner_reject = events(h); + REQUIRE(!owner_reject.empty()); + CHECK(owner_reject.back().reason == no::RequestRejectReason::InvalidOwner); + CHECK(owner_reject.back().target() == parent); + + auto bad_trig = tx(1, "bad-stop"); + bad_trig.trigger = no::Stop{0}; + const auto trig = h.replace(parent, bad_trig); + CHECK(trig.status == no::ReplaceStatus::ReplaceRejected); + CHECK(trig.reason == no::RequestRejectReason::InvalidTrigger); + CHECK(!trig.successor); + + auto still_parent = tx(1, "still-live"); + still_parent.trigger = no::Stop{0}; + const auto parent_probe = h.replace(parent, still_parent); + CHECK(parent_probe.status == no::ReplaceStatus::ReplaceRejected); + CHECK(parent_probe.reason == no::RequestRejectReason::InvalidTrigger); + auto still_child = tx(-1, "child-live"); + still_child.trigger = no::Stop{0}; + const auto child_probe = h.replace(child, still_child); + CHECK(child_probe.status == no::ReplaceStatus::ReplaceRejected); + CHECK(child_probe.reason == no::RequestRejectReason::InvalidTrigger); + auto still_grand = reduce(1, "grand-live"); + still_grand.trigger = no::Stop{0}; + const auto grand_probe = h.replace(grandchild, still_grand); + CHECK(grand_probe.status == no::ReplaceStatus::ReplaceRejected); + CHECK(grand_probe.reason == no::RequestRejectReason::InvalidTrigger); + CHECK(events(h).empty()); + CHECK(cancellations(h, parent).empty()); + CHECK(cancellations(h, child).empty()); + CHECK(cancellations(h, grandchild).empty()); + finish(h); +} + +void v4_foreign_invalid_nonworking_beat_candidate() { + Host h; + start(h, "V4-target-first"); + auto live_req = tx(1, "live"); + live_req.trigger = no::Limit{far_open(1)}; + const auto live = put(h, live_req); + auto bad = tx(1, "candidate"); + bad.trigger = no::Stop{0}; + + no::RequestHandle foreign{{"other-run", 1}, live.incarnation}; + const auto foreign_result = h.replace(foreign, bad); + CHECK(foreign_result.status == no::ReplaceStatus::InvalidHandle); + CHECK(!foreign_result.reason); + const auto invalid = events(h); + REQUIRE(!invalid.empty()); + CHECK(invalid.back().target.run.session_key == "other-run"); + REQUIRE(invalid.back().attempted); + CHECK(std::holds_alternative(invalid.back().attempted->trigger)); + + no::RequestHandle zero{{"V4-target-first", 1}, 0}; + const auto zero_result = h.replace(zero, bad); + CHECK(zero_result.status == no::ReplaceStatus::InvalidHandle); + + const auto filled = put(h, tx(1, "fill-me")); + h.tick(0, 100); + REQUIRE(fills(h, filled).size() == 1); + const auto dead = h.replace(filled, bad); + CHECK(dead.status == no::ReplaceStatus::NotWorking); + CHECK(!dead.reason); + const auto not_working = events(h); + REQUIRE(!not_working.empty()); + CHECK(not_working.back().target == filled); + REQUIRE(not_working.back().attempted); + CHECK(std::holds_alternative(not_working.back().attempted->trigger)); + + const auto live_probe = h.replace(live, bad); + CHECK(live_probe.status == no::ReplaceStatus::ReplaceRejected); + CHECK(live_probe.reason == no::RequestRejectReason::InvalidTrigger); + CHECK(events(h).empty()); + finish(h); +} + +// Internal duplicate delivery is not a public host operation. +void core_duplicate_delivery_and_successor_exclusion() { + const no::RunIdentity run{"group-core", 1}; + no::WorkingRequestCore core(run); + uint64_t inc = 1; + uint64_t ord = 1; + auto parent_req = tx(5, "parent"); + parent_req.trigger = no::Limit{50}; + const auto parent = core.submit(parent_req, 0, inc, ord); + REQUIRE(parent.status == no::SubmitStatus::Accepted); + auto first_req = no::Request{no::Reduce{no::OwnerOpenedUnits{}}, "first", ""}; + first_req.owner = no::WaitForApplied{*parent.handle}; + first_req.group = no::Member{7, 2, no::GroupEffect::Reduce}; + const auto first = core.submit(first_req, 0, inc, ord); + REQUIRE(first.status == no::SubmitStatus::Accepted); + auto second_req = no::Request{no::Reduce{no::OwnerOpenedUnits{}}, "second", ""}; + second_req.owner = no::WaitForApplied{*parent.handle}; + second_req.group = no::Member{7, 3, no::GroupEffect::Reduce}; + const auto second = core.submit(second_req, 0, inc, ord); + REQUIRE(second.status == no::SubmitStatus::Accepted); + auto filler_req = tx(2, "F10"); + filler_req.group = no::Member{7, 1, no::GroupEffect::Reduce}; + const auto filler = core.submit(filler_req, 0, inc, ord); + REQUIRE(filler.status == no::SubmitStatus::Accepted); + + auto eval = core.prepare_evaluation(*filler.handle, + [] { + no::EvaluationContext ctx; + ctx.cursor.point.ordinal = 20; + ctx.cursor.point.effective_time_ms = 1000; + ctx.cursor.point.provenance = + NativePriceProvenance::ObservedPrint; + ctx.driver_class = no::DriverEligibilityClass::ObservedPrint; + ctx.existing_matching_bit = true; + return ctx; + }(), + [] { + no::TargetObservation obs; + obs.current_position = no::PositionFlat{}; + return obs; + }(), + ord); + REQUIRE(std::holds_alternative(eval)); + auto eval_installed = core.install_mutation(std::get(std::move(eval))); + REQUIRE(std::holds_alternative(eval_installed)); + ord += std::get(eval_installed).events.count; + + no::ExecutionProposal proposal; + const auto* live = core.find_live(*filler.handle); + REQUIRE(live); + proposal.cursor.point.ordinal = std::get(live->allowance).point_ordinal; + proposal.cursor.point.effective_time_ms = 1000; + proposal.raw_price = 100; + proposal.resolved_price = 100; + proposal.physical_action = no::Transact{2}; + proposal.inspected_opened_units = 2; + proposal.inspected_current_ticket = 1; + auto prep = core.prepare_execution(*filler.handle, proposal, ord); + REQUIRE(std::holds_alternative(prep)); + no::CommittedExecutionFacts facts; + facts.result.status = execution::Status::Applied; + facts.result.opened_units = 2; + facts.result.current_ticket = 1; + facts.cycle_after = 1; + facts.post_target.current_position = no::PositionNonflat{1, no::Side::Long}; + facts.committed_action = no::Transact{2}; + auto installed = core.install_execution(std::get(std::move(prep)), facts); + REQUIRE(std::holds_alternative(installed)); + ord += std::get(installed).events.count; + const auto* applied = std::get_if(&core.history().back()); + REQUIRE(applied); + const no::EventId cause{run, applied->ordinal}; + + auto recipients = core.group_recipients(cause); + REQUIRE(recipients.size() == 2); + CHECK(recipients[0] == *first.handle); + CHECK(recipients[1] == *second.handle); + + auto d1 = core.prepare_group_effect(cause, *first.handle, ord); + REQUIRE(std::holds_alternative(d1)); + auto d1i = core.install_mutation(std::get(std::move(d1))); + REQUIRE(std::holds_alternative(d1i)); + ord += std::get(d1i).events.count; + CHECK(std::holds_alternative(core.history().back())); + auto d2 = core.prepare_group_effect(cause, *second.handle, ord); + REQUIRE(std::holds_alternative(d2)); + auto d2i = core.install_mutation(std::get(std::move(d2))); + REQUIRE(std::holds_alternative(d2i)); + ord += std::get(d2i).events.count; + CHECK(std::holds_alternative(core.history().back())); + + const std::size_t after_both = core.history().size(); + const uint64_t ord_before_replay = ord; + auto replay_first = core.prepare_group_effect(cause, *first.handle, ord); + REQUIRE(std::holds_alternative(replay_first)); + CHECK(std::get(replay_first).reason == no::NoChangeReason::AlreadyApplied); + auto replay_second = core.prepare_group_effect(cause, *second.handle, ord); + REQUIRE(std::holds_alternative(replay_second)); + CHECK(std::get(replay_second).reason == no::NoChangeReason::AlreadyApplied); + CHECK(core.history().size() == after_both); + CHECK(ord == ord_before_replay); + + auto successor_req = no::Request{no::Reduce{no::OwnerOpenedUnits{}}, "second", ""}; + successor_req.owner = no::WaitForApplied{*parent.handle}; + successor_req.group = no::Member{7, 3, no::GroupEffect::Reduce}; + const auto successor = core.replace(*second.handle, successor_req, 0, inc, ord); + REQUIRE(successor.status == no::ReplaceStatus::Replaced); + REQUIRE(successor.successor); + auto after_replace = core.group_recipients(cause); + REQUIRE(after_replace.size() == 1); + CHECK(after_replace[0] == *first.handle); + auto late = core.prepare_group_effect(cause, *successor.successor, ord); + REQUIRE(std::holds_alternative(late)); + CHECK(std::get(late).code == no::CoreFailure::InvalidCause); + CHECK(core.history().size() == after_both + 1); + auto replay_dead = core.prepare_group_effect(cause, *second.handle, ord); + REQUIRE(std::holds_alternative(replay_dead)); + CHECK(std::get(replay_dead).reason == no::NoChangeReason::AlreadyApplied); + CHECK(core.find_live(*first.handle)); + CHECK(!core.find_live(*second.handle)); + CHECK(core.find_live(*successor.successor)); +} + +void run_case(const char* name, const std::function& body) { + scenario = name; + ++cases; + const int previous = failures; + try { + body(); + } catch (const StopCase&) { + } catch (const std::exception& e) { + ++failures; + std::printf("FAIL %s unexpected exception: %s\n", name, e.what()); + } catch (...) { + ++failures; + std::printf("FAIL %s unexpected nonstandard exception\n", name); + } + std::printf("%s %s\n", failures == previous ? "PASS" : "FAIL", name); +} +} // namespace + +int main() { + for (double sign : {1.0, -1.0}) { + run_case(sign > 0 ? "G1 long partial Cancel silent until terminal" + : "G1 short partial Cancel silent until terminal", + [&] { g1_partial_cancel_until_terminal(sign); }); + run_case(sign > 0 ? "G1 long TargetExhausted Cancel still emits" + : "G1 short TargetExhausted Cancel still emits", + [&] { g1_target_exhausted_cancel_emits(sign); }); + run_case(sign > 0 ? "G3 long parent Reduce binds unbound child" + : "G3 short parent Reduce binds unbound child", + [&] { g3_parent_reduce_unbinds_child(sign); }); + run_case(sign > 0 ? "G4 long delta5 clips Units2" + : "G4 short delta5 clips Units2", + [&] { g4_delta_clips_units_and_cancels(sign); }); + run_case(sign > 0 ? "G7 long two recipients and later successor" + : "G7 short two recipients and later successor", + [&] { g7_two_recipients_and_later_successor(sign); }); + } + run_case("V1 representable owner/action/size matrix", [] { v1_representable_owner_action_size(); }); + run_case("V3 rejection precedence", [] { v3_rejection_precedence(); }); + run_case("V4 self-wait replacement preserves predecessor/dependents", + [] { v4_self_wait_and_invalid_trigger_preserve_dependents(); }); + run_case("V4 foreign/invalid/nonworking replace target beats candidate", + [] { v4_foreign_invalid_nonworking_beat_candidate(); }); + run_case("G7 core duplicate key and successor exclusion", + [] { core_duplicate_delivery_and_successor_exclusion(); }); + std::printf("%s native resting group contract: %d cases, %d checks, %d failures\n", + failures ? "FAIL" : "PASS", cases, checks, failures); + return failures ? 1 : 0; +} diff --git a/tests/test_native_resting_matching_contract.cpp b/tests/test_native_resting_matching_contract.cpp new file mode 100644 index 00000000..6aa9c166 --- /dev/null +++ b/tests/test_native_resting_matching_contract.cpp @@ -0,0 +1,157 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; +namespace { +int checks=0, failures=0, cases=0; +const char* scenario="setup"; +#define CHECK(x) do { ++checks; if (!(x)) { ++failures; std::printf("FAIL %s:%d %s\n",scenario,__LINE__,#x); } } while(0) +constexpr int64_t T=1736121600000LL; +void near(double actual,double expected) { + if (std::abs(actual-expected)>1e-11*std::max(1.0,std::abs(expected))) + std::printf("actual=%.17g expected=%.17g\n",actual,expected); + CHECK(std::isfinite(actual) && std::abs(actual-expected)<=1e-11*std::max(1.0,std::abs(expected))); +} +struct Host final : NativeStrategyHost { + std::function begin; + uint64_t sequence=0; + void on_native_run_begin() override { if (begin) begin(*this); } + void on_native_bar(const Bar&,const NativeDecisionContext&) override {} + void tick(int64_t offset,double price) { + CHECK(stream_push_tick(TradeTick{T+offset+1,++sequence,price,1})); + CHECK(native_state().kind==NativeLifecycleKind::Running); + } +}; +NativeRunSpec spec(const char* key,int slippage=0) { + NativeRunSpec s; + s.identity={key,1}; s.input_tf="1";s.script_tf="1"; + s.ticker="N";s.tickerid="TEST:N";s.type="crypto";s.currency="USD"; + s.basecurrency="USD";s.description="native matching acceptance";s.volumetype="base"; + s.timezone="UTC";s.session="24x7";s.initial_capital=10000; + s.point_value=1;s.account_fx=1;s.price_tick=.01; + s.slippage_ticks=slippage;s.fee_kind=NativeFeeKind::CashPerExecution;s.fee_value=0; + return s; +} +void start(Host& h,const NativeRunSpec& s) { + CHECK(h.configure_native(s).status==NativeSetupStatus::Applied); + auto begin=std::exchange(h.begin,{}); + const Bar warmup{100,100,100,100,1,T-60000}; + CHECK(h.stream_begin(&warmup,1,"1","1")); + CHECK(h.stream_push_tick(TradeTick{T,++h.sequence,100,0})); + h.begin=std::move(begin);if(h.begin)h.begin(h); +} +void finish(Host& h) { CHECK(h.stream_end(false));CHECK(h.native_state().kind==NativeLifecycleKind::Completed); } +no::Request tx(double units,const char* label="") { return {no::Transact{units},label,""}; } +no::RequestHandle put(Host& h,const no::Request& r) { + auto x=h.submit(r);CHECK(x.status==no::SubmitStatus::Accepted && x.handle.has_value()); + return x.handle.value_or(no::RequestHandle{}); +} +template std::vector events(const Host& h,const no::RequestHandle& target) { + std::vector out; + for(const auto& e:h.native_events(0))if(e.command)if(const auto* x=std::get_if(&*e.command)) { + CHECK(bool(x->definition)); + if(x->definition && x->definition->handle==target)out.push_back(*x); + } + return out; +} +using Applied=no::ExecutionAppliedEvent; +void independent_market_and_limit(double sign) { + ++cases;scenario="T1 independent Market and Limit";Host h;no::RequestHandle limit,market; + h.begin=[&](Host& x){auto r=tx(sign);r.trigger=no::Limit{100-sign};limit=put(x,r);market=put(x,tx(sign));}; + CHECK(h.configure_native(spec(sign>0?"T1-L":"T1-S")).status==NativeSetupStatus::Applied); + // Non-tied AUTO paths reach the first extreme before the later limit. + const Bar b=sign>0?Bar{100,109,90,101,1,T}:Bar{100,110,91,99,1,T}; + h.run(&b,1);CHECK(h.native_state().kind==NativeLifecycleKind::Completed); + auto m=events(h,market),l=events(h,limit);CHECK(m.size()==1 && l.size()==1); + if(m.size()==1 && l.size()==1) { + CHECK(limit.incarnation0?NativePathPhase::Low:NativePathPhase::High)); + near(l[0].cursor.t,10.0/19.0); + } + near(h.physical_position().signed_units,sign*2);near(h.physical_position().average_price,100-sign*.5); +} +void continuous_stop_limit(double sign,int slip) { + ++cases;scenario="T3 mirrored continuous StopLimit";Host h;no::RequestHandle id; + h.begin=[&](Host& x){auto r=tx(sign);r.trigger=no::StopLimit{100+sign,100+sign*2};id=put(x,r);}; + CHECK(h.configure_native(spec(sign>0?"T3-L":"T3-S",slip)).status==NativeSetupStatus::Applied); + const Bar b=sign>0?Bar{100,103,99,102,1,T}:Bar{100,101,97,98,1,T}; + h.run(&b,1);CHECK(h.native_state().kind==NativeLifecycleKind::Completed); + auto a=events(h,id);auto f=events(h,id);CHECK(a.size()==1 && f.size()==1); + if(a.size()==1 && f.size()==1) { + CHECK(a[0].kind==no::ActivationKind::StopLimit && a[0].reached_price==100+sign); + CHECK(f[0].raw_price==a[0].reached_price); + CHECK(f[0].resolved_price==100+sign*(slip?2:1)); + CHECK(f[0].cursor.point.ordinal==a[0].cursor.point.ordinal && f[0].cursor.t==a[0].cursor.t); + CHECK(f[0].cursor.point.provenance==NativePriceProvenance::Confirmed); + } +} +void discrete_stop_limit_partial(double sign,int slip) { + ++cases;scenario="T4 StopLimit gap and persistent partial";Host h;no::RequestHandle id; + h.begin=[&](Host& x){auto r=tx(sign*2);r.trigger=no::StopLimit{100+sign,100+sign*2};r.capacity=no::PointBudget{1};id=put(x,r);}; + start(h,spec(sign>0?"T4-L":"T4-S",slip)); + h.tick(0,100);h.tick(1,100+sign*3);CHECK(events(h,id).empty()); + auto a=events(h,id);CHECK(a.size()==1); + if(a.size()==1){CHECK(a[0].reached_price==100+sign*3 && a[0].cursor.t==0);CHECK(a[0].cursor.point.provenance==NativePriceProvenance::ObservedPrint);} + h.tick(2,100+sign*2);CHECK(events(h,id).size()==1); + h.tick(3,100+sign*3);CHECK(events(h,id).size()==1); + h.tick(4,100);auto f=events(h,id);CHECK(f.size()==2); + if(f.size()==2){CHECK(f[0].raw_price==100+sign*2 && f[1].raw_price==100);CHECK(f[0].resolved_price==100+sign*2);CHECK(f[1].resolved_price==100+sign*(slip?2:0));CHECK(!f[0].terminal && f[1].terminal);} + CHECK(events(h,id).size()==1);near(h.physical_position().signed_units,sign*2);finish(h); +} +void active_stop_across_flat(double sign) { + ++cases;scenario="T5 active Stop remainder across flat";Host h;h.begin=[&](Host& x){put(x,tx(-sign));};start(h,spec(sign>0?"T5-L":"T5-S"));h.tick(0,100); + auto r=tx(sign*2);r.trigger=no::Stop{100};r.capacity=no::PointBudget{1};auto id=put(h,r); + h.tick(1,100+sign);near(h.physical_position().signed_units,0); + h.tick(2,100-sign);auto f=events(h,id);CHECK(f.size()==2); + if(f.size()==2){CHECK(f[0].closed_units==1 && f[0].opened_units==0 && !f[0].terminal);CHECK(f[1].opened_units==sign && f[1].raw_price==100-sign && f[1].terminal);} + CHECK(events(h,id).size()==1);near(h.physical_position().signed_units,sign);finish(h); +} +void resting_resulting_caps(double sign,double budget) { + ++cases;scenario=budget==2?"P1 budgeted crossing admitted":"P1 oversized crossing rejected atomically"; + Host h;h.begin=[&](Host& x){put(x,tx(-sign,"seed"));}; + auto s=spec(sign>0?"P1-L":"P1-S");s.max_abs_units=1;s.max_open_lots=1;s.fee_kind=NativeFeeKind::CashPerUnit;s.fee_value=.5; + start(h,s);h.tick(0,100); + auto r=tx(sign*3,"parent");r.trigger=no::StopLimit{100,100};r.capacity=no::PointBudget{budget};r.group=no::Member{7,1,no::GroupEffect::Cancel};auto id=put(h,r); + auto peer=tx(sign,"peer");peer.trigger=no::Limit{sign>0?1.0:200.0};peer.group=no::Member{7,2,no::GroupEffect::Cancel};auto sibling=put(h,peer); + h.tick(1,100);auto f=events(h,id); + if(budget==2) { + CHECK(f.size()==1); + if(f.size()==1){CHECK(f[0].closed_units==1 && f[0].opened_units==sign && f[0].current_ticket==1 && !f[0].terminal);} + near(h.physical_position().signed_units,sign);CHECK(h.physical_position().lot_count==1);near(h.native_marked_equity(100),9998.5); + h.tick(2,100);CHECK(events(h,id).size()==1);near(h.physical_position().signed_units,sign);near(h.native_marked_equity(100),9998.5); + } else { + CHECK(f.empty());near(h.physical_position().signed_units,-sign);CHECK(h.physical_position().lot_count==1);CHECK(h.trade_count()==0);near(h.native_marked_equity(100),9999.5); + } + auto rejected=events(h,id);CHECK(rejected.size()==1); + if(rejected.size()==1)CHECK(rejected[0].reason==no::MatchRejectReason::MaxAbsUnits); + CHECK(events(h,sibling).empty());CHECK(h.cancel(sibling).status==no::CancelStatus::Cancelled);finish(h); +} +void close_only_direction(double sign) { + ++cases;scenario="P1 closing transaction bypasses opening-only direction cap"; + Host h;h.begin=[&](Host& x){put(x,tx(-sign));};auto s=spec(sign>0?"P1-close-L":"P1-close-S"); + s.allowed_open_directions=sign>0?NativeOpenDirections::Short:NativeOpenDirections::Long; + s.fee_kind=NativeFeeKind::CashPerUnit;s.fee_value=.5;start(h,s);h.tick(0,100); + auto bad=tx(sign*2);bad.trigger=no::StopLimit{100,100};auto rejected=put(h,bad);h.tick(1,100); + CHECK(events(h,rejected).empty());near(h.physical_position().signed_units,-sign);near(h.native_marked_equity(100),9999.5); + auto close=tx(sign);close.trigger=no::StopLimit{100,100};auto id=put(h,close);h.tick(2,100);auto f=events(h,id);CHECK(f.size()==1); + if(f.size()==1)CHECK(f[0].closed_units==1 && f[0].opened_units==0 && f[0].current_ticket==.5); + near(h.physical_position().signed_units,0);near(h.native_marked_equity(100),9999);finish(h); +} +} +int main() { + for(double sign:{1.0,-1.0}) { + independent_market_and_limit(sign); + for(int slip:{0,200}){continuous_stop_limit(sign,slip);discrete_stop_limit_partial(sign,slip);} + active_stop_across_flat(sign);resting_resulting_caps(sign,2);resting_resulting_caps(sign,3);close_only_direction(sign); + } + std::printf("%s native resting matching: %d cases, %d checks, %d failures\n",failures?"FAIL":"PASS",cases,checks,failures); + return failures?1:0; +} diff --git a/tests/test_native_resting_owner_contract.cpp b/tests/test_native_resting_owner_contract.cpp new file mode 100644 index 00000000..bc88f02e --- /dev/null +++ b/tests/test_native_resting_owner_contract.cpp @@ -0,0 +1,754 @@ +// Bounded R2 owner/lifetime acceptance: C3/C4/C6/C7 and O2-O5/O7-O9. +// Real NativeStrategyHost commands, warmup+tick or confirmed-bar drivers, and +// physical observations only. O1/O6/O10-O12 are covered elsewhere. +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; +namespace { +int checks = 0, failures = 0, cases = 0; +const char* scenario = "setup"; +struct StopCase {}; +#define CHECK(x) do { ++checks; if (!(x)) { ++failures; \ + std::printf("FAIL %s:%d %s\n", scenario, __LINE__, #x); } } while (0) +#define REQUIRE(x) do { const bool ok_ = bool(x); CHECK(ok_); if (!ok_) throw StopCase{}; } while (0) + +constexpr int64_t T = 1736121600000LL; + +void near(double actual, double expected) { + const bool ok = std::isfinite(actual) && std::isfinite(expected) + && std::abs(actual - expected) <= 1e-12 * std::max(1.0, std::abs(expected)); + if (!ok) std::printf(" actual=%.17g expected=%.17g\n", actual, expected); + CHECK(ok); +} + +struct Host final : NativeStrategyHost { + std::function beginning; + std::function calculation; + int calculations = 0; + uint64_t sequence = 0; + + void on_native_run_begin() override { if (beginning) beginning(*this); } + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + ++calculations; + if (calculation) calculation(*this); + } + bool input(int64_t offset, double price) { + return stream_push_tick(TradeTick{T + offset + 1, ++sequence, price, 1}); + } + void tick(int64_t offset, double price) { + REQUIRE(input(offset, price)); + REQUIRE(native_state().kind == NativeLifecycleKind::Running); + } + const std::vector& lots() const { return pyramid_entries_; } +}; + +NativeRunSpec configuration(const char* key, uint64_t run = 1, double fee = 0) { + NativeRunSpec s; + s.identity = {key, run}; s.input_tf = "1"; s.script_tf = "1"; + s.ticker = "N"; s.tickerid = "TEST:N"; s.type = "crypto"; + s.currency = "USD"; s.basecurrency = "USD"; s.description = "R2 owner acceptance"; + s.volumetype = "base"; s.timezone = "UTC"; s.session = "24x7"; + s.initial_capital = 10000; s.point_value = 1; s.account_fx = 1; + s.price_tick = .01; s.fee_kind = NativeFeeKind::CashPerExecution; s.fee_value = fee; + return s; +} + +void start(Host& h, const char* key, uint64_t run = 1, double fee = 0, + double first_price = 100, + const std::function& tweak = {}) { + auto spec = configuration(key, run, fee); + if (tweak) tweak(spec); + REQUIRE(h.configure_native(spec).status == NativeSetupStatus::Applied); + auto beginning = std::exchange(h.beginning, {}); + const Bar warmup{first_price, first_price, first_price, first_price, 1, T - 60000}; + h.sequence = 0; + REQUIRE(h.stream_begin(&warmup, 1, "1", "1")); + REQUIRE(h.stream_push_tick(TradeTick{T, ++h.sequence, first_price, 0})); + h.beginning = std::move(beginning); + if (h.beginning) h.beginning(h); + REQUIRE(h.native_state().kind == NativeLifecycleKind::Running); +} + +void finish(Host& h) { + REQUIRE(h.stream_end(false)); + CHECK(h.native_state().kind == NativeLifecycleKind::Completed); +} + +no::Request tx(double q, const char* label = "") { return {no::Transact{q}, label, ""}; } +no::Request reduce(double q, const char* label = "") { + return {no::Reduce{no::ExplicitUnits{q}}, label, ""}; +} +no::Request event_reduce(const char* label = "event-child") { + return {no::Reduce{no::OwnerOpenedUnits{}}, label, ""}; +} +no::Request flatten(const char* label = "flatten") { return {no::Flatten{}, label, ""}; } + +no::RequestHandle put(Host& h, const no::Request& request) { + auto result = h.submit(request); + REQUIRE(result.status == no::SubmitStatus::Accepted); + REQUIRE(result.handle.has_value()); + return *result.handle; +} + +template std::vector events(const Host& h) { + std::vector result; + for (const auto& event : h.native_events(0)) { + if (event.command) if (const auto* value = std::get_if(&*event.command)) + result.push_back(*value); + } + return result; +} +template +std::vector events(const Host& h, const no::RequestHandle& handle) { + std::vector result; + for (const auto& event : events(h)) { + REQUIRE(event.definition); + if (event.definition->handle == handle) result.push_back(event); + } + return result; +} +std::vector fills(const Host& h, const no::RequestHandle& handle) { + return events(h, handle); +} +std::vector cancellations(const Host& h, const no::RequestHandle& handle) { + return events(h, handle); +} + +double units_after(const no::ExecutionAppliedEvent& event) { + const auto* remaining = std::get_if(&event.remaining_after); + REQUIRE(remaining); + return remaining->q; +} +double units_before(const no::ExecutionAppliedEvent& event) { + const auto* remaining = std::get_if(&event.remaining_before); + REQUIRE(remaining); + return remaining->q; +} +void check_point_budget(const no::ExecutionAppliedEvent& event, double filled) { + const auto* before = std::get_if(&event.allowance_before); + const auto* after = std::get_if(&event.allowance_after); + REQUIRE(before && after); + CHECK(before->point_ordinal == event.cursor.point.ordinal); + CHECK(after->point_ordinal == before->point_ordinal); + near(before->left, filled); + near(after->left, 0); + near(event.filled_working, filled); +} +void check_scope(const no::ExecutionAppliedEvent& event, + const no::RequestHandle& opening, int64_t cycle) { + const auto* scope = std::get_if(&event.scope); + REQUIRE(scope); + CHECK(scope->incarnation == opening.incarnation); + CHECK(scope->cycle == cycle); +} +int64_t opening_cycle(const Host& h, const no::RequestHandle& handle) { + const auto f = fills(h, handle); + REQUIRE(!f.empty()); + REQUIRE(f.front().opened_units != 0); + REQUIRE(f.front().cycle_after > 0); + return f.front().cycle_after; +} +const PyramidEntry* lot_of(const Host& h, const no::RequestHandle& handle) { + const auto found = std::find_if(h.lots().begin(), h.lots().end(), [&](const auto& value) { + return value.entry_incarnation == handle.incarnation; + }); + return found == h.lots().end() ? nullptr : &*found; +} +void one_cancel(const Host& h, const no::RequestHandle& handle, no::CancelReason reason, + uint64_t cause_ordinal) { + const auto ce = cancellations(h, handle); + REQUIRE(ce.size() == 1); + CHECK(ce[0].reason == reason); + REQUIRE(ce[0].cause); + CHECK(ce[0].cause->ordinal == cause_ordinal); + CHECK(ce[0].cause->run == handle.run); + CHECK(fills(h, handle).empty()); +} +void no_cancel(const Host& h, const no::RequestHandle& handle) { + CHECK(cancellations(h, handle).empty()); +} +bool wait_authority(const no::Authority& authority, const no::RequestHandle& parent) { + const auto* wait = std::get_if(&authority); + return wait && wait->parent == parent; +} + +void independent_reduce_budget(double sign) { + Host h; start(h, sign > 0 ? "C3-long" : "C3-short"); + const auto seed = put(h, tx(sign * 5, "seed")); h.tick(0, 100); + const auto cycle = opening_cycle(h, seed); + auto closing = reduce(5, "C3"); closing.capacity = no::PointBudget{2}; + const auto handle = put(h, closing); + h.tick(1, 100); h.tick(2, 100); h.tick(3, 100); + const auto bound = events(h, handle); + REQUIRE(bound.size() == 1); + CHECK(bound[0].cycle == cycle); + CHECK(bound[0].side == (sign > 0 ? no::Side::Long : no::Side::Short)); + CHECK(std::holds_alternative(bound[0].before)); + REQUIRE(std::holds_alternative(bound[0].after)); + const auto f = fills(h, handle); + REQUIRE(f.size() == 3); + const double closed[] = {2, 2, 1}; + const double before[] = {5, 3, 1}; + const double after[] = {3, 1, 0}; + for (size_t i = 0; i < 3; ++i) { + near(f[i].closed_units, closed[i]); CHECK(f[i].opened_units == 0); + near(units_before(f[i]), before[i]); near(units_after(f[i]), after[i]); + check_point_budget(f[i], closed[i]); + CHECK(f[i].terminal == (i == 2)); + CHECK(std::holds_alternative(f[i].scope)); + if (i < 2) { + CHECK(f[i].cycle_before == cycle && f[i].cycle_after == cycle); + CHECK(!f[i].terminal_reason); + } + } + CHECK(f[0].cursor.point.ordinal != f[1].cursor.point.ordinal); + CHECK(f[1].cursor.point.ordinal != f[2].cursor.point.ordinal); + CHECK(bound[0].ordinal < f[0].ordinal); + REQUIRE(f[2].terminal_reason); + CHECK(*f[2].terminal_reason == no::AppliedTerminalReason::WorkingUnitsSatisfied); + CHECK(f[2].cycle_before == cycle && f[2].cycle_after == 0); + no_cancel(h, handle); + CHECK(h.cancel(handle).status == no::CancelStatus::NotWorking); + CHECK(cancellations(h, handle).empty()); + near(h.physical_position().signed_units, 0); + REQUIRE(h.trade_count() == 3); + near(h.get_trade(0).qty, 2); near(h.get_trade(1).qty, 2); near(h.get_trade(2).qty, 1); + finish(h); +} + +void target_exhausted_remainder(double sign) { + Host h; start(h, sign > 0 ? "C4-long" : "C4-short"); + put(h, tx(sign * 2, "seed")); h.tick(0, 100); + auto closing = reduce(5, "C4"); closing.capacity = no::PointBudget{2}; + const auto handle = put(h, closing); + h.tick(1, 100); + const auto f = fills(h, handle); + REQUIRE(f.size() == 1); + CHECK(f[0].terminal); + REQUIRE(f[0].terminal_reason); + CHECK(*f[0].terminal_reason == no::AppliedTerminalReason::TargetExhausted); + near(f[0].closed_units, 2); CHECK(f[0].opened_units == 0); + near(units_before(f[0]), 5); near(units_after(f[0]), 3); + check_point_budget(f[0], 2); + no_cancel(h, handle); + CHECK(h.cancel(handle).status == no::CancelStatus::NotWorking); + const auto later = put(h, tx(sign * 3, "later")); h.tick(2, 100); + CHECK(fills(h, handle).size() == 1); + REQUIRE(fills(h, later).size() == 1); + near(h.physical_position().signed_units, sign * 3); + REQUIRE(lot_of(h, later)); + near(lot_of(h, later)->qty, 3); + finish(h); +} + +void trail_canceled_by_reversal(double sign) { + Host h; start(h, sign > 0 ? "C6-long" : "C6-short"); + put(h, tx(sign, "seed")); h.tick(0, 100); + auto trail_req = reduce(1, "trail"); + trail_req.trigger = no::Trail{2, 100 + sign * 5}; + const auto trail = put(h, trail_req); + h.tick(1, 100); + CHECK(fills(h, trail).empty()); + CHECK(events(h, trail).empty()); + REQUIRE(events(h, trail).size() == 1); + h.tick(2, 100 + sign * 6); + auto activated = events(h, trail); + REQUIRE(activated.size() == 1); + CHECK(activated[0].kind == no::ActivationKind::TrailArm); + REQUIRE(std::holds_alternative(activated[0].after)); + near(std::get(activated[0].after).best, 100 + sign * 6); + CHECK(fills(h, trail).empty()); + h.tick(3, 100 + sign * 10); + CHECK(fills(h, trail).empty()); + CHECK(events(h, trail).size() == 1); + const auto reverse = put(h, tx(-sign * 2, "reverse")); + h.tick(4, 100 + sign * 10); + REQUIRE(fills(h, reverse).size() == 1); + CHECK(fills(h, trail).empty()); + CHECK(events(h, trail).size() == 1); + one_cancel(h, trail, no::CancelReason::OwnerGone, fills(h, reverse)[0].ordinal); + REQUIRE(std::holds_alternative(cancellations(h, trail)[0].prior_authority)); + near(h.physical_position().signed_units, -sign); + h.tick(5, 100 + sign * 12); + CHECK(fills(h, trail).empty()); + CHECK(cancellations(h, trail).size() == 1); + CHECK(events(h, trail).size() == 1); + near(h.physical_position().signed_units, -sign); + CHECK(h.replace(trail, reduce(1, "dead-replace")).status == no::ReplaceStatus::NotWorking); + auto fresh = reduce(1, "new-lifetime"); + fresh.trigger = no::Trail{2, std::nullopt}; + const auto replacement = put(h, fresh); + CHECK(replacement.incarnation != trail.incarnation); + h.tick(6, 100 + sign * 12); + const auto rebound = events(h, replacement); + REQUIRE(rebound.size() == 1); + CHECK(rebound[0].cycle == fills(h, reverse)[0].cycle_after); + CHECK(rebound[0].side == (sign > 0 ? no::Side::Short : no::Side::Long)); + CHECK(cancellations(h, trail).size() == 1); + CHECK(fills(h, trail).empty()); + finish(h); +} + +void flat_unhit_independent_close(double sign) { + Host h; start(h, sign > 0 ? "C7-long" : "C7-short"); + auto resting = reduce(1, "unhit"); + resting.trigger = no::Stop{100 - sign * 10}; + const auto handle = put(h, resting); + h.tick(0, 100); + CHECK(fills(h, handle).empty()); + CHECK(events(h, handle).empty()); + CHECK(events(h, handle).empty()); + const auto none = events(h, handle); + REQUIRE(none.size() == 1); + CHECK(std::holds_alternative(none[0].authority)); + no_cancel(h, handle); + CHECK(h.cancel(handle).status == no::CancelStatus::NotWorking); + near(h.physical_position().signed_units, 0); + finish(h); + + Host waiting; start(waiting, sign > 0 ? "C7-wait-long" : "C7-wait-short"); + auto parent_req = tx(sign, "unfilled-parent"); + parent_req.trigger = no::Stop{100 + sign * 50}; + const auto parent = put(waiting, parent_req); + auto child_req = reduce(1, "still-wait"); + child_req.owner = no::WaitForApplied{parent}; + const auto child = put(waiting, child_req); + waiting.tick(0, 100); + CHECK(fills(waiting, parent).empty()); + CHECK(fills(waiting, child).empty()); + CHECK(events(waiting, child).empty()); + no_cancel(waiting, child); + CHECK(waiting.cancel(child).status == no::CancelStatus::Cancelled); + finish(waiting); +} + +void cancel_cleanup_before_return(double sign) { + Host h; start(h, sign > 0 ? "O2-cancel-long" : "O2-cancel-short"); + auto parent_req = tx(sign, "parent"); + parent_req.trigger = no::Stop{100 + sign * 50}; + const auto parent = put(h, parent_req); + auto child_req = reduce(1, "child"); + child_req.owner = no::WaitForApplied{parent}; + const auto child = put(h, child_req); + const auto cancelled = h.cancel(parent); + REQUIRE(cancelled.status == no::CancelStatus::Cancelled); + REQUIRE(cancelled.event_ordinal != 0); + const auto parent_cancel = cancellations(h, parent); + REQUIRE(parent_cancel.size() == 1); + CHECK(parent_cancel[0].reason == no::CancelReason::User); + CHECK(parent_cancel[0].ordinal == cancelled.event_ordinal); + one_cancel(h, child, no::CancelReason::OwnerGone, cancelled.event_ordinal); + REQUIRE(wait_authority(cancellations(h, child)[0].prior_authority, parent)); + CHECK(h.cancel(child).status == no::CancelStatus::NotWorking); + CHECK(cancellations(h, child).size() == 1); + const auto successor = put(h, tx(sign, "unrelated")); + h.tick(0, 100); + REQUIRE(fills(h, successor).size() == 1); + CHECK(fills(h, child).empty()); + CHECK(cancellations(h, child).size() == 1); + finish(h); +} + +void replace_cleanup_before_return(double sign) { + Host h; start(h, sign > 0 ? "O2-replace-long" : "O2-replace-short"); + auto parent_req = tx(sign, "parent"); + parent_req.trigger = no::Stop{100 + sign * 50}; + const auto parent = put(h, parent_req); + auto child_req = reduce(1, "child"); + child_req.owner = no::WaitForApplied{parent}; + const auto child = put(h, child_req); + const auto replaced = h.replace(parent, tx(sign, "successor")); + REQUIRE(replaced.status == no::ReplaceStatus::Replaced); + REQUIRE(replaced.successor); + CHECK(replaced.successor->incarnation != parent.incarnation); + const auto rows = events(h); + REQUIRE(rows.size() == 1); + CHECK(rows[0].predecessor() == parent); + CHECK(rows[0].successor() == *replaced.successor); + CHECK(rows[0].ordinal == replaced.event_ordinal); + one_cancel(h, child, no::CancelReason::OwnerGone, replaced.event_ordinal); + REQUIRE(wait_authority(cancellations(h, child)[0].prior_authority, parent)); + auto follow = reduce(1, "must-not-follow"); + follow.owner = no::WaitForApplied{parent}; + const auto refused = h.submit(follow); + CHECK(refused.status == no::SubmitStatus::Rejected); + CHECK(refused.reason == no::RequestRejectReason::InvalidOwner); + h.tick(0, 100); + REQUIRE(fills(h, *replaced.successor).size() == 1); + CHECK(fills(h, child).empty()); + CHECK(cancellations(h, child).size() == 1); + CHECK(events(h, child).empty()); + finish(h); +} + +void parent_match_rejected_cascade(double sign) { + Host h; + start(h, sign > 0 ? "O2-reject-long" : "O2-reject-short", 1, 0, 100, [&](NativeRunSpec& spec) { + spec.allowed_open_directions = sign > 0 ? NativeOpenDirections::Short + : NativeOpenDirections::Long; + }); + const auto parent = put(h, tx(sign, "disallowed-open")); + auto child_req = reduce(1, "child"); + child_req.owner = no::WaitForApplied{parent}; + const auto child = put(h, child_req); + h.tick(0, 100); + const auto rejected = events(h, parent); + REQUIRE(rejected.size() == 1); + CHECK(rejected[0].reason == no::MatchRejectReason::OpeningDirection); + one_cancel(h, child, no::CancelReason::OwnerGone, rejected[0].ordinal); + REQUIRE(wait_authority(cancellations(h, child)[0].prior_authority, parent)); + CHECK(fills(h, parent).empty()); + near(h.physical_position().signed_units, 0); + finish(h); +} + +void parent_no_effect_cascade(double sign) { + Host h; start(h, sign > 0 ? "O2-noeffect-long" : "O2-noeffect-short"); + const auto parent = put(h, reduce(1, "flat-parent")); + auto child_req = tx(sign, "child"); + child_req.owner = no::WaitForApplied{parent}; + const auto child = put(h, child_req); + h.tick(0, 100); + const auto none = events(h, parent); + REQUIRE(none.size() == 1); + CHECK(std::holds_alternative(none[0].authority)); + one_cancel(h, child, no::CancelReason::OwnerGone, none[0].ordinal); + REQUIRE(wait_authority(cancellations(h, child)[0].prior_authority, parent)); + CHECK(events(h, child).empty()); + CHECK(fills(h, child).empty()); + near(h.physical_position().signed_units, 0); + finish(h); +} + +void partial_parent_oneshot_flatten(double sign) { + Host h; start(h, sign > 0 ? "O3-long" : "O3-short"); + const auto a = put(h, tx(sign, "A")); h.tick(0, 100); + const auto cycle = opening_cycle(h, a); + auto parent_req = tx(sign * 2, "parent"); parent_req.capacity = no::PointBudget{1}; + const auto parent = put(h, parent_req); + auto child_req = flatten("oneshot"); child_req.owner = no::WaitForApplied{parent}; + const auto child = put(h, child_req); + h.tick(1, 100); + const auto pf = fills(h, parent), cf = fills(h, child); + REQUIRE(pf.size() == 1 && cf.size() == 1); + near(pf[0].opened_units, sign); CHECK(!pf[0].terminal); + CHECK(pf[0].cycle_after == cycle); + CHECK(cf[0].terminal); + REQUIRE(cf[0].terminal_reason); + CHECK(*cf[0].terminal_reason == no::AppliedTerminalReason::Flattened); + check_scope(cf[0], parent, cycle); + near(cf[0].closed_units, 1); + no_cancel(h, child); + const auto armed = events(h, child); + REQUIRE(armed.size() == 1); + CHECK(armed[0].ordinal < cf[0].ordinal && pf[0].ordinal < armed[0].ordinal); + REQUIRE(lot_of(h, a)); + near(lot_of(h, a)->qty, 1); + CHECK(lot_of(h, parent) == nullptr); + near(h.physical_position().signed_units, sign); + h.tick(2, 100); + const auto later = fills(h, parent); + REQUIRE(later.size() == 2); + CHECK(later[1].terminal); + near(later[1].opened_units, sign); + CHECK(later[1].cycle_after == cycle); + CHECK(fills(h, child).size() == 1); + no_cancel(h, child); + REQUIRE(lot_of(h, a) && lot_of(h, parent)); + near(lot_of(h, a)->qty, 1); near(lot_of(h, parent)->qty, 1); + near(h.physical_position().signed_units, sign * 2); + finish(h); +} + +void replacement_keeps_predecessor_scope(double sign) { + Host h; start(h, sign > 0 ? "O4-long" : "O4-short"); + auto parent_req = tx(sign * 2, "parent"); parent_req.capacity = no::PointBudget{1}; + const auto parent = put(h, parent_req); + auto child_req = reduce(1, "armed-child"); + child_req.owner = no::WaitForApplied{parent}; + child_req.trigger = no::Limit{100 + sign * 10}; + const auto child = put(h, child_req); + h.tick(0, 100); + const auto first = fills(h, parent); + REQUIRE(first.size() == 1); + CHECK(!first[0].terminal); + const auto cycle = first[0].cycle_after; + const auto armed = events(h, child); + REQUIRE(armed.size() == 1); + const auto* opened = std::get_if(&armed[0].after); + REQUIRE(opened); + CHECK(opened->opening == parent && opened->cycle == cycle); + CHECK(fills(h, child).empty()); + const auto replaced = h.replace(parent, tx(sign, "successor")); + REQUIRE(replaced.status == no::ReplaceStatus::Replaced); + REQUIRE(replaced.successor); + no_cancel(h, child); + h.tick(1, 100); + REQUIRE(fills(h, *replaced.successor).size() == 1); + CHECK(fills(h, *replaced.successor)[0].cycle_after == cycle); + CHECK(fills(h, child).empty()); + REQUIRE(lot_of(h, parent) && lot_of(h, *replaced.successor)); + h.tick(2, 100 + sign * 10); + const auto cf = fills(h, child); + REQUIRE(cf.size() == 1); + CHECK(cf[0].terminal); + near(cf[0].closed_units, 1); + check_scope(cf[0], parent, cycle); + CHECK(lot_of(h, parent) == nullptr); + REQUIRE(lot_of(h, *replaced.successor)); + near(lot_of(h, *replaced.successor)->qty, 1); + no_cancel(h, child); + CHECK(h.cancel(child).status == no::CancelStatus::NotWorking); + near(h.physical_position().signed_units, sign); + finish(h); +} + +void close_only_parent_arms_transaction(double sign) { + Host h; start(h, sign > 0 ? "O5-long" : "O5-short"); + put(h, tx(sign, "seed")); h.tick(0, 100); + const auto parent = put(h, flatten("close-only")); + auto reduce_req = reduce(1, "closing-child"); + reduce_req.owner = no::WaitForApplied{parent}; + const auto closing = put(h, reduce_req); + auto tx_req = tx(sign, "transaction-child"); + tx_req.owner = no::WaitForApplied{parent}; + const auto opening = put(h, tx_req); + h.tick(1, 100); + const auto pf = fills(h, parent); + REQUIRE(pf.size() == 1); + CHECK(pf[0].terminal && pf[0].opened_units == 0); + REQUIRE(pf[0].terminal_reason); + CHECK(*pf[0].terminal_reason == no::AppliedTerminalReason::Flattened); + one_cancel(h, closing, no::CancelReason::UnsupportedRelation, pf[0].ordinal); + REQUIRE(wait_authority(cancellations(h, closing)[0].prior_authority, parent)); + const auto armed = events(h, opening); + REQUIRE(armed.size() == 1); + REQUIRE(std::holds_alternative(armed[0].after)); + const auto& after = std::get(armed[0].after); + CHECK(after.parent == parent && after.cause.ordinal == pf[0].ordinal); + const auto of = fills(h, opening); + REQUIRE(of.size() == 1); + CHECK(of[0].ordinal > armed[0].ordinal); + near(of[0].opened_units, sign); + CHECK(std::holds_alternative(of[0].scope)); + no_cancel(h, opening); + CHECK(fills(h, closing).empty()); + near(h.physical_position().signed_units, sign); + finish(h); +} + +void late_bind_rejects_event_size(double sign) { + Host h; start(h, sign > 0 ? "O7-long" : "O7-short"); + auto parent_req = tx(sign * 4, "parent"); parent_req.capacity = no::PointBudget{2}; + const auto parent = put(h, parent_req); + h.tick(0, 100); h.tick(1, 100); + const auto pf = fills(h, parent); + REQUIRE(pf.size() == 2); + CHECK(pf[1].terminal); + const auto cycle = pf[0].cycle_after; + CHECK(pf[1].cycle_after == cycle); + put(h, reduce(1, "trim")); h.tick(2, 100); + near(h.physical_position().signed_units, sign * 3); + auto bad = event_reduce("late-event"); + bad.owner = no::BindOpening{parent, cycle}; + const auto refused = h.submit(bad); + CHECK(refused.status == no::SubmitStatus::Rejected && !refused.handle); + CHECK(refused.reason == no::RequestRejectReason::InvalidQuantityBasis); + near(h.physical_position().signed_units, sign * 3); + auto ok = reduce(1, "explicit-bind"); + ok.owner = no::BindOpening{parent, cycle}; + const auto child = put(h, ok); + h.tick(3, 100); + const auto cf = fills(h, child); + REQUIRE(cf.size() == 1); + CHECK(cf[0].terminal); + near(cf[0].closed_units, 1); + check_scope(cf[0], parent, cycle); + no_cancel(h, child); + near(h.physical_position().signed_units, sign * 2); + finish(h); +} + +void event_child_never_resizes(double sign) { + Host h; start(h, sign > 0 ? "O8-long" : "O8-short"); + put(h, tx(-sign * 3, "opposite")); h.tick(0, 100); + auto parent_req = tx(sign * 10, "parent"); parent_req.capacity = no::PointBudget{5}; + const auto parent = put(h, parent_req); + auto child_req = event_reduce("frozen"); + child_req.owner = no::WaitForApplied{parent}; + child_req.trigger = no::Limit{100 + sign * 10}; + const auto child = put(h, child_req); + h.tick(1, 100); + const auto first = fills(h, parent); + REQUIRE(first.size() == 1); + near(first[0].closed_units, 3); near(first[0].opened_units, sign * 2); + CHECK(!first[0].terminal); + const auto bound = events(h, child); + REQUIRE(bound.size() == 1); + CHECK(bound[0].source.ordinal == first[0].ordinal); + near(bound[0].source_units, 2); + near(bound[0].pending_total, 0); + REQUIRE(std::holds_alternative(bound[0].remaining)); + near(std::get(bound[0].remaining).q, 2); + CHECK(fills(h, child).empty()); + const auto cycle = first[0].cycle_after; + h.tick(2, 100); + const auto second = fills(h, parent); + REQUIRE(second.size() == 2); + CHECK(second[1].terminal); + near(second[1].opened_units, sign * 5); + CHECK(second[1].cycle_after == cycle); + CHECK(events(h, child).size() == 1); + CHECK(fills(h, child).empty()); + h.tick(3, 100 + sign * 10); + const auto cf = fills(h, child); + REQUIRE(cf.size() == 1); + CHECK(cf[0].terminal); + near(cf[0].closed_units, 2); + near(units_before(cf[0]), 2); near(units_after(cf[0]), 0); + REQUIRE(cf[0].terminal_reason); + CHECK(*cf[0].terminal_reason == no::AppliedTerminalReason::WorkingUnitsSatisfied); + check_scope(cf[0], parent, cycle); + no_cancel(h, child); + CHECK(events(h, child).size() == 1); + REQUIRE(lot_of(h, parent)); + near(lot_of(h, parent)->qty, 5); + near(h.physical_position().signed_units, sign * 5); + finish(h); +} + +void sibling_keeps_cycle_no_revival(double sign) { + Host h; start(h, sign > 0 ? "O9-long" : "O9-short"); + const auto a = put(h, tx(sign, "A")); h.tick(0, 100); + const auto cycle = opening_cycle(h, a); + auto parent_req = tx(sign * 2, "parent"); parent_req.capacity = no::PointBudget{1}; + const auto parent = put(h, parent_req); + auto resting_req = reduce(1, "resting"); + resting_req.owner = no::WaitForApplied{parent}; + resting_req.trigger = no::Limit{100 + sign * 10}; + const auto resting = put(h, resting_req); + auto flat_req = flatten("prearmed"); flat_req.owner = no::WaitForApplied{parent}; + const auto closer = put(h, flat_req); + h.tick(1, 100); + const auto pf = fills(h, parent), ff = fills(h, closer); + REQUIRE(pf.size() == 1 && ff.size() == 1); + CHECK(!pf[0].terminal); + CHECK(pf[0].cycle_after == cycle); + CHECK(ff[0].terminal); + REQUIRE(ff[0].terminal_reason); + CHECK(*ff[0].terminal_reason == no::AppliedTerminalReason::Flattened); + check_scope(ff[0], parent, cycle); + no_cancel(h, closer); + one_cancel(h, resting, no::CancelReason::OwnerGone, ff[0].ordinal); + const auto armed_resting = events(h, resting); + REQUIRE(armed_resting.size() == 1); + const auto* armed_close = std::get_if(&armed_resting[0].after); + REQUIRE(armed_close); + CHECK(armed_close->opening == parent); + CHECK(armed_close->cycle == cycle); + const auto resting_cancel = cancellations(h, resting); + REQUIRE(resting_cancel.size() == 1); + const auto* prior = std::get_if(&resting_cancel[0].prior_authority); + REQUIRE(prior); + CHECK(prior->opening == parent); + CHECK(prior->cycle == cycle); + CHECK(resting_cancel[0].ordinal > ff[0].ordinal); + REQUIRE(lot_of(h, a)); + CHECK(lot_of(h, parent) == nullptr); + near(h.physical_position().signed_units, sign); + h.tick(2, 100); + const auto later = fills(h, parent); + REQUIRE(later.size() == 2); + CHECK(later[1].terminal); + near(later[1].opened_units, sign); + CHECK(later[1].cycle_after == cycle); + CHECK(later[1].definition->handle.incarnation == parent.incarnation); + CHECK(fills(h, resting).empty()); + CHECK(cancellations(h, resting).size() == 1); + CHECK(events(h, resting).size() == 1); + CHECK(h.cancel(resting).status == no::CancelStatus::NotWorking); + REQUIRE(lot_of(h, a) && lot_of(h, parent)); + near(lot_of(h, a)->qty, 1); near(lot_of(h, parent)->qty, 1); + near(h.physical_position().signed_units, sign * 2); + h.tick(3, 100 + sign * 10); + CHECK(fills(h, resting).empty()); + CHECK(cancellations(h, resting).size() == 1); + finish(h); +} + +void run_case(const char* name, const std::function& body) { + scenario = name; ++cases; + const int previous = failures; + try { body(); } + catch (const StopCase&) {} + catch (const std::exception& e) { + std::printf("FAIL %s unexpected exception: %s\n", name, e.what()); ++failures; + } + catch (...) { + std::printf("FAIL %s unexpected nonstandard exception\n", name); ++failures; + } + std::printf("%s %s\n", failures == previous ? "PASS" : "FAIL", name); +} +} // namespace + +int main() { + for (double sign : {1.0, -1.0}) { + run_case(sign > 0 ? "C3 long Reduce5 budget2 across points" + : "C3 short Reduce5 budget2 across points", + [&] { independent_reduce_budget(sign); }); + run_case(sign > 0 ? "C4 long target-exhausted remainder" + : "C4 short target-exhausted remainder", + [&] { target_exhausted_remainder(sign); }); + run_case(sign > 0 ? "C6 long trail canceled by reversal" + : "C6 short trail canceled by reversal", + [&] { trail_canceled_by_reversal(sign); }); + run_case(sign > 0 ? "C7 long flat unhit independent close" + : "C7 short flat unhit independent close", + [&] { flat_unhit_independent_close(sign); }); + run_case(sign > 0 ? "O2 long cancel cleanup before return" + : "O2 short cancel cleanup before return", + [&] { cancel_cleanup_before_return(sign); }); + run_case(sign > 0 ? "O2 long replace cleanup before return" + : "O2 short replace cleanup before return", + [&] { replace_cleanup_before_return(sign); }); + run_case(sign > 0 ? "O2 long parent MatchRejected cascade" + : "O2 short parent MatchRejected cascade", + [&] { parent_match_rejected_cascade(sign); }); + run_case(sign > 0 ? "O2 long parent NoEffect cascade" + : "O2 short parent NoEffect cascade", + [&] { parent_no_effect_cascade(sign); }); + run_case(sign > 0 ? "O3 long partial parent oneshot Flatten" + : "O3 short partial parent oneshot Flatten", + [&] { partial_parent_oneshot_flatten(sign); }); + run_case(sign > 0 ? "O4 long predecessor scope after replace" + : "O4 short predecessor scope after replace", + [&] { replacement_keeps_predecessor_scope(sign); }); + run_case(sign > 0 ? "O5 long close-only parent arms transaction" + : "O5 short close-only parent arms transaction", + [&] { close_only_parent_arms_transaction(sign); }); + run_case(sign > 0 ? "O7 long late Bind OwnerOpenedUnits" + : "O7 short late Bind OwnerOpenedUnits", + [&] { late_bind_rejects_event_size(sign); }); + run_case(sign > 0 ? "O8 long frozen opening2 vs later5" + : "O8 short frozen opening2 vs later5", + [&] { event_child_never_resizes(sign); }); + run_case(sign > 0 ? "O9 long sibling cycle no revival" + : "O9 short sibling cycle no revival", + [&] { sibling_keeps_cycle_no_revival(sign); }); + } + std::printf("%s native resting owner contract: %d cases, %d checks, %d failures\n", + failures ? "FAIL" : "PASS", cases, checks, failures); + return failures ? 1 : 0; +} diff --git a/tests/test_native_resting_replay_contract.cpp b/tests/test_native_resting_replay_contract.cpp new file mode 100644 index 00000000..20acc974 --- /dev/null +++ b/tests/test_native_resting_replay_contract.cpp @@ -0,0 +1,1348 @@ +// Bounded R2 P2 replay/reset/hash acceptance. Real native hosts only. +// Compares observations and projections; does not reimplement hashing. +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; +namespace { +int checks = 0, failures = 0, cases = 0; +const char* scenario = "setup"; +const char* boundary = ""; +struct StopCase {}; +#define CHECK(x) do { ++checks; if (!(x)) { ++failures; \ + std::printf("FAIL %s %s:%d %s\n", scenario, boundary, __LINE__, #x); } } while (0) +#define REQUIRE(x) do { const bool ok_ = bool(x); CHECK(ok_); if (!ok_) throw StopCase{}; } while (0) + +constexpr int64_t T = 1736121600000LL; + +bool bits_eq(double a, double b) { + std::uint64_t x = 0, y = 0; + std::memcpy(&x, &a, sizeof x); + std::memcpy(&y, &b, sizeof y); + return x == y; +} +std::uint64_t bits_of(double v) { + std::uint64_t x = 0; + std::memcpy(&x, &v, sizeof x); + return x; +} +double bits_to(std::uint64_t x) { + double v = 0; + std::memcpy(&v, &x, sizeof v); + return v; +} +void same_d(double a, double b) { + if (!bits_eq(a, b)) + std::printf(" bits %a (%llx) vs %a (%llx)\n", + a, (unsigned long long)bits_of(a), b, (unsigned long long)bits_of(b)); + CHECK(bits_eq(a, b)); +} +void same_u64(std::uint64_t a, std::uint64_t b) { CHECK(a == b); } +void same_i64(std::int64_t a, std::int64_t b) { CHECK(a == b); } +void same_str(const std::string& a, const std::string& b) { CHECK(a == b); } + +struct Host final : NativeStrategyHost { + std::function beginning; + std::function calculation; + int calculations = 0; + uint64_t sequence = 0; + void on_native_run_begin() override { if (beginning) beginning(*this); } + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + ++calculations; + if (calculation) calculation(*this); + } + bool input(int64_t offset, double price) { + return stream_push_tick(TradeTick{T + offset + 1, ++sequence, price, 1}); + } + void tick(int64_t offset, double price) { + REQUIRE(input(offset, price)); + REQUIRE(native_state().kind == NativeLifecycleKind::Running); + } + const std::vector& lots() const { return pyramid_entries_; } +}; + +NativeRunSpec configuration(const char* key, uint64_t run = 1, double fee = 0) { + NativeRunSpec s; + s.identity = {key, run}; + s.input_tf = "1"; s.script_tf = "1"; + s.ticker = "N"; s.tickerid = "TEST:N"; s.type = "crypto"; + s.currency = "USD"; s.basecurrency = "USD"; s.description = "R2 replay"; + s.volumetype = "base"; s.timezone = "UTC"; s.session = "24x7"; + s.initial_capital = 10000; s.point_value = 1; s.account_fx = 1; + s.price_tick = .01; s.fee_kind = NativeFeeKind::CashPerExecution; s.fee_value = fee; + return s; +} +void start(Host& h, const char* key, uint64_t run = 1, double fee = 0, double first = 100) { + REQUIRE(h.configure_native(configuration(key, run, fee)).status == NativeSetupStatus::Applied); + auto beginning = std::exchange(h.beginning, {}); + const Bar warmup{first, first, first, first, 1, T - 60000}; + h.sequence = 0; + h.calculations = 0; + REQUIRE(h.stream_begin(&warmup, 1, "1", "1")); + REQUIRE(h.stream_push_tick(TradeTick{T, ++h.sequence, first, 0})); + h.beginning = std::move(beginning); + if (h.beginning) h.beginning(h); + REQUIRE(h.native_state().kind == NativeLifecycleKind::Running); +} +void finish(Host& h) { + REQUIRE(h.stream_end(false)); + CHECK(h.native_state().kind == NativeLifecycleKind::Completed); +} +no::Request tx(double q, const char* label = "", const char* comment = "") { + return {no::Transact{q}, label, comment}; +} +no::Request reduce(double q, const char* label = "", const char* comment = "") { + return {no::Reduce{no::ExplicitUnits{q}}, label, comment}; +} +no::RequestHandle put(Host& h, const no::Request& request) { + auto result = h.submit(request); + REQUIRE(result.status == no::SubmitStatus::Accepted); + REQUIRE(result.handle.has_value()); + return *result.handle; +} + +void same_handle(const no::RequestHandle& a, const no::RequestHandle& b) { + same_str(a.run.session_key, b.run.session_key); + same_u64(a.run.run_number, b.run.run_number); + same_u64(a.incarnation, b.incarnation); +} +void same_event_id(const no::EventId& a, const no::EventId& b) { + same_str(a.run.session_key, b.run.session_key); + same_u64(a.run.run_number, b.run.run_number); + same_u64(a.ordinal, b.ordinal); +} +void same_coord(const NativeCoordinate& a, const NativeCoordinate& b) { + same_u64(a.ordinal, b.ordinal); + CHECK(a.interval_index == b.interval_index); + same_i64(a.open_ms, b.open_ms); + same_i64(a.eligible_open_ms, b.eligible_open_ms); + same_i64(a.last_traded_close_ms, b.last_traded_close_ms); + same_i64(a.next_period_open_ms, b.next_period_open_ms); + same_i64(a.next_input_open_ms, b.next_input_open_ms); + same_i64(a.effective_time_ms, b.effective_time_ms); + same_i64(a.source_price_time_ms, b.source_price_time_ms); + CHECK(a.provenance == b.provenance); + CHECK(a.path_phase == b.path_phase); + CHECK(a.completion == b.completion); +} +void same_cursor(const no::MatchCursor& a, const no::MatchCursor& b) { + same_coord(a.point, b.point); + same_d(a.t, b.t); +} +void same_intent(const no::OrderIntent& a, const no::OrderIntent& b) { + REQUIRE(a.index() == b.index()); + if (const auto* t = std::get_if(&a)) + same_d(t->signed_units, std::get(b).signed_units); + else if (const auto* r = std::get_if(&a)) { + REQUIRE(r->size.index() == std::get(b).size.index()); + if (const auto* u = std::get_if(&r->size)) + same_d(u->units, std::get(std::get(b).size).units); + } +} +void same_trigger(const no::Trigger& a, const no::Trigger& b) { + REQUIRE(a.index() == b.index()); + if (const auto* l = std::get_if(&a)) + same_d(l->price, std::get(b).price); + else if (const auto* s = std::get_if(&a)) + same_d(s->price, std::get(b).price); + else if (const auto* sl = std::get_if(&a)) { + same_d(sl->stop, std::get(b).stop); + same_d(sl->limit, std::get(b).limit); + } else if (const auto* t = std::get_if(&a)) { + same_d(t->offset, std::get(b).offset); + CHECK(t->arm_price.has_value() == std::get(b).arm_price.has_value()); + if (t->arm_price) same_d(*t->arm_price, *std::get(b).arm_price); + } +} +void same_capacity(const no::Capacity& a, const no::Capacity& b) { + REQUIRE(a.index() == b.index()); + if (const auto* p = std::get_if(&a)) + same_d(p->units, std::get(b).units); +} +void same_owner(const no::Owner& a, const no::Owner& b) { + REQUIRE(a.index() == b.index()); + if (const auto* w = std::get_if(&a)) + same_handle(w->parent, std::get(b).parent); + else if (const auto* o = std::get_if(&a)) { + same_handle(o->opening, std::get(b).opening); + same_i64(o->cycle, std::get(b).cycle); + } +} +void same_group(const no::Group& a, const no::Group& b) { + REQUIRE(a.index() == b.index()); + if (const auto* m = std::get_if(&a)) { + same_u64(m->group, std::get(b).group); + same_i64(m->cohort, std::get(b).cohort); + CHECK(m->effect == std::get(b).effect); + } +} +void same_request(const no::Request& a, const no::Request& b, bool text) { + same_intent(a.intent, b.intent); + if (text) { same_str(a.label, b.label); same_str(a.comment, b.comment); } + same_trigger(a.trigger, b.trigger); + same_capacity(a.capacity, b.capacity); + same_owner(a.owner, b.owner); + same_group(a.group, b.group); +} +void same_birth(const no::Birth& a, const no::Birth& b) { + same_u64(a.acceptance_ordinal, b.acceptance_ordinal); + same_i64(a.decision_time_lower_bound, b.decision_time_lower_bound); +} +void same_definition(const no::DefinitionRef& a, const no::DefinitionRef& b, bool text) { + CHECK(static_cast(a) == static_cast(b)); + if (!a || !b) return; + same_handle(a->handle, b->handle); + same_request(a->request, b->request, text); + same_birth(a->birth, b->birth); + CHECK(a->predecessor.has_value() == b->predecessor.has_value()); + if (a->predecessor) same_handle(*a->predecessor, *b->predecessor); +} +void same_remaining_proj(const no::RemainingProjection& a, const no::RemainingProjection& b) { + REQUIRE(a.index() == b.index()); + if (const auto* u = std::get_if(&a)) + same_d(u->q, std::get(b).q); +} +void same_remaining(const no::Remaining& a, const no::Remaining& b) { + REQUIRE(a.index() == b.index()); + if (const auto* u = std::get_if(&a)) + same_d(u->q, std::get(b).q); +} +void same_enrollment(const no::Enrollment& a, const no::Enrollment& b) { + REQUIRE(a.index() == b.index()); + if (const auto* c = std::get_if(&a)) + same_event_id(c->accepted, std::get(b).accepted); + else if (const auto* p = std::get_if(&a)) { + same_event_id(p->cause, std::get(b).cause); + same_cursor(p->cursor, std::get(b).cursor); + } +} +void same_authority(const no::Authority& a, const no::Authority& b) { + REQUIRE(a.index() == b.index()); + if (const auto* w = std::get_if(&a)) + same_handle(w->parent, std::get(b).parent); + else if (const auto* t = std::get_if(&a)) { + same_handle(t->parent, std::get(b).parent); + same_event_id(t->cause, std::get(b).cause); + same_cursor(t->cause_cursor, std::get(b).cause_cursor); + } else if (const auto* c = std::get_if(&a)) { + same_i64(c->cycle, std::get(b).cycle); + CHECK(c->side == std::get(b).side); + same_event_id(c->binding_event, std::get(b).binding_event); + same_cursor(c->binding_cursor, std::get(b).binding_cursor); + } else if (const auto* o = std::get_if(&a)) { + same_handle(o->opening, std::get(b).opening); + same_i64(o->cycle, std::get(b).cycle); + CHECK(o->side == std::get(b).side); + same_enrollment(o->enrollment, std::get(b).enrollment); + } +} +void same_trigger_state(const no::TriggerState& a, const no::TriggerState& b) { + REQUIRE(a.index() == b.index()); + if (const auto* t = std::get_if(&a)) + same_d(t->best, std::get(b).best); + else if (const auto* t = std::get_if(&a)) + same_d(t->best_at_trigger, std::get(b).best_at_trigger); +} +void same_allowance(const no::Allowance& a, const no::Allowance& b) { + REQUIRE(a.index() == b.index()); + if (const auto* u = std::get_if(&a)) { + same_u64(u->point_ordinal, std::get(b).point_ordinal); + same_d(u->initial, std::get(b).initial); + same_d(u->left, std::get(b).left); + } else if (const auto* all = std::get_if(&a)) { + same_u64(all->point_ordinal, std::get(b).point_ordinal); + } +} +void same_pending(const no::PendingAdjustments& a, const no::PendingAdjustments& b) { + REQUIRE(a.index() == b.index()); + if (const auto* d = std::get_if(&a)) { + same_d(d->total, std::get(b).total); + same_u64(d->count, std::get(b).count); + same_event_id(d->tail_receipt, std::get(b).tail_receipt); + } +} +void same_scope(const execution::CloseScope& a, const execution::CloseScope& b) { + REQUIRE(a.index() == b.index()); + if (const auto* o = std::get_if(&a)) { + same_u64(o->incarnation, std::get(b).incarnation); + same_i64(o->cycle, std::get(b).cycle); + } +} +void same_opt_event(const std::optional& a, const std::optional& b) { + CHECK(a.has_value() == b.has_value()); + if (a) same_event_id(*a, *b); +} +void same_opt_request(const std::optional& a, const std::optional& b, bool text) { + CHECK(a.has_value() == b.has_value()); + if (a) same_request(*a, *b, text); +} + +void same_accepted(const no::AcceptedEvent& a, const no::AcceptedEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.definition, b.definition, text); + CHECK(a.surface == b.surface); +} +void same_rejected(const no::RejectedEvent& a, const no::RejectedEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_request(a.request, b.request, text); + CHECK(a.reason == b.reason); + CHECK(a.surface == b.surface); +} +void same_replaced(const no::ReplacedEvent& a, const no::ReplacedEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.predecessor_definition, b.predecessor_definition, text); + same_definition(a.successor_definition, b.successor_definition, text); + CHECK(a.surface == b.surface); +} +void same_replace_rejected(const no::ReplaceRejectedEvent& a, const no::ReplaceRejectedEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.live_definition, b.live_definition, text); + same_request(a.attempted, b.attempted, text); + CHECK(a.reason == b.reason); + CHECK(a.surface == b.surface); +} +void same_cancelled(const no::CancelledEvent& a, const no::CancelledEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.definition, b.definition, text); + CHECK(a.reason == b.reason); + same_opt_event(a.cause, b.cause); + same_authority(a.prior_authority, b.prior_authority); + same_remaining_proj(a.unexecuted, b.unexecuted); + same_pending(a.pending, b.pending); +} +void same_not_working(const no::NotWorkingEvent& a, const no::NotWorkingEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_handle(a.target, b.target); + same_opt_request(a.attempted, b.attempted, text); + CHECK(a.surface == b.surface); +} +void same_invalid_handle(const no::InvalidHandleEvent& a, const no::InvalidHandleEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_handle(a.target, b.target); + same_opt_request(a.attempted, b.attempted, text); + CHECK(a.surface == b.surface); +} +void same_no_effect(const no::NoEffectEvent& a, const no::NoEffectEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.definition, b.definition, text); + same_remaining_proj(a.remaining, b.remaining); + same_authority(a.authority, b.authority); + same_cursor(a.cursor, b.cursor); +} +void same_match_rejected(const no::MatchRejectedEvent& a, const no::MatchRejectedEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + CHECK(a.reason == b.reason); + same_definition(a.definition, b.definition, text); + same_remaining_proj(a.remaining, b.remaining); + same_authority(a.authority, b.authority); + same_cursor(a.cursor, b.cursor); +} +void same_close_bound(const no::CloseBoundEvent& a, const no::CloseBoundEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.definition, b.definition, text); + same_i64(a.cycle, b.cycle); + CHECK(a.side == b.side); + same_cursor(a.cursor, b.cursor); + same_authority(a.before, b.before); + same_authority(a.after, b.after); +} +void same_activated(const no::ActivatedEvent& a, const no::ActivatedEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.definition, b.definition, text); + CHECK(a.kind == b.kind); + same_trigger_state(a.before, b.before); + same_trigger_state(a.after, b.after); + same_d(a.reached_price, b.reached_price); + same_cursor(a.cursor, b.cursor); +} +void same_applied(const no::ExecutionAppliedEvent& a, const no::ExecutionAppliedEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.definition, b.definition, text); + same_d(a.raw_price, b.raw_price); + same_d(a.resolved_price, b.resolved_price); + same_d(a.current_ticket, b.current_ticket); + CHECK(a.first_trade_index == b.first_trade_index); + CHECK(a.closed_trade_count == b.closed_trade_count); + same_u64(a.opened_lot_incarnation, b.opened_lot_incarnation); + same_d(a.closed_units, b.closed_units); + same_d(a.opened_units, b.opened_units); + same_d(a.filled_working, b.filled_working); + same_remaining_proj(a.remaining_before, b.remaining_before); + same_remaining_proj(a.remaining_after, b.remaining_after); + same_allowance(a.allowance_before, b.allowance_before); + same_allowance(a.allowance_after, b.allowance_after); + CHECK(a.terminal == b.terminal); + CHECK(a.terminal_reason.has_value() == b.terminal_reason.has_value()); + if (a.terminal_reason) CHECK(*a.terminal_reason == *b.terminal_reason); + same_i64(a.cycle_before, b.cycle_before); + same_i64(a.cycle_after, b.cycle_after); + same_scope(a.scope, b.scope); + same_cursor(a.cursor, b.cursor); +} +void same_reservation(const no::ReservationReducedEvent& a, const no::ReservationReducedEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.definition, b.definition, text); + same_event_id(a.cause, b.cause); + same_handle(a.recipient, b.recipient); + CHECK(a.effect == b.effect); + same_d(a.requested_delta, b.requested_delta); + same_d(a.actual_deduction, b.actual_deduction); + same_d(a.before.q, b.before.q); + same_remaining_proj(a.after, b.after); +} +void same_deferred(const no::DeferredGroupAdjustmentEvent& a, const no::DeferredGroupAdjustmentEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.definition, b.definition, text); + same_event_id(a.cause, b.cause); + same_handle(a.recipient, b.recipient); + CHECK(a.effect == b.effect); + same_d(a.deferred_delta, b.deferred_delta); + same_pending(a.pending_before, b.pending_before); + same_d(a.pending_after.total, b.pending_after.total); + same_u64(a.pending_after.count, b.pending_after.count); + same_event_id(a.pending_after.tail_receipt, b.pending_after.tail_receipt); + same_opt_event(a.previous_pending_receipt, b.previous_pending_receipt); +} +void same_qty_bound(const no::QuantityBoundEvent& a, const no::QuantityBoundEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.definition, b.definition, text); + same_event_id(a.source, b.source); + same_d(a.source_units, b.source_units); + REQUIRE(a.prior_adjustment_ids.size() == b.prior_adjustment_ids.size()); + for (size_t i = 0; i < a.prior_adjustment_ids.size(); ++i) + same_event_id(a.prior_adjustment_ids[i], b.prior_adjustment_ids[i]); + same_d(a.pending_total, b.pending_total); + same_d(a.effective_deduction, b.effective_deduction); + same_remaining_proj(a.remaining, b.remaining); +} +void same_armed(const no::ArmedEvent& a, const no::ArmedEvent& b, bool text) { + same_u64(a.ordinal, b.ordinal); + same_definition(a.definition, b.definition, text); + same_authority(a.before, b.before); + same_authority(a.after, b.after); + same_enrollment(a.enrollment, b.enrollment); + same_opt_event(a.quantity_resolution, b.quantity_resolution); +} +void same_command(const no::CommandEvent& a, const no::CommandEvent& b, bool text) { + REQUIRE(a.index() == b.index()); + if (const auto* e = std::get_if(&a)) + same_accepted(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_rejected(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_replaced(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_replace_rejected(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_cancelled(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_not_working(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_invalid_handle(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_no_effect(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_match_rejected(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_applied(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_close_bound(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_activated(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_reservation(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_deferred(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_qty_bound(*e, std::get(b), text); + else if (const auto* e = std::get_if(&a)) + same_armed(*e, std::get(b), text); +} +void same_driver(const NativeDriverPoint& a, const NativeDriverPoint& b) { + same_coord(a.coordinate, b.coordinate); + same_d(a.raw_price, b.raw_price); + CHECK(a.sequence.has_value() == b.sequence.has_value()); + if (a.sequence) same_u64(*a.sequence, *b.sequence); + CHECK(a.matching == b.matching); + CHECK(a.excursion == b.excursion); +} +void same_account(const NativeAccountObservation& a, const NativeAccountObservation& b) { + same_u64(a.ordinal, b.ordinal); + same_i64(a.effective_time_ms, b.effective_time_ms); + same_d(a.marked_equity, b.marked_equity); + same_d(a.realized_balance, b.realized_balance); + same_d(a.signed_units, b.signed_units); +} +void same_market_event(const NativeMarketEvent& a, const NativeMarketEvent& b, bool text) { + CHECK(a.kind == b.kind); + same_u64(a.ordinal, b.ordinal); + CHECK(a.command.has_value() == b.command.has_value()); + CHECK(a.driver.has_value() == b.driver.has_value()); + CHECK(a.account.has_value() == b.account.has_value()); + if (a.command) same_command(*a.command, *b.command, text); + if (a.driver) same_driver(*a.driver, *b.driver); + if (a.account) same_account(*a.account, *b.account); +} +void same_trade(const Trade& a, const Trade& b, bool text) { + same_i64(a.entry_time, b.entry_time); + same_i64(a.exit_time, b.exit_time); + same_d(a.entry_price, b.entry_price); + same_d(a.exit_price, b.exit_price); + same_d(a.qty, b.qty); + same_d(a.pnl, b.pnl); + same_d(a.pnl_pct, b.pnl_pct); + CHECK(a.is_long == b.is_long); + CHECK(a.entry_bar_index == b.entry_bar_index); + CHECK(a.exit_bar_index == b.exit_bar_index); + if (text) { + same_str(a.entry_id, b.entry_id); + same_str(a.entry_comment, b.entry_comment); + same_str(a.exit_comment, b.exit_comment); + same_str(a.exit_id, b.exit_id); + } + CHECK(a.exit_from_bracket == b.exit_from_bracket); + same_d(a.max_runup, b.max_runup); + same_d(a.max_drawdown, b.max_drawdown); + same_d(a.commission, b.commission); + same_u64(a.entry_incarnation, b.entry_incarnation); + CHECK(a.open_at_end == b.open_at_end); +} +void same_lot(const PyramidEntry& a, const PyramidEntry& b, bool text) { + same_d(a.price, b.price); + same_i64(a.time, b.time); + same_d(a.qty, b.qty); + if (text) { + same_str(a.entry_id, b.entry_id); + same_str(a.entry_comment, b.entry_comment); + } + CHECK(a.entry_bar_index == b.entry_bar_index); + same_d(a.max_runup, b.max_runup); + same_d(a.max_drawdown, b.max_drawdown); + CHECK(a.skip_entry_bar_high == b.skip_entry_bar_high); + CHECK(a.skip_entry_bar_low == b.skip_entry_bar_low); + CHECK(a.market_pyramid_add == b.market_pyramid_add); + same_d(a.entry_path_position, b.entry_path_position); + same_d(a.entry_commission_account, b.entry_commission_account); + same_u64(a.entry_incarnation, b.entry_incarnation); + CHECK(a.bracket_slot_shadowed == b.bracket_slot_shadowed); + CHECK(a.ordinary_market_open == b.ordinary_market_open); + CHECK(a.pooc_terminal_market_entry == b.pooc_terminal_market_entry); + CHECK(a.ordinary_stop_open == b.ordinary_stop_open); +} +void same_action(const StreamOrderAction& a, const StreamOrderAction& b, bool text) { + same_u64(a.sequence, b.sequence); + same_i64(a.timestamp_ms, b.timestamp_ms); + CHECK(a.bar_index == b.bar_index); + CHECK(a.is_entry == b.is_entry); + CHECK(a.is_long == b.is_long); + same_d(a.quantity, b.quantity); + same_d(a.price, b.price); + if (text) { + same_str(a.order_id, b.order_id); + same_str(a.comment, b.comment); + } + same_u64(a.entry_incarnation, b.entry_incarnation); +} +void same_failure(const NativeFailure& a, const NativeFailure& b) { + CHECK(a.code == b.code); + CHECK(a.operation == b.operation); + same_u64(a.ordinal, b.ordinal); + CHECK(a.discriminator == b.discriminator); + CHECK(a.context.kind == b.context.kind); + same_u64(a.context.cause.ordinal, b.context.cause.ordinal); + same_u64(a.context.recipient.incarnation, b.context.recipient.incarnation); + same_coord(a.context.cursor.point, b.context.cursor.point); + same_d(a.context.cursor.t, b.context.cursor.t); +} +void same_physical(const Host& a, const Host& b, bool text) { + same_d(a.physical_position().signed_units, b.physical_position().signed_units); + same_d(a.physical_position().average_price, b.physical_position().average_price); + CHECK(a.physical_position().lot_count == b.physical_position().lot_count); + REQUIRE(a.lots().size() == b.lots().size()); + for (size_t i = 0; i < a.lots().size(); ++i) same_lot(a.lots()[i], b.lots()[i], text); + REQUIRE(a.trade_count() == b.trade_count()); + for (int i = 0; i < a.trade_count(); ++i) same_trade(a.get_trade(i), b.get_trade(i), text); + REQUIRE(a.stream_order_actions_len() == b.stream_order_actions_len()); + for (int i = 0; i < a.stream_order_actions_len(); ++i) + same_action(a.stream_order_action_at(i), b.stream_order_action_at(i), text); + same_d(a.native_marked_equity(100), b.native_marked_equity(100)); +} +void same_history(const Host& a, const Host& b, bool text) { + const auto ea = a.native_events(0), eb = b.native_events(0); + if (ea.size() != eb.size()) + std::printf(" event count %zu vs %zu\n", ea.size(), eb.size()); + REQUIRE(ea.size() == eb.size()); + for (size_t i = 0; i < ea.size(); ++i) same_market_event(ea[i], eb[i], text); +} +void same_hosts(const char* at, const Host& a, const Host& b) { + boundary = at; + const auto sa = a.native_state(), sb = b.native_state(); + CHECK(sa.kind == sb.kind); + CHECK(sa.phase == sb.phase); + CHECK(sa.completion == sb.completion); + same_u64(sa.consumed_high_water, sb.consumed_high_water); + same_i64(sa.decision_floor_ms, sb.decision_floor_ms); + same_failure(sa.failure, sb.failure); + same_u64(a.native_consumed_high_water(), b.native_consumed_high_water()); + same_i64(a.native_decision_floor(), b.native_decision_floor()); + const auto ha = a.native_continuation_hash(); + const auto hb = b.native_continuation_hash(); + if (ha != hb) std::printf(" hash %llx vs %llx\n", (unsigned long long)ha, (unsigned long long)hb); + CHECK(ha == hb); + CHECK(a.native_continuation_hash() == ha); + CHECK(b.native_continuation_hash() == hb); + CHECK(ha != 0); + same_u64(a.stream_state_hash(), b.stream_state_hash()); + same_history(a, b, true); + same_physical(a, b, true); + boundary = ""; +} + +template +std::vector events_of(const Host& h) { + std::vector out; + for (const auto& row : h.native_events(0)) { + if (row.command) if (const auto* v = std::get_if(&*row.command)) out.push_back(*v); + } + return out; +} +std::vector fills(const Host& h, const no::RequestHandle& handle) { + std::vector out; + for (const auto& e : events_of(h)) { + REQUIRE(e.definition); + if (e.definition->handle == handle) out.push_back(e); + } + return out; +} +std::vector activations(const Host& h, const no::RequestHandle& handle) { + std::vector out; + for (const auto& e : events_of(h)) { + REQUIRE(e.definition); + if (e.definition->handle == handle) out.push_back(e); + } + return out; +} +std::vector cancellations(const Host& h, const no::RequestHandle& handle) { + std::vector out; + for (const auto& e : events_of(h)) { + REQUIRE(e.definition); + if (e.definition->handle == handle) out.push_back(e); + } + return out; +} + +void units_after(const no::ExecutionAppliedEvent& e, double before, double after) { + REQUIRE(std::holds_alternative(e.remaining_before)); + REQUIRE(std::holds_alternative(e.remaining_after)); + same_d(std::get(e.remaining_before).q, before); + same_d(std::get(e.remaining_after).q, after); +} + +void failed_is_permanent(Host& h, int64_t next_offset) { + REQUIRE(h.native_state().kind == NativeLifecycleKind::Failed); + const auto before = h.native_state().failure; + const auto hash = h.native_continuation_hash(); + const auto count = h.native_events(0).size(); + const auto position = h.physical_position(); + CHECK(!h.input(next_offset, 100)); + CHECK(!h.stream_advance_time(T + next_offset + 100)); + CHECK(!h.stream_end(false)); + bool threw = false; + try { (void)h.submit(tx(1)); } catch (const std::exception&) { threw = true; } + CHECK(threw); + CHECK(h.native_state().kind == NativeLifecycleKind::Failed); + same_failure(h.native_state().failure, before); + CHECK(h.native_events(0).size() == count); + CHECK(h.native_continuation_hash() == hash); + same_d(h.physical_position().signed_units, position.signed_units); +} + +void replay_partial_budget(double sign) { + Host a, b; + const char* key = sign > 0 ? "P2-budget-L" : "P2-budget-S"; + start(a, key); start(b, key); + same_hosts("start", a, b); + auto r = tx(sign * 5, "budget"); + r.trigger = no::Limit{100}; + r.capacity = no::PointBudget{2}; + const auto ha = put(a, r), hb = put(b, r); + CHECK(ha.incarnation == hb.incarnation); + same_hosts("submit", a, b); + a.tick(0, 100); b.tick(0, 100); + same_hosts("p1", a, b); + a.tick(1, 100); b.tick(1, 100); + same_hosts("p2", a, b); + a.tick(2, 100); b.tick(2, 100); + same_hosts("p3", a, b); + const auto fa = fills(a, ha); + REQUIRE(fa.size() == 3); + const double before[] = {5, 3, 1}, after[] = {3, 1, 0}, amt[] = {2, 2, 1}; + for (size_t i = 0; i < 3; ++i) { + same_d(fa[i].filled_working, amt[i]); + same_d(fa[i].opened_units, sign * amt[i]); + units_after(fa[i], before[i], after[i]); + REQUIRE(std::holds_alternative(fa[i].allowance_before)); + REQUIRE(std::holds_alternative(fa[i].allowance_after)); + const auto& left = std::get(fa[i].allowance_before); + const auto& gone = std::get(fa[i].allowance_after); + same_u64(left.point_ordinal, fa[i].cursor.point.ordinal); + same_d(left.left, amt[i]); + same_d(left.initial, amt[i]); + same_u64(gone.point_ordinal, left.point_ordinal); + same_d(gone.left, 0); + CHECK(fa[i].terminal == (i == 2)); + CHECK(fa[i].cursor.point.provenance == NativePriceProvenance::ObservedPrint); + CHECK(fa[i].cursor.t == 0); + same_d(fa[i].raw_price, 100); + if (i) CHECK(fa[i].cursor.point.ordinal != fa[i - 1].cursor.point.ordinal); + } + same_d(a.physical_position().signed_units, sign * 5); + finish(a); finish(b); + same_hosts("end", a, b); +} + +void replay_stop_stoplimit_trail(double sign) { + Host a, b; + const char* key = sign > 0 ? "P2-triggers-L" : "P2-triggers-S"; + start(a, key); start(b, key); + const auto open_a = put(a, tx(sign * 3, "open")), open_b = put(b, tx(sign * 3, "open")); + a.tick(0, 100); b.tick(0, 100); + same_hosts("opened", a, b); + REQUIRE(fills(a, open_a).size() == 1); + auto s = reduce(1, "stop"); s.trigger = no::Stop{100 - sign}; + auto sl = reduce(1, "stop-limit"); sl.trigger = no::StopLimit{100 - sign, 100 - sign * 2}; + auto tr = reduce(1, "trail"); tr.trigger = no::Trail{2, 100 + sign * 5}; + const auto sa = put(a, s), sb = put(b, s); + const auto sla = put(a, sl), slb = put(b, sl); + const auto ta = put(a, tr), tb = put(b, tr); + (void)sb; (void)slb; (void)tb; + same_hosts("working", a, b); + a.tick(1, 100); b.tick(1, 100); + same_hosts("quiet", a, b); + CHECK(fills(a, sa).empty() && fills(a, sla).empty() && fills(a, ta).empty()); + CHECK(activations(a, sa).empty() && activations(a, sla).empty() && activations(a, ta).empty()); + a.tick(2, 100 + sign * 6); b.tick(2, 100 + sign * 6); + same_hosts("arm", a, b); + auto arm = activations(a, ta); + REQUIRE(arm.size() == 1); + CHECK(arm[0].kind == no::ActivationKind::TrailArm); + same_d(arm[0].reached_price, 100 + sign * 6); + REQUIRE(std::holds_alternative(arm[0].after)); + same_d(std::get(arm[0].after).best, 100 + sign * 6); + CHECK(arm[0].cursor.point.provenance == NativePriceProvenance::ObservedPrint); + CHECK(arm[0].cursor.t == 0); + CHECK(fills(a, ta).empty()); + a.tick(3, 100 + sign * 10); b.tick(3, 100 + sign * 10); + same_hosts("best", a, b); + CHECK(fills(a, ta).empty()); + CHECK(activations(a, ta).size() == 1); + a.tick(4, 100 + sign * 8); b.tick(4, 100 + sign * 8); + same_hosts("trail-fill", a, b); + auto tf = fills(a, ta); + REQUIRE(tf.size() == 1); + same_d(tf[0].raw_price, 100 + sign * 8); + same_d(tf[0].closed_units, 1); + CHECK(tf[0].terminal); + auto trig = activations(a, ta); + REQUIRE(trig.size() == 2); + CHECK(trig[1].kind == no::ActivationKind::TrailTrigger); + REQUIRE(std::holds_alternative(trig[1].after)); + same_d(std::get(trig[1].after).best_at_trigger, 100 + sign * 10); + same_d(trig[1].reached_price, 100 + sign * 8); + a.tick(5, 100 - sign); b.tick(5, 100 - sign); + same_hosts("stop-print", a, b); + auto sf = fills(a, sa); + REQUIRE(sf.size() == 1); + same_d(sf[0].raw_price, 100 - sign); + auto sact = activations(a, sa); + REQUIRE(sact.size() == 1); + CHECK(sact[0].kind == no::ActivationKind::Stop); + same_d(sact[0].reached_price, 100 - sign); + CHECK(std::holds_alternative(sact[0].after)); + auto sl_act = activations(a, sla); + REQUIRE(sl_act.size() == 1); + CHECK(sl_act[0].kind == no::ActivationKind::StopLimit); + CHECK(std::holds_alternative(sl_act[0].after)); + a.tick(6, 100 - sign * 2); b.tick(6, 100 - sign * 2); + same_hosts("stop-limit-fill", a, b); + auto slf = fills(a, sla); + REQUIRE(slf.size() == 1); + CHECK(slf[0].terminal); + CHECK(activations(a, sla).size() == 1); + CHECK(slf[0].cursor.point.provenance == NativePriceProvenance::ObservedPrint); + same_d(a.physical_position().signed_units, 0); + CHECK(open_b.incarnation == open_a.incarnation); + finish(a); finish(b); + same_hosts("end", a, b); +} + +void replay_deferred_bind(double sign) { + Host a, b; + const char* key = sign > 0 ? "P2-deferred-L" : "P2-deferred-S"; + start(a, key); start(b, key); + auto p = tx(sign * 5, "parent"); p.trigger = no::Stop{100 + sign * 5}; + const auto pa = put(a, p), pb = put(b, p); + no::Request child{no::Reduce{no::OwnerOpenedUnits{}}, "child", ""}; + child.owner = no::WaitForApplied{pa}; + child.trigger = no::Limit{sign > 0 ? 200.0 : 1.0}; + child.group = no::Member{7, 0, no::GroupEffect::Reduce}; + no::Request child_b = child; child_b.owner = no::WaitForApplied{pb}; + const auto ca = put(a, child), cb = put(b, child_b); + auto f = tx(sign * 2, "A"); f.group = no::Member{7, 1, no::GroupEffect::Reduce}; + const auto aa = put(a, f), ab = put(b, f); + f.label = "B"; + const auto ba = put(a, f), bb = put(b, f); + same_hosts("accepted", a, b); + a.tick(0, 100); b.tick(0, 100); + same_hosts("deferred", a, b); + CHECK(fills(a, pa).empty()); + REQUIRE(fills(a, aa).size() == 1 && fills(a, ba).size() == 1); + auto deferred = events_of(a); + REQUIRE(deferred.size() == 2); + same_d(deferred[0].deferred_delta, 2); + same_d(deferred[1].deferred_delta, 2); + CHECK(deferred[0].recipient == ca && deferred[1].recipient == ca); + CHECK(deferred[0].cause.ordinal == fills(a, aa)[0].ordinal); + CHECK(deferred[1].cause.ordinal == fills(a, ba)[0].ordinal); + REQUIRE(std::holds_alternative(deferred[0].pending_before)); + same_d(deferred[0].pending_after.total, 2); + same_u64(deferred[0].pending_after.count, 1); + same_d(deferred[1].pending_after.total, 4); + same_u64(deferred[1].pending_after.count, 2); + a.tick(1, 100 + sign * 5); b.tick(1, 100 + sign * 5); + same_hosts("bound", a, b); + auto pf = fills(a, pa); + REQUIRE(pf.size() == 1); + same_d(pf[0].opened_units, sign * 5); + auto bound = events_of(a); + REQUIRE(bound.size() == 1); + CHECK(bound[0].source.ordinal == pf[0].ordinal); + same_d(bound[0].source_units, 5); + same_d(bound[0].pending_total, 4); + same_d(bound[0].effective_deduction, 4); + REQUIRE(bound[0].prior_adjustment_ids.size() == 2); + CHECK(bound[0].prior_adjustment_ids[0].ordinal == deferred[0].ordinal); + CHECK(bound[0].prior_adjustment_ids[1].ordinal == deferred[1].ordinal); + REQUIRE(std::holds_alternative(bound[0].remaining)); + same_d(std::get(bound[0].remaining).q, 1); + CHECK(fills(a, ca).empty()); + a.tick(2, sign > 0 ? 200.0 : 1.0); b.tick(2, sign > 0 ? 200.0 : 1.0); + same_hosts("child-fill", a, b); + auto cf = fills(a, ca); + REQUIRE(cf.size() == 1); + same_d(cf[0].closed_units, 1); + CHECK(cf[0].terminal); + const auto* scope = std::get_if(&cf[0].scope); + REQUIRE(scope); + CHECK(scope->incarnation == pa.incarnation); + CHECK(scope->cycle == pf[0].cycle_after); + same_d(a.physical_position().signed_units, sign * 8); + CHECK(cb.incarnation == ca.incarnation); + CHECK(ab.incarnation == aa.incarnation && bb.incarnation == ba.incarnation); + finish(a); finish(b); + same_hosts("end", a, b); +} + +void replay_owner_expiry_replace(double sign) { + Host a, b; + const char* key = sign > 0 ? "P2-owner-L" : "P2-owner-S"; + start(a, key); start(b, key); + auto p = tx(sign, "parent"); p.trigger = no::Stop{200}; + const auto pa = put(a, p), pb = put(b, p); + auto c = reduce(1, "child"); c.owner = no::WaitForApplied{pa}; + auto c2 = c; c2.owner = no::WaitForApplied{pb}; + const auto ca = put(a, c), cb = put(b, c2); + same_hosts("waiting", a, b); + const auto xa = a.cancel(pa), xb = b.cancel(pb); + CHECK(xa.status == no::CancelStatus::Cancelled); + CHECK(xb.status == no::CancelStatus::Cancelled); + same_hosts("cancelled", a, b); + auto pc = cancellations(a, pa), cc = cancellations(a, ca); + REQUIRE(pc.size() == 1 && cc.size() == 1); + CHECK(pc[0].reason == no::CancelReason::User); + CHECK(cc[0].reason == no::CancelReason::OwnerGone); + REQUIRE(cc[0].cause); + CHECK(cc[0].cause->ordinal == pc[0].ordinal); + CHECK(std::holds_alternative(cc[0].prior_authority)); + CHECK(cb.incarnation == ca.incarnation); + auto q = tx(sign, "replace-me"); q.trigger = no::Stop{200}; + const auto qa = put(a, q), qb = put(b, q); + auto w = reduce(1, "wait-replace"); w.owner = no::WaitForApplied{qa}; + auto w2 = w; w2.owner = no::WaitForApplied{qb}; + const auto wa = put(a, w), wb = put(b, w2); + auto nxt = tx(sign, "successor"); nxt.trigger = no::Stop{201}; + const auto ra = a.replace(qa, nxt), rb = b.replace(qb, nxt); + CHECK(ra.status == no::ReplaceStatus::Replaced && ra.successor); + CHECK(rb.status == no::ReplaceStatus::Replaced && rb.successor); + same_hosts("replaced", a, b); + auto wc = cancellations(a, wa); + REQUIRE(wc.size() == 1); + CHECK(wc[0].reason == no::CancelReason::OwnerGone); + REQUIRE(events_of(a).size() >= 1); + CHECK(wb.incarnation == wa.incarnation); + REQUIRE(ra.successor->incarnation != qa.incarnation); + finish(a); finish(b); + same_hosts("end", a, b); +} + +void replay_confirmed_bar(double sign) { + Host a, b; + std::vector ha, hb; + std::vector> sa, sb; + no::RequestHandle trail_a, stop_a, trail_b, stop_b; + auto setup = [&](Host& h, no::RequestHandle& trail, no::RequestHandle& stop, + std::vector& hashes, + std::vector>& snaps) { + h.beginning = [&](Host& self) { put(self, tx(sign * 2, "open")); }; + h.calculation = [&](Host& self) { + if (self.calculations == 1) { + auto t = reduce(1, "trail"); t.trigger = no::Trail{2, 100 + sign * 5}; trail = put(self, t); + auto s = reduce(1, "stop"); s.trigger = no::Stop{100 - sign}; stop = put(self, s); + } + hashes.push_back(self.native_continuation_hash()); + snaps.push_back(self.native_events(0)); + }; + }; + setup(a, trail_a, stop_a, ha, sa); + setup(b, trail_b, stop_b, hb, sb); + const char* key = sign > 0 ? "P2-ohlc-L" : "P2-ohlc-S"; + REQUIRE(a.configure_native(configuration(key)).status == NativeSetupStatus::Applied); + REQUIRE(b.configure_native(configuration(key)).status == NativeSetupStatus::Applied); + const Bar tape[] = {{100, 100, 100, 100, 1, T}, + sign > 0 ? Bar{100, 110, 90, 100, 1, T + 60000} : Bar{100, 110, 90, 100, 1, T + 60000}}; + a.run(tape, 2); b.run(tape, 2); + REQUIRE(a.native_state().kind == NativeLifecycleKind::Completed); + REQUIRE(b.native_state().kind == NativeLifecycleKind::Completed); + REQUIRE(ha.size() == hb.size()); + REQUIRE(sa.size() == sb.size()); + for (size_t i = 0; i < ha.size(); ++i) { + boundary = "bar-boundary"; + CHECK(ha[i] == hb[i]); + REQUIRE(sa[i].size() == sb[i].size()); + for (size_t j = 0; j < sa[i].size(); ++j) same_market_event(sa[i][j], sb[i][j], true); + } + same_hosts("complete", a, b); + auto arm = activations(a, trail_a); + REQUIRE(arm.size() >= 1); + CHECK(arm[0].kind == no::ActivationKind::TrailArm); + same_d(arm[0].reached_price, 100 + sign * 5); + CHECK(arm[0].cursor.point.provenance == NativePriceProvenance::Confirmed); + auto sf = fills(a, stop_a); + REQUIRE(sf.size() == 1); + same_d(sf[0].raw_price, 100 - sign); + CHECK(stop_b.incarnation == stop_a.incarnation); + CHECK(trail_b.incarnation == trail_a.incarnation); +} + +void replay_failed_capacity(double sign) { + Host a, b; + const char* key = sign > 0 ? "P2-C8-L" : "P2-C8-S"; + start(a, key); start(b, key); + auto r = tx(sign * 0x1p60, "huge"); r.capacity = no::PointBudget{1}; + const auto ha = put(a, r), hb = put(b, r); + same_hosts("accepted", a, b); + CHECK(!a.input(0, 100)); CHECK(!b.input(0, 100)); + same_hosts("failed", a, b); + REQUIRE(a.native_state().kind == NativeLifecycleKind::Failed); + CHECK(a.native_state().failure.code == NativeFailureCode::SettlementFailure); + CHECK(a.native_state().failure.discriminator + == static_cast(no::CoreFailure::NonrepresentableQuantity)); + CHECK(native_failure_has_recipient(a.native_state().failure.context)); + CHECK(a.native_state().failure.context.recipient.incarnation == ha.incarnation); + CHECK(fills(a, ha).empty()); + CHECK(events_of(a).empty()); + same_d(a.physical_position().signed_units, 0); + failed_is_permanent(a, 1); + failed_is_permanent(b, 1); + same_hosts("refused", a, b); + CHECK(hb.incarnation == ha.incarnation); +} + +void replay_failed_group(double sign) { + Host a, b; + const char* key = sign > 0 ? "P2-G5-L" : "P2-G5-S"; + start(a, key, 1, 2); start(b, key, 1, 2); + auto waiting = tx(sign * 0x1p60, "recipient"); + waiting.trigger = no::Limit{sign > 0 ? 1.0 : 200.0}; + waiting.group = no::Member{7, 2, no::GroupEffect::Cancel}; + const auto ra = put(a, waiting), rb = put(b, waiting); + auto emit = tx(sign, "filler"); emit.group = no::Member{7, 1, no::GroupEffect::Reduce}; + const auto fa = put(a, emit), fb = put(b, emit); + same_hosts("accepted", a, b); + CHECK(!a.input(0, 100)); CHECK(!b.input(0, 100)); + same_hosts("failed", a, b); + REQUIRE(fills(a, fa).size() == 1); + CHECK(fills(a, fa)[0].terminal); + CHECK(fills(a, ra).empty()); + CHECK(events_of(a).empty()); + const auto failure = a.native_state().failure; + CHECK(failure.discriminator == static_cast(no::CoreFailure::UnrepresentableReservation)); + CHECK(native_failure_has_cause(failure.context) && native_failure_has_recipient(failure.context)); + CHECK(failure.context.cause.ordinal == fills(a, fa)[0].ordinal); + CHECK(failure.context.recipient.incarnation == ra.incarnation); + same_d(a.physical_position().signed_units, sign); + failed_is_permanent(a, 1); + failed_is_permanent(b, 1); + same_hosts("refused", a, b); + CHECK(fb.incarnation == fa.incarnation && rb.incarnation == ra.incarnation); +} + +void perturb_one_field(double sign) { + struct Step { + const char* name; + no::Request base; + no::Request changed; + bool physical_diverges; + }; + auto limit = tx(sign * 2, "base", "c"); + limit.trigger = no::Limit{100}; + auto qty = limit; std::get(qty.intent).signed_units = sign * 3; + auto trig = limit; trig.trigger = no::Limit{100 - sign}; + auto cap = limit; cap.capacity = no::PointBudget{1}; + auto grp = limit; grp.group = no::Member{9, 1, no::GroupEffect::Cancel}; + const Step steps[] = { + {"units", limit, qty, true}, + {"trigger", limit, trig, true}, + {"capacity", limit, cap, true}, + {"group", limit, grp, false}, + }; + for (const auto& step : steps) { + Host a, b; + const std::string key = std::string("P2-pert-") + step.name + (sign > 0 ? "-L" : "-S"); + start(a, key.c_str()); start(b, key.c_str()); + put(a, step.base); put(b, step.changed); + boundary = step.name; + const auto ha = a.native_continuation_hash(), hb = b.native_continuation_hash(); + CHECK(ha != hb); + // Distinction is in request history before any physical fill. + same_d(a.physical_position().signed_units, 0); + same_d(b.physical_position().signed_units, 0); + CHECK(a.trade_count() == 0 && b.trade_count() == 0); + REQUIRE(events_of(a).size() == 1); + REQUIRE(events_of(b).size() == 1); + same_request(events_of(a)[0].request(), step.base, true); + same_request(events_of(b)[0].request(), step.changed, true); + CHECK(events_of(a).empty()); + CHECK(events_of(b).empty()); + a.tick(0, 100); b.tick(0, 100); + CHECK(a.native_continuation_hash() != b.native_continuation_hash()); + if (step.physical_diverges) { + CHECK(!bits_eq(a.physical_position().signed_units, b.physical_position().signed_units) + || a.trade_count() != b.trade_count() + || fills(a, events_of(a)[0].handle()).size() + != fills(b, events_of(b)[0].handle()).size()); + } else { + same_d(a.physical_position().signed_units, b.physical_position().signed_units); + same_d(a.physical_position().average_price, b.physical_position().average_price); + CHECK(a.trade_count() == b.trade_count()); + } + if (a.native_state().kind == NativeLifecycleKind::Running) finish(a); + if (b.native_state().kind == NativeLifecycleKind::Running) finish(b); + } +} + +void perturb_owner(double sign) { + Host a, b; + const char* key = sign > 0 ? "P2-pert-owner-L" : "P2-pert-owner-S"; + start(a, key); start(b, key); + put(a, tx(sign * 2, "seed")); put(b, tx(sign * 2, "seed")); + a.tick(0, 100); b.tick(0, 100); + auto parent_req = tx(sign, "later"); + parent_req.trigger = no::Limit{sign > 0 ? 1.0 : 10000.0}; + const auto pa = put(a, parent_req), pb = put(b, parent_req); + auto independent = reduce(1, "close"); + auto waiting = independent; waiting.owner = no::WaitForApplied{pb}; + put(a, independent); put(b, waiting); + boundary = "owner-submit"; + CHECK(a.native_continuation_hash() != b.native_continuation_hash()); + same_d(a.physical_position().signed_units, sign * 2); + same_d(b.physical_position().signed_units, sign * 2); + a.tick(1, 100); b.tick(1, 100); + CHECK(a.native_continuation_hash() != b.native_continuation_hash()); + same_d(a.physical_position().signed_units, sign); + same_d(b.physical_position().signed_units, sign * 2); + CHECK(events_of(a).size() + > events_of(b).size()); + CHECK(pa.incarnation == pb.incarnation); + finish(a); finish(b); +} + +void inert_text(double sign, bool comment) { + Host a, b; + const char* key = comment + ? (sign > 0 ? "P2-comment-L" : "P2-comment-S") + : (sign > 0 ? "P2-label-L" : "P2-label-S"); + start(a, key, 1, 6); start(b, key, 1, 6); + no::Request ra = tx(sign * 2, "keep", "keep"); + no::Request rb = ra; + if (comment) rb.comment = "other"; else rb.label = "other"; + const auto ha = put(a, ra), hb = put(b, rb); + boundary = "text-submit"; + CHECK(a.native_continuation_hash() != b.native_continuation_hash()); + same_physical(a, b, false); + same_d(a.physical_position().signed_units, 0); + a.tick(0, 100); b.tick(0, 100); + CHECK(a.native_continuation_hash() != b.native_continuation_hash()); + same_physical(a, b, false); + same_d(a.physical_position().signed_units, sign * 2); + same_d(a.physical_position().average_price, 100); + REQUIRE(a.lots().size() == 1 && b.lots().size() == 1); + same_d(a.lots()[0].qty, b.lots()[0].qty); + same_d(a.lots()[0].price, b.lots()[0].price); + same_d(a.lots()[0].entry_commission_account, b.lots()[0].entry_commission_account); + CHECK(a.lots()[0].entry_incarnation == ha.incarnation); + CHECK(b.lots()[0].entry_incarnation == hb.incarnation); + auto fa = fills(a, ha), fb = fills(b, hb); + REQUIRE(fa.size() == 1 && fb.size() == 1); + same_d(fa[0].raw_price, fb[0].raw_price); + same_d(fa[0].opened_units, fb[0].opened_units); + same_d(fa[0].current_ticket, fb[0].current_ticket); + same_d(fa[0].current_ticket, 6); + CHECK(ha.incarnation == hb.incarnation); + finish(a); finish(b); + same_physical(a, b, false); +} + +void labels_do_not_select(double sign) { + Host a, b; + const char* key = sign > 0 ? "P2-label-select-L" : "P2-label-select-S"; + start(a, key); start(b, key); + // Same labels on two openings; ownership is the handle, not the text. + const auto a1 = put(a, tx(sign, "same")); + const auto a2 = put(a, tx(sign, "same")); + const auto b1 = put(b, tx(sign, "other")); + const auto b2 = put(b, tx(sign, "other")); + auto ca = reduce(1, "child"); ca.owner = no::WaitForApplied{a2}; + auto cb = reduce(1, "child"); cb.owner = no::WaitForApplied{b2}; + const auto cha = put(a, ca), chb = put(b, cb); + CHECK(a.native_continuation_hash() != b.native_continuation_hash()); + a.tick(0, 100); b.tick(0, 100); + auto fa2 = fills(a, a2), fc = fills(a, cha); + REQUIRE(fa2.size() == 1 && fc.size() == 1); + const auto* scope = std::get_if(&fc[0].scope); + REQUIRE(scope); + CHECK(scope->incarnation == a2.incarnation); + CHECK(scope->incarnation != a1.incarnation); + auto fb2 = fills(b, b2), fbc = fills(b, chb); + REQUIRE(fb2.size() == 1 && fbc.size() == 1); + const auto* scope_b = std::get_if(&fbc[0].scope); + REQUIRE(scope_b); + CHECK(scope_b->incarnation == b2.incarnation); + CHECK(scope_b->incarnation != b1.incarnation); + same_physical(a, b, false); + same_d(a.physical_position().signed_units, sign); + CHECK(a1.incarnation == b1.incarnation && a2.incarnation == b2.incarnation); + finish(a); finish(b); +} + +void reset_clears_state(double sign) { + Host h; + const char* key = sign > 0 ? "P2-reset-L" : "P2-reset-S"; + start(h, key, 1); + const auto seed = put(h, tx(sign * 2, "seed")); + CHECK(seed.incarnation == 1); + h.tick(0, 100); + const auto opened1 = fills(h, seed); + REQUIRE(opened1.size() == 1); + const auto cycle = opened1[0].cycle_after; + auto parent = tx(sign * 5, "parent"); + parent.trigger = no::Limit{sign > 0 ? 1.0 : 10000.0}; + const auto p1 = put(h, parent); + no::Request child{no::Reduce{no::OwnerOpenedUnits{}}, "child", ""}; + child.owner = no::WaitForApplied{p1}; + child.group = no::Member{7, 0, no::GroupEffect::Reduce}; + const auto c1 = put(h, child); + auto filler = tx(sign * 2, "filler"); filler.group = no::Member{7, 1, no::GroupEffect::Reduce}; + put(h, filler); + auto stop = reduce(1, "live-stop"); stop.trigger = no::Stop{100 - sign}; + stop.capacity = no::PointBudget{1}; + const auto stop_h = put(h, stop); + h.tick(1, 100); + REQUIRE(!events_of(h).empty()); + h.tick(2, 100 - sign); + REQUIRE(!activations(h, stop_h).empty()); + CHECK(seed.run.run_number == 1); + finish(h); + const auto old_hash = h.native_continuation_hash(); + const auto old_events = h.native_events(0).size(); + start(h, key, 2); + CHECK(h.native_state().kind == NativeLifecycleKind::Running); + CHECK(h.native_continuation_hash() != old_hash); + CHECK(h.native_events(0).size() < old_events); + CHECK(events_of(h).empty()); + CHECK(events_of(h).empty()); + CHECK(events_of(h).empty()); + CHECK(events_of(h).empty()); + CHECK(events_of(h).empty()); + same_d(h.physical_position().signed_units, 0); + CHECK(h.lots().empty()); + CHECK(h.trade_count() == 0); + const auto fresh = put(h, tx(sign * 2, "new")); + CHECK(fresh.incarnation == 1); + CHECK(fresh.incarnation == seed.incarnation); + CHECK(fresh.run.run_number == 2); + CHECK(seed.run.run_number == 1); + h.tick(0, 100); + const auto opened = fills(h, fresh); + REQUIRE(opened.size() == 1); + CHECK(opened[0].cycle_after == cycle); + auto stale_bind = reduce(1); + stale_bind.owner = no::BindOpening{seed, cycle}; + const auto refused = h.submit(stale_bind); + CHECK(refused.status == no::SubmitStatus::Rejected && !refused.handle); + CHECK(refused.reason == no::RequestRejectReason::InvalidOwner); + const auto stale_cancel = h.cancel(seed); + CHECK(stale_cancel.status == no::CancelStatus::InvalidHandle); + const auto stale_child = h.cancel(c1); + CHECK(stale_child.status == no::CancelStatus::InvalidHandle); + const auto stale_parent = h.cancel(p1); + CHECK(stale_parent.status == no::CancelStatus::InvalidHandle); + auto invalid = events_of(h); + REQUIRE(invalid.size() >= 3); + CHECK(invalid[0].target.run.run_number == 1); + CHECK(invalid[0].target.incarnation == seed.incarnation); + auto current_bind = reduce(1); + current_bind.owner = no::BindOpening{fresh, opened[0].cycle_after}; + const auto ok = put(h, current_bind); + h.tick(1, 100); + auto cf = fills(h, ok); + REQUIRE(cf.size() == 1); + const auto* scope = std::get_if(&cf[0].scope); + REQUIRE(scope); + CHECK(scope->incarnation == fresh.incarnation); + CHECK(scope->cycle == opened[0].cycle_after); + auto again = reduce(1, "new-stop"); again.trigger = no::Stop{100 + sign * 50}; + const auto idle = put(h, again); + CHECK(activations(h, idle).empty()); + CHECK(events_of(h).empty()); + finish(h); +} + +void ieee_rejection_payloads() { + Host a, b; + start(a, "P2-ieee"); start(b, "P2-ieee"); + const double qnan = std::numeric_limits::quiet_NaN(); + const double nnan = std::copysign(qnan, -1.0); + const double n1 = bits_to(0x7ff8000000000001ULL); + const double n2 = bits_to(0x7ff8000000000002ULL); + const double neg0 = std::copysign(0.0, -1.0); + struct Row { no::Request request; no::RequestRejectReason reason; }; + const Row rows[] = { + {tx(qnan, "qnan"), no::RequestRejectReason::InvalidQuantity}, + {tx(nnan, "nnan"), no::RequestRejectReason::InvalidQuantity}, + {tx(n1, "n1"), no::RequestRejectReason::InvalidQuantity}, + {tx(n2, "n2"), no::RequestRejectReason::InvalidQuantity}, + {tx(neg0, "neg0"), no::RequestRejectReason::InvalidQuantity}, + {reduce(qnan, "rqnan"), no::RequestRejectReason::InvalidQuantity}, + {[] { auto r = tx(1, "stop-nan"); r.trigger = no::Stop{std::numeric_limits::quiet_NaN()}; return r; }(), + no::RequestRejectReason::InvalidTrigger}, + {[] { auto r = tx(1, "stop-n1"); r.trigger = no::Stop{bits_to(0x7ff8000000000001ULL)}; return r; }(), + no::RequestRejectReason::InvalidTrigger}, + }; + for (const auto& row : rows) { + const auto ra = a.submit(row.request); + const auto rb = b.submit(row.request); + CHECK(ra.status == no::SubmitStatus::Rejected && rb.status == no::SubmitStatus::Rejected); + CHECK(ra.reason == row.reason && rb.reason == row.reason); + CHECK(!ra.handle && !rb.handle); + } + same_hosts("ieee-both", a, b); + auto rejected = events_of(a); + REQUIRE(rejected.size() == sizeof(rows) / sizeof(rows[0])); + for (size_t i = 0; i < rejected.size(); ++i) { + CHECK(rejected[i].reason == rows[i].reason); + same_request(rejected[i].request, rows[i].request, true); + if (const auto* t = std::get_if(&rows[i].request.intent)) { + REQUIRE(std::holds_alternative(rejected[i].request.intent)); + same_d(std::get(rejected[i].request.intent).signed_units, t->signed_units); + } + if (const auto* r = std::get_if(&rows[i].request.intent)) { + if (const auto* u = std::get_if(&r->size)) { + REQUIRE(std::holds_alternative(rejected[i].request.intent)); + same_d(std::get( + std::get(rejected[i].request.intent).size).units, + u->units); + } + } + if (const auto* s = std::get_if(&rows[i].request.trigger)) { + REQUIRE(std::holds_alternative(rejected[i].request.trigger)); + same_d(std::get(rejected[i].request.trigger).price, s->price); + } + } + CHECK(!bits_eq(n1, n2)); + CHECK(bits_of(std::get(rejected[2].request.intent).signed_units) + == 0x7ff8000000000001ULL); + CHECK(bits_of(std::get(rejected[3].request.intent).signed_units) + == 0x7ff8000000000002ULL); + same_d(a.physical_position().signed_units, 0); + Host c, d; + start(c, "P2-ieee-distinct-a"); start(d, "P2-ieee-distinct-b"); + (void)c.submit(tx(n1, "n1")); + (void)d.submit(tx(n2, "n1")); + CHECK(c.native_continuation_hash() != d.native_continuation_hash()); + same_d(c.physical_position().signed_units, 0); + same_d(d.physical_position().signed_units, 0); + finish(a); finish(b); +} + +void run_case(const char* name, const std::function& body) { + scenario = name; boundary = ""; ++cases; + const int previous = failures; + try { body(); } + catch (const StopCase&) {} + catch (const std::exception& e) { + std::printf("FAIL %s unexpected exception: %s\n", name, e.what()); ++failures; + } catch (...) { + std::printf("FAIL %s unexpected nonstandard exception\n", name); ++failures; + } + std::printf("%s %s\n", failures == previous ? "PASS" : "FAIL", name); +} +} // namespace + +int main() { + for (double sign : {1.0, -1.0}) { + run_case(sign > 0 ? "identical partial-budget long" : "identical partial-budget short", + [&] { replay_partial_budget(sign); }); + run_case(sign > 0 ? "identical Stop/StopLimit/Trail long" : "identical Stop/StopLimit/Trail short", + [&] { replay_stop_stoplimit_trail(sign); }); + run_case(sign > 0 ? "identical deferred bind long" : "identical deferred bind short", + [&] { replay_deferred_bind(sign); }); + run_case(sign > 0 ? "identical owner expiry/replace long" : "identical owner expiry/replace short", + [&] { replay_owner_expiry_replace(sign); }); + run_case(sign > 0 ? "identical confirmed-bar long" : "identical confirmed-bar short", + [&] { replay_confirmed_bar(sign); }); + run_case(sign > 0 ? "failed capacity prefix long" : "failed capacity prefix short", + [&] { replay_failed_capacity(sign); }); + run_case(sign > 0 ? "failed group prefix long" : "failed group prefix short", + [&] { replay_failed_group(sign); }); + run_case(sign > 0 ? "perturb fields long" : "perturb fields short", + [&] { perturb_one_field(sign); }); + run_case(sign > 0 ? "perturb owner long" : "perturb owner short", + [&] { perturb_owner(sign); }); + run_case(sign > 0 ? "inert label long" : "inert label short", + [&] { inert_text(sign, false); }); + run_case(sign > 0 ? "inert comment long" : "inert comment short", + [&] { inert_text(sign, true); }); + run_case(sign > 0 ? "labels do not select long" : "labels do not select short", + [&] { labels_do_not_select(sign); }); + run_case(sign > 0 ? "reset next run long" : "reset next run short", + [&] { reset_clears_state(sign); }); + } + run_case("IEEE rejection payloads", [] { ieee_rejection_payloads(); }); + std::printf("%s native resting replay: %d cases, %d checks, %d failures\n", + failures ? "FAIL" : "PASS", cases, checks, failures); + return failures ? 1 : 0; +} From c57e8687d91027df09474b414369162e47b94bbb Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 13 Sep 2026 00:27:52 +0800 Subject: [PATCH 5/5] Start crash-recovery writer in a fresh process --- tests/test_native_live_store.cpp | 41 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/tests/test_native_live_store.cpp b/tests/test_native_live_store.cpp index 203b574e..7330abf0 100644 --- a/tests/test_native_live_store.cpp +++ b/tests/test_native_live_store.cpp @@ -155,27 +155,42 @@ void ledger_transactions() { CHECK(throws([&] { Ledger reordered(temp.file("event-order.sqlite"), "ordered"); })); } -void crash_recovery() { +[[noreturn]] void crash_writer(const char* path) { + try { + Ledger ledger(path, "crash-v1"); + ledger.commit_input(0, "{\"synthetic\":true}", 42, {{"crash-event", "{\"qty\":1}"}}); + ledger.begin_delivery("crash-event"); + _exit(0); // Deliberately skip destructors/checkpoint/acknowledgement. + } catch (const std::exception& e) { + std::fprintf(stderr, "crash writer: %s\n", e.what()); + _exit(1); + } catch (...) { _exit(1); } +} + +void crash_recovery(const char* executable) { TempDir temp; const auto path = temp.file("crash.sqlite"); const pid_t child = fork(); if (child < 0) throw std::runtime_error("test fork failed"); if (child == 0) { - try { - Ledger ledger(path, "crash-v1"); - ledger.commit_input(0, "{\"synthetic\":true}", 42, {{"crash-event", "{\"qty\":1}"}}); - ledger.begin_delivery("crash-event"); - _exit(0); // Deliberately skip destructors/checkpoint/acknowledgement. - } catch (...) { _exit(1); } + // SQLite's platform logging state is not safe to reuse after fork. + // Start a fresh writer process, then exercise the same abrupt exit. + execl(executable, executable, "--crash-writer", path.c_str(), + static_cast(nullptr)); + _exit(127); } int status = 0; CHECK(waitpid(child, &status, 0) == child); CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0); Ledger recovered(path, "crash-v1"); CHECK(recovered.input_count() == 1); - CHECK(recovered.pending_event()->id == "crash-event"); - CHECK(recovered.pending_event()->attempts == 1); - CHECK(recovered.pending_event()->payload == "{\"qty\":1}"); + const auto pending = recovered.pending_event(); + CHECK(pending.has_value()); + if (pending) { + CHECK(pending->id == "crash-event"); + CHECK(pending->attempts == 1); + CHECK(pending->payload == "{\"qty\":1}"); + } } struct Reply { @@ -332,10 +347,12 @@ void native_http() { } // namespace -int main() { +int main(int argc, char** argv) { + if (argc == 3 && std::strcmp(argv[1], "--crash-writer") == 0) + crash_writer(argv[2]); try { ledger_transactions(); - crash_recovery(); + crash_recovery(argv[0]); native_http(); } catch (const std::exception& e) { std::fprintf(stderr, "UNEXPECTED: %s\n", e.what());