From 33d1b73244d7b5d80ddd0a6b361e6e07e51a5fa0 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 13 Sep 2026 09:58:47 +0800 Subject: [PATCH] Isolate source-day observations from native settlement Keep native settlement and financial counter checks independent of source-day state. Preserve source preflight ordering and observe only committed rows through the three source coordinators, with native margin and physical-observation coverage. --- docs/native-settlement.md | 71 ++- include/pineforge/engine.hpp | 43 +- src/engine_execution.cpp | 133 +++-- src/engine_orders.cpp | 43 +- src/engine_risk.cpp | 53 ++ tests/CMakeLists.txt | 1 + tests/test_engine_risk.cpp | 29 +- .../test_native_market_vertical_contract.cpp | 158 +++++- tests/test_native_settlement_callers.cpp | 107 +++- .../test_settlement_observation_boundary.cpp | 480 ++++++++++++++++++ 10 files changed, 998 insertions(+), 120 deletions(-) create mode 100644 tests/test_settlement_observation_boundary.cpp diff --git a/docs/native-settlement.md b/docs/native-settlement.md index 4ea89c837..62e9cf71c 100644 --- a/docs/native-settlement.md +++ b/docs/native-settlement.md @@ -1,12 +1,17 @@ # Native resolved execution settlement -`BacktestEngine::settle_resolved_execution` is a protected C++ extension for -trusted matching adapters. It applies an already admitted, matched execution -to the engine's existing physical lot book and emits the resulting close and -open observations. It adds no second position ledger or persistent selector. -Callers that supply transient EXIT lifecycle effects use the separately named -`settle_execution_with_lifecycle` seam; the two-argument symbol is unchanged -and forwards empty effects. +`BacktestEngine::settle_native_execution_at` is a protected C++ extension for +trusted native matching adapters. It applies an already admitted, matched +execution using a supplied `PhysicalExecutionContext` and the engine's one +physical lot book. Scoped and selected variants use the same financial owner. +Use `PhysicalExecutionContext{}` when zero time/index and no preceding-path +facts are appropriate; native settlement has no implicit source chart clock. + +The original two-argument `settle_resolved_execution` symbol is a source +compatibility alias of `settle_execution_with_lifecycle` with empty effects. +It uses current chart context and preserves source-day checks and observations. +It is distinct from the explicit native/context seams, which do not depend on +those source fields. Both routes book physical and financial effects once. The caller supplies an `execution::Action` and `execution::Fill` from ``. These values describe immediate effects: @@ -65,7 +70,7 @@ nonvirtual methods: | `inspect_native_reversal_v1(reversal, fill)` | Non-mutating `SettlementInspection` of physical effects and the current ticket. | | `project_native_reversal_v1(reversal, fill)` | Non-mutating `AccountEffectProjection`, including account effects and the next cycle without consuming it. | | `settle_native_reversal_at_v1(reversal, fill, context)` | `Result` from settlement using the supplied `PhysicalExecutionContext` and empty lifecycle effects. | -| `settle_reversal_with_lifecycle_v1(reversal, fill, lifecycle)` | `Result` from settlement using the engine's current time, interval and preceding exit-path facts with supplied transient lifecycle effects. | +| `settle_reversal_with_lifecycle_v1(reversal, fill, lifecycle)` | Source-coordinated `Result` using current chart context, supplied lifecycle effects and source-day preflight/observation. | For example, reversing a held long position of `1` unit to a short target of `0.1` opens the exact binary64 quantity supplied as `-0.1`. The opening quantity @@ -162,6 +167,49 @@ and paid cost in roster order. A projection is data, not commit authority: later settlement revalidates against the current book. Source percent sizing can consume a close-only projection's equity before resolving one reversal opening quantity. +## Source-day observation boundary + +Native Book/scoped/selected/ReverseTo settlement and the generic +`settle_with_context` seam do not read or update Pine intraday PnL or +consecutive-loss-day counters. Their inspection and account projection methods +are likewise independent of these source fields. An exhausted source day count +or nonfinite cached source intraday value cannot refuse an otherwise valid +native execution. Financial win/loss/even, entry/cycle/stream capacity and +lifecycle checks remain generic and still run before physical effects. + +Three source coordinators preserve the existing source behavior: +`settle_execution_with_lifecycle`, `settle_execution_selected_with_lifecycle` +and `settle_reversal_with_lifecycle_v1`. They share the native stage and quote, +prepare this execution's close rows once, validate source observations before +effects, and use the same physical/financial commit. Only after Applied do they +observe the newly committed slice identified by the Result. Historical and +range-end report rows are not observed again. + +Source preflight first completes the full ordered intraday-PnL sum and checks +that it is finite. It then simulates consecutive-loss-day counters, using the +existing chart timezone and `dayofmonth * 100 + month` key. That day is captured +before effects, so the committed-row observer performs no timezone work. A loss +increments the count only on a different last-loss day; a win resets the count +without clearing the last-loss-day key; zero PnL leaves the count unchanged. +The observer updates source fields only and never applies cash or fees again. + +Source intraday invalidity remains `InvalidAccounting`, before source or +financial close-counter overflow. Source day exhaustion retains +`"closed trade counter exhausted"` before later lifecycle/stream checks. +Generic cycle readiness still precedes source preflight. A Ready opening-only +source call also requires finite cached intraday PnL, despite having no close +rows; it performs no observer writes. Invalid and NoEffect stages return before +source preflight. These checks and observations are private to the synchronous +call and introduce no saved settlement plan or persistent state. + +Source risk/halt evaluation stays at its existing fill-loop call sites after +the coordinator returns. Native admission remains its own direction, resulting +position and initial-margin gate; Pine entry/risk/quota rules stay in source +adapters. Physical positions and marked equity continue to follow actual lots, +while the existing script/C position view may remain frozen. Range-end reporting +can update report rows/equity-curve state without committing physical closes or +source-day observations. Existing source fields remain represented in hashes. + ## Integration and limits The kernel serves native execution and the source adapters' full, partial, @@ -209,6 +257,7 @@ run rather than retry a partially committed execution in place. Strong rollback on allocation failure is not promised. The work retains `ShortSeedCollisionRole` while its source-policy consumers -remain. Admission, source day/quota counters, script-visible observations and -complete Pine lowering remain separate refactor work. Existing executable state -remains represented in ABI projections and fingerprints. +remain. Source quota, TV-money/day-loss policy and complete Pine lowering remain +separate refactor work. Source-day observation ownership is separated from the +native financial owner; existing executable state remains represented in ABI +projections and fingerprints. diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 729f246cb..cd9804fcb 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -1807,16 +1807,17 @@ class BacktestEngine { // --- Per-trade extreme tracking --- void update_per_trade_extremes(); - // Trusted native matching-adapter extension: settle an already resolved - // execution into this engine's one physical book. This does not place an - // order, perform admission/slippage, or provide cancellation/replay. The - // owning run must abort on an exception; failed commits are not retryable - // in place. A saved arithmetic plan is not execution authority. + // Source compatibility extension: settle an already resolved execution + // using the current chart context and source-day preflight/observation. + // This does not place an order, perform admission/slippage, or provide + // cancellation/replay. Explicit native/context seams below are source-day + // independent. The owning run must abort on an exception; failed commits + // are not retryable in place. A saved plan is not execution authority. // Empty lifecycle effects. Member-pointer type is the original two-argument // symbol; it forwards to settle_execution_with_lifecycle. execution::Result settle_resolved_execution( const execution::Action& action, const execution::Fill& fill); - // Trusted matching-adapter extension for one execution plus lifecycle effects. + // Source coordinator for one execution plus lifecycle effects and source days. // Pre-close operations, then close observations and old-cycle unbind, then // the listed pending removals, then the quoted opening path which binds // only remaining exits. Native settlement does not call compat::pine @@ -1853,7 +1854,8 @@ class BacktestEngine { const execution::Fill& fill, const execution::PhysicalExecutionContext& context, const execution::SelectedOpeningSet& selection); - // Legacy lifecycle copy of current_bar_/fold flags, then selected commit. + // Source lifecycle coordinator: current_bar_/fold context, source-day + // preflight, selected commit, then observation of its committed close rows. // Empty lifecycle is valid. Do not fold selection into LifecycleEffects. execution::Result settle_execution_selected_with_lifecycle( const execution::Action& action, @@ -1886,6 +1888,7 @@ class BacktestEngine { const execution::ReverseTo& reversal, const execution::Fill& fill, const execution::PhysicalExecutionContext& context); + // Source reversal coordinator with current chart context and source days. execution::Result settle_reversal_with_lifecycle_v1( const execution::ReverseTo& reversal, const execution::Fill& fill, @@ -3910,6 +3913,7 @@ class BacktestEngine { execution::CloseScope book_or_opening, const execution::SelectedOpeningSet* selected); struct NativeSettlementStage; + struct NativeSettlementRows; execution::Status validate_native_settlement_book(double& held) const; execution::Status allocate_native_settlement_closes( NativeSettlementStage& stage, @@ -3925,6 +3929,27 @@ class BacktestEngine { NativeSettlementStage& stage, const execution::Fill& fill, const execution::LifecycleEffects& lifecycle, const execution::PhysicalExecutionContext& context); + void build_native_settlement_close_rows( + const NativeSettlementStage& stage, const execution::Fill& fill, + const execution::PhysicalExecutionContext& context, + NativeSettlementRows& rows) const; + execution::Status prepare_native_settlement_commit( + const NativeSettlementStage& stage, const execution::Fill& fill, + const execution::PhysicalExecutionContext& context, + NativeSettlementRows& rows) const; + execution::Status preflight_native_settlement_effects( + const NativeSettlementStage& stage, + const execution::LifecycleEffects& lifecycle, + const NativeSettlementRows& rows); + execution::Result commit_prepared_native_settlement_stage( + NativeSettlementStage& stage, const execution::Fill& fill, + const execution::LifecycleEffects& lifecycle, + const execution::PhysicalExecutionContext& context, + NativeSettlementRows& rows); + execution::Result settle_source_staged_execution( + NativeSettlementStage& stage, const execution::Fill& fill, + const execution::LifecycleEffects& lifecycle, + const execution::PhysicalExecutionContext& context); void stage_native_settlement( NativeSettlementStage& stage, const execution::Action& action, @@ -4339,6 +4364,10 @@ class BacktestEngine { double fill_price, bool was_long); void record_close_trade(Trade trade); void validate_close_trade_counters(const Trade* rows, size_t count) const; + execution::Status preflight_source_close_observation( + const Trade* rows, size_t count, std::optional& loss_day) const; + void observe_source_close_rows( + const Trade* rows, size_t count, std::optional loss_day); // Quote one resolved execution's current charges. Entry costs on the // closed rows are historical allocations. Returns close shares in FIFO // order followed by the opening share (zero when there is no opening). diff --git a/src/engine_execution.cpp b/src/engine_execution.cpp index fd83b629c..0804d1bc0 100644 --- a/src/engine_execution.cpp +++ b/src/engine_execution.cpp @@ -156,6 +156,12 @@ struct BacktestEngine::NativeSettlementStage { std::unordered_set selected_ids; }; +// Only this synchronous call's prospective closes; never stored on the engine +// or returned as authority to apply an earlier inspection/projection. +struct BacktestEngine::NativeSettlementRows { + std::vector closed_trades; +}; + execution::Status BacktestEngine::validate_native_settlement_book(double& held) const { using execution::Status; held = 0.0; @@ -459,7 +465,7 @@ execution::Result BacktestEngine::settle_reversal_with_lifecycle_v1( } NativeSettlementStage stage; stage_native_settlement(stage, reversal, fill, &lifecycle); - return commit_native_settlement_stage(stage, fill, lifecycle, context); + return settle_source_staged_execution(stage, fill, lifecycle, context); } execution::Result BacktestEngine::settle_resolved_execution( @@ -477,7 +483,10 @@ execution::Result BacktestEngine::settle_execution_with_lifecycle( if (!std::isnan(fold_exit_trail_peak_)) { context.preceding_exit_trail_peak = fold_exit_trail_peak_; } - return settle_with_context(action, fill, lifecycle, context); + NativeSettlementStage stage; + stage_native_settlement( + stage, action, fill, execution::Book{}, nullptr, &lifecycle); + return settle_source_staged_execution(stage, fill, lifecycle, context); } execution::Result BacktestEngine::settle_native_execution_at( @@ -518,7 +527,10 @@ execution::Result BacktestEngine::settle_execution_selected_with_lifecycle( if (!std::isnan(fold_exit_trail_peak_)) { context.preceding_exit_trail_peak = fold_exit_trail_peak_; } - return settle_with_context_selected(action, fill, lifecycle, context, selection); + NativeSettlementStage stage; + stage_native_settlement( + stage, action, fill, execution::Book{}, &selection, &lifecycle); + return settle_source_staged_execution(stage, fill, lifecycle, context); } execution::Result BacktestEngine::settle_with_context_scoped( @@ -554,16 +566,48 @@ execution::Result BacktestEngine::commit_native_settlement_stage( NativeSettlementStage& stage, const execution::Fill& fill, const execution::LifecycleEffects& lifecycle, const execution::PhysicalExecutionContext& context) { - using execution::Status; - if (stage.phase != NativeSettlementStage::Phase::Ready) - return {stage.status}; - const auto no_effect = [&]() -> execution::Result { - return {fill.commission_account && *fill.commission_account != 0.0 - ? Status::InvalidAccounting : Status::NoEffect}; - }; - if (stage.scoped && stage.closed == 0.0) return no_effect(); + NativeSettlementRows rows; + if (const auto status = prepare_native_settlement_commit(stage, fill, context, rows); + status != execution::Status::Applied) + return {status}; + if (const auto status = preflight_native_settlement_effects(stage, lifecycle, rows); + status != execution::Status::Applied) + return {status}; + return commit_prepared_native_settlement_stage(stage, fill, lifecycle, context, rows); +} - std::vector closed_trades; +execution::Result BacktestEngine::settle_source_staged_execution( + NativeSettlementStage& stage, const execution::Fill& fill, + const execution::LifecycleEffects& lifecycle, + const execution::PhysicalExecutionContext& context) { + NativeSettlementRows rows; + if (const auto status = prepare_native_settlement_commit(stage, fill, context, rows); + status != execution::Status::Applied) + return {status}; + // Source intraday readiness precedes all close-counter checks, including + // Ready opening-only calls. Invalid/NoEffect returned before this point. + std::optional loss_day; + if (const auto status = preflight_source_close_observation( + rows.closed_trades.data(), rows.closed_trades.size(), loss_day); + status != execution::Status::Applied) + return {status}; + if (const auto status = preflight_native_settlement_effects(stage, lifecycle, rows); + status != execution::Status::Applied) + return {status}; + const auto result = commit_prepared_native_settlement_stage( + stage, fill, lifecycle, context, rows); + if (result.status == execution::Status::Applied && result.closed_trade_count != 0) { + observe_source_close_rows(trades_.data() + result.first_trade_index, + result.closed_trade_count, loss_day); + } + return result; +} + +void BacktestEngine::build_native_settlement_close_rows( + const NativeSettlementStage& stage, const execution::Fill& fill, + const execution::PhysicalExecutionContext& context, + NativeSettlementRows& rows) const { + auto& closed_trades = rows.closed_trades; closed_trades.reserve(stage.closing_indices.size()); for (size_t i = 0; i < stage.closing_indices.size(); ++i) { const auto& lot = pyramid_entries_[stage.closing_indices[i]]; @@ -575,29 +619,49 @@ execution::Result BacktestEngine::commit_native_settlement_stage( trade.exit_comment = fill.comment; closed_trades.push_back(std::move(trade)); } +} + +execution::Status BacktestEngine::prepare_native_settlement_commit( + const NativeSettlementStage& stage, const execution::Fill& fill, + const execution::PhysicalExecutionContext& context, + NativeSettlementRows& rows) const { + using execution::Status; + if (stage.phase != NativeSettlementStage::Phase::Ready) + return stage.status; + if (stage.scoped && stage.closed == 0.0) + return fill.commission_account && *fill.commission_account != 0.0 + ? Status::InvalidAccounting : Status::NoEffect; + + build_native_settlement_close_rows(stage, fill, context, rows); if (stage.opening > 0.0 && (position_side_ == PositionSide::FLAT || stage.survivors.empty()) && (next_position_cycle_seq_ <= 0 || next_position_cycle_seq_ == std::numeric_limits::max())) throw std::overflow_error("position cycle sequence exhausted"); if (!std::isfinite(stage.after_qty) || !std::isfinite(stage.after_price)) - return {Status::InvalidAccounting}; + return Status::InvalidAccounting; double next_profit = net_profit_sum_; double next_gross_profit = gross_profit_sum_; double next_gross_loss = gross_loss_sum_; - double next_intraday = intraday_pnl_; - for (const auto& trade : closed_trades) { + for (const auto& trade : rows.closed_trades) { if (!std::isfinite(trade.pnl) || !std::isfinite(trade.commission)) - return {Status::InvalidAccounting}; + return Status::InvalidAccounting; next_profit += trade.pnl; - next_intraday += trade.pnl; if (trade.pnl > 0.0) next_gross_profit += trade.pnl; if (trade.pnl < 0.0) next_gross_loss += trade.pnl; } if (!std::isfinite(next_profit) || !std::isfinite(next_gross_profit) - || !std::isfinite(next_gross_loss) || !std::isfinite(next_intraday)) - return {Status::InvalidAccounting}; + || !std::isfinite(next_gross_loss)) + return Status::InvalidAccounting; + return Status::Applied; +} + +execution::Status BacktestEngine::preflight_native_settlement_effects( + const NativeSettlementStage& stage, + const execution::LifecycleEffects& lifecycle, + const NativeSettlementRows& rows) { + const auto& closed_trades = rows.closed_trades; validate_close_trade_counters(closed_trades.data(), closed_trades.size()); if (stage.opening > 0.0 && !stage.survivors.empty() && position_entry_count_ == std::numeric_limits::max()) @@ -608,10 +672,8 @@ execution::Result BacktestEngine::commit_native_settlement_stage( && (position_side_ == PositionSide::FLAT || stage.survivors.empty()); if (auto invalid = preflight_settlement_lifecycle( lifecycle, will_reset, will_open_quoted)) - return {*invalid}; + return *invalid; - const size_t first_trade = trades_.size(); - const size_t first_action = stream_order_actions_.size(); const size_t events = closed_trades.size() + (stage.opening > 0.0 ? 1 : 0); if (stream_observe_actions_) { if (events > std::numeric_limits::max() - stream_action_sequence_) @@ -620,6 +682,17 @@ execution::Result BacktestEngine::commit_native_settlement_stage( } reserve_effects(trades_, closed_trades.size()); if (stage.opening > 0.0) reserve_effects(pyramid_entries_, 1); + return execution::Status::Applied; +} + +execution::Result BacktestEngine::commit_prepared_native_settlement_stage( + NativeSettlementStage& stage, const execution::Fill& fill, + const execution::LifecycleEffects& lifecycle, + const execution::PhysicalExecutionContext& context, + NativeSettlementRows& rows) { + auto& closed_trades = rows.closed_trades; + const size_t first_trade = trades_.size(); + const size_t first_action = stream_order_actions_.size(); // Commit through the existing accounting/observation sinks. Allocation or // lifecycle exceptions still abort the owning engine run; this internal @@ -654,7 +727,7 @@ execution::Result BacktestEngine::commit_native_settlement_stage( } if (stream_observe_actions_) stream_refresh_action_metadata(first_action, first_trade); execution::Result applied; - applied.status = Status::Applied; + applied.status = execution::Status::Applied; applied.closed_units = stage.closed; applied.opened_units = stage.incoming == PositionSide::SHORT ? -stage.opening : stage.opening; @@ -802,19 +875,11 @@ execution::AccountEffectProjection BacktestEngine::project_native_settlement_sta throw std::overflow_error("position cycle sequence exhausted"); execution::PhysicalExecutionContext context; - std::vector closed_trades; - closed_trades.reserve(stage.closing_indices.size()); - for (size_t i = 0; i < stage.closing_indices.size(); ++i) { - const auto& lot = pyramid_entries_[stage.closing_indices[i]]; - auto trade = build_close_trade_with_costs(lot, stage.closing_quantities[i], - fill.price, stage.was_long, - allocated_entry_commission(lot, stage.closing_quantities[i]), - stage.current_costs[i], context); - closed_trades.push_back(std::move(trade)); - } + NativeSettlementRows rows; + build_native_settlement_close_rows(stage, fill, context, rows); double realized = net_profit_sum_; - for (const auto& trade : closed_trades) { + for (const auto& trade : rows.closed_trades) { if (!std::isfinite(trade.pnl) || !std::isfinite(trade.commission)) return invalid_projection(Status::InvalidAccounting); realized += trade.pnl; diff --git a/src/engine_orders.cpp b/src/engine_orders.cpp index 0eb95db9d..a45adb5d3 100644 --- a/src/engine_orders.cpp +++ b/src/engine_orders.cpp @@ -719,13 +719,9 @@ void BacktestEngine::purge_exit_orders(bool retain_for_pending_entries) { // Shared close-side / position-state helpers // ──────────────────────────────────────────────────────────────────── -// Emit one Trade record for closing close_qty of pyramid entry pe at -// fill_price (already slippage-adjusted). Updates trades_, profit/loss -// aggregates, intraday PnL, and consecutive-loss-day tracking exactly the -// same way every closing path does — the body was previously inlined in -// execute_market_exit, execute_partial_exit_qty, execute_partial_exit_by_entry, -// execute_partial_exit_by_entry_percent, and the flip branch of -// execute_market_entry. Mirrors TradingView's per-pyramid trade reporting. +// Build a close row using the current source context and resolved price. +// This does not book cash, emit a live trade or update source-day observations; +// range-end reporting also uses this non-mutating row builder. Trade BacktestEngine::build_close_trade(const PyramidEntry& pe, double close_qty, double fill_price, bool was_long) const { execution::PhysicalExecutionContext context; @@ -867,7 +863,6 @@ void BacktestEngine::emit_close_trade(const PyramidEntry& pe, double close_qty, void BacktestEngine::record_close_trade(Trade trade) { validate_close_trade_counters(&trade, 1); - const double pnl = trade.pnl; const double trade_pnl = trade.pnl; trades_.push_back(std::move(trade)); if (stream_observe_actions_) stream_observe_exit(trades_.size() - 1); @@ -901,54 +896,30 @@ void BacktestEngine::record_close_trade(Trade trade) { if (trade_pnl > 0) { gross_profit_sum_ += trade_pnl; win_trades_count_++; } else if (trade_pnl < 0) { gross_loss_sum_ += trade_pnl; loss_trades_count_++; } else { ++eventrades_count_; } // strategy.eventrades: exact zero P&L (TV uses == 0) - - // Update risk state: intraday PnL and consecutive loss day tracking - intraday_pnl_ += pnl; - if (pnl < 0.0) { - BarTime bt = _decompose_bar_time_chart_tz(); - int cur_day = bt.dayofmonth * 100 + bt.month; - if (cur_day != last_loss_day_) { - last_loss_day_ = cur_day; - cons_loss_day_count_++; - } - } else if (pnl > 0.0) { - cons_loss_day_count_ = 0; - } } void BacktestEngine::validate_close_trade_counters(const Trade* rows, size_t count) const { const int maximum = std::numeric_limits::max(); // In an ordinary run each counter can advance at most once per row. - // Only an exhausted/near-exhausted counter needs the exact risk-day walk. + // Only an exhausted/near-exhausted counter needs the exact financial walk. if (count <= static_cast(maximum)) { const int room = maximum - static_cast(count); if (win_trades_count_ <= room && loss_trades_count_ <= room - && eventrades_count_ <= room && cons_loss_day_count_ <= room) + && eventrades_count_ <= room) return; } int64_t wins = win_trades_count_, losses = loss_trades_count_; - int64_t evens = eventrades_count_, loss_days = cons_loss_day_count_; - int last_day = last_loss_day_; - std::optional current_day; + int64_t evens = eventrades_count_; for (size_t i = 0; i < count; ++i) { const double pnl = rows[i].pnl; if (pnl > 0.0) { ++wins; - loss_days = 0; } else if (pnl < 0.0) { ++losses; - if (!current_day) { - const BarTime time = _decompose_bar_time_chart_tz(); - current_day = time.dayofmonth * 100 + time.month; - } - if (*current_day != last_day) { - last_day = *current_day; - ++loss_days; - } } else { ++evens; } - if (wins > maximum || losses > maximum || evens > maximum || loss_days > maximum) + if (wins > maximum || losses > maximum || evens > maximum) throw std::overflow_error("closed trade counter exhausted"); } } diff --git a/src/engine_risk.cpp b/src/engine_risk.cpp index e9a5249ce..6b697349c 100644 --- a/src/engine_risk.cpp +++ b/src/engine_risk.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include "engine_internal.hpp" #include "timezone.hpp" @@ -54,6 +56,57 @@ BacktestEngine::BarTime BacktestEngine::_decompose_bar_time_chart_tz() const { return bt; } +execution::Status BacktestEngine::preflight_source_close_observation( + const Trade* rows, size_t count, std::optional& loss_day) const { + loss_day.reset(); + // Complete this pass before walking day counters: a later row can overflow + // intraday PnL even when the first row would exhaust the source day count. + // Ready opening-only source executions also validate the starting value. + double next_intraday = intraday_pnl_; + for (size_t i = 0; i < count; ++i) next_intraday += rows[i].pnl; + if (!std::isfinite(next_intraday)) return execution::Status::InvalidAccounting; + + int64_t loss_days = cons_loss_day_count_; + int last_day = last_loss_day_; + for (size_t i = 0; i < count; ++i) { + const double pnl = rows[i].pnl; + if (pnl > 0.0) { + loss_days = 0; + } else if (pnl < 0.0) { + if (!loss_day) { + const BarTime time = _decompose_bar_time_chart_tz(); + loss_day = time.dayofmonth * 100 + time.month; + } + if (*loss_day != last_day) { + last_day = *loss_day; + ++loss_days; + } + } + if (loss_days > std::numeric_limits::max()) + throw std::overflow_error("closed trade counter exhausted"); + } + return execution::Status::Applied; +} + +void BacktestEngine::observe_source_close_rows( + const Trade* rows, size_t count, std::optional loss_day) { + // The source coordinator preflighted these exact rows before committing + // them. Only its newly committed slice is observed, using the day already + // captured before effects rather than performing fallible timezone work. + for (size_t i = 0; i < count; ++i) { + const double pnl = rows[i].pnl; + intraday_pnl_ += pnl; + if (pnl < 0.0) { + if (*loss_day != last_loss_day_) { + last_loss_day_ = *loss_day; + ++cons_loss_day_count_; + } + } else if (pnl > 0.0) { + cons_loss_day_count_ = 0; + } + } +} + bool BacktestEngine::check_risk_allow_entry(bool is_long) const { if (risk_halted_) return false; if (risk_direction_ == RiskDirection::LONG_ONLY && !is_long) return false; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5c18c009f..9a45daa2b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,6 +9,7 @@ set(TEST_SOURCES test_native_reverse_to test_source_exact_reversal test_source_fifo_endpoints + test_settlement_observation_boundary test_order_action test_native_order_core test_native_order_terminal diff --git a/tests/test_engine_risk.cpp b/tests/test_engine_risk.cpp index 012504fb8..c4146a177 100644 --- a/tests/test_engine_risk.cpp +++ b/tests/test_engine_risk.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -83,23 +84,21 @@ class RiskProbe : public BacktestEngine { void set_position_qty(double v) { position_qty_ = v; } void set_bar(const Bar& b) { current_bar_ = b; } - // --- direct halt-state injectors mirroring engine_orders.cpp's exit path --- - // Replicates the cons-loss-day accounting that execute_market_exit applies - // when a trade closes negative/positive, so the day-rollover gate can be - // exercised without running a full fill cycle. + // Produce a real committed close through the source coordinator. The risk + // fixture must exercise the production observer, not copy its day walk. void record_trade_pnl_for_day(double pnl, const Bar& bar) { current_bar_ = bar; - intraday_pnl_ += pnl; - if (pnl < 0.0) { - BarTime bt = _decompose_bar_time_chart_tz(); - int cur_day = bt.dayofmonth * 100 + bt.month; - if (cur_day != last_loss_day_) { - last_loss_day_ = cur_day; - cons_loss_day_count_++; - } - } else if (pnl > 0.0) { - cons_loss_day_count_ = 0; - } + const execution::PhysicalExecutionContext context{ + current_bar_.timestamp, bar_index_, {}, {}}; + const auto opened = settle_native_execution_at(order_action::Transact{1}, + execution::Fill{1000, "risk-day", "", 0, 0}, context); + if (opened.status != execution::Status::Applied) + throw std::runtime_error("risk fixture native opening failed"); + const auto closed = settle_execution_with_lifecycle(execution::Flatten{}, + execution::Fill{1000 + pnl, "risk-day-close", "", 0, 0}, {}); + if (closed.status != execution::Status::Applied || closed.closed_trade_count != 1 + || trades_.back().pnl != pnl) + throw std::runtime_error("risk fixture source close failed"); } // --- intraday-loss surface (TradingView's day-scoped rule) --- diff --git a/tests/test_native_market_vertical_contract.cpp b/tests/test_native_market_vertical_contract.cpp index a02291012..fba926937 100644 --- a/tests/test_native_market_vertical_contract.cpp +++ b/tests/test_native_market_vertical_contract.cpp @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include @@ -166,6 +168,21 @@ class FixtureHost final : public NativeStrategyHost { bool source_caught = false; void invoke_source_entry() { strategy_entry("legacy", true); } + void poison_source_observation() { + intraday_pnl_ = kNaN; + cons_loss_day_count_ = std::numeric_limits::max(); + last_loss_day_ = -1; + intraday_pnl_day_ = 42; + } + auto source_observation() const { + uint64_t intraday_bits; + std::memcpy(&intraday_bits, &intraday_pnl_, sizeof intraday_bits); + return std::make_tuple(intraday_bits, cons_loss_day_count_, + last_loss_day_, intraday_pnl_day_); + } + const auto& physical_lots() const { return pyramid_entries_; } + int64_t position_cycle() const { return position_cycle_seq_; } + double realized_balance() const { return initial_capital_ + net_profit_sum_; } void on_native_bar(const Bar& bar, const NativeDecisionContext& context) override { ++callbacks; @@ -310,6 +327,25 @@ std::vector copy_commands(const FixtureHost& host) { return out; } +std::vector copy_accounts_after_applied( + const FixtureHost& host) { + std::vector out; + std::vector applied; + for (const auto& event : host.native_events(0)) { + if (event.command && as_event(*event.command)) + applied.push_back(event_ordinal(*event.command)); + if (event.kind == NativeEventKind::Account) { + CHECK(event.account.has_value()); + if (!event.account) continue; + CHECK(event.account->ordinal == event.ordinal); + CHECK(std::find(applied.begin(), applied.end(), event.ordinal) != applied.end()); + out.push_back(*event.account); + } + } + CHECK(out.size() == applied.size()); + return out; +} + int count_applied(const std::vector& events) { int n = 0; for (const auto& e : events) if (as_event(e)) ++n; @@ -429,9 +465,12 @@ std::string inputs_json(const std::vector& inputs) { return out.str(); } -std::string default_calendar_json() { - return "{\"timezone\":\"UTC\",\"session\":\"24x7\",\"inputTf\":\"1\"," - "\"scriptTf\":\"1\",\"chartTimezone\":\"\"}"; +std::string calendar_json(const NativeRunSpec& spec) { + return "{\"timezone\":" + json_escape(spec.timezone) + + ",\"session\":" + json_escape(spec.session) + + ",\"inputTf\":" + json_escape(spec.input_tf) + + ",\"scriptTf\":" + json_escape(spec.script_tf) + + ",\"chartTimezone\":" + json_escape(spec.chart_timezone) + "}"; } std::string config_json(const NativeRunSpec& spec) { @@ -442,7 +481,10 @@ std::string config_json(const NativeRunSpec& spec) { << ",\"priceTick\":" << json_num(spec.price_tick) << ",\"pointValue\":" << json_num(spec.point_value) << ",\"accountFx\":" << json_num(spec.account_fx) - << ",\"slippageTicks\":" << json_u64(spec.slippage_ticks) << "}"; + << ",\"slippageTicks\":" << json_u64(spec.slippage_ticks); + if (spec.initial_margin_fraction) + out << ",\"initialMarginFraction\":" << json_num(*spec.initial_margin_fraction); + out << "}"; return out.str(); } @@ -478,7 +520,7 @@ ScenarioArt make_art(const std::string& id, const NativeRunSpec& spec, s.status = (failures == fail_before) ? "passed" : "failed"; s.native_configuration = config_json(spec); s.run_identity = identity_json(spec); - s.calendar = default_calendar_json(); + s.calendar = calendar_json(spec); s.logical_inputs = inputs_json(inputs); s.lifecycle_events = lifecycle_json(events); s.physical_effects = physical_json(host, events); @@ -493,6 +535,8 @@ void require_applied_setup(FixtureHost& host, const NativeRunSpec& spec) { } void expect_completed(const FixtureHost& host) { + if (!host.last_error().empty()) + std::printf("native host error: %s\n", host.last_error().c_str()); CHECK(host.last_error().empty()); CHECK(host.native_state().kind == NativeLifecycleKind::Completed); } @@ -949,6 +993,110 @@ int main() { fed_three, kOpen104, before)); } + // F4 InitialMargin is checked before either leg of a crossing settles. + // Equality uses the same actual host path with unrelated source state poisoned. + for (bool equality : {false, true}) { + const int before = failures; + FixtureHost host; + const char* id = equality ? "R3-native-InitialMargin-equality-source-inert" + : "R3-native-InitialMargin-reject-crossing"; + auto spec = spec_for(id, 1); + spec.initial_capital = equality ? 222.0 : 210.0; + spec.initial_margin_fraction = 1.0; + spec.chart_timezone = "America/New_York"; + pineforge::PyramidEntry old_lot; + int64_t old_cycle = 0; + double old_balance = 0; + bool captured = false; + auto poisoned = host.source_observation(); + host.script = [&](FixtureHost& h, const Bar&, const NativeDecisionContext&) { + if (h.callbacks == 1) + CHECK(h.submit_market(tx(-1.0, "short")).status == SubmitStatus::Accepted); + if (h.callbacks == 2) { + CHECK(h.physical_lots().size() == 1); + if (h.physical_lots().size() == 1) old_lot = h.physical_lots().front(); + old_cycle = h.position_cycle(); + old_balance = h.realized_balance(); + captured = true; + if (equality) { + h.poison_source_observation(); + poisoned = h.source_observation(); + } + CHECK(h.submit_market(tx(3.0, "cross")).status == SubmitStatus::Accepted); + } + }; + require_applied_setup(host, spec); + host.run(three_cross, 3); + expect_completed(host); + CHECK(captured); + const auto events = copy_commands(host); + const auto accounts = copy_accounts_after_applied(host); + const auto position = host.physical_position(); + CHECK(position.lot_count == 1); + CHECK(host.physical_lots().size() == 1); + if (equality) { + CHECK(count_kind(events, "MatchRejected") == 0); + CHECK(count_applied(events) == 2); + CHECK(accounts.size() == 2); + CHECK(host.trade_count() == 1); + near(position.signed_units, 2.0); + near(position.average_price, 104.0); + CHECK(host.position_cycle() == old_cycle + 1); + near(host.realized_balance(), 212.0); + near(host.native_marked_equity(104.0), 208.0); + near(sum_tickets(events), 12.0); + CHECK(host.source_observation() == poisoned); + if (host.physical_lots().size() == 1) + near(host.physical_lots().front().entry_commission_account, 4.0); + if (host.trade_count() == 1) { + const auto& trade = host.get_trade(0); + near(trade.qty, 1.0); near(trade.entry_price, 102.0); + near(trade.exit_price, 104.0); near(trade.commission, 8.0); + near(trade.pnl, -10.0); + } + if (accounts.size() == 2) { + near(accounts[0].realized_balance, 222.0); + near(accounts[0].marked_equity, 216.0); + near(accounts[1].realized_balance, 212.0); + near(accounts[1].marked_equity, 208.0); + near(accounts[1].signed_units, 2.0); + CHECK(accounts[1].effective_time_ms == kT2); + } + } else { + CHECK(count_applied(events) == 1); + CHECK(count_kind(events, "MatchRejected") == 1); + bool initial_margin = false; + for (const auto& event : events) + if (const auto* rejected = as_event(event)) + initial_margin = rejected->reason == MatchRejectReason::InitialMargin; + CHECK(initial_margin); + CHECK(accounts.size() == 1); + CHECK(host.trade_count() == 0); + near(position.signed_units, -1.0); + near(position.average_price, 102.0); + CHECK(host.position_cycle() == old_cycle); + CHECK(host.realized_balance() == old_balance); + near(host.realized_balance(), 210.0); + near(host.native_marked_equity(104.0), 202.0); + near(sum_tickets(events), 6.0); + if (host.physical_lots().size() == 1) { + const auto& kept = host.physical_lots().front(); + CHECK(kept.qty == old_lot.qty && kept.price == old_lot.price); + CHECK(kept.entry_incarnation == old_lot.entry_incarnation); + CHECK(kept.time == old_lot.time); + CHECK(kept.entry_commission_account == old_lot.entry_commission_account); + near(kept.entry_commission_account, 6.0); + } + if (accounts.size() == 1) { + near(accounts[0].realized_balance, 210.0); + near(accounts[0].marked_equity, 204.0); + near(accounts[0].signed_units, -1.0); + CHECK(accounts[0].effective_time_ms == kT1); + } + } + arts.push_back(make_art(id, spec, host, fed_three, kOpen104, before)); + } + // F4: adding a same-side lot would exceed the resulting-book lot limit. { const int before = failures; diff --git a/tests/test_native_settlement_callers.cpp b/tests/test_native_settlement_callers.cpp index 923bab0a0..f288e8e17 100644 --- a/tests/test_native_settlement_callers.cpp +++ b/tests/test_native_settlement_callers.cpp @@ -1,10 +1,12 @@ // R3 live adapter helpers: actual source selection/sizing followed by the one book owner. // Private member access uses the same explicit-instantiation pattern as R3a tests. -#include +#include +#include #include #include #include #include +#include #include #include #include @@ -68,9 +70,6 @@ struct Book final:BacktestEngine{ (this->*access(Raw{}))(o,p,trail,closed_bar,closed_inc,closed_long);} void exit(PendingOrder& o,double p){int closed_bar=-1;uint64_t closed_inc=0;bool closed_long=false; (this->*access(Exit{}))(o,p,closed_bar,closed_inc,closed_long);} - void range(){record_equity_point(current_bar_.timestamp);(this->*access(Range{}))();} - void freeze(){freeze_script_position_view();} void unfreeze(){clear_script_position_view();} - double source_position()const{return signed_position_size();} double size50(double p)const{return calc_qty_for_type(p,50,static_cast(QtyType::PERCENT_OF_EQUITY));} x::AccountEffectProjection flatten_quote(double p)const{return project_native_settlement_v1(x::Flatten{},x::Fill{p,"quote","",90,{}});} double physical()const{return position_side_==PositionSide::SHORT?-position_qty_:position_qty_;} @@ -228,15 +227,99 @@ void direction_blocked_one_slip(double sign){ CHECK(b.rows()[0].exit_price==100-sign*.5);near(b.rows()[0].pnl,-6.5); } +// One authoritative freeze proof. The native observer is the real consumer +// bound by NativeStrategyHost; direct settlement keeps the chart index stable. +struct ObservationHost final : NativeStrategyHost { + ObservationHost() { + initial_capital_=1000; + commission_type_=CommissionType::CASH_PER_ORDER; + commission_value_=6; + syminfo_.pointvalue=1;account_currency_fx_=1; + syminfo_.mintick=.25;syminfo_mintick_=.25;slippage_=2; + stream_observe_actions_=true; + bar_index_=3; + current_bar_={100,120,80,100,1,1736121780000LL}; + } + void on_native_bar(const Bar&,const NativeDecisionContext&) override {} + void open(double quantity,uint64_t incarnation,double paid) { + const auto result=settle_native_execution_at(order_action::Transact{quantity}, + x::Fill{100,"seed","",incarnation,paid}, + {current_bar_.timestamp,bar_index_,{}, {}}); + REQUIRE(result.status==x::Status::Applied); + } + void source_sentinels() { + intraday_pnl_=17.25;cons_loss_day_count_=7;last_loss_day_=104;intraday_pnl_day_=42; + } + void freeze(){freeze_script_position_view();} + void unfreeze(){clear_script_position_view();} + double source_position()const{return signed_position_size();} + int chart_index()const{return bar_index_;} + x::Result reduce() { + current_bar_.close=110; // Same source interval: the freeze remains active. + return settle_native_execution_at(order_action::Reduce{1},x::Fill{110,"reduce","",99,{}}, + {current_bar_.timestamp,bar_index_,{}, {}}); + } + void range(){record_equity_point(current_bar_.timestamp);(this->*access(Range{}))();} + static uint64_t raw(double value){uint64_t out;std::memcpy(&out,&value,sizeof out);return out;} + auto source_snapshot()const { + return std::make_tuple(raw(intraday_pnl_),cons_loss_day_count_,last_loss_day_,intraday_pnl_day_); + } + std::vector financial_snapshot()const { + return {raw(net_profit_sum_),raw(net_profit_roundoff_value_),raw(net_profit_roundoff_bound_), + raw(gross_profit_sum_),raw(gross_loss_sum_),static_cast(win_trades_count_), + static_cast(loss_trades_count_),static_cast(eventrades_count_), + static_cast(position_cycle_seq_),static_cast(next_position_cycle_seq_), + stream_action_sequence_,static_cast(trades_.size()), + static_cast(stream_order_actions_.size())}; + } + size_t account_count()const { + const auto events=native_events(0); + return static_cast(std::count_if(events.begin(),events.end(),[](const auto& event){ + return event.kind==NativeEventKind::Account; + })); + } + const auto& lots()const{return pyramid_entries_;} + const auto& rows()const{return trades_;} +}; + void nonphysical_observations(){ - scenario="range-end and frozen source view do not settle exposure";Book b;b.open(1,100,11,"A",2);b.open(3,100,22,"B",6); - const auto lots=b.lots();const auto rows=b.rows().size(),actions=b.actions().size();const auto cycle=b.cycle(),next=b.next_cycle(); - const double balance=b.balance();b.freeze();CHECK(b.source_position()==4);CHECK(b.physical()==4);b.unfreeze(); - b.bar(4,110);b.range();CHECK(b.rows().size()==rows&&b.actions().size()==actions); - CHECK(b.report_trade_count()==2&&b.get_report_trade(0).open_at_end&&b.get_report_trade(1).open_at_end); - CHECK(b.cycle()==cycle&&b.next_cycle()==next&&b.balance()==balance&&b.lots().size()==lots.size()); - for(size_t i=0;i(static_cast(&b)); + b.freeze();CHECK(b.source_position()==4);CHECK(strategy_position_size(handle)==4); + CHECK(b.physical_position().signed_units==4&&b.physical_position().lot_count==2); + const int frozen_index=b.chart_index(); + const auto reduced=b.reduce();REQUIRE(reduced.status==x::Status::Applied); + CHECK(b.chart_index()==frozen_index); + REQUIRE(b.rows().size()==1&&b.lots().size()==1); + near(b.rows()[0].pnl,2);CHECK(b.rows()[0].exit_price==110);near(b.rows()[0].commission,8); + const auto physical=b.physical_position(); + CHECK(physical.signed_units==3&&physical.lot_count==1);near(physical.average_price,100); + CHECK(b.source_position()==4&&strategy_position_size(handle)==4); + near(strategy_current_equity(handle),1002);near(b.native_marked_equity(110),1026); + CHECK(b.source_snapshot()==source); + + const auto lot=b.lots().front();const auto trade=b.rows().front(); + const auto financial=b.financial_snapshot();const auto range_source=b.source_snapshot(); + const auto accounts=b.account_count(); + b.range(); + CHECK(b.chart_index()==frozen_index); + CHECK(b.financial_snapshot()==financial&&b.source_snapshot()==range_source); + CHECK(b.account_count()==accounts); + REQUIRE(b.rows().size()==1&&b.lots().size()==1); + CHECK(b.rows()[0].qty==trade.qty&&b.rows()[0].pnl==trade.pnl&&b.rows()[0].commission==trade.commission); + CHECK(b.rows()[0].entry_time==trade.entry_time&&b.rows()[0].exit_time==trade.exit_time); + const auto& kept=b.lots().front(); + CHECK(kept.qty==lot.qty&&kept.price==lot.price&&kept.time==lot.time); + CHECK(kept.entry_incarnation==lot.entry_incarnation&&kept.entry_commission_account==lot.entry_commission_account); + CHECK(b.report_trade_count()==2); + CHECK(!b.get_report_trade(0).open_at_end&&b.get_report_trade(1).open_at_end); + CHECK(b.get_report_trade(1).qty==3&&b.get_report_trade(1).exit_price==110); // No source slippage. + CHECK(b.physical_position().signed_units==3&&b.physical_position().lot_count==1); + CHECK(b.source_position()==4&&strategy_position_size(handle)==4); + near(strategy_current_equity(handle),1002);near(b.native_marked_equity(110),1026); + b.unfreeze();CHECK(b.source_position()==3&&strategy_position_size(handle)==3); } templatevoid run(F fn){try{fn();}catch(const Abort&){}catch(const std::exception& e){++failures;std::printf("FAIL %s exception %s\n",scenario,e.what());}} } diff --git a/tests/test_settlement_observation_boundary.cpp b/tests/test_settlement_observation_boundary.cpp new file mode 100644 index 000000000..af5523e9d --- /dev/null +++ b/tests/test_settlement_observation_boundary.cpp @@ -0,0 +1,480 @@ +// Actual native/source settlement boundaries. No run(), tape or copied observer. +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace x = pineforge::execution; +namespace { +int checks = 0, failures = 0; +const char* scenario = "setup"; +struct Abort {}; +#define CHECK(value) do { ++checks; if (!(value)) { ++failures; \ + std::printf("FAIL %s:%d %s\n", scenario, __LINE__, #value); } } while (0) +#define REQUIRE(value) do { ++checks; if (!(value)) { ++failures; \ + std::printf("FAIL %s:%d %s\n", scenario, __LINE__, #value); throw Abort{}; } } while (0) + +enum class Seam { NativeBook, NativeOpening, NativeSelected, NativeReverse, Context, + SourceBook, SourceSelected, SourceReverse }; +constexpr Seam natives[] = {Seam::NativeBook, Seam::NativeOpening, Seam::NativeSelected, + Seam::NativeReverse, Seam::Context}; +constexpr Seam sources[] = {Seam::SourceBook, Seam::SourceSelected, Seam::SourceReverse}; +bool reversal(Seam s) { return s == Seam::NativeReverse || s == Seam::SourceReverse; } +bool selected(Seam s) { return s == Seam::NativeSelected || s == Seam::SourceSelected; } +bool source(Seam s) { return s == Seam::SourceBook || s == Seam::SourceSelected || s == Seam::SourceReverse; } +Seam native_peer(Seam s) { + return s == Seam::SourceReverse ? Seam::NativeReverse + : s == Seam::SourceSelected ? Seam::NativeSelected : Seam::NativeBook; +} +uint64_t bits(double v) { uint64_t u; std::memcpy(&u, &v, sizeof(u)); return u; } +void exact(double actual, double expected) { + if (bits(actual) != bits(expected)) + std::printf(" actual=%.17g expected=%.17g\n", actual, expected); + CHECK(bits(actual) == bits(expected)); +} +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); +} +constexpr double nan = std::numeric_limits::quiet_NaN(); +constexpr double huge = std::numeric_limits::max(); +constexpr int maximum = std::numeric_limits::max(); +constexpr int64_t chart_time = 1743436800000LL; // 2025-04-01 00:00 Taipei, day104 +x::Fill fill(double price = 100, double fee = 0) { return {price, "effect", "literal", 90, fee}; } + +struct Book final : BacktestEngine { + std::vector members{11}; + double target = -.1; + x::PhysicalExecutionContext native_context{999000, 9, {}, {}}; + Book() { + initial_capital_ = 1000; + commission_type_ = CommissionType::CASH_PER_ORDER; + commission_value_ = 0; + syminfo_.pointvalue = 1; + account_currency_fx_ = 1; + stream_observe_actions_ = true; + clock(chart_time, "Asia/Taipei"); + bar_index_ = 7; + } + void on_bar(const Bar&) override {} + void clock(int64_t time, const char* timezone) { + current_bar_ = {100, 100, 100, 100, 1, time}; + set_chart_timezone(timezone); + } + void open(double price = 100, uint64_t id = 11, double quantity = 1, double paid = 0) { + REQUIRE(settle_native_execution_at(order_action::Transact{quantity}, + x::Fill{price, "seed", "historical", id, paid}, native_context).status == x::Status::Applied); + } + x::Result effect(Seam s, const x::Action& a, const x::Fill& f, + const x::LifecycleEffects& lifecycle = {}) { + switch (s) { + case Seam::NativeBook: return settle_native_execution_at(a, f, native_context); + case Seam::NativeOpening: return settle_native_execution_scoped_at(a, f, native_context, x::OpeningExposure{11, cycle()}); + case Seam::NativeSelected: return settle_native_execution_selected_at(a, f, native_context, x::SelectedOpeningSet{cycle(), members}); + case Seam::NativeReverse: return settle_native_reversal_at_v1(x::ReverseTo{target}, f, native_context); + case Seam::Context: return settle_with_context(a, f, lifecycle, native_context); + case Seam::SourceBook: return settle_execution_with_lifecycle(a, f, lifecycle); + case Seam::SourceSelected: return settle_execution_selected_with_lifecycle(a, f, lifecycle, x::SelectedOpeningSet{cycle(), members}); + case Seam::SourceReverse: return settle_reversal_with_lifecycle_v1(x::ReverseTo{target}, f, lifecycle); + } + throw std::logic_error("unreachable seam"); + } + x::SettlementInspection inspect(Seam s, const x::Action& a, const x::Fill& f) const { + if (reversal(s)) return inspect_native_reversal_v1(x::ReverseTo{target}, f); + if (selected(s)) return inspect_native_settlement_selected(a, f, x::SelectedOpeningSet{cycle(), members}); + if (s == Seam::NativeOpening) return inspect_native_settlement_scoped(a, f, x::OpeningExposure{11, cycle()}); + return inspect_native_settlement(a, f); + } + x::AccountEffectProjection project(Seam s, const x::Action& a, const x::Fill& f) const { + if (reversal(s)) return project_native_reversal_v1(x::ReverseTo{target}, f); + if (selected(s)) return project_native_settlement_selected_v1(a, f, x::SelectedOpeningSet{cycle(), members}); + if (s == Seam::NativeOpening) return project_native_settlement_scoped_v1(a, f, x::OpeningExposure{11, cycle()}); + return project_native_settlement_v1(a, f); + } + void fields(int days, int last, double pnl, int unused = 42) { + cons_loss_day_count_ = days; last_loss_day_ = last; intraday_pnl_ = pnl; intraday_pnl_day_ = unused; + } + int days() const { return cons_loss_day_count_; } + int last_day() const { return last_loss_day_; } + double intraday() const { return intraday_pnl_; } + int unused_day() const { return intraday_pnl_day_; } + int64_t cycle() const { return position_cycle_seq_; } + int64_t next_cycle() const { return next_position_cycle_seq_; } + void exhaust_cycle() { next_position_cycle_seq_ = std::numeric_limits::max(); } + void exhaust_stream() { stream_action_sequence_ = UINT64_MAX; } + void financial_counter(int kind, int remaining = 0) { + if (kind == 0) win_trades_count_ = maximum - remaining; + if (kind == 1) loss_trades_count_ = maximum - remaining; + if (kind == 2) eventrades_count_ = maximum - remaining; + } + void net(double value) { net_profit_sum_ = value; } + double net() const { return net_profit_sum_; } + double marked(double price) const { return marked_equity(price); } + double gross_profit() const { return gross_profit_sum_; } + double gross_loss() const { return gross_loss_sum_; } + int wins() const { return win_trades_count_; } + int losses() const { return loss_trades_count_; } + int evens() const { return eventrades_count_; } + const auto& rows() const { return trades_; } + const auto& lots() const { return pyramid_entries_; } + size_t actions() const { return stream_order_actions_.size(); } + uint64_t stream_sequence() const { return stream_action_sequence_; } + uint64_t lifecycle_sequence() const { return exit_leg_event_seq_; } + uint64_t next_order() const { return next_order_incarnation_; } + void pending_exit() { + PendingOrder order; + order.id = "pending"; order.type = OrderType::EXIT; order.incarnation = 700; order.created_seq = 700; + order.legs.attach(order.incarnation, position_cycle_seq_); + pending_orders_.push_back(std::move(order)); + } + size_t pending() const { return pending_orders_.size(); } + const PendingOrder* pending_data() const { return pending_orders_.data(); } +}; +struct Fields { + int days, last, unused; uint64_t intraday; + explicit Fields(const Book& b) : days(b.days()), last(b.last_day()), unused(b.unused_day()), intraday(bits(b.intraday())) {} + void unchanged(const Book& b) const { + CHECK(b.days() == days && b.last_day() == last && b.unused_day() == unused); + CHECK(bits(b.intraday()) == intraday); + } +}; +struct Snapshot { + Fields fields; + uint64_t broker, stream, order, stream_sequence, lifecycle; + int64_t cycle, next_cycle; + size_t rows, lots, actions, pending; + const PendingOrder* pending_data; + double net, profit, loss; + int wins, losses, evens; + explicit Snapshot(const Book& b) : fields(b), broker(b.broker_state_hash()), stream(b.stream_state_hash()), + order(b.next_order()), stream_sequence(b.stream_sequence()), lifecycle(b.lifecycle_sequence()), + cycle(b.cycle()), next_cycle(b.next_cycle()), rows(b.rows().size()), lots(b.lots().size()), + actions(b.actions()), pending(b.pending()), pending_data(b.pending_data()), net(b.net()), + profit(b.gross_profit()), loss(b.gross_loss()), wins(b.wins()), losses(b.losses()), evens(b.evens()) {} + void unchanged(const Book& b) const { + fields.unchanged(b); + CHECK(b.broker_state_hash() == broker && b.stream_state_hash() == stream); + CHECK(b.next_order() == order && b.stream_sequence() == stream_sequence && b.lifecycle_sequence() == lifecycle); + CHECK(b.cycle() == cycle && b.next_cycle() == next_cycle); + CHECK(b.rows().size() == rows && b.lots().size() == lots && b.actions() == actions); + CHECK(b.pending() == pending && b.pending_data() == pending_data); + CHECK(b.wins() == wins && b.losses() == losses && b.evens() == evens); + exact(b.net(), net); exact(b.gross_profit(), profit); exact(b.gross_loss(), loss); + } +}; +void seed(Book& b, Seam s, double price = 100) { + b.open(price); + if (selected(s) || s == Seam::NativeOpening) b.open(price, 99); +} +void quote_is_read_only(Book& b, Seam s, const x::Action& a, const x::Fill& f, + x::Status expected = x::Status::Applied) { + const Snapshot before(b); + const auto inspect = b.inspect(s, a, f); + const auto project = b.project(s, a, f); + CHECK(inspect.status == expected && project.status == expected); + before.unchanged(b); + if (expected == x::Status::Applied && reversal(s)) { + exact(inspect.opened_units, b.target); exact(project.opened_units, b.target); + exact(project.signed_units_after, b.target); + CHECK(project.cycle_after == b.next_cycle()); + } +} +void status_atomic(Book& b, Seam s, const x::Action& a, const x::Fill& f, x::Status status, + const x::LifecycleEffects& lifecycle = {}) { + const Snapshot before(b); + const auto r = b.effect(s, a, f, lifecycle); + CHECK(r.status == status); + CHECK(r.closed_units == 0 && r.opened_units == 0 && r.current_ticket == 0); + CHECK(r.first_trade_index == 0 && r.closed_trade_count == 0 && r.opened_lot_incarnation == 0); + before.unchanged(b); +} +void overflow_atomic(Book& b, Seam s, const x::Fill& f, const char* message) { + const Snapshot before(b); bool threw = false; + try { (void)b.effect(s, x::Flatten{}, f); } + catch (const std::overflow_error& e) { threw = true; CHECK(std::string(e.what()) == message); } + CHECK(threw); before.unchanged(b); +} + +void native_inert(Seam s, bool default_context, bool nan_intraday) { + scenario = "all native/context routes ignore exhausted or poisoned source state"; + Book b; seed(b, s); + if (default_context) b.native_context = {}; + b.fields(nan_intraday ? 2 : maximum, -1, nan_intraday ? nan : 17.25); + const Fields fields(b); const auto next = b.next_cycle(); + quote_is_read_only(b, s, x::Flatten{}, fill(90)); + const auto r = b.effect(s, x::Flatten{}, fill(90)); + REQUIRE(r.status == x::Status::Applied && r.closed_trade_count == 1); + fields.unchanged(b); exact(r.closed_units, 1); exact(b.rows().back().pnl, -10); + CHECK(b.rows().back().exit_time == b.native_context.effective_time_ms); + CHECK(b.rows().back().exit_bar_index == b.native_context.interval_index); + if (reversal(s)) { REQUIRE(b.lots().size() == 1); exact(b.lots()[0].qty, .1); + CHECK(b.cycle() == next && b.next_cycle() == next + 1); } + else if (selected(s) || s == Seam::NativeOpening) { + REQUIRE(b.lots().size() == 1); CHECK(b.lots()[0].entry_incarnation == 99); exact(b.lots()[0].qty, 1); + } else CHECK(b.lots().empty()); +} + +void giant_intraday(Seam s) { + scenario = "finite source intraday overflow is not native cash overflow"; + Book b; seed(b, s, 0); b.fields(2, -1, huge); + quote_is_read_only(b, s, x::Flatten{}, fill(huge / 2)); + if (source(s)) status_atomic(b, s, x::Flatten{}, fill(huge / 2), x::Status::InvalidAccounting); + else { + const Fields fields(b); const auto r = b.effect(s, x::Flatten{}, fill(huge / 2)); + REQUIRE(r.status == x::Status::Applied); fields.unchanged(b); + exact(b.rows()[0].pnl, huge / 2); CHECK(std::isfinite(b.net())); + } +} + +void opening_and_noeffect() { + scenario = "Ready opening-only separates source finite-state preflight from native"; + for (Seam s : {Seam::NativeBook, Seam::Context, Seam::SourceBook}) { + Book b; b.fields(maximum, -1, nan); + const Fields fields(b); const auto f = fill(100, 6); + quote_is_read_only(b, s, order_action::Transact{1}, f); + if (source(s)) status_atomic(b, s, order_action::Transact{1}, f, x::Status::InvalidAccounting); + else { + const auto r = b.effect(s, order_action::Transact{1}, f); + REQUIRE(r.status == x::Status::Applied && b.lots().size() == 1 && b.rows().empty()); + fields.unchanged(b); exact(b.lots()[0].entry_commission_account, 6); exact(b.marked(100), 994); + } + } + Book finite; finite.fields(maximum, -1, huge); const Fields fields(finite); + REQUIRE(finite.effect(Seam::SourceBook, order_action::Transact{1}, fill()).status == x::Status::Applied); + fields.unchanged(finite); CHECK(finite.rows().empty()); + scenario = "NoEffect skips source preflight observer and late stream checks"; + for (Seam s : {Seam::NativeBook, Seam::NativeOpening, Seam::NativeSelected, Seam::Context, + Seam::SourceBook, Seam::SourceSelected}) { + Book b; seed(b, s); b.fields(maximum, -1, nan); b.exhaust_stream(); + quote_is_read_only(b, s, order_action::Reduce{0}, fill(), x::Status::NoEffect); + status_atomic(b, s, order_action::Reduce{0}, fill(), x::Status::NoEffect); + } + Book flat; flat.fields(maximum, -1, nan); flat.exhaust_stream(); + status_atomic(flat, Seam::SourceBook, x::Flatten{}, fill(), x::Status::NoEffect); +} + +void source_failures(Seam s) { + scenario = "all three source wrappers retain pre-mutation source refusal"; + Book day; seed(day, s); day.pending_exit(); day.fields(maximum, -1, 17.25); + quote_is_read_only(day, s, x::Flatten{}, fill(90)); + overflow_atomic(day, s, fill(90), "closed trade counter exhausted"); + Book poison; seed(poison, s); poison.fields(maximum, -1, nan); + status_atomic(poison, s, x::Flatten{}, fill(90), x::Status::InvalidAccounting); + giant_intraday(s); + scenario = "full intraday pass wins over first-row day overflow"; + Book two; two.open(1, 11); two.open(-huge / 2, 12); two.members = {11, 12}; + if (selected(s)) two.open(100, 99); + two.fields(maximum, -1, huge); + quote_is_read_only(two, s, x::Flatten{}, fill(0)); + status_atomic(two, s, x::Flatten{}, fill(0), x::Status::InvalidAccounting); + scenario = "source intraday InvalidAccounting precedes financial counter exception"; + Book cash; seed(cash, s); cash.fields(2, -1, nan); cash.financial_counter(0); + status_atomic(cash, s, x::Flatten{}, fill(110), x::Status::InvalidAccounting); + Book native_cash; seed(native_cash, native_peer(s)); native_cash.fields(2, -1, nan); native_cash.financial_counter(0); + quote_is_read_only(native_cash, native_peer(s), x::Flatten{}, fill(110)); + overflow_atomic(native_cash, native_peer(s), fill(110), "closed trade counter exhausted"); + scenario = "source day overflow precedes stream while native reaches stream"; + Book late; seed(late, s); late.fields(maximum, -1, 17.25); late.exhaust_stream(); + overflow_atomic(late, s, fill(90), "closed trade counter exhausted"); + Book native_late; seed(native_late, native_peer(s)); native_late.fields(maximum, -1, 17.25); native_late.exhaust_stream(); + overflow_atomic(native_late, native_peer(s), fill(90), "stream action sequence overflow"); + scenario = "stage invalid lifecycle precedes source poison"; + Book invalid; seed(invalid, s); invalid.fields(maximum, -1, nan); + x::LifecycleEffects lifecycle; lifecycle.removals.push_back({999, 999, {}, 0}); + status_atomic(invalid, s, x::Flatten{}, fill(), x::Status::InvalidLifecycle, lifecycle); +} + +void cycle_and_generic_cash_precedence() { + scenario = "cycle exhaustion stays before source intraday finite validation"; + for (Seam s : {Seam::NativeReverse, Seam::SourceReverse}) { + Book b; seed(b, s); b.fields(maximum, -1, nan); b.exhaust_cycle(); + overflow_atomic(b, s, fill(), "position cycle sequence exhausted"); + } + scenario = "generic cash overflow stays before source day counter"; + for (Seam s : {Seam::NativeBook, Seam::SourceBook}) { + Book b; b.open(huge / 2); b.net(-huge); b.fields(maximum, -1, 0); + status_atomic(b, s, x::Flatten{}, fill(0), x::Status::InvalidAccounting); + } +} + +void source_row_order(Seam s) { + scenario = "same-day source loss cannot overflow the existing day count"; + Book same; seed(same, s); same.fields(maximum, 104, 17.25); + REQUIRE(same.effect(s, x::Flatten{}, fill(90)).status == x::Status::Applied); + CHECK(same.days() == maximum && same.last_day() == 104); exact(same.intraday(), 7.25); + scenario = "source committed loss zero win rows keep exact observation order"; + Book mixed; mixed.open(110, 11); mixed.open(100, 12); mixed.open(90, 13); mixed.members = {11, 12, 13}; + if (selected(s)) mixed.open(100, 99); + mixed.fields(4, -1, 17.25); + const auto r = mixed.effect(s, x::Flatten{}, fill()); + REQUIRE(r.status == x::Status::Applied && r.closed_trade_count == 3); + CHECK(mixed.days() == 0 && mixed.last_day() == 104 && mixed.unused_day() == 42); + exact(mixed.intraday(), 17.25); exact(mixed.net(), 0); + CHECK(mixed.wins() == 1 && mixed.losses() == 1 && mixed.evens() == 1); + for (const auto& row : mixed.rows()) { + CHECK(row.exit_time == chart_time && row.exit_bar_index == 7); + CHECK(row.exit_id == "effect" && row.exit_comment == "literal"); + } + if (reversal(s)) { + REQUIRE(mixed.lots().size() == 1); exact(mixed.lots()[0].qty, .1); + CHECK(mixed.lots()[0].time == chart_time && mixed.lots()[0].entry_bar_index == 7); + } + scenario = "win preserves last-loss-day through a second same-day loss"; + Book quirk; quirk.open(110, 11); quirk.open(90, 12); quirk.open(110, 13); quirk.members = {11, 12, 13}; + if (selected(s)) quirk.open(100, 99); + quirk.fields(4, -1, 17.25); + REQUIRE(quirk.effect(s, x::Flatten{}, fill()).status == x::Status::Applied); + CHECK(quirk.days() == 0 && quirk.last_day() == 104); exact(quirk.intraday(), 7.25); + scenario = "winning first row frees exhausted source day count before loss"; + Book recovery; recovery.open(90, 11); recovery.open(110, 12); recovery.members = {11, 12}; + if (selected(s)) recovery.open(100, 99); + recovery.fields(maximum, -1, 17.25); + REQUIRE(recovery.effect(s, x::Flatten{}, fill()).status == x::Status::Applied); + CHECK(recovery.days() == 1 && recovery.last_day() == 104); exact(recovery.intraday(), 17.25); + scenario = "later winning row cannot erase earlier source preflight overflow"; + Book refusal; refusal.open(110, 11); refusal.open(90, 12); refusal.members = {11, 12}; + if (selected(s)) refusal.open(100, 99); + refusal.fields(maximum, -1, 17.25); + overflow_atomic(refusal, s, fill(), "closed trade counter exhausted"); + scenario = "zero PnL source row changes neither day identity nor intraday value"; + Book zero; seed(zero, s); zero.fields(4, 3103, 17.25); const Fields fields(zero); + REQUIRE(zero.effect(s, x::Flatten{}, fill()).status == x::Status::Applied); + fields.unchanged(zero); CHECK(zero.evens() == 1); +} + +void committed_slice(Seam s) { + scenario = "source observer consumes only newly committed rows once"; + Book b; b.open(100, 1); + REQUIRE(b.effect(Seam::NativeBook, x::Flatten{}, fill(107)).status == x::Status::Applied); + b.open(101, 11); b.open(102, 12); b.members = {11, 12}; + if (selected(s)) b.open(100, 99); + b.fields(1, 3103, 100); const auto actions = b.actions(); + const auto r = b.effect(s, x::Flatten{}, fill()); + REQUIRE(r.status == x::Status::Applied && r.first_trade_index == 1 && r.closed_trade_count == 2); + CHECK(b.rows().size() == 3 && b.actions() == actions + 2 + (reversal(s) ? 1 : 0)); + exact(b.rows()[1].pnl, -1); exact(b.rows()[2].pnl, -2); exact(b.net(), 4); + exact(b.intraday(), 97); CHECK(b.days() == 2 && b.last_day() == 104); +} + +void generic_counter_guards() { + scenario = "financial win loss even counters remain generic and projections ignore commit capacity"; + for (Seam s : {Seam::NativeBook, Seam::NativeReverse, Seam::SourceBook, Seam::SourceReverse}) { + for (int kind : {0, 1, 2}) { + Book b; seed(b, s); b.fields(2, 104, source(s) ? 17.25 : nan); b.financial_counter(kind); + const auto f = fill(kind == 0 ? 110 : kind == 1 ? 90 : 100); + quote_is_read_only(b, s, x::Flatten{}, f); + overflow_atomic(b, s, f, "closed trade counter exhausted"); + } + Book batch; batch.open(100, 11); batch.open(100, 12); batch.financial_counter(0, 1); + overflow_atomic(batch, s, fill(110), "closed trade counter exhausted"); + } +} + +void invalid_projections_and_source_stage() { + scenario = "invalid native projections remain zeroed and never trigger source validation"; + for (Seam s : {Seam::NativeBook, Seam::NativeOpening, Seam::NativeSelected, Seam::NativeReverse, + Seam::Context, Seam::SourceBook, Seam::SourceSelected, Seam::SourceReverse}) { + Book b; seed(b, s); b.fields(maximum, -1, nan); const Snapshot before(b); + const auto p = b.project(s, x::Flatten{}, fill(nan)); + CHECK(p.status == x::Status::InvalidPrice); + CHECK(p.closed_units == 0 && p.opened_units == 0 && p.current_ticket == 0 && p.resulting_lot_count == 0); + CHECK(p.realized_balance == 0 && p.remaining_entry_cost == 0 && p.marked_equity == 0 && p.cycle_after == 0); + before.unchanged(b); + status_atomic(b, s, x::Flatten{}, fill(nan), x::Status::InvalidPrice); + } + for (Seam s : {Seam::NativeReverse, Seam::SourceReverse}) { + Book flat; flat.fields(maximum, -1, nan); + status_atomic(flat, s, x::Flatten{}, fill(), x::Status::InvalidCloseTarget); + Book zero; seed(zero, s); zero.fields(maximum, -1, nan); zero.target = 0; + status_atomic(zero, s, x::Flatten{}, fill(), x::Status::InvalidQuantity); + } + Book missing; seed(missing, Seam::SourceSelected); missing.fields(maximum, -1, nan); missing.members = {777}; + status_atomic(missing, Seam::SourceSelected, x::Flatten{}, fill(), x::Status::InvalidCloseTarget); +} + +void one_fee_one_owner() { + scenario = "native and source selected close share one fee and one financial commit"; + Book native, legacy; + for (Book* b : {&native, &legacy}) { + b->open(100, 11, 1, 2); b->open(100, 12, 3, 6); b->open(100, 99, 1, 17); + b->members = {11, 12}; b->fields(4, -1, 17.25); + } + const Fields fields(native); + const auto n = native.effect(Seam::NativeSelected, x::Flatten{}, fill(110, 6)); + const auto l = legacy.effect(Seam::SourceSelected, x::Flatten{}, fill(110, 6)); + REQUIRE(n.status == x::Status::Applied && l.status == x::Status::Applied); + REQUIRE(native.rows().size() == 2 && legacy.rows().size() == 2); + exact(n.current_ticket, 6); exact(l.current_ticket, 6); exact(native.net(), 26); exact(legacy.net(), 26); + for (size_t i = 0; i < 2; ++i) { + exact(native.rows()[i].qty, legacy.rows()[i].qty); + exact(native.rows()[i].pnl, legacy.rows()[i].pnl); + exact(native.rows()[i].commission, legacy.rows()[i].commission); + } + near(legacy.rows()[0].commission + legacy.rows()[1].commission, 14); + REQUIRE(native.lots().size() == 1 && legacy.lots().size() == 1); + CHECK(native.lots()[0].entry_incarnation == 99 && legacy.lots()[0].entry_incarnation == 99); + exact(native.lots()[0].entry_commission_account, 17); exact(legacy.lots()[0].entry_commission_account, 17); + fields.unchanged(native); exact(legacy.intraday(), 43.25); CHECK(legacy.days() == 0 && legacy.last_day() == -1); +} + +void source_hash_domains() { + scenario = "native isolation retains source trio and unused-day fingerprint domains"; + Book b; b.fields(2, 3103, 17.25); const auto original = b.broker_state_hash(); + for (int field : {0, 1, 2, 3}) { + b.fields(field == 0 ? 3 : 2, field == 1 ? 104 : 3103, field == 2 ? 18.25 : 17.25, field == 3 ? 43 : 42); + CHECK(b.broker_state_hash() != original); + b.fields(2, 3103, 17.25); CHECK(b.broker_state_hash() == original); + } +} + +struct DayPoint { int64_t utc_ms; int key; int count; }; +void calendar_pair(const char* zone, const std::vector& points) { + scenario = "production source chart day and DST rules remain separate from native closes"; + Book legacy, native; legacy.fields(0, -1, 0); native.fields(8, 777, 23.5); const Fields fields(native); + size_t index = 0; + for (const auto& point : points) { + legacy.clock(point.utc_ms, zone); native.clock(point.utc_ms, zone); + native.native_context = {point.utc_ms + 123, 9, {}, {}}; + legacy.open(); native.open(); + REQUIRE(legacy.effect(Seam::SourceBook, x::Flatten{}, fill(90)).status == x::Status::Applied); + REQUIRE(native.effect(Seam::NativeBook, x::Flatten{}, fill(90)).status == x::Status::Applied); + ++index; + CHECK(legacy.last_day() == point.key && legacy.days() == point.count); + exact(legacy.intraday(), -10 * static_cast(index)); + exact(legacy.net(), native.net()); fields.unchanged(native); + CHECK(native.rows().back().exit_time == point.utc_ms + 123); + CHECK(legacy.rows().back().exit_time == point.utc_ms); + } +} + +template void run(F fn) { + try { fn(); } catch (const Abort&) {} + catch (const std::exception& e) { ++failures; std::printf("FAIL %s exception %s\n", scenario, e.what()); } +} +} // namespace + +int main() { + for (Seam s : natives) for (bool defaults : {false, true}) for (bool poison : {false, true}) + run([&] { native_inert(s, defaults, poison); }); + for (Seam s : {Seam::NativeSelected, Seam::NativeReverse}) run([&] { giant_intraday(s); }); + run(opening_and_noeffect); + for (Seam s : sources) { run([&] { source_failures(s); }); run([&] { source_row_order(s); }); run([&] { committed_slice(s); }); } + run(cycle_and_generic_cash_precedence); run(generic_counter_guards); run(invalid_projections_and_source_stage); + run(one_fee_one_owner); run(source_hash_domains); + run([] { calendar_pair("Asia/Taipei", {{1743435000000LL, 3103, 1}, {1743435600000LL, 3103, 1}, {1743436800000LL, 104, 2}}); }); + run([] { calendar_pair("America/New_York", {{1741503540000LL, 903, 1}, {1741503600000LL, 903, 1}, {1741579140000LL, 903, 1}, {1741579200000LL, 1003, 2}}); }); + run([] { calendar_pair("America/New_York", {{1762061400000LL, 211, 1}, {1762065000000LL, 211, 1}, {1762145940000LL, 211, 1}, {1762146000000LL, 311, 2}}); }); + run([] { calendar_pair("UTC", {{1736121600000LL, 601, 1}, {1767657600000LL, 601, 1}}); }); // Existing day key omits year. + std::printf("%s settlement observation boundary: %d checks, %d failures\n", failures ? "FAIL" : "PASS", checks, failures); + return failures ? 1 : 0; +}