From 117141a2ca64c97e97cd9ee5fd44e5b0667030db Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Thu, 16 Jul 2026 16:05:51 +0800 Subject: [PATCH 1/3] Gate terminal close reversals on gross margin --- include/pineforge/engine.hpp | 1 + src/engine_fills.cpp | 110 +++++++++ src/engine_run.cpp | 5 + tests/CMakeLists.txt | 1 + ...est_pooc_coof_reversal_gross_admission.cpp | 212 ++++++++++++++++++ 5 files changed, 329 insertions(+) create mode 100644 tests/test_pooc_coof_reversal_gross_admission.cpp diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 2fb7079..7b0d8dc 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -2121,6 +2121,7 @@ class BacktestEngine { void update_trail_best_for_bar_open(const Bar& bar); void sort_exit_siblings_by_path_fill(const Bar& bar); bool pending_flat_market_pair_scope_is_live() const; + void apply_pooc_coof_explicit_flat_market_gross_admission(); void finalize_pending_flat_market_pairs(const Bar& bar); void sort_orders_by_fill_phase(const Bar& bar); // TradingView binds a valid, single/full, non-trailing strategy.exit to a diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index 3e81d4f..56e1500 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -810,6 +810,116 @@ bool BacktestEngine::pending_flat_market_pair_scope_is_live() const { && !risk_halted_; } +// TradingView admission for the exact Fran-470 terminal-close shape. Two +// distinct explicit-FIXED opposite MARKET strategy.entry calls are emitted +// from true flat in one ordinary historical evaluation with both POOC and COOF +// enabled. Each own quantity first passes strategy_entry's normal signal-time +// check. Before the terminal-C broker pass, TV additionally costs the LATER +// call as the gross reversal transaction: +// +// (first own qty + later own qty) * signal close * pointvalue * fx * margin +// <= placement equity +// +// If the pair exceeds that budget, the later call is silently declined and the +// first call remains the sole fill. The clean-room N=2 probe separates this +// from order priority and bracket interaction: fixed qty=1 executes both in +// source order, while Fran's ~95%-of-equity explicit qty keeps only the first, +// with and without a position-scoped bracket. +// +// Keep this independent from the established KI-65 pending MARKET pair. KI-65 +// owns a non-POOC/non-COOF pyramiding=2 buy-before-sell transaction model; this +// terminal-C shape preserves source order and only adds the gross admission +// fence. The complete-book and default-risk guards deliberately fail closed +// for third entries, OCA/raw/priced/resting siblings, same-direction calls, +// after-close creation, COOF-born calls, magnifier, slippage, custom margin, +// commission, or non-default risk policy. +void BacktestEngine::apply_pooc_coof_explicit_flat_market_gross_admission() { + if (!process_orders_on_close_ + || !calc_on_order_fills_ + || bar_magnifier_enabled_ + || pyramiding_ != 0 + || slippage_ != 0 + || pending_orders_.size() != 2 + || std::abs(margin_long_ - 100.0) >= 1e-12 + || std::abs(margin_short_ - 100.0) >= 1e-12 + || risk_direction_ != RiskDirection::BOTH + || risk_max_cons_loss_days_ != 0 + || risk_max_drawdown_ > 0.0 + || risk_max_intraday_loss_ > 0.0 + || risk_max_position_size_ > 0.0 + || max_intraday_filled_orders_ > 0 + || risk_halted_) { + return; + } + + PendingOrder* first = &pending_orders_[0]; + PendingOrder* second = &pending_orders_[1]; + if (second->created_seq < first->created_seq) std::swap(first, second); + + auto eligible = [&](const PendingOrder& order) { + return order.type == OrderType::MARKET + && order.explicit_flat_admission_candidate + && std::isfinite(order.qty) + && order.qty > kQtyEpsilon + && (order.qty_type < 0 + || order.qty_type == static_cast(QtyType::FIXED)) + && order.oca_name.empty() + && order.created_bar == bar_index_ + && order.created_position_side == PositionSide::FLAT + && !order.created_after_position_close_in_bar + && !order.created_during_coof_recalc + && !order.coof_born_at_close_recalc + && !order.coof_born_mid_bar + && std::isfinite(order.explicit_placement_equity) + && order.explicit_placement_equity > 0.0 + && std::isfinite(order.explicit_slipped_signal_close) + && order.explicit_slipped_signal_close > 0.0; + }; + if (!eligible(*first) + || !eligible(*second) + || first->id == second->id + || first->is_long == second->is_long + || first->created_bar != second->created_bar) { + return; + } + + const double first_qty = std::abs(apply_qty_step(first->qty)); + const double second_qty = std::abs(apply_qty_step(second->qty)); + if (!(first_qty > kQtyEpsilon) || !(second_qty > kQtyEpsilon)) return; + + const double equity = second->explicit_placement_equity; + const double equity_guard = std::max(1e-9, std::abs(equity) * 1e-12); + if (std::abs(first->explicit_placement_equity - equity) > equity_guard) { + return; + } + const double signal_close = second->explicit_slipped_signal_close; + const double price_guard = std::max(1e-12, std::abs(signal_close) * 1e-12); + if (std::abs(first->explicit_slipped_signal_close - signal_close) + > price_guard) { + return; + } + if (calc_commission(signal_close, first_qty) != 0.0 + || calc_commission(signal_close, second_qty) != 0.0) { + return; + } + + const double notional_k = syminfo_.pointvalue * account_currency_fx_; + if (!std::isfinite(notional_k) || !(notional_k > 0.0)) return; + const double required_margin = + (first_qty + second_qty) * signal_close * notional_k; + if (!(required_margin > equity + equity_guard)) return; + + const int64_t rejected_seq = second->created_seq; + invalidate_pending_flat_market_pair(rejected_seq); + pending_orders_.erase( + std::remove_if( + pending_orders_.begin(), pending_orders_.end(), + [&](const PendingOrder& order) { + return order.created_seq == rejected_seq; + }), + pending_orders_.end()); +} + // Finalize the deferred KI-65 MARKET/MARKET candidate set only after on_bar // has completed and the broker sees every call from that source bar. Each call // has already passed the ordinary own-qty placement gate. Exactly two eligible diff --git a/src/engine_run.cpp b/src/engine_run.cpp index e86028c..b333758 100644 --- a/src/engine_run.cpp +++ b/src/engine_run.cpp @@ -429,6 +429,11 @@ void BacktestEngine::dispatch_bar_calc_on_order_fills() { // eligible order remains. if (process_orders_on_close_) { const Bar close_point = coof_point_bar(script_bar, cursor); + // The COOF terminal-C loop bypasses process_pending_orders(), so apply + // the exact two-call explicit reversal gross-admission fence once, + // after the ordinary close body has emitted the complete sibling book + // and before either sibling can fill. + apply_pooc_coof_explicit_flat_market_gross_admission(); int c_guard = 0; while (++c_guard <= kCoofLoopGuard) { current_bar_ = close_point; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f2e445f..fcc7274 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -75,6 +75,7 @@ set(TEST_SOURCES test_stop_decline_continue_path test_frozen_flat_gap_reject test_explicit_qty_fill_admission + test_pooc_coof_reversal_gross_admission test_limit_fill_slippage test_strategy_commands_extra test_multi_tier_exit_precedence diff --git a/tests/test_pooc_coof_reversal_gross_admission.cpp b/tests/test_pooc_coof_reversal_gross_admission.cpp new file mode 100644 index 0000000..f884819 --- /dev/null +++ b/tests/test_pooc_coof_reversal_gross_admission.cpp @@ -0,0 +1,212 @@ +/* + * TradingView terminal-C gross admission for the Fran-470 shape. + * + * Two distinct explicit-FIXED opposite MARKET strategy.entry calls are queued + * from true flat with process_orders_on_close + calc_on_order_fills. Each own + * qty passes the ordinary signal-time margin check. TV nevertheless declines + * the later source call when the pair's gross reversal transaction exceeds + * placement equity. Fixed/smaller pairs still execute both in source order. + * + * Clean-room TV anchors: + * pf-probe-coof-pooc-opposite-market-ordering + * pf-probe-coof-pooc-opposite-market-fran-factors + */ + +#include +#include +#include +#include + +#include +#include + +using namespace pineforge; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#define CHECK_NEAR(a, b, tol) \ + do { \ + const double _a = (a); \ + const double _b = (b); \ + if (!(std::fabs(_a - _b) <= (tol))) { \ + std::printf(" FAIL %s:%d %s == %.10f, expected %.10f\n", \ + __FILE__, __LINE__, #a, _a, _b); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +static constexpr double kNaN = std::numeric_limits::quiet_NaN(); + +static Bar bar(int64_t ts, double price) { + return Bar{price, price, price, price, 1.0, ts}; +} + +class Probe final : public BacktestEngine { +public: + Probe(bool first_long, double qty) : first_long_(first_long), qty_(qty) { + initial_capital_ = 10'000.0; + default_qty_type_ = QtyType::FIXED; + default_qty_value_ = 1.0; + commission_type_ = CommissionType::PERCENT; + commission_value_ = 0.0; + margin_long_ = 100.0; + margin_short_ = 100.0; + syminfo_mintick_ = 0.01; + qty_step_ = 0.0; + slippage_ = 0; + pyramiding_ = 0; + process_orders_on_close_ = true; + calc_on_order_fills_ = true; + margin_call_enabled_ = false; + } + + bool coof = true; + bool pooc = true; + bool add_third = false; + double commission_pct = 0.0; + double margin_pct = 100.0; + int slip_ticks = 0; + + void on_bar(const Bar&) override { + calc_on_order_fills_ = coof; + process_orders_on_close_ = pooc; + commission_value_ = commission_pct; + margin_long_ = margin_pct; + margin_short_ = margin_pct; + slippage_ = slip_ticks; + + if (bar_index_ == 0) { + strategy_entry("E1", first_long_, kNaN, kNaN, qty_); + strategy_entry("E2", !first_long_, kNaN, kNaN, qty_); + if (add_third) { + strategy_entry("E3", first_long_, kNaN, kNaN, qty_); + } + } else if (position_side_ != PositionSide::FLAT) { + strategy_cancel_all(); + strategy_close_all(); + } + } + + double signed_size() const { return signed_position_size(); } + +private: + bool first_long_; + double qty_; +}; + +static std::vector flat_feed() { + return {bar(0, 100.0), bar(900'000, 100.0), bar(1'800'000, 100.0)}; +} + +static void assert_first_only(bool first_long) { + Probe p(first_long, 95.0); // own=95%, gross=190% of equity + auto bars = flat_feed(); + p.run(bars.data(), static_cast(bars.size())); + + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == 1); + CHECK_NEAR(p.signed_size(), 0.0, 1e-9); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(t.is_long == first_long); + CHECK_NEAR(t.qty, 95.0, 1e-9); + CHECK(t.entry_bar_index == 0); + CHECK(t.exit_bar_index == 1); + } +} + +static void assert_both_fill(bool first_long, double qty) { + Probe p(first_long, qty); + auto bars = flat_feed(); + p.run(bars.data(), static_cast(bars.size())); + + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == 2); + CHECK_NEAR(p.signed_size(), 0.0, 1e-9); + if (p.trade_count() == 2) { + const Trade& scratch = p.get_trade(0); + const Trade& survivor = p.get_trade(1); + CHECK(scratch.is_long == first_long); + CHECK(survivor.is_long != first_long); + CHECK(scratch.entry_bar_index == 0); + CHECK(scratch.exit_bar_index == 0); + CHECK(survivor.entry_bar_index == 0); + CHECK(survivor.exit_bar_index == 1); + CHECK_NEAR(scratch.qty, qty, 1e-9); + CHECK_NEAR(survivor.qty, qty, 1e-9); + } +} + +static void test_red_gross_over_equity_declines_later_call() { + std::printf("test_red_gross_over_equity_declines_later_call\n"); + assert_first_only(true); + assert_first_only(false); +} + +static void test_green_fixed_and_exact_boundary_pairs_fill_both() { + std::printf("test_green_fixed_and_exact_boundary_pairs_fill_both\n"); + for (bool first_long : {true, false}) { + assert_both_fill(first_long, 1.0); + assert_both_fill(first_long, 50.0); // gross exactly equals equity + } +} + +static void assert_excluded_pair_uses_legacy_result( + const char* label, bool coof, bool pooc, double commission, + double margin, int slippage, int expected_trades = 2) { + std::printf("%s\n", label); + Probe p(true, 95.0); + p.coof = coof; + p.pooc = pooc; + p.commission_pct = commission; + p.margin_pct = margin; + p.slip_ticks = slippage; + auto bars = flat_feed(); + p.run(bars.data(), static_cast(bars.size())); + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == expected_trades); +} + +static void test_green_scope_exclusions() { + assert_excluded_pair_uses_legacy_result( + "exclude without COOF", false, true, 0.0, 100.0, 0); + assert_excluded_pair_uses_legacy_result( + "exclude without POOC", true, false, 0.0, 100.0, 0, + /*expected_trades=*/1); + assert_excluded_pair_uses_legacy_result( + "exclude commissioned pair", true, true, 0.1, 100.0, 0); + assert_excluded_pair_uses_legacy_result( + "exclude custom margin", true, true, 0.0, 50.0, 0); + assert_excluded_pair_uses_legacy_result( + "exclude slippage", true, true, 0.0, 100.0, 1); + + std::printf("exclude three-call book\n"); + Probe three(true, 30.0); + three.add_third = true; + auto bars = flat_feed(); + three.run(bars.data(), static_cast(bars.size())); + CHECK(three.last_error().empty()); + CHECK(three.trade_count() == 3); +} + +int main() { + test_red_gross_over_equity_declines_later_call(); + test_green_fixed_and_exact_boundary_pairs_fill_both(); + test_green_scope_exclusions(); + + std::printf("\n%d passed, %d failed\n", tests_passed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} From 195f6bb91161aea227ec8f6a2f654d80f470dd5c Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Thu, 16 Jul 2026 16:32:58 +0800 Subject: [PATCH 2/3] Fail closed on mutated terminal reversal books --- include/pineforge/engine.hpp | 13 +- src/engine_fills.cpp | 43 ++++-- src/engine_strategy_commands.cpp | 84 ++++++++--- ...est_pooc_coof_reversal_gross_admission.cpp | 140 +++++++++++++++++- 4 files changed, 247 insertions(+), 33 deletions(-) diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 7b0d8dc..f8706b1 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -192,6 +192,12 @@ struct PendingOrder { // which intentionally survives same-id replacement to keep broker ordering // stable, incarnation is never copied or reused by a replacement. uint64_t incarnation = 0; + // True when this pending object was created by reissuing an id that was + // already live. The fresh incarnation above identifies the new call, but + // created_seq intentionally retains the replaced order's broker priority. + // Exact clean-room two-call rules must fail closed on this provenance + // rather than mistaking retained priority for current source order. + bool created_by_same_id_replacement = false; // Entry stop-limit activation is durable broker state. Once the stop leg // fires, later bars—and later COOF scheduler segments on the same bar— // evaluate only the live limit leg until the order fills or is replaced. @@ -916,9 +922,10 @@ class BacktestEngine { std::vector trades_; std::vector pending_orders_; // Source bars whose otherwise eligible flat MARKET candidate set was - // mutated (same-id replacement/cancel). Even if two orders survive, the - // original bar contained more/different calls and is outside the exact - // two-call oracle, so finalization must leave it ordinary. + // mutated (same-id replacement/cancel) or contained an extra rejected + // call. Even if two orders survive, the original bar contained more or + // different calls and is outside both exact two-call oracles (KI-65 and + // terminal-C POOC+COOF), so finalization must leave it ordinary. std::unordered_set pending_flat_market_pair_disqualified_bars_; // strategy.exit partial orders are one-shot per open position for a given id diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index 56e1500..8070c95 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -822,24 +822,38 @@ bool BacktestEngine::pending_flat_market_pair_scope_is_live() const { // // If the pair exceeds that budget, the later call is silently declined and the // first call remains the sole fill. The clean-room N=2 probe separates this -// from order priority and bracket interaction: fixed qty=1 executes both in -// source order, while Fran's ~95%-of-equity explicit qty keeps only the first, -// with and without a position-scoped bracket. +// from order priority and bracket interaction: the duration-one survivor pins +// the second source call for fixed qty=1, while Fran's ~95%-of-equity explicit +// qty keeps only the first, with and without a position-scoped bracket. The +// exact-equality comparator is inherited from the already-pinned KI-65 rule. // // Keep this independent from the established KI-65 pending MARKET pair. KI-65 // owns a non-POOC/non-COOF pyramiding=2 buy-before-sell transaction model; this // terminal-C shape preserves source order and only adds the gross admission // fence. The complete-book and default-risk guards deliberately fail closed // for third entries, OCA/raw/priced/resting siblings, same-direction calls, -// after-close creation, COOF-born calls, magnifier, slippage, custom margin, -// commission, or non-default risk policy. +// replacement-tainted/cancel-rearmed books, after-close creation, COOF-born +// calls, magnifier, slippage, custom margin, commission, or non-default risk +// policy. void BacktestEngine::apply_pooc_coof_explicit_flat_market_gross_admission() { + for (auto it = pending_flat_market_pair_disqualified_bars_.begin(); + it != pending_flat_market_pair_disqualified_bars_.end();) { + if (*it < bar_index_) { + it = pending_flat_market_pair_disqualified_bars_.erase(it); + } else { + ++it; + } + } + const bool source_bar_disqualified = + pending_flat_market_pair_disqualified_bars_.find(bar_index_) + != pending_flat_market_pair_disqualified_bars_.end(); if (!process_orders_on_close_ || !calc_on_order_fills_ || bar_magnifier_enabled_ || pyramiding_ != 0 || slippage_ != 0 || pending_orders_.size() != 2 + || source_bar_disqualified || std::abs(margin_long_ - 100.0) >= 1e-12 || std::abs(margin_short_ - 100.0) >= 1e-12 || risk_direction_ != RiskDirection::BOTH @@ -854,7 +868,7 @@ void BacktestEngine::apply_pooc_coof_explicit_flat_market_gross_admission() { PendingOrder* first = &pending_orders_[0]; PendingOrder* second = &pending_orders_[1]; - if (second->created_seq < first->created_seq) std::swap(first, second); + if (second->incarnation < first->incarnation) std::swap(first, second); auto eligible = [&](const PendingOrder& order) { return order.type == OrderType::MARKET @@ -865,6 +879,8 @@ void BacktestEngine::apply_pooc_coof_explicit_flat_market_gross_admission() { || order.qty_type == static_cast(QtyType::FIXED)) && order.oca_name.empty() && order.created_bar == bar_index_ + && order.incarnation > 0 + && !order.created_by_same_id_replacement && order.created_position_side == PositionSide::FLAT && !order.created_after_position_close_in_bar && !order.created_during_coof_recalc @@ -879,7 +895,14 @@ void BacktestEngine::apply_pooc_coof_explicit_flat_market_gross_admission() { || !eligible(*second) || first->id == second->id || first->is_long == second->is_long - || first->created_bar != second->created_bar) { + || first->created_bar != second->created_bar + // No admitted order object may have been created between the two + // calls. Together with the mutation tombstone above, this excludes + // three-call books reduced back to two by replacement/cancel-rearm. + || second->incarnation != first->incarnation + 1 + // A clean pair's retained broker sequence and fresh call identity + // have the same order. Any disagreement is replacement provenance. + || first->created_seq >= second->created_seq) { return; } @@ -909,13 +932,13 @@ void BacktestEngine::apply_pooc_coof_explicit_flat_market_gross_admission() { (first_qty + second_qty) * signal_close * notional_k; if (!(required_margin > equity + equity_guard)) return; - const int64_t rejected_seq = second->created_seq; - invalidate_pending_flat_market_pair(rejected_seq); + const uint64_t rejected_incarnation = second->incarnation; + invalidate_pending_flat_market_pair(second->created_seq); pending_orders_.erase( std::remove_if( pending_orders_.begin(), pending_orders_.end(), [&](const PendingOrder& order) { - return order.created_seq == rejected_seq; + return order.incarnation == rejected_incarnation; }), pending_orders_.end()); } diff --git a/src/engine_strategy_commands.cpp b/src/engine_strategy_commands.cpp index 08e57ea..0520a41 100644 --- a/src/engine_strategy_commands.cpp +++ b/src/engine_strategy_commands.cpp @@ -116,6 +116,13 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, && std::isnan(limit_price) && std::isnan(stop_price) && oca_name.empty() && (qty_type < 0 || qty_type == static_cast(QtyType::FIXED)); + const bool pooc_coof_ordinary_flat_market_call = + std::isfinite(qty) + && std::isnan(limit_price) && std::isnan(stop_price) + && process_orders_on_close_ + && calc_on_order_fills_ + && !coof_fill_recalc_active_ + && position_side_ == PositionSide::FLAT; const double paired_flat_market_own_qty = explicit_fixed_market_call ? std::abs(apply_qty_step(qty)) : std::numeric_limits::quiet_NaN(); @@ -124,6 +131,9 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, && paired_flat_market_own_qty > kQtyEpsilon && pending_flat_market_pair_scope_is_live() && position_side_ == PositionSide::FLAT; + const bool pooc_coof_explicit_flat_market_candidate = + explicit_fixed_market_call + && pooc_coof_ordinary_flat_market_call; // TradingView broker rule: market-entry orders are admitted only // when ``qty * * margin_pct/100 <= equity``. @@ -172,6 +182,13 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, double available_equity = current_equity(); double epsilon = std::max(1e-9, std::abs(available_equity) * 1e-12); if (required_margin > available_equity + epsilon) { + // A rejected third call leaves no PendingOrder or incarnation, + // but it still means the source body was not the clean exact + // two-call terminal-C shape. Preserve that invisible history. + if (pooc_coof_ordinary_flat_market_call) { + pending_flat_market_pair_disqualified_bars_.insert( + bar_index_); + } return; } } @@ -198,6 +215,13 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, if (paired_flat_market_candidate && entry_like) { pending_flat_market_pair_disqualified_bars_.insert(bar_index_); } + // The POOC+COOF gross-admission oracle covers two fresh calls, + // not a current call that inherits an older order's broker + // priority. Taint the current source bar even when the replaced + // object came from a prior bar. + if (pooc_coof_explicit_flat_market_candidate) { + pending_flat_market_pair_disqualified_bars_.insert(bar_index_); + } if (entry_like && pending.created_position_side == PositionSide::FLAT) { pending_flat_market_pair_disqualified_bars_.insert( @@ -243,6 +267,7 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, order.created_bar = bar_index_; order.created_seq = preserved_seq > 0 ? preserved_seq : next_order_seq_++; order.incarnation = next_order_incarnation_++; + order.created_by_same_id_replacement = preserved_seq > 0; order.created_during_coof_recalc = coof_fill_recalc_active_; order.coof_born_at_close_recalc = coof_fill_recalc_active_ && coof_cursor_is_bar_close_; @@ -1256,12 +1281,20 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro void BacktestEngine::strategy_cancel(const std::string& id) { for (const PendingOrder& order : pending_orders_) { if (order.id == id) { - if ((order.type == OrderType::MARKET - || order.type == OrderType::ENTRY - || order.type == OrderType::RAW_ORDER) - && order.created_position_side == PositionSide::FLAT) { - pending_flat_market_pair_disqualified_bars_.insert( - order.created_bar); + const bool entry_like = + order.type == OrderType::MARKET + || order.type == OrderType::ENTRY + || order.type == OrderType::RAW_ORDER; + if (entry_like) { + if (order.created_position_side == PositionSide::FLAT) { + pending_flat_market_pair_disqualified_bars_.insert( + order.created_bar); + } + if (process_orders_on_close_ && calc_on_order_fills_ + && !coof_fill_recalc_active_) { + pending_flat_market_pair_disqualified_bars_.insert( + bar_index_); + } } invalidate_pending_flat_market_pair(order.created_seq); } @@ -1274,12 +1307,19 @@ void BacktestEngine::strategy_cancel(const std::string& id) { void BacktestEngine::strategy_cancel_all() { for (const PendingOrder& order : pending_orders_) { - if ((order.type == OrderType::MARKET - || order.type == OrderType::ENTRY - || order.type == OrderType::RAW_ORDER) - && order.created_position_side == PositionSide::FLAT) { - pending_flat_market_pair_disqualified_bars_.insert( - order.created_bar); + const bool entry_like = + order.type == OrderType::MARKET + || order.type == OrderType::ENTRY + || order.type == OrderType::RAW_ORDER; + if (entry_like) { + if (order.created_position_side == PositionSide::FLAT) { + pending_flat_market_pair_disqualified_bars_.insert( + order.created_bar); + } + if (process_orders_on_close_ && calc_on_order_fills_ + && !coof_fill_recalc_active_) { + pending_flat_market_pair_disqualified_bars_.insert(bar_index_); + } } } pending_orders_.clear(); @@ -1300,12 +1340,20 @@ void BacktestEngine::strategy_order(const std::string& id, bool is_long, double // Remove existing pending order with same id for (const PendingOrder& pending : pending_orders_) { if (pending.id == id) { - if ((pending.type == OrderType::MARKET - || pending.type == OrderType::ENTRY - || pending.type == OrderType::RAW_ORDER) - && pending.created_position_side == PositionSide::FLAT) { - pending_flat_market_pair_disqualified_bars_.insert( - pending.created_bar); + const bool entry_like = + pending.type == OrderType::MARKET + || pending.type == OrderType::ENTRY + || pending.type == OrderType::RAW_ORDER; + if (entry_like) { + if (pending.created_position_side == PositionSide::FLAT) { + pending_flat_market_pair_disqualified_bars_.insert( + pending.created_bar); + } + if (process_orders_on_close_ && calc_on_order_fills_ + && !coof_fill_recalc_active_) { + pending_flat_market_pair_disqualified_bars_.insert( + bar_index_); + } } invalidate_pending_flat_market_pair(pending.created_seq); } diff --git a/tests/test_pooc_coof_reversal_gross_admission.cpp b/tests/test_pooc_coof_reversal_gross_admission.cpp index f884819..54ff284 100644 --- a/tests/test_pooc_coof_reversal_gross_admission.cpp +++ b/tests/test_pooc_coof_reversal_gross_admission.cpp @@ -5,7 +5,9 @@ * from true flat with process_orders_on_close + calc_on_order_fills. Each own * qty passes the ordinary signal-time margin check. TV nevertheless declines * the later source call when the pair's gross reversal transaction exceeds - * placement equity. Fixed/smaller pairs still execute both in source order. + * placement equity. For fixed/smaller pairs, the duration-one survivor pins + * the second source call; TV's scratch-row direction is only report + * attribution and is not asserted here. * * Clean-room TV anchors: * pf-probe-coof-pooc-opposite-market-ordering @@ -160,10 +162,143 @@ static void test_green_fixed_and_exact_boundary_pairs_fill_both() { std::printf("test_green_fixed_and_exact_boundary_pairs_fill_both\n"); for (bool first_long : {true, false}) { assert_both_fill(first_long, 1.0); - assert_both_fill(first_long, 50.0); // gross exactly equals equity + // Exact equality is inherited from the already-pinned KI-65 + // 50%+50% admission boundary; the new N=2 probe pins this scope. + assert_both_fill(first_long, 50.0); } } +class MutationProbe final : public BacktestEngine { +public: + enum class Shape { + SameBarReplacement, + PriorBarReplacement, + CanceledThird, + CancelRearm, + PriorRestingCancelRearm, + RejectedExtra, + }; + + explicit MutationProbe(Shape shape) : shape_(shape) { + initial_capital_ = 10'000.0; + default_qty_type_ = QtyType::FIXED; + commission_type_ = CommissionType::PERCENT; + commission_value_ = 0.0; + margin_long_ = 100.0; + margin_short_ = 100.0; + syminfo_mintick_ = 0.01; + qty_step_ = 0.0; + slippage_ = 0; + pyramiding_ = 0; + process_orders_on_close_ = true; + calc_on_order_fills_ = true; + margin_call_enabled_ = false; + } + + double observed_size = 0.0; + int observed_trades = 0; + + void on_bar(const Bar&) override { + if (shape_ == Shape::SameBarReplacement) { + if (bar_index_ == 0) { + strategy_entry("SR-A", true, kNaN, kNaN, 55.0); + strategy_entry("SR-B", false, kNaN, kNaN, 55.0); + strategy_entry("SR-A", true, kNaN, kNaN, 55.0); + } else if (bar_index_ == 1) { + observe_and_close(); + } + return; + } + + if (shape_ == Shape::PriorBarReplacement) { + if (bar_index_ == 0) { + strategy_entry("REP-A", true, 90.0, kNaN, 1.0); + } else if (bar_index_ == 1) { + strategy_entry("REP-B", false, kNaN, kNaN, 55.0); + strategy_entry("REP-A", true, kNaN, kNaN, 55.0); + } else if (bar_index_ == 2) { + observe_and_close(); + } + return; + } + + if (shape_ == Shape::PriorRestingCancelRearm) { + if (bar_index_ == 0) { + strategy_entry("PR-A", true, 90.0, kNaN, 1.0); + } else if (bar_index_ == 1) { + strategy_cancel("PR-A"); + strategy_entry("PR-B", false, kNaN, kNaN, 55.0); + strategy_entry("PR-A", true, kNaN, kNaN, 55.0); + } else if (bar_index_ == 2) { + observe_and_close(); + } + return; + } + + if (bar_index_ != 0) { + if (bar_index_ == 1) observe_and_close(); + return; + } + if (shape_ == Shape::CanceledThird) { + strategy_entry("CT-A", true, kNaN, kNaN, 55.0); + strategy_entry("CT-B", false, kNaN, kNaN, 55.0); + strategy_entry("CT-C", true, kNaN, kNaN, 55.0); + strategy_cancel("CT-C"); + } else if (shape_ == Shape::CancelRearm) { + strategy_entry("CR-A", true, kNaN, kNaN, 55.0); + strategy_entry("CR-B", false, kNaN, kNaN, 55.0); + strategy_cancel("CR-A"); + strategy_entry("CR-C", true, kNaN, kNaN, 55.0); + } else if (shape_ == Shape::RejectedExtra) { + strategy_entry("RX-X", true, kNaN, kNaN, 101.0); + strategy_entry("RX-A", true, kNaN, kNaN, 55.0); + strategy_entry("RX-B", false, kNaN, kNaN, 55.0); + } + } + +private: + void observe_and_close() { + observed_size = signed_position_size(); + observed_trades = trade_count(); + strategy_cancel_all(); + strategy_close_all(); + } + + Shape shape_; +}; + +static void test_green_mutated_two_order_books_fail_closed() { + auto run = [](const char* label, MutationProbe::Shape shape, + double expected_size, bool prior_bar = false) { + std::printf("%s\n", label); + MutationProbe probe(shape); + auto bars = prior_bar + ? std::vector{ + bar(0, 100.0), bar(900'000, 100.0), + bar(1'800'000, 100.0), bar(2'700'000, 100.0)} + : flat_feed(); + probe.run(bars.data(), static_cast(bars.size())); + CHECK(probe.last_error().empty()); + CHECK(probe.observed_trades == 1); + CHECK_NEAR(probe.observed_size, expected_size, 1e-9); + }; + + run("same-bar replacement stays on ordinary path", + MutationProbe::Shape::SameBarReplacement, -55.0); + run("prior-bar replacement stays on ordinary path", + MutationProbe::Shape::PriorBarReplacement, -55.0, + /*prior_bar=*/true); + run("canceled third call stays on ordinary path", + MutationProbe::Shape::CanceledThird, -55.0); + run("cancel-rearm three-call set stays on ordinary path", + MutationProbe::Shape::CancelRearm, 55.0); + run("prior-resting cancel-rearm stays on ordinary path", + MutationProbe::Shape::PriorRestingCancelRearm, 55.0, + /*prior_bar=*/true); + run("signal-rejected extra call stays on ordinary path", + MutationProbe::Shape::RejectedExtra, -55.0); +} + static void assert_excluded_pair_uses_legacy_result( const char* label, bool coof, bool pooc, double commission, double margin, int slippage, int expected_trades = 2) { @@ -205,6 +340,7 @@ static void test_green_scope_exclusions() { int main() { test_red_gross_over_equity_declines_later_call(); test_green_fixed_and_exact_boundary_pairs_fill_both(); + test_green_mutated_two_order_books_fail_closed(); test_green_scope_exclusions(); std::printf("\n%d passed, %d failed\n", tests_passed, tests_failed); From 216d97ff0a579ef1a6947618697af29674bccdea Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Thu, 16 Jul 2026 16:53:48 +0800 Subject: [PATCH 3/3] Close rejected-call provenance gaps --- src/engine_fills.cpp | 5 +- src/engine_strategy_commands.cpp | 2 +- ...est_pooc_coof_reversal_gross_admission.cpp | 50 ++++++++++++++++--- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index 8070c95..ed0fe9b 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -824,8 +824,9 @@ bool BacktestEngine::pending_flat_market_pair_scope_is_live() const { // first call remains the sole fill. The clean-room N=2 probe separates this // from order priority and bracket interaction: the duration-one survivor pins // the second source call for fixed qty=1, while Fran's ~95%-of-equity explicit -// qty keeps only the first, with and without a position-scoped bracket. The -// exact-equality comparator is inherited from the already-pinned KI-65 rule. +// qty keeps only the first, with and without a position-scoped bracket. Those +// controls bracket the behavior below and above budget; they do not pin exact +// gross-equality behavior, which retains the engine's ordinary margin model. // // Keep this independent from the established KI-65 pending MARKET pair. KI-65 // owns a non-POOC/non-COOF pyramiding=2 buy-before-sell transaction model; this diff --git a/src/engine_strategy_commands.cpp b/src/engine_strategy_commands.cpp index 0520a41..01b2bde 100644 --- a/src/engine_strategy_commands.cpp +++ b/src/engine_strategy_commands.cpp @@ -117,7 +117,7 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, && oca_name.empty() && (qty_type < 0 || qty_type == static_cast(QtyType::FIXED)); const bool pooc_coof_ordinary_flat_market_call = - std::isfinite(qty) + !std::isnan(qty) && std::isnan(limit_price) && std::isnan(stop_price) && process_orders_on_close_ && calc_on_order_fills_ diff --git a/tests/test_pooc_coof_reversal_gross_admission.cpp b/tests/test_pooc_coof_reversal_gross_admission.cpp index 54ff284..38fe7c6 100644 --- a/tests/test_pooc_coof_reversal_gross_admission.cpp +++ b/tests/test_pooc_coof_reversal_gross_admission.cpp @@ -158,12 +158,13 @@ static void test_red_gross_over_equity_declines_later_call() { assert_first_only(false); } -static void test_green_fixed_and_exact_boundary_pairs_fill_both() { - std::printf("test_green_fixed_and_exact_boundary_pairs_fill_both\n"); +static void test_green_fixed_probe_and_margin_equality_policy() { + std::printf("test_green_fixed_probe_and_margin_equality_policy\n"); for (bool first_long : {true, false}) { assert_both_fill(first_long, 1.0); - // Exact equality is inherited from the already-pinned KI-65 - // 50%+50% admission boundary; the new N=2 probe pins this scope. + // Equality admission preserves the engine's ordinary `required > + // equity` margin model. The TV probes bracket low/high gross cases but + // do not claim to pin the exact equality point. assert_both_fill(first_long, 50.0); } } @@ -177,6 +178,9 @@ class MutationProbe final : public BacktestEngine { CancelRearm, PriorRestingCancelRearm, RejectedExtra, + RejectedInfiniteExtra, + CancelAllThenPair, + NextBarCleanPair, }; explicit MutationProbe(Shape shape) : shape_(shape) { @@ -235,6 +239,19 @@ class MutationProbe final : public BacktestEngine { return; } + if (shape_ == Shape::NextBarCleanPair) { + if (bar_index_ == 0) { + strategy_entry("NB-X", true, kNaN, kNaN, 1.0); + strategy_cancel_all(); + } else if (bar_index_ == 1) { + strategy_entry("NB-A", true, kNaN, kNaN, 95.0); + strategy_entry("NB-B", false, kNaN, kNaN, 95.0); + } else if (bar_index_ == 2) { + observe_and_close(); + } + return; + } + if (bar_index_ != 0) { if (bar_index_ == 1) observe_and_close(); return; @@ -253,6 +270,17 @@ class MutationProbe final : public BacktestEngine { strategy_entry("RX-X", true, kNaN, kNaN, 101.0); strategy_entry("RX-A", true, kNaN, kNaN, 55.0); strategy_entry("RX-B", false, kNaN, kNaN, 55.0); + } else if (shape_ == Shape::RejectedInfiniteExtra) { + strategy_entry( + "RI-X", true, kNaN, kNaN, + std::numeric_limits::infinity()); + strategy_entry("RI-A", true, kNaN, kNaN, 55.0); + strategy_entry("RI-B", false, kNaN, kNaN, 55.0); + } else if (shape_ == Shape::CancelAllThenPair) { + strategy_entry("CA-X", true, kNaN, kNaN, 1.0); + strategy_cancel_all(); + strategy_entry("CA-A", true, kNaN, kNaN, 55.0); + strategy_entry("CA-B", false, kNaN, kNaN, 55.0); } } @@ -269,7 +297,8 @@ class MutationProbe final : public BacktestEngine { static void test_green_mutated_two_order_books_fail_closed() { auto run = [](const char* label, MutationProbe::Shape shape, - double expected_size, bool prior_bar = false) { + double expected_size, bool prior_bar = false, + int expected_trades = 1) { std::printf("%s\n", label); MutationProbe probe(shape); auto bars = prior_bar @@ -279,7 +308,7 @@ static void test_green_mutated_two_order_books_fail_closed() { : flat_feed(); probe.run(bars.data(), static_cast(bars.size())); CHECK(probe.last_error().empty()); - CHECK(probe.observed_trades == 1); + CHECK(probe.observed_trades == expected_trades); CHECK_NEAR(probe.observed_size, expected_size, 1e-9); }; @@ -297,6 +326,13 @@ static void test_green_mutated_two_order_books_fail_closed() { /*prior_bar=*/true); run("signal-rejected extra call stays on ordinary path", MutationProbe::Shape::RejectedExtra, -55.0); + run("infinite signal-rejected call stays on ordinary path", + MutationProbe::Shape::RejectedInfiniteExtra, -55.0); + run("cancel-all then pair stays on ordinary path", + MutationProbe::Shape::CancelAllThenPair, -55.0); + run("prior-bar tombstone does not suppress a clean next-bar pair", + MutationProbe::Shape::NextBarCleanPair, 95.0, + /*prior_bar=*/true, /*expected_trades=*/0); } static void assert_excluded_pair_uses_legacy_result( @@ -339,7 +375,7 @@ static void test_green_scope_exclusions() { int main() { test_red_gross_over_equity_declines_later_call(); - test_green_fixed_and_exact_boundary_pairs_fill_both(); + test_green_fixed_probe_and_margin_equality_policy(); test_green_mutated_two_order_books_fail_closed(); test_green_scope_exclusions();