diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 561d278..84cdedd 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -176,6 +176,29 @@ struct PendingOrder { int oca_type; // 0=none, 1=cancel, 2=reduce int created_bar; // bar_index when order was created int64_t created_seq = 0; + // 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. + bool stop_limit_activated = false; + // A stop/limit leg emitted by a COOF recalc on the position's entry bar + // cannot consume the fill cursor that caused that recalc when the leg is + // already marketable there. Suppression is deliberately per-leg: the + // other, correctly-sided bracket leg remains live on the remaining path. + // Both bits expire automatically once bar_index_ advances, so an unfilled + // suppressed leg carries into the next bar as an ordinary order. + bool coof_suppress_stop_on_entry_bar = false; + bool coof_suppress_limit_on_entry_bar = false; + // True only when this order was emitted by a historical + // calc_on_order_fills execution. POOC must not confuse that intrabar + // origin with an order emitted by the ordinary close-time execution. + bool created_during_coof_recalc = false; + // Stronger provenance for orders born specifically in the recalculation + // triggered by a close-point (C) fill. C has already been consumed: no + // order from that recalculation may refill at C or inspect the elapsed + // wick. A POOC market instruction has missed its only eligible close and + // expires unless an ordinary execution reissues it; priced GTC orders + // become ordinary carried orders on the next bar. + bool coof_born_at_close_recalc = false; PositionSide created_position_side = PositionSide::FLAT; // True when a successful strategy.close/close_all call earlier in this // same on_bar already targeted live quantity. This remains distinct from @@ -308,6 +331,7 @@ struct StrategyOverrides { int commission_type = -1; int default_qty_type = -1; int process_orders_on_close = -1; + int calc_on_order_fills = -1; int close_entries_rule = -1; }; @@ -394,6 +418,9 @@ class BacktestEngine { // --- Strategy parameters (set from strategy() declaration) --- double initial_capital_ = 1000000.0; bool process_orders_on_close_ = false; + // Historical fill-triggered recalculation is strictly opt-in. The false + // branch in dispatch_bar remains the legacy control path. + bool calc_on_order_fills_ = false; QtyType default_qty_type_ = QtyType::FIXED; double default_qty_value_ = 1.0; int pyramiding_ = 1; // max additional entries in same direction @@ -533,6 +560,14 @@ class BacktestEngine { // Advance every native source series by the current bar. Mirrors the // subclass ``_s_`` idiom: push on the first tick, update intrabar // (magnifier). Called at each on_bar dispatch point; no-op when inactive. + // A historical post-C fill recalculation remains barstate.isnew, but the + // completed ordinary close execution already owns this bar's history + // slot. Generated history/TA code uses the same predicate so that such an + // execution recomputes the slot instead of appending a duplicate bar. + bool history_advances_new_bar() const { + return is_first_tick_ && history_slot_is_new_; + } + void _push_source_series() { if (!_src_series_active_) return; const double o = current_bar_.open; @@ -544,7 +579,7 @@ class BacktestEngine { const double hlc3 = (h + l + c) / 3.0; const double ohlc4 = (o + h + l + c) / 4.0; const double hlcc4 = (h + l + c + c) / 4.0; - if (is_first_tick_) { + if (history_advances_new_bar()) { _src_open_.push(o); _src_high_.push(h); _src_low_.push(l); _src_close_.push(c); _src_volume_.push(v); _src_hl2_.push(hl2); _src_hlc3_.push(hlc3); @@ -756,6 +791,15 @@ class BacktestEngine { int oca_type = 0); void process_pending_orders(const Bar& bar); + struct CoofFillResult { + bool filled = false; + double fill_price = std::numeric_limits::quiet_NaN(); + uint64_t fill_events = 0; + }; + CoofFillResult process_next_pending_order(const Bar& bar, + bool allow_market_orders, + int& exit_closed_from_bar, + bool& exit_closed_was_long); // TradingView forced-liquidation (margin call). Finite-price liquidation // paths use the bar's adverse extreme. A 100%-margin long has no later @@ -1237,6 +1281,10 @@ class BacktestEngine { bool is_first_tick_ = true; bool is_last_tick_ = true; bool barstate_islast_ = false; + // Independent from barstate.isnew. False only when a COOF execution + // restores a completed ordinary-close checkpoint that already contains + // the current bar's one committed history slot. + bool history_slot_is_new_ = true; int magnifier_samples_ = 4; MagnifierDistribution magnifier_dist_ = MagnifierDistribution::ENDPOINTS; // When true, run_magnified_bar scales per-sub-bar sample count by @@ -1244,6 +1292,34 @@ class BacktestEngine { // tick approximation on high-volume sub-bars without real tick data. bool magnifier_volume_weighted_ = false; + // KI-60 scheduler transients. Script executions see the complete + // historical bar, while direct POOC/immediate market closes use the + // monotonic broker cursor price held here. + bool coof_scheduler_active_ = false; + bool coof_fill_recalc_active_ = false; + bool coof_cursor_is_bar_close_ = false; + bool coof_evaluating_path_segment_ = false; + bool coof_checkpoint_contains_current_bar_ = false; + double coof_cursor_price_ = std::numeric_limits::quiet_NaN(); + // Direct strategy.close / POOC fills can occur inside on_bar rather than + // through process_next_pending_order. The scheduler refreshes this budget + // before every speculative execution so those fills consume the same + // finite historical/magnifier event budget as every other broker fill. + uint64_t coof_direct_fill_events_remaining_ = 0; + uint64_t broker_fill_event_seq_ = 0; + + // input.source histories are base-owned script state and must roll back + // with generated state between historical fill recalculations. + Series coof_checkpoint_src_open_; + Series coof_checkpoint_src_high_; + Series coof_checkpoint_src_low_; + Series coof_checkpoint_src_close_; + Series coof_checkpoint_src_volume_; + Series coof_checkpoint_src_hl2_; + Series coof_checkpoint_src_hlc3_; + Series coof_checkpoint_src_ohlc4_; + Series coof_checkpoint_src_hlcc4_; + // --- Session predicate bar-state tracking --- // Tracks whether the previous bar was inside the regular session. // Used to compute session.isfirstbar (in_session && !prev_in_session_) @@ -1448,8 +1524,20 @@ class BacktestEngine { virtual void evaluate_security(int sec_id, const Bar& bar, bool is_complete) {} virtual void clear_security(int sec_id) {} + // Generated-state transaction hooks for calc_on_order_fills. Snapshot is + // called once before the broker walks a historical bar; restore precedes + // every fill recalc and the ordinary close execution. The completed + // ordinary-close execution becomes the committed checkpoint. Historical + // post-C fill recalculations start from it, recompute its current-bar + // history slot, and are rolled back after their broker effects persist. + virtual void snapshot_script_state() {} + virtual void restore_script_state() {} + virtual void commit_script_state() {} + // Magnifier helpers void run_magnified_bar(const std::vector& sub_bars, int64_t script_bar_ts); + void run_magnified_bar_calc_on_order_fills(const std::vector& sub_bars, + int64_t script_bar_ts); virtual void finalize_bar() {} // --- Equity extremes update (called after each on_bar) --- @@ -1824,6 +1912,21 @@ class BacktestEngine { // path. The magnifier tick loop does NOT use this — it gates the sequence // on is_last_tick_ and forces is_first_tick_ before on_bar. void dispatch_bar(); + void dispatch_bar_calc_on_order_fills(); + void snapshot_coof_script_state(); + void restore_coof_script_state(); + void commit_coof_script_state(); + uint64_t execute_coof_script_body(const Bar& script_bar, + double broker_cursor_price, + bool is_fill_recalc, + bool cursor_is_bar_close, + uint64_t direct_fill_event_budget); + uint64_t run_coof_recalc_chain(const Bar& script_bar, + double broker_cursor_price, + bool cursor_is_bar_close, + uint64_t triggering_events, + uint64_t max_events, + uint64_t events_already); void run_simple_bar_loop(const Bar* input_bars, int n_input); void run_aggregation_bar_loop(const Bar* input_bars, int n_input, bool bar_magnifier, int expected_script_bars); diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index 7d50192..2061749 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -41,7 +41,8 @@ void BacktestEngine::process_pending_orders(const Bar& bar) { pass0_opposing_skip_ids.clear(); DualEntryStopPathWinner dual_entry_path_ = DualEntryStopPathWinner::None; if (position_side_ == PositionSide::FLAT) { - dual_entry_path_ = dual_entry_stop_path_winner(bar, pending_orders_); + dual_entry_path_ = dual_entry_stop_path_winner( + bar, pending_orders_, bar_index_); } for (int opposing_pass = 0; opposing_pass < 2; ++opposing_pass) { @@ -94,6 +95,193 @@ void BacktestEngine::process_pending_orders(const Bar& bar) { } } +// Flag-gated KI-60 counterpart to process_pending_orders. It preserves the +// established eligibility / price / application kernels, but returns after +// exactly one ACTUAL broker fill so the scheduler can restore script state and +// execute on_bar before any later order sees the remaining path. Orders that +// are cancelled, rejected by risk/margin, or quantize to zero are compacted +// without producing a fill event and scanning continues. +BacktestEngine::CoofFillResult BacktestEngine::process_next_pending_order( + const Bar& bar, + bool allow_market_orders, + int& exit_closed_from_bar, + bool& exit_closed_was_long) { + CoofFillResult result; + + update_risk_state(); + + double trail_best_path_state = trail_best_price_; + update_trail_best_for_bar_open(bar); + materialize_relative_exit_prices_for_live_position(); + sort_exit_siblings_by_path_fill(bar); + sort_orders_by_fill_phase(bar); + + if (priced_entry_activity_bar_ != bar_index_) { + priced_entry_activity_bar_ = bar_index_; + priced_entry_filled_this_bar_ = false; + } + + std::unordered_set& pass0_opposing_skip_ids = scratch_skip_ids_; + pass0_opposing_skip_ids.clear(); + DualEntryStopPathWinner dual_entry_path = DualEntryStopPathWinner::None; + if (position_side_ == PositionSide::FLAT) { + dual_entry_path = dual_entry_stop_path_winner( + bar, pending_orders_, bar_index_); + } + + auto commit_stop_limit_activation_through = [&](double cursor_price) { + if (!(calc_on_order_fills_ && coof_scheduler_active_)) return; + Bar traversed = bar; + traversed.high = std::max(bar.open, cursor_price); + traversed.low = std::min(bar.open, cursor_price); + traversed.close = cursor_price; + for (PendingOrder& pending : pending_orders_) { + if (pending.coof_born_at_close_recalc + && pending.created_bar == bar_index_) { + continue; + } + if (pending.type != OrderType::ENTRY + || std::isnan(pending.stop_price) + || std::isnan(pending.limit_price) + || pending.stop_limit_activated) { + continue; + } + bool activated = false; + double ignored_fill = 0.0; + resolve_entry_stop_limit_fill( + traversed, pending.is_long, pending.stop_price, + pending.limit_price, &ignored_fill, &activated); + pending.stop_limit_activated = activated; + } + }; + + for (int opposing_pass = 0; opposing_pass < 2; ++opposing_pass) { + if (opposing_pass == 1 && pass0_opposing_skip_ids.empty()) break; + + std::vector& filled_indices = scratch_filled_indices_; + filled_indices.clear(); + + struct FillCandidate { + size_t order_index; + FillEvaluation fill; + double path_position; + bool was_trail; + int64_t created_seq; + }; + std::vector candidates; + candidates.reserve(pending_orders_.size()); + + for (size_t i = 0; i < pending_orders_.size(); ++i) { + PendingOrder& order = pending_orders_[i]; + auto eligibility = classify_order_eligibility( + order, opposing_pass, dual_entry_path, pass0_opposing_skip_ids, + exit_closed_from_bar, exit_closed_was_long, bar); + if (eligibility == OrderEligibility::Remove) { + filled_indices.push_back(i); + continue; + } + if (eligibility == OrderEligibility::Skip) continue; + + const bool has_priced_leg = + !std::isnan(order.stop_price) + || !std::isnan(order.limit_price) + || !std::isnan(order.trail_points) + || !std::isnan(order.trail_price); + if (!allow_market_orders && !has_priced_leg) { + continue; + } + + auto fill = evaluate_fill_price( + order, i, bar, opposing_pass, trail_best_path_state, + pass0_opposing_skip_ids); + if (fill.kind != FillEvaluation::Kind::Fill) continue; + + double path_position = 0.0; + // The COOF scheduler passes either a point bar or one monotonic + // remaining-path segment. Ranking every currently fillable order + // by its first touch on that segment makes broker time, rather + // than declaration order, select the next fill. Gap/point fills + // naturally tie at position zero and fall back to creation order. + internal::first_touch_position(bar, fill.fill_price, &path_position); + candidates.push_back({ + i, fill, path_position, last_exit_fill_was_trail_, + order.created_seq}); + } + + std::stable_sort( + candidates.begin(), candidates.end(), + [](const FillCandidate& a, const FillCandidate& b) { + if (a.path_position < b.path_position - kPathPosEps) return true; + if (b.path_position < a.path_position - kPathPosEps) return false; + return a.created_seq < b.created_seq; + }); + + for (const FillCandidate& candidate : candidates) { + PendingOrder& order = pending_orders_[candidate.order_index]; + last_exit_fill_was_trail_ = candidate.was_trail; + + // Candidate discovery looks across the whole remaining segment, + // but broker state may advance only through the chronological + // winner. Commit stop-limit activation on that consumed prefix; + // later stop crossings remain speculative until the cursor truly + // reaches them on a subsequent scheduler call. + commit_stop_limit_activation_through(candidate.fill.fill_price); + + const PositionSide side_before_fill = position_side_; + const uint64_t events_before = broker_fill_event_seq_; + apply_filled_order_to_state( + order, candidate.order_index, candidate.fill.fill_price, + candidate.fill.is_limit_fill, bar, + trail_best_path_state, exit_closed_from_bar, + exit_closed_was_long, filled_indices); + materialize_relative_exit_prices_for_live_position(); + + const uint64_t produced = broker_fill_event_seq_ - events_before; + if (produced == 0) { + continue; + } + + std::sort(filled_indices.begin(), filled_indices.end()); + filled_indices.erase( + std::unique(filled_indices.begin(), filled_indices.end()), + filled_indices.end()); + compact_filled_pending_orders( + filled_indices, exit_closed_from_bar, exit_closed_was_long); + if (side_before_fill == PositionSide::FLAT + && position_side_ != PositionSide::FLAT) { + // The old cycle's same-direction cleanup has already swept + // every order that existed when this fresh opening filled. + // Orders born in its subsequent recalcs belong to the new + // position cycle and must not inherit the old close marker. + exit_closed_from_bar = -1; + } + if (position_side_ == PositionSide::FLAT) { + purge_exit_orders(/*retain_for_pending_entries=*/true); + } + result.filled = true; + result.fill_price = candidate.fill.fill_price; + result.fill_events = produced; + return result; + } + + std::sort(filled_indices.begin(), filled_indices.end()); + filled_indices.erase( + std::unique(filled_indices.begin(), filled_indices.end()), + filled_indices.end()); + compact_filled_pending_orders( + filled_indices, exit_closed_from_bar, exit_closed_was_long); + } + + // No fill consumed this segment, so the broker reached its endpoint and + // every stop activation on the traversed path is now durable. + commit_stop_limit_activation_through(bar.close); + + if (position_side_ == PositionSide::FLAT) { + purge_exit_orders(/*retain_for_pending_entries=*/true); + } + return result; +} + // TradingView force-liquidation (margin call). // // Run once per script bar (end of dispatch_bar / magnifier bar) after all @@ -292,6 +480,9 @@ void BacktestEngine::process_margin_call(const Bar& bar) { } else { execute_partial_exit_qty(raw_exit_fill_base, qty_liq); } + if (trades_.size() != trades_before) { + ++broker_fill_event_seq_; + } // Tag every trade row this liquidation produced with TV's "Margin call". for (size_t ti = trades_before; ti < trades_.size(); ++ti) { trades_[ti].exit_comment = "Margin call"; @@ -390,10 +581,16 @@ void BacktestEngine::sort_orders_by_fill_phase(const Bar& bar) { std::stable_sort(pending_orders_.begin(), pending_orders_.end(), [&](const PendingOrder& a, const PendingOrder& b) { auto fill_phase = [&](const PendingOrder& o) { - bool has_stop = !std::isnan(o.stop_price); - bool has_limit = !std::isnan(o.limit_price); - bool has_trail = !std::isnan(o.trail_points) || !std::isnan(o.trail_price); bool exit_style = order_is_exit_style(o, position_side_); + const bool suppress_entry_bar_leg = + exit_style && position_open_bar_ == bar_index_; + bool has_stop = !std::isnan(o.stop_price) + && !(suppress_entry_bar_leg + && o.coof_suppress_stop_on_entry_bar); + bool has_limit = !std::isnan(o.limit_price) + && !(suppress_entry_bar_leg + && o.coof_suppress_limit_on_entry_bar); + bool has_trail = !std::isnan(o.trail_points) || !std::isnan(o.trail_price); if (o.type == OrderType::MARKET || (!has_stop && !has_limit && !has_trail)) { @@ -901,6 +1098,19 @@ void BacktestEngine::apply_filled_order_to_state( fold_exit_trail_peak_ = std::numeric_limits::quiet_NaN(); } // fill_kind_guard dtor clears current_fill_is_limit_ + // One matched pending order is one broker fill event, regardless of + // whether it opens, adds, partially exits, fully exits, or reverses. A + // rejected/zero-quantity attempt changes none of these broker observables + // and must not trigger calc_on_order_fills or consume its event budget. + const bool primary_fill_applied = + position_side_ != position_side_before_fill + || std::abs(position_qty_ - position_qty_before_fill) > kQtyEpsilon + || pyramid_entries_.size() != pyramid_lots_before_fill + || trades_.size() != trades_before; + if (primary_fill_applied) { + ++broker_fill_event_seq_; + } + // Queue the one-shot 1x-long post-fill affordability event at the single // dispatch point shared by MARKET, priced ENTRY, and RAW_ORDER fills while // the exact raw matched base is still available. A rejected or zero-effect @@ -1079,7 +1289,14 @@ void BacktestEngine::apply_filled_order_to_state( } } size_t close_trades_before = trades_.size(); + PositionSide cap_side_before = position_side_; + double cap_qty_before = position_qty_; execute_market_exit(cap_close_price); + if (position_side_ != cap_side_before + || std::abs(position_qty_ - cap_qty_before) > kQtyEpsilon + || trades_.size() != close_trades_before) { + ++broker_fill_event_seq_; + } for (size_t ti = close_trades_before; ti < trades_.size(); ++ti) { trades_[ti].exit_comment = "Close Position (Max number of filled orders in one day)"; @@ -1136,7 +1353,9 @@ void BacktestEngine::apply_market_order_fill(PendingOrder& order, double fill_pr // above/below a level the position never actually saw, which then // gap-fills the next bar's exit at its open instead of TV's real // intrabar retrace price. See apply_entry_order_fill's matching guard. - bool same_bar_close_fill = process_orders_on_close_ && order.created_bar == bar_index_; + bool same_bar_close_fill = process_orders_on_close_ + && order.created_bar == bar_index_ + && !order.created_during_coof_recalc; if (!same_bar_close_fill) { if (position_side_ == PositionSide::LONG) trail_best_price_ = std::max(trail_best_price_, bar.high); @@ -1205,7 +1424,9 @@ void BacktestEngine::apply_entry_order_fill(PendingOrder& order, double fill_pri // See apply_market_order_fill's matching guard: skip folding this // bar's pre-fill high/low into the trail when the fill happened AT // the bar's close (a POOC entry created and filled this same bar). - bool same_bar_close_fill = process_orders_on_close_ && order.created_bar == bar_index_; + bool same_bar_close_fill = process_orders_on_close_ + && order.created_bar == bar_index_ + && !order.created_during_coof_recalc; if (!same_bar_close_fill) { if (position_side_ == PositionSide::LONG) trail_best_price_ = std::max(trail_best_price_, bar.high); @@ -1474,6 +1695,26 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility( bool exit_style = order_is_exit_style(order, position_side_); + // The close cursor is a single broker point. A fill-triggered script + // execution at C may create orders, but those orders cannot consume C a + // second time or replay O/H/L. Priced GTC orders wake on the next bar. A + // POOC market instruction born after C has missed its eligible broker + // point and expires unless a later ordinary-close execution reissues it; + // carrying it creates Delta's spurious out-of-session lifecycle. + if (calc_on_order_fills_ && coof_scheduler_active_ + && order.coof_born_at_close_recalc) { + if (order.created_bar == bar_index_) { + return OrderEligibility::Skip; + } + const bool market_order = std::isnan(order.stop_price) + && std::isnan(order.limit_price) + && std::isnan(order.trail_points) + && std::isnan(order.trail_price); + if (process_orders_on_close_ && market_order) { + return OrderEligibility::Remove; + } + } + bool stale_close_order_for_new_position = order.type == OrderType::EXIT && order.created_while_in_position @@ -1502,7 +1743,12 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility( // position has since closed (probe 72/93: S placed during L and S2 // placed during L2 both fire on the same bar when their stops are // touched together — TV emits both as separate trades). - if (priced_entry_filled_this_bar_ && order.type == OrderType::ENTRY) { + const bool coof_fill_recalc_entry = + calc_on_order_fills_ && coof_scheduler_active_ + && order.created_during_coof_recalc + && order.created_bar == bar_index_; + if (priced_entry_filled_this_bar_ && order.type == OrderType::ENTRY + && !coof_fill_recalc_entry) { PositionSide requested = order.is_long ? PositionSide::LONG : PositionSide::SHORT; bool flat_armed = order.created_position_side == PositionSide::FLAT && position_side_ != PositionSide::FLAT; @@ -1609,7 +1855,8 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility( // deferred, still gets its normal intrabar stop/limit-touch evaluation // from the next bar on. See evaluate_fill_price's has_limit/has_stop // branches for the matching same-bar fill-price rules. - if (process_orders_on_close_ && order.created_bar == bar_index_) { + if (process_orders_on_close_ && order.created_bar == bar_index_ + && !order.created_during_coof_recalc) { bool has_stop_or_trail = !std::isnan(order.stop_price) || !std::isnan(order.trail_points) || !std::isnan(order.trail_price); @@ -1680,9 +1927,18 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility( bool has_price = !std::isnan(order.stop_price) || !std::isnan(order.limit_price) || !std::isnan(order.trail_points) || !std::isnan(order.trail_price); if (!has_price) { - return OrderEligibility::Skip; // skip market exits on entry bar + // Legacy/default mode skips a market exit on the entry bar because + // no strategy execution occurs between its open fill and the bar + // close. Under calc_on_order_fills, a post-fill execution can + // legitimately create this close and the monotonic scheduler owns + // its same-bar eligibility. + if (!(calc_on_order_fills_ && coof_scheduler_active_)) { + return OrderEligibility::Skip; + } } - if (!bar_magnifier_enabled_) { + if (!bar_magnifier_enabled_ + && !(calc_on_order_fills_ && coof_scheduler_active_ + && order.created_during_coof_recalc)) { double ep = position_entry_price_; if (position_side_ == PositionSide::LONG) { if (!std::isnan(order.stop_price) && order.stop_price > ep) return OrderEligibility::Skip; @@ -1706,8 +1962,17 @@ BacktestEngine::FillEvaluation BacktestEngine::evaluate_fill_price( int opposing_pass, double trail_best_path_state, std::unordered_set& pass0_opposing_skip_ids) { bool exit_style = order_is_exit_style(order, position_side_); - bool has_stop = !std::isnan(order.stop_price); - bool has_limit = !std::isnan(order.limit_price); + bool is_entry_bar = (exit_style && position_open_bar_ == bar_index_); + const bool suppress_stop = + is_entry_bar && order.coof_suppress_stop_on_entry_bar; + const bool suppress_limit = + is_entry_bar && order.coof_suppress_limit_on_entry_bar; + const double stop_price = suppress_stop + ? std::numeric_limits::quiet_NaN() : order.stop_price; + const double limit_price = suppress_limit + ? std::numeric_limits::quiet_NaN() : order.limit_price; + bool has_stop = !std::isnan(stop_price); + bool has_limit = !std::isnan(limit_price); bool has_trail = !std::isnan(order.trail_points) || !std::isnan(order.trail_price); last_exit_fill_was_trail_ = false; @@ -1717,14 +1982,22 @@ BacktestEngine::FillEvaluation BacktestEngine::evaluate_fill_price( return {FillEvaluation::Kind::NoFill, 0.0}; } - bool is_entry_bar = (exit_style && position_open_bar_ == bar_index_); double fill_price = 0.0; bool should_fill = false; bool is_limit_fill = false; + // If every non-trailing priced leg is suppressed on the entry bar, the + // order is dormant rather than becoming a market exit. The original + // prices remain stored on PendingOrder and become active next bar. + if (exit_style && !has_stop && !has_limit && !has_trail + && (suppress_stop || suppress_limit)) { + return {FillEvaluation::Kind::NoFill, 0.0}; + } + bool exit_same_bar_reissue = exit_style && !has_trail - && process_orders_on_close_ && order.created_bar == bar_index_; - if (exit_same_bar_reissue && (has_stop || has_limit)) { + && process_orders_on_close_ && order.created_bar == bar_index_ + && !order.created_during_coof_recalc; + if (!should_fill && exit_same_bar_reissue && (has_stop || has_limit)) { // A mid-trade exit re-issue (e.g. a break-even stop moved by a // time-gated block) that's already marketable against THIS bar's // close at the moment it's placed (see classify_order_eligibility's @@ -1736,28 +2009,28 @@ BacktestEngine::FillEvaluation BacktestEngine::evaluate_fill_price( // bar it could have interacted with the market. bool is_long = position_side_ == PositionSide::LONG; bool stop_marketable = has_stop - && (is_long ? (bar.close <= order.stop_price) : (bar.close >= order.stop_price)); + && (is_long ? (bar.close <= stop_price) : (bar.close >= stop_price)); bool limit_marketable = has_limit - && (is_long ? (bar.close >= order.limit_price) : (bar.close <= order.limit_price)); + && (is_long ? (bar.close >= limit_price) : (bar.close <= limit_price)); if (stop_marketable) { // Exit stop for a LONG is a SELL (worse execution = lower // price); for a SHORT it's a BUY (worse = higher price) -- // opposite direction from an ENTRY stop on the same side. - fill_price = is_long ? std::min(bar.close, order.stop_price) - : std::max(bar.close, order.stop_price); + fill_price = is_long ? std::min(bar.close, stop_price) + : std::max(bar.close, stop_price); should_fill = true; } else if (limit_marketable) { - fill_price = is_long ? std::max(bar.close, order.limit_price) - : std::min(bar.close, order.limit_price); + fill_price = is_long ? std::max(bar.close, limit_price) + : std::min(bar.close, limit_price); should_fill = true; is_limit_fill = true; } - } else if (exit_style && (has_stop || has_limit || has_trail)) { + } else if (!should_fill && exit_style && (has_stop || has_limit || has_trail)) { ExitPathFill exit_fill = resolve_exit_path_fill( bar, position_side_, - order.stop_price, - order.limit_price, + stop_price, + limit_price, order.trail_points, order.trail_price, order.trail_offset, @@ -1772,32 +2045,36 @@ BacktestEngine::FillEvaluation BacktestEngine::evaluate_fill_price( last_exit_fill_was_trail_ = exit_fill.is_trail; is_limit_fill = exit_fill.is_limit; } - } else if (order.type == OrderType::MARKET || - (!has_stop && !has_limit && !has_trail)) { + } else if (!should_fill && (order.type == OrderType::MARKET || + (!has_stop && !has_limit && !has_trail))) { fill_price = process_orders_on_close_ ? bar.close : bar.open; should_fill = true; - } else if (has_stop && has_limit) { + } else if (!should_fill && has_stop && has_limit) { // Entry stop-limit semantics: the stop activates the limit order, // and the limit can only fill after activation along the OHLC path. // The actual fill is the LIMIT leg (at the limit price or better), // so it takes the unslipped limit-or-better price path. + bool activated = calc_on_order_fills_ && coof_scheduler_active_ + ? order.stop_limit_activated : false; should_fill = resolve_entry_stop_limit_fill( bar, order.is_long, - order.stop_price, - order.limit_price, - &fill_price); + stop_price, + limit_price, + &fill_price, + &activated); is_limit_fill = should_fill; - } else if (has_stop) { + } else if (!should_fill && has_stop) { // Entry stop order if (position_side_ == PositionSide::FLAT && opposing_pass == 0 && - opposing_stop_entry_hits_first(bar, pending_orders_, order_index)) { + opposing_stop_entry_hits_first( + bar, pending_orders_, order_index, bar_index_)) { pass0_opposing_skip_ids.insert(order.id); return {FillEvaluation::Kind::DeferredToOpposingPass, 0.0}; } if (order.is_long) { - if (bar.high >= order.stop_price) { - fill_price = std::max(bar.open, order.stop_price); + if (bar.high >= stop_price) { + fill_price = std::max(bar.open, stop_price); // TradingView snaps stop entry fills to mintick in the // conservative direction (long stop -> ceil). if (fill_price > bar.open) { @@ -1806,17 +2083,18 @@ BacktestEngine::FillEvaluation BacktestEngine::evaluate_fill_price( should_fill = true; } } else { - if (bar.low <= order.stop_price) { - fill_price = std::min(bar.open, order.stop_price); + if (bar.low <= stop_price) { + fill_price = std::min(bar.open, stop_price); if (fill_price < bar.open) { fill_price = round_to_mintick_directional(fill_price, false); } should_fill = true; } } - } else if (has_limit) { + } else if (!should_fill && has_limit) { // Entry limit order - if (process_orders_on_close_ && order.created_bar == bar_index_) { + if (process_orders_on_close_ && order.created_bar == bar_index_ + && !order.created_during_coof_recalc) { // Same-bar pure-limit entry (see classify_order_eligibility's // matching carve-out): TV evaluates it against THIS bar's // close (the moment it was placed), not the bar's full @@ -1829,22 +2107,22 @@ BacktestEngine::FillEvaluation BacktestEngine::evaluate_fill_price( // differs when the close has gapped past the limit, where TV // prices the fill at the better close rather than the bare // limit. - if (order.is_long ? (bar.close <= order.limit_price) - : (bar.close >= order.limit_price)) { - fill_price = order.is_long ? std::min(bar.close, order.limit_price) - : std::max(bar.close, order.limit_price); + if (order.is_long ? (bar.close <= limit_price) + : (bar.close >= limit_price)) { + fill_price = order.is_long ? std::min(bar.close, limit_price) + : std::max(bar.close, limit_price); should_fill = true; is_limit_fill = true; } } else if (order.is_long) { - if (bar.low <= order.limit_price) { - fill_price = std::min(bar.open, order.limit_price); + if (bar.low <= limit_price) { + fill_price = std::min(bar.open, limit_price); should_fill = true; is_limit_fill = true; } } else { - if (bar.high >= order.limit_price) { - fill_price = std::max(bar.open, order.limit_price); + if (bar.high >= limit_price) { + fill_price = std::max(bar.open, limit_price); should_fill = true; is_limit_fill = true; } diff --git a/src/engine_internal.hpp b/src/engine_internal.hpp index 18dbcd4..e8a4416 100644 --- a/src/engine_internal.hpp +++ b/src/engine_internal.hpp @@ -136,12 +136,14 @@ bool entry_stop_first_touch(const Bar& bar, double stop_level, // For flat-position opposing stop entries (long stop vs short stop), return // true if any opposite stop is touched earlier on the bar path than `current`. bool opposing_stop_entry_hits_first(const Bar& bar, - const std::vector& orders, - std::size_t current_idx); + const std::vector& orders, + std::size_t current_idx, + int current_bar_index = -1); DualEntryStopPathWinner dual_entry_stop_path_winner(const Bar& bar, - const std::vector& orders); + const std::vector& orders, + int current_bar_index = -1); // For OCA exit siblings (e.g., separate TP and SL strategy.order calls), @@ -188,7 +190,8 @@ bool resolve_entry_stop_limit_fill(const Bar& bar, bool is_long, double stop_price, double limit_price, - double* fill_price); + double* fill_price, + bool* activated); // Earliest intra-bar path coordinate [0, 3) where this EXIT's stop/limit would diff --git a/src/engine_path_resolve.cpp b/src/engine_path_resolve.cpp index b0247b2..febffd4 100644 --- a/src/engine_path_resolve.cpp +++ b/src/engine_path_resolve.cpp @@ -164,9 +164,16 @@ bool entry_stop_first_touch(const Bar& bar, double stop_level, // true if any opposite stop is touched earlier on the bar path than `current`. bool opposing_stop_entry_hits_first(const Bar& bar, const std::vector& orders, - std::size_t current_idx) { + std::size_t current_idx, + int current_bar_index) { if (current_idx >= orders.size()) return false; const PendingOrder& current = orders[current_idx]; + auto deferred_at_consumed_close = [&](const PendingOrder& order) { + return current_bar_index >= 0 + && order.coof_born_at_close_recalc + && order.created_bar == current_bar_index; + }; + if (deferred_at_consumed_close(current)) return false; if (current.type != OrderType::ENTRY) return false; if (std::isnan(current.stop_price) || !std::isnan(current.limit_price)) return false; @@ -182,6 +189,7 @@ bool opposing_stop_entry_hits_first(const Bar& bar, for (std::size_t j = 0; j < orders.size(); ++j) { if (j == current_idx) continue; const PendingOrder& other = orders[j]; + if (deferred_at_consumed_close(other)) continue; if (other.type != OrderType::ENTRY) continue; if (other.is_long == current.is_long) continue; if (std::isnan(other.stop_price) || !std::isnan(other.limit_price)) continue; @@ -204,10 +212,16 @@ bool opposing_stop_entry_hits_first(const Bar& bar, DualEntryStopPathWinner dual_entry_stop_path_winner(const Bar& bar, - const std::vector& orders) { + const std::vector& orders, + int current_bar_index) { const PendingOrder* long_ord = nullptr; const PendingOrder* short_ord = nullptr; for (const PendingOrder& o : orders) { + if (current_bar_index >= 0 + && o.coof_born_at_close_recalc + && o.created_bar == current_bar_index) { + continue; + } if (o.type != OrderType::ENTRY) continue; if (!std::isnan(o.limit_price)) continue; if (std::isnan(o.stop_price)) continue; @@ -430,15 +444,19 @@ bool resolve_entry_stop_limit_fill(const Bar& bar, bool is_long, double stop_price, double limit_price, - double* fill_price) { - if (fill_price == nullptr || std::isnan(stop_price) || std::isnan(limit_price)) { + double* fill_price, + bool* activated) { + if (fill_price == nullptr || activated == nullptr + || std::isnan(stop_price) || std::isnan(limit_price)) { return false; } double path[4]; fill_bar_path_points(bar, path); - bool active = is_long ? (path[0] >= stop_price) : (path[0] <= stop_price); + bool active = *activated + || (is_long ? (path[0] >= stop_price) : (path[0] <= stop_price)); + *activated = active; auto limit_is_marketable = [&](double price) { return is_long ? (price <= limit_price) : (price >= limit_price); @@ -462,6 +480,7 @@ bool resolve_entry_stop_limit_fill(const Bar& bar, } active = true; + *activated = true; from_price = stop_price; if (limit_is_marketable(from_price)) { *fill_price = from_price; @@ -713,8 +732,17 @@ double exit_order_earliest_path_metric_no_trail( } const bool is_long = (position_side == PositionSide::LONG); - const double stop_price = order.stop_price; - const double limit_price = order.limit_price; + // COOF entry-bar suppression is leg-scoped. A wrong-side leg is dormant + // only for the creation/entry bar; a correctly-sided sibling must retain + // its real path coordinate so sibling ordering cannot hide its fill. + const double stop_price = + is_entry_bar && order.coof_suppress_stop_on_entry_bar + ? std::numeric_limits::quiet_NaN() + : order.stop_price; + const double limit_price = + is_entry_bar && order.coof_suppress_limit_on_entry_bar + ? std::numeric_limits::quiet_NaN() + : order.limit_price; if (std::isnan(stop_price) && std::isnan(limit_price)) { return std::numeric_limits::infinity(); } diff --git a/src/engine_run.cpp b/src/engine_run.cpp index 752df78..61aada5 100644 --- a/src/engine_run.cpp +++ b/src/engine_run.cpp @@ -27,6 +27,11 @@ using namespace internal; // 4. New market orders fill at bar.close; new stop/limit wait for next bar // When process_orders_on_close_ is false, only steps 1-3 run. void BacktestEngine::dispatch_bar() { + if (calc_on_order_fills_) { + dispatch_bar_calc_on_order_fills(); + return; + } + // Advance native source-series history before strategy logic so // get_input_source()'s returned series is current for this bar. Covers // the simple run() loop, run_simple_bar_loop, and the no-magnifier @@ -61,6 +66,284 @@ void BacktestEngine::dispatch_bar() { } } +void BacktestEngine::snapshot_coof_script_state() { + if (_src_series_active_) { + coof_checkpoint_src_open_ = _src_open_; + coof_checkpoint_src_high_ = _src_high_; + coof_checkpoint_src_low_ = _src_low_; + coof_checkpoint_src_close_ = _src_close_; + coof_checkpoint_src_volume_ = _src_volume_; + coof_checkpoint_src_hl2_ = _src_hl2_; + coof_checkpoint_src_hlc3_ = _src_hlc3_; + coof_checkpoint_src_ohlc4_ = _src_ohlc4_; + coof_checkpoint_src_hlcc4_ = _src_hlcc4_; + } + snapshot_script_state(); + coof_checkpoint_contains_current_bar_ = false; +} + +void BacktestEngine::restore_coof_script_state() { + if (_src_series_active_) { + _src_open_ = coof_checkpoint_src_open_; + _src_high_ = coof_checkpoint_src_high_; + _src_low_ = coof_checkpoint_src_low_; + _src_close_ = coof_checkpoint_src_close_; + _src_volume_ = coof_checkpoint_src_volume_; + _src_hl2_ = coof_checkpoint_src_hl2_; + _src_hlc3_ = coof_checkpoint_src_hlc3_; + _src_ohlc4_ = coof_checkpoint_src_ohlc4_; + _src_hlcc4_ = coof_checkpoint_src_hlcc4_; + } + restore_script_state(); +} + +void BacktestEngine::commit_coof_script_state() { + if (_src_series_active_) { + coof_checkpoint_src_open_ = _src_open_; + coof_checkpoint_src_high_ = _src_high_; + coof_checkpoint_src_low_ = _src_low_; + coof_checkpoint_src_close_ = _src_close_; + coof_checkpoint_src_volume_ = _src_volume_; + coof_checkpoint_src_hl2_ = _src_hl2_; + coof_checkpoint_src_hlc3_ = _src_hlc3_; + coof_checkpoint_src_ohlc4_ = _src_ohlc4_; + coof_checkpoint_src_hlcc4_ = _src_hlcc4_; + } + commit_script_state(); + coof_checkpoint_contains_current_bar_ = true; +} + +uint64_t BacktestEngine::execute_coof_script_body( + const Bar& script_bar, + double broker_cursor_price, + bool is_fill_recalc, + bool cursor_is_bar_close, + uint64_t direct_fill_event_budget) { + restore_coof_script_state(); + current_bar_ = script_bar; + // TradingView historical fill recalculations are both new and confirmed. + // History advancement is a separate axis: after the completed ordinary + // close execution has been committed, a post-C recalc recomputes that + // current-bar slot instead of pushing a duplicate bar. + is_first_tick_ = true; + is_last_tick_ = true; + history_slot_is_new_ = !coof_checkpoint_contains_current_bar_; + pending_close_qty_in_bar_ = 0.0; + _push_source_series(); + update_per_trade_extremes(); + + coof_scheduler_active_ = true; + coof_fill_recalc_active_ = is_fill_recalc; + coof_cursor_is_bar_close_ = cursor_is_bar_close; + coof_cursor_price_ = broker_cursor_price; + coof_direct_fill_events_remaining_ = direct_fill_event_budget; + const uint64_t before = broker_fill_event_seq_; + on_bar(current_bar_); + if (process_orders_on_close_) { + // A same-bar close batch is a broker fill at the current monotonic + // cursor. At the ordinary close execution that cursor is C; during a + // fill recalc it is the fill point that triggered the execution. + flush_same_bar_close(); + } + coof_fill_recalc_active_ = false; + coof_direct_fill_events_remaining_ = 0; + return broker_fill_event_seq_ - before; +} + +uint64_t BacktestEngine::run_coof_recalc_chain( + const Bar& script_bar, + double broker_cursor_price, + bool cursor_is_bar_close, + uint64_t triggering_events, + uint64_t max_events, + uint64_t events_already) { + uint64_t total_events = triggering_events; + uint64_t pending_recalcs = triggering_events; + uint64_t handled = 0; + while (pending_recalcs > 0 && events_already + handled < max_events) { + --pending_recalcs; + ++handled; + const uint64_t used = events_already + total_events; + const uint64_t direct_budget = + used < max_events ? max_events - used : 0; + const uint64_t direct = execute_coof_script_body( + script_bar, broker_cursor_price, /*is_fill_recalc=*/true, + cursor_is_bar_close, + direct_budget); + total_events += direct; + pending_recalcs += direct; + } + return total_events; +} + +namespace { + +Bar coof_point_bar(const Bar& script_bar, double price) { + Bar out = script_bar; + out.open = price; + out.high = price; + out.low = price; + out.close = price; + return out; +} + +Bar coof_segment_bar(const Bar& script_bar, double from, double to) { + Bar out = script_bar; + out.open = from; + out.high = std::max(from, to); + out.low = std::min(from, to); + out.close = to; + return out; +} + +} // namespace + +void BacktestEngine::dispatch_bar_calc_on_order_fills() { + const Bar script_bar = current_bar_; + constexpr uint64_t kHistoricalPathFillEvents = 4; + uint64_t fill_events = 0; + int exit_closed_from_bar = -1; + bool exit_closed_was_long = false; + + snapshot_coof_script_state(); + coof_scheduler_active_ = true; + coof_cursor_is_bar_close_ = false; + coof_evaluating_path_segment_ = false; + + double path[4]; + fill_bar_path_points(script_bar, path); + double cursor = path[0]; + int next_waypoint = 1; + bool evaluate_current_point = true; + + auto consume_fill = [&](const CoofFillResult& fill, + bool cursor_is_close, + bool filled_at_bar_open_point) { + const uint64_t before = fill_events; + cursor = fill.fill_price; + fill_events += run_coof_recalc_chain( + script_bar, cursor, cursor_is_close, fill.fill_events, + kHistoricalPathFillEvents, fill_events); + // The carried order's open fill triggers one execution at O, and the + // order born in that first execution may also fill at O. Every later + // fill—including the first fill when it occurs inside a path segment— + // advances monotonically toward the next historical waypoint. + evaluate_current_point = + filled_at_bar_open_point && before == 0 && fill_events == 1; + }; + + while (fill_events < kHistoricalPathFillEvents) { + if (evaluate_current_point) { + const bool cursor_is_close = next_waypoint >= 4; + const Bar point = coof_point_bar(script_bar, cursor); + current_bar_ = point; + CoofFillResult fill = process_next_pending_order( + point, /*allow_market_orders=*/true, + exit_closed_from_bar, exit_closed_was_long); + if (fill.filled) { + consume_fill( + fill, cursor_is_close, + /*filled_at_bar_open_point=*/next_waypoint == 1); + continue; + } + evaluate_current_point = false; + } + + if (next_waypoint >= 4) break; + + const double target = path[next_waypoint]; + const Bar segment = coof_segment_bar(script_bar, cursor, target); + current_bar_ = segment; + coof_evaluating_path_segment_ = true; + CoofFillResult fill = process_next_pending_order( + segment, /*allow_market_orders=*/false, + exit_closed_from_bar, exit_closed_was_long); + coof_evaluating_path_segment_ = false; + if (fill.filled) { + const bool reached_target = + std::abs(fill.fill_price - target) <= kSegmentDenomEps; + const bool cursor_is_close = next_waypoint == 3 + && reached_target; + consume_fill( + fill, cursor_is_close, + /*filled_at_bar_open_point=*/false); + // H/L/C itself has been consumed by this priced fill. Only O has + // the same-point two-fill exception; a market order born in the + // recalc must wait for the next historical waypoint. + if (reached_target) ++next_waypoint; + continue; + } + + cursor = target; + ++next_waypoint; + evaluate_current_point = true; + } + + // The regular historical close execution is still required after all + // fill-triggered executions. It starts from the prior committed checkpoint + // and becomes this bar's committed Pine state. + cursor = path[3]; + uint64_t direct = execute_coof_script_body( + script_bar, cursor, /*is_fill_recalc=*/false, + /*cursor_is_bar_close=*/true, + fill_events < kHistoricalPathFillEvents + ? kHistoricalPathFillEvents - fill_events : 0); + // C is the terminal historical tick. Direct fills produced by this + // ordinary-close execution count against the broker-event budget, but do + // not trigger another script body after the bar has ended. + commit_coof_script_state(); + fill_events += direct; + + // POOC's close-time market/priced orders share C and must never replay the + // already-consumed high/low. Process every ordinary-C sibling at that same + // broker epoch, without a fill-triggered body between siblings, until the + // four historical events are exhausted. + if (process_orders_on_close_) { + const Bar close_point = coof_point_bar(script_bar, cursor); + while (fill_events < kHistoricalPathFillEvents) { + current_bar_ = close_point; + CoofFillResult fill = process_next_pending_order( + close_point, /*allow_market_orders=*/true, + exit_closed_from_bar, exit_closed_was_long); + if (!fill.filled) break; + fill_events += fill.fill_events; + } + } + + // Preserve the existing once-per-script-bar liquidation placement. A + // liquidation is itself a broker fill and therefore triggers a C-point + // historical recalc when event budget remains. + current_bar_ = script_bar; + const size_t trades_before_mc = trades_.size(); + const uint64_t fill_seq_before_mc = broker_fill_event_seq_; + process_margin_call(current_bar_); + if (trades_.size() != trades_before_mc) { + refresh_frozen_default_sizing_after_margin_call(); + } + const uint64_t margin_events = broker_fill_event_seq_ - fill_seq_before_mc; + if (margin_events > 0 && fill_events < kHistoricalPathFillEvents) { + fill_events += run_coof_recalc_chain( + script_bar, cursor, /*cursor_is_bar_close=*/true, margin_events, + kHistoricalPathFillEvents, fill_events); + } + + // Broker fills and eligible priced GTC orders persist. A margin-call + // recalculation remains speculative and cannot replace the completed + // ordinary-close checkpoint. + restore_coof_script_state(); + coof_scheduler_active_ = false; + coof_fill_recalc_active_ = false; + coof_cursor_is_bar_close_ = false; + coof_evaluating_path_segment_ = false; + coof_direct_fill_events_remaining_ = 0; + coof_checkpoint_contains_current_bar_ = false; + history_slot_is_new_ = true; + coof_cursor_price_ = std::numeric_limits::quiet_NaN(); + current_bar_ = script_bar; + is_first_tick_ = true; + is_last_tick_ = true; +} + // Reset all per-run STATE (not configuration) so a reused handle's run N is // bit-identical to a fresh handle's run 1. See header doc + tests/ @@ -115,6 +398,15 @@ void BacktestEngine::reset_run_state() { intraday_day_ = -1; intraday_cap_hit_ = false; intraday_fill_count_ = 0; + broker_fill_event_seq_ = 0; + coof_scheduler_active_ = false; + coof_fill_recalc_active_ = false; + coof_cursor_is_bar_close_ = false; + coof_evaluating_path_segment_ = false; + coof_cursor_price_ = std::numeric_limits::quiet_NaN(); + coof_direct_fill_events_remaining_ = 0; + coof_checkpoint_contains_current_bar_ = false; + history_slot_is_new_ = true; // Per-bar cursor + session-predicate state. bar_index_ = 0; @@ -230,6 +522,10 @@ void BacktestEngine::run(const Bar* bars, int n) { // --- run_magnified_bar --- void BacktestEngine::run_magnified_bar(const std::vector& sub_bars, int64_t script_bar_ts) { if (sub_bars.empty()) return; + if (calc_on_order_fills_) { + run_magnified_bar_calc_on_order_fills(sub_bars, script_bar_ts); + return; + } double bar_open = sub_bars.front().open; double running_high = sub_bars.front().open; @@ -357,6 +653,219 @@ void BacktestEngine::run_magnified_bar(const std::vector& sub_bars, int64_t finalize_bar(); } +void BacktestEngine::run_magnified_bar_calc_on_order_fills( + const std::vector& sub_bars, + int64_t script_bar_ts) { + if (sub_bars.empty()) return; + + struct BrokerTick { + double price; + int64_t timestamp; + // A real lower-timeframe bar starts a fresh broker epoch at its open. + // The jump from the prior sub-bar's close to this price is a gap, not + // a continuously traversed segment. + bool starts_subbar; + }; + + Bar script_bar{}; + script_bar.open = sub_bars.front().open; + script_bar.high = sub_bars.front().high; + script_bar.low = sub_bars.front().low; + script_bar.close = sub_bars.back().close; + script_bar.volume = 0.0; + script_bar.timestamp = script_bar_ts; + for (const Bar& sb : sub_bars) { + script_bar.high = std::max(script_bar.high, sb.high); + script_bar.low = std::min(script_bar.low, sb.low); + script_bar.volume += sb.volume; + } + + const int total_sub = static_cast(sub_bars.size()); + const bool real_lower_tf = total_sub > 1; + diag_magnifier_sub_bars_processed_ += total_sub; + + double mean_vol = 0.0; + if (magnifier_volume_weighted_ && !real_lower_tf) { + for (const Bar& sb : sub_bars) mean_vol += sb.volume; + mean_vol /= static_cast(total_sub); + } + + std::vector ticks; + std::vector samples; + for (const Bar& sb : sub_bars) { + // Historical script executions see the completed security state for + // the script bar. Feeding all committed lower-TF bars before taking + // the script-state checkpoint mirrors the standard path, where + // security evaluators are fed before dispatch_bar. + for (auto& state : security_eval_states_) { + feed_security_eval_state(state, sb); + } + + if (real_lower_tf) { + sample_price_path(sb, 4, MagnifierDistribution::ENDPOINTS, samples); + } else if (magnifier_volume_weighted_) { + sample_price_path_volume_weighted( + sb, magnifier_samples_, mean_vol, + /*min_samples=*/2, + /*max_samples=*/std::max(magnifier_samples_ * 4, 8), + magnifier_dist_, samples); + } else { + sample_price_path(sb, magnifier_samples_, magnifier_dist_, samples); + } + diag_magnifier_sample_ticks_processed_ += + static_cast(samples.size()); + for (std::size_t sample_idx = 0; sample_idx < samples.size(); + ++sample_idx) { + ticks.push_back({ + samples[sample_idx], sb.timestamp, + real_lower_tf && sample_idx == 0, + }); + } + } + if (ticks.empty()) return; + + // Unlike a fixed arbitrary loop guard, termination is derived from the + // actual lower-timeframe broker ticks supplied by the magnifier. + const uint64_t max_fill_events = static_cast(ticks.size()); + uint64_t fill_events = 0; + int exit_closed_from_bar = -1; + bool exit_closed_was_long = false; + snapshot_coof_script_state(); + coof_scheduler_active_ = true; + coof_cursor_is_bar_close_ = false; + coof_evaluating_path_segment_ = false; + + double cursor = ticks.front().price; + int64_t cursor_ts = ticks.front().timestamp; + std::size_t next_tick = 1; + bool evaluate_current_point = true; + + auto consume_fill = [&](const CoofFillResult& fill, + bool cursor_is_close, + bool filled_at_first_tick) { + const uint64_t before = fill_events; + cursor = fill.fill_price; + fill_events += run_coof_recalc_chain( + script_bar, cursor, cursor_is_close, fill.fill_events, + max_fill_events, fill_events); + evaluate_current_point = filled_at_first_tick + && before == 0 && fill_events == 1; + }; + + while (fill_events < max_fill_events) { + if (evaluate_current_point) { + const bool cursor_is_close = next_tick >= ticks.size(); + Bar point = coof_point_bar(script_bar, cursor); + point.timestamp = cursor_ts; + current_bar_ = point; + CoofFillResult fill = process_next_pending_order( + point, /*allow_market_orders=*/true, + exit_closed_from_bar, exit_closed_was_long); + if (fill.filled) { + consume_fill( + fill, cursor_is_close, + /*filled_at_first_tick=*/next_tick == 1); + continue; + } + evaluate_current_point = false; + } + + if (next_tick >= ticks.size()) break; + + const BrokerTick target = ticks[next_tick]; + if (target.starts_subbar) { + // Every real magnifier sub-bar opens fresh. Resting priced orders + // evaluate the new open as a point (and therefore use gap-fill + // pricing); they must never interpolate a touch through the + // previous close -> new open discontinuity. + cursor = target.price; + cursor_ts = target.timestamp; + ++next_tick; + evaluate_current_point = true; + continue; + } + Bar segment = coof_segment_bar(script_bar, cursor, target.price); + segment.timestamp = target.timestamp; + current_bar_ = segment; + coof_evaluating_path_segment_ = true; + CoofFillResult fill = process_next_pending_order( + segment, /*allow_market_orders=*/false, + exit_closed_from_bar, exit_closed_was_long); + coof_evaluating_path_segment_ = false; + if (fill.filled) { + cursor_ts = target.timestamp; + const bool reached_target = + std::abs(fill.fill_price - target.price) <= kSegmentDenomEps; + const bool cursor_is_close = next_tick + 1 == ticks.size() + && reached_target; + consume_fill( + fill, cursor_is_close, + /*filled_at_first_tick=*/false); + // The real lower-TF endpoint is already consumed. Do not replay + // a market-enabled point at the same H/L/C tick; O remains the + // sole intentional same-tick exception. + if (reached_target) ++next_tick; + continue; + } + + cursor = target.price; + cursor_ts = target.timestamp; + ++next_tick; + evaluate_current_point = true; + } + + cursor = ticks.back().price; + uint64_t direct = execute_coof_script_body( + script_bar, cursor, /*is_fill_recalc=*/false, + /*cursor_is_bar_close=*/true, + fill_events < max_fill_events ? max_fill_events - fill_events : 0); + commit_coof_script_state(); + // The last real lower-TF close is also terminal: count direct fills but do + // not execute another script body after that completed broker tick. + fill_events += direct; + + if (process_orders_on_close_) { + Bar close_point = coof_point_bar(script_bar, cursor); + close_point.timestamp = ticks.back().timestamp; + while (fill_events < max_fill_events) { + current_bar_ = close_point; + CoofFillResult fill = process_next_pending_order( + close_point, /*allow_market_orders=*/true, + exit_closed_from_bar, exit_closed_was_long); + if (!fill.filled) break; + fill_events += fill.fill_events; + } + } + + current_bar_ = script_bar; + const size_t trades_before_mc = trades_.size(); + const uint64_t fill_seq_before_mc = broker_fill_event_seq_; + process_margin_call(current_bar_); + if (trades_.size() != trades_before_mc) { + refresh_frozen_default_sizing_after_margin_call(); + } + const uint64_t margin_events = broker_fill_event_seq_ - fill_seq_before_mc; + if (margin_events > 0 && fill_events < max_fill_events) { + fill_events += run_coof_recalc_chain( + script_bar, cursor, /*cursor_is_bar_close=*/true, margin_events, + max_fill_events, fill_events); + } + + restore_coof_script_state(); + coof_scheduler_active_ = false; + coof_fill_recalc_active_ = false; + coof_cursor_is_bar_close_ = false; + coof_evaluating_path_segment_ = false; + coof_direct_fill_events_remaining_ = 0; + coof_checkpoint_contains_current_bar_ = false; + history_slot_is_new_ = true; + coof_cursor_price_ = std::numeric_limits::quiet_NaN(); + current_bar_ = script_bar; + is_first_tick_ = true; + is_last_tick_ = true; + finalize_bar(); +} + // --- New run() overload with full parameter set --- void BacktestEngine::run(const Bar* input_bars, int n_input, @@ -759,6 +1268,8 @@ void BacktestEngine::run(const Bar* input_bars, int n_input, default_qty_type_ = static_cast(overrides->default_qty_type); if (overrides->process_orders_on_close >= 0) process_orders_on_close_ = (overrides->process_orders_on_close != 0); + if (overrides->calc_on_order_fills >= 0) + calc_on_order_fills_ = (overrides->calc_on_order_fills != 0); if (overrides->close_entries_rule >= 0) close_entries_rule_any_ = (overrides->close_entries_rule != 0); } diff --git a/src/engine_strategy_commands.cpp b/src/engine_strategy_commands.cpp index a6da78e..ea12853 100644 --- a/src/engine_strategy_commands.cpp +++ b/src/engine_strategy_commands.cpp @@ -179,6 +179,9 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, order.oca_type = oca_type; order.created_bar = bar_index_; order.created_seq = preserved_seq > 0 ? preserved_seq : next_order_seq_++; + order.created_during_coof_recalc = coof_fill_recalc_active_; + order.coof_born_at_close_recalc = + coof_fill_recalc_active_ && coof_cursor_is_bar_close_; order.created_position_side = position_side_; order.created_after_position_close_in_bar = pending_close_qty_in_bar_ > kQtyEpsilon; @@ -285,7 +288,15 @@ void BacktestEngine::strategy_close(const std::string& id, const std::string& co // flush_same_bar_close(). Everything else (ANY rule, explicit qty, // close_all, immediately=true, non-POC deferred closes) keeps the // existing paths. - if (process_orders_on_close_ && !immediately + const bool pooc_can_fill_at_this_cursor = + process_orders_on_close_ + && (!coof_scheduler_active_ || coof_cursor_is_bar_close_) + // A fill recalculation at C occurs after that broker point has been + // consumed. Only an explicit immediately=true close may execute at + // the current cursor; ordinary POOC closes are materialized as + // pending instructions and expire if no ordinary pass reissues them. + && !(coof_scheduler_active_ && coof_fill_recalc_active_); + if (pooc_can_fill_at_this_cursor && !immediately && !close_entries_rule_any_ && !id.empty() && std::isnan(qty) && std::isnan(qty_percent)) { enqueue_same_bar_close(id, comment); @@ -322,7 +333,9 @@ void BacktestEngine::strategy_close(const std::string& id, const std::string& co cancel_orders_for_full_close(id, closing_long); } - if (process_orders_on_close_ || immediately) { + if ((pooc_can_fill_at_this_cursor || immediately) + && !(coof_scheduler_active_ + && coof_direct_fill_events_remaining_ == 0)) { execute_immediate_close(id, comment, qty_to_close, matching_qty, closes_full_position, closes_fifo_qty, closes_any_qty); return; @@ -330,6 +343,17 @@ void BacktestEngine::strategy_close(const std::string& id, const std::string& co queue_deferred_close_order(id, comment, qty_to_close, matching_qty, closes_full_position, closes_any_qty); + // A default-FIFO close consumes id_unclosed_qty_ while resolving its + // target above. When the command was born after an already-consumed POOC + // close, its market order expires without a broker tick; keep the logical + // entry ledger available so a later ordinary-close execution can reissue + // and actually fill the close (Delta's next-bar lifecycle). + if (coof_scheduler_active_ && coof_fill_recalc_active_ + && coof_cursor_is_bar_close_ && process_orders_on_close_ + && !close_entries_rule_any_ && !id.empty() + && std::isnan(qty) && std::isnan(qty_percent)) { + id_unclosed_qty_[id] += qty_to_close; + } } void BacktestEngine::strategy_close_all() { @@ -436,6 +460,22 @@ void BacktestEngine::flush_same_bar_close() { double target = std::min(unclosed, avail); if (target <= eps) return; + bool closes_full_position = target >= position_qty_ - eps; + if (coof_scheduler_active_ + && ((coof_fill_recalc_active_ && coof_cursor_is_bar_close_) + || !coof_cursor_is_bar_close_ + || coof_direct_fill_events_remaining_ == 0)) { + // The script execution still occurs after the last allowed fill, but + // its close cannot manufacture an extra historical broker event. + // Materialize the command so same-bar broker/order side effects remain + // explicit; as a post-C POOC market instruction it expires unless a + // later ordinary-close execution reissues it. + queue_deferred_close_order( + id, comment, target, target, closes_full_position, + /*closes_any_qty=*/false); + return; + } + if (sole_call) { // Single close call this bar: classic semantics — consume the // ledger and release any prior reservation for this id. @@ -446,19 +486,23 @@ void BacktestEngine::flush_same_bar_close() { close_reserved_qty_.erase(id); } - bool closes_full_position = target >= position_qty_ - eps; size_t trades_before = trades_.size(); + PositionSide side_before = position_side_; + double qty_before = position_qty_; + const double broker_price = + coof_scheduler_active_ && std::isfinite(coof_cursor_price_) + ? coof_cursor_price_ : current_bar_.close; if (closes_full_position) { const bool closed_long = (position_side_ == PositionSide::LONG); // Exit-order cancel/purge already ran at CALL time in // enqueue_same_bar_close — orders armed after the close call // must survive exactly as they did under the immediate path. - execute_market_exit(current_bar_.close); + execute_market_exit(broker_price); if (position_side_ == PositionSide::FLAT) { cancel_same_bar_market_reentries_after_full_close(closed_long); } } else { - execute_partial_exit_qty(current_bar_.close, target); + execute_partial_exit_qty(broker_price, target); if (position_side_ == PositionSide::FLAT) { // Retain from_entry brackets whose parent entry is still a // pending order (it fills right after this flush). @@ -469,6 +513,14 @@ void BacktestEngine::flush_same_bar_close() { trades_[ti].exit_comment = comment; trades_[ti].exit_id = "__close__" + id; } + if (position_side_ != side_before + || std::abs(position_qty_ - qty_before) > eps + || trades_.size() != trades_before) { + ++broker_fill_event_seq_; + if (coof_scheduler_active_ && coof_direct_fill_events_remaining_ > 0) { + --coof_direct_fill_events_remaining_; + } + } if (!sole_call && position_side_ != PositionSide::FLAT) { // Surviving multi-call close: ledger NOT consumed (TV leaves the // id closable again later); the filled amount stays reserved @@ -658,6 +710,23 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro order.oca_type = oca_name.empty() ? 0 : 1; // strategy.exit semantics: cancel order.created_bar = bar_index_; order.created_seq = preserved_seq > 0 ? preserved_seq : next_order_seq_++; + order.created_during_coof_recalc = coof_fill_recalc_active_; + order.coof_born_at_close_recalc = + coof_fill_recalc_active_ && coof_cursor_is_bar_close_; + if (coof_fill_recalc_active_ && coof_scheduler_active_ + && std::isfinite(coof_cursor_price_) + && position_side_ != PositionSide::FLAT + && position_open_bar_ == bar_index_) { + const bool closing_long = position_side_ == PositionSide::LONG; + const bool stop_marketable = !std::isnan(stop_price) + && (closing_long ? coof_cursor_price_ <= stop_price + : coof_cursor_price_ >= stop_price); + const bool limit_marketable = !std::isnan(limit_price) + && (closing_long ? coof_cursor_price_ >= limit_price + : coof_cursor_price_ <= limit_price); + order.coof_suppress_stop_on_entry_bar = stop_marketable; + order.coof_suppress_limit_on_entry_bar = limit_marketable; + } // Position-derived captures use the post-batched-close view (see // live_pos_qty above) so an exit armed after a same-bar strategy.close // records the same state it did when the close executed mid-bar. @@ -712,6 +781,9 @@ void BacktestEngine::strategy_order(const std::string& id, bool is_long, double order.oca_type = oca_type; order.created_bar = bar_index_; order.created_seq = preserved_seq > 0 ? preserved_seq : next_order_seq_++; + order.created_during_coof_recalc = coof_fill_recalc_active_; + order.coof_born_at_close_recalc = + coof_fill_recalc_active_ && coof_cursor_is_bar_close_; order.created_position_side = position_side_; order.created_after_position_close_in_bar = pending_close_qty_in_bar_ > kQtyEpsilon; @@ -908,21 +980,26 @@ void BacktestEngine::execute_immediate_close(const std::string& id, bool closes_any_qty) { const double eps = kQtyEpsilon; size_t trades_before = trades_.size(); + PositionSide side_before = position_side_; + double qty_before = position_qty_; + const double broker_price = + coof_scheduler_active_ && std::isfinite(coof_cursor_price_) + ? coof_cursor_price_ : current_bar_.close; if (closes_full_position) { const bool closed_long = (position_side_ == PositionSide::LONG); - execute_market_exit(current_bar_.close); + execute_market_exit(broker_price); purge_exit_orders(); if (position_side_ == PositionSide::FLAT) { cancel_same_bar_market_reentries_after_full_close(closed_long); } } else if (closes_fifo_qty) { - execute_partial_exit_qty(current_bar_.close, qty_to_close); + execute_partial_exit_qty(broker_price, qty_to_close); if (position_side_ == PositionSide::FLAT) { purge_exit_orders(); } } else if (closes_any_qty) { double pct = matching_qty > eps ? (qty_to_close / matching_qty) * 100.0 : 100.0; - execute_partial_exit_by_entry_percent(current_bar_.close, id, pct); + execute_partial_exit_by_entry_percent(broker_price, id, pct); if (position_side_ == PositionSide::FLAT) { purge_exit_orders(); } @@ -931,6 +1008,14 @@ void BacktestEngine::execute_immediate_close(const std::string& id, trades_[ti].exit_comment = comment; trades_[ti].exit_id = "__close__" + id; } + if (position_side_ != side_before + || std::abs(position_qty_ - qty_before) > eps + || trades_.size() != trades_before) { + ++broker_fill_event_seq_; + if (coof_scheduler_active_ && coof_direct_fill_events_remaining_ > 0) { + --coof_direct_fill_events_remaining_; + } + } } // Build the deferred EXIT pending order representing this close, to @@ -968,6 +1053,9 @@ void BacktestEngine::queue_deferred_close_order(const std::string& id, order.oca_type = 0; order.created_bar = bar_index_; order.created_seq = next_order_seq_++; + order.created_during_coof_recalc = coof_fill_recalc_active_; + order.coof_born_at_close_recalc = + coof_fill_recalc_active_ && coof_cursor_is_bar_close_; order.created_position_side = position_side_; order.tv_carry_qty = position_qty_; order.comment = comment; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 20990f2..f058d95 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -82,6 +82,7 @@ set(TEST_SOURCES test_drawing test_margin_call test_streaming + test_calc_on_order_fills ) find_package(Threads REQUIRED) diff --git a/tests/test_calc_on_order_fills.cpp b/tests/test_calc_on_order_fills.cpp new file mode 100644 index 0000000..a4a5c66 --- /dev/null +++ b/tests/test_calc_on_order_fills.cpp @@ -0,0 +1,1547 @@ +/* + * KI-60: calc_on_order_fills historical broker scheduling. + * + * These fixtures intentionally exercise the semantic seams that a broad + * "run on_bar again after process_pending_orders" loop misses: + * - one broker fill per recalc, with a monotonic O -> near -> far -> C path; + * - the four historical fill-event budget (including exits, not just opens); + * - orders born in a recalc can only inspect the current/remaining path; + * - process_orders_on_close fills recalc at C without replaying the wick; + * - historical recalc executions expose barstate.isnew/isconfirmed together; + * - script state rolls back to the committed checkpoint, broker state does not; + * - the flag-off path and an explicit false override retain legacy behaviour. + */ + +#include +#include +#include +#include +#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) + +namespace { + +constexpr double kNaN = std::numeric_limits::quiet_NaN(); + +bool near(double a, double b, double eps = 1e-9) { + return std::fabs(a - b) <= eps; +} + +std::vector standard_feed() { + return { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 110.0, 90.0, 105.0, 1000.0, 1'800'000}, + {105.0, 106.0, 104.0, 105.0, 1000.0, 2'700'000}, + }; +} + +class CoofBase : public BacktestEngine { +public: + explicit CoofBase(bool enabled = true) { + calc_on_order_fills_ = enabled; + initial_capital_ = 100'000.0; + default_qty_type_ = QtyType::FIXED; + default_qty_value_ = 1.0; + pyramiding_ = 10; + slippage_ = 0; + commission_value_ = 0.0; + } + + double signed_size() const { return signed_position_size(); } + int open_lot_count() const { return static_cast(pyramid_entries_.size()); } + std::vector open_lot_prices() const { + std::vector out; + for (const auto& lot : pyramid_entries_) out.push_back(lot.price); + return out; + } + std::vector open_lot_ids() const { + std::vector out; + for (const auto& lot : pyramid_entries_) out.push_back(lot.entry_id); + return out; + } + bool coof_enabled() const { return calc_on_order_fills_; } +}; + +// Q1 TV pin: a carried market entry fills at O, its post-fill strategy.close +// fills on that same historical bar at the same price. +class MarketCloseProbe final : public CoofBase { +public: + using CoofBase::CoofBase; + + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT + && trades_.empty()) { + strategy_entry("L", true); + } else if (position_side_ == PositionSide::LONG) { + strategy_close("L"); + } + } +}; + +void test_market_close_fills_same_bar_at_entry_price() { + std::printf("test_market_close_fills_same_bar_at_entry_price\n"); + MarketCloseProbe p; + auto bars = standard_feed(); + p.run(bars.data(), static_cast(bars.size())); + + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(t.entry_bar_index == 1); + CHECK(t.exit_bar_index == 1); + CHECK(near(t.entry_price, 100.0)); + CHECK(near(t.exit_price, 100.0)); + } +} + +// Q3 TV pin: the bracket does not exist until the entry-fill recalc. Its stop +// must become live for the REMAINING path and fill at its level, not at the +// later endpoint and not on the following bar. +class BracketProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT + && trades_.empty()) { + strategy_entry("L", true); + } + if (position_side_ == PositionSide::LONG) { + strategy_exit("X", "L", kNaN, 99.0); + } + } +}; + +void test_recalc_bracket_uses_remaining_path() { + std::printf("test_recalc_bracket_uses_remaining_path\n"); + BracketProbe p; + auto bars = standard_feed(); + p.run(bars.data(), 2); + + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(t.entry_bar_index == 1); + CHECK(t.exit_bar_index == 1); + CHECK(near(t.entry_price, 100.0)); + CHECK(near(t.exit_price, 99.0)); + } + + // The same contract on real lower-TF magnifier data: endpoint count and + // termination come from the supplied lower bars (4 OHLC ticks each), and + // the recalc-created stop sees only endpoints after the entry fill. + BracketProbe magnified; + Bar lower[] = { + {100.0, 101.0, 99.0, 100.0, 500.0, 60'000}, + {100.0, 101.0, 99.0, 100.0, 500.0, 120'000}, + {100.0, 102.0, 98.0, 101.0, 500.0, 180'000}, + {101.0, 103.0, 100.0, 102.0, 500.0, 240'000}, + }; + magnified.run(lower, 4, "1", "2", /*bar_magnifier=*/true, + /*magnifier_samples=*/4, + MagnifierDistribution::ENDPOINTS); + CHECK(magnified.last_error().empty()); + CHECK(magnified.trade_count() == 1); + if (magnified.trade_count() == 1) { + const Trade& t = magnified.get_trade(0); + CHECK(t.entry_bar_index == 1); + CHECK(t.exit_bar_index == 1); + CHECK(near(t.entry_price, 100.0)); + CHECK(near(t.exit_price, 99.0)); + } +} + +// Q2 TV pin: a historical non-magnified bar supplies four broker fill events. +// A carried market order and the first recalc order both execute at O; later +// recalc orders advance monotonically to the near and far endpoints. For this +// tie-distance bar the standard path is O -> L -> H -> C, so use a high-near +// bar below to pin the exported O,O,H,L sequence exactly. +class RefillProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ <= 1 && position_qty_ < 6.0) { + strategy_entry("L" + std::to_string(position_entry_count_), true); + } + } +}; + +void test_historical_refill_is_exact_o_o_near_far_and_capped_at_four() { + std::printf("test_historical_refill_is_exact_o_o_near_far_and_capped_at_four\n"); + RefillProbe p; + // |H-O|=1 < |O-L|=10 => O -> H -> L -> C. + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 101.0, 90.0, 95.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + CHECK(p.open_lot_count() == 4); + const std::vector px = p.open_lot_prices(); + CHECK(px.size() == 4); + if (px.size() == 4) { + CHECK(near(px[0], 100.0)); + CHECK(near(px[1], 100.0)); + CHECK(near(px[2], 101.0)); + CHECK(near(px[3], 90.0)); + } + + // Real lower-TF magnifier data supplies 60 endpoint ticks for the second + // script bar (15 lower bars x O/H/L/C), so the six-unit strategy cap—not a + // hard-coded four/16-iteration loop—must become the binding limit. + RefillProbe magnified; + std::vector lower; + lower.reserve(30); + for (int i = 0; i < 30; ++i) { + const double o = (i < 15) ? 100.0 : 100.0 + (i - 15) * 0.1; + lower.push_back({o, o + 1.0, o - 1.0, o + 0.25, + 500.0, static_cast(i) * 60'000}); + } + magnified.run(lower.data(), static_cast(lower.size()), + "1", "15", /*bar_magnifier=*/true, + /*magnifier_samples=*/4, + MagnifierDistribution::ENDPOINTS); + CHECK(magnified.last_error().empty()); + CHECK(magnified.open_lot_count() == 6); +} + +// Only the historical bar's O has the documented same-point two-fill +// exception. When a resting priced entry lands exactly on H/L, that endpoint +// is consumed before its fill recalc runs; a recalc-born market add must wait +// for the NEXT waypoint/tick even when the fill price equals the endpoint. +class EndpointMarketAddProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT) { + strategy_entry("Stop", true, kNaN, 105.0); + } else if (bar_index_ == 1 && coof_fill_recalc_active_ + && position_entry_count_ == 1) { + strategy_entry("Add", true); + } + } +}; + +void test_non_open_endpoint_fill_consumes_point_before_market_add() { + std::printf( + "test_non_open_endpoint_fill_consumes_point_before_market_add\n"); + EndpointMarketAddProbe p; + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 105.0, 90.0, 100.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + const std::vector px = p.open_lot_prices(); + CHECK(px.size() == 2); + if (px.size() == 2) { + CHECK(near(px[0], 105.0)); + CHECK(near(px[1], 90.0)); + } +} + +void test_magnifier_endpoint_fill_consumes_tick_before_market_add() { + std::printf( + "test_magnifier_endpoint_fill_consumes_tick_before_market_add\n"); + EndpointMarketAddProbe p; + Bar lower[] = { + {100.0, 101.0, 99.0, 100.0, 500.0, 0}, + {100.0, 101.0, 99.0, 100.0, 500.0, 60'000}, + {100.0, 105.0, 90.0, 100.0, 500.0, 120'000}, + {100.0, 101.0, 99.0, 100.0, 500.0, 180'000}, + }; + p.run(lower, 4, "1", "2", /*bar_magnifier=*/true, + /*magnifier_samples=*/4, MagnifierDistribution::ENDPOINTS); + + CHECK(p.last_error().empty()); + const std::vector px = p.open_lot_prices(); + CHECK(px.size() == 2); + if (px.size() == 2) { + CHECK(near(px[0], 105.0)); + CHECK(near(px[1], 90.0)); + } +} + +// Real lower-timeframe bars are distinct broker epochs. A gap from one +// sub-bar's close to the next sub-bar's open is not a traversed price segment: +// a resting limit crossed by that gap fills at the new open, never at an +// interpolated price inside the gap. The non-COOF magnifier path already +// preserves this boundary; this fixture pins the COOF scheduler to the same +// contract. +class MagnifierGapBoundaryProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT + && pending_orders_.empty() && trades_.empty()) { + strategy_entry("GapLimit", true, 95.0, kNaN, 1.0); + } + } +}; + +void test_real_magnifier_gap_fills_limit_at_fresh_subbar_open() { + std::printf( + "test_real_magnifier_gap_fills_limit_at_fresh_subbar_open\n"); + MagnifierGapBoundaryProbe p; + Bar lower[] = { + // Script bar 0: place the carried 95 limit at the completed close. + {100.0, 101.0, 99.0, 100.0, 1000.0, 0}, + {100.0, 101.0, 99.0, 100.0, 1000.0, 60'000}, + // Script bar 1: first sub-bar stays above 95; the second gaps to 90. + {100.0, 101.0, 99.0, 100.0, 1000.0, 120'000}, + { 90.0, 92.0, 88.0, 91.0, 1000.0, 180'000}, + }; + p.run(lower, 4, "1", "2", /*bar_magnifier=*/true, + /*magnifier_samples=*/4, MagnifierDistribution::ENDPOINTS); + + CHECK(p.last_error().empty()); + CHECK(near(p.signed_size(), 1.0)); + CHECK(p.open_lot_count() == 1); + if (p.open_lot_count() == 1) { + CHECK(near(p.open_lot_prices().front(), 90.0)); + } +} + +class MagnifierGapStopBoundaryProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT + && pending_orders_.empty() && trades_.empty()) { + strategy_entry("GapStop", true, kNaN, 105.0, 1.0); + } + } +}; + +void test_real_magnifier_gap_fills_stop_at_fresh_subbar_open() { + std::printf( + "test_real_magnifier_gap_fills_stop_at_fresh_subbar_open\n"); + MagnifierGapStopBoundaryProbe p; + Bar lower[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 0}, + {100.0, 101.0, 99.0, 100.0, 1000.0, 60'000}, + {100.0, 104.0, 99.0, 100.0, 1000.0, 120'000}, + {110.0, 112.0,108.0, 111.0, 1000.0, 180'000}, + }; + p.run(lower, 4, "1", "2", /*bar_magnifier=*/true, + /*magnifier_samples=*/4, MagnifierDistribution::ENDPOINTS); + + CHECK(p.last_error().empty()); + CHECK(near(p.signed_size(), 1.0)); + CHECK(p.open_lot_count() == 1); + if (p.open_lot_count() == 1) { + CHECK(near(p.open_lot_prices().front(), 110.0)); + } +} + +// Mutation killer for termination counters that count only entries (or only +// newly-created trade rows). Entry and market-close fills must each consume an +// event. Four events produce exactly two round trips: O/O then H/L. +class AlternatingFillKindsProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0 && trades_.empty() + && position_side_ == PositionSide::FLAT) { + strategy_entry("L0", true); + return; + } + if (bar_index_ != 1) return; + if (position_side_ == PositionSide::LONG) { + strategy_close(pyramid_entries_.front().entry_id); + } else if (trades_.size() < 2) { + strategy_entry("L" + std::to_string(trades_.size() + 1), true); + } + } +}; + +void test_exit_fills_consume_historical_event_budget() { + std::printf("test_exit_fills_consume_historical_event_budget\n"); + AlternatingFillKindsProbe p; + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 101.0, 90.0, 95.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == 2); + if (p.trade_count() == 2) { + CHECK(near(p.get_trade(0).entry_price, 100.0)); + CHECK(near(p.get_trade(0).exit_price, 100.0)); + CHECK(near(p.get_trade(1).entry_price, 101.0)); + CHECK(near(p.get_trade(1).exit_price, 90.0)); + } + CHECK(near(p.signed_size(), 0.0)); +} + +// A source-order scan is not a chronological scheduler. Both resting buy +// stops are touched on the same rising segment, but the farther stop was +// created first. TV fills Near@105 before Far@108; after the first fill the +// cursor must continue from 105 so the farther trigger remains reachable. +class RestingPricedChronologyProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0) { + strategy_entry("Far", true, kNaN, 108.0); + strategy_entry("Near", true, kNaN, 105.0); + } + } +}; + +void test_same_segment_priced_orders_fill_nearest_first() { + std::printf("test_same_segment_priced_orders_fill_nearest_first\n"); + RestingPricedChronologyProbe p; + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 110.0, 99.0, 100.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + const auto ids = p.open_lot_ids(); + const auto px = p.open_lot_prices(); + CHECK(ids.size() == 2); + CHECK(px.size() == 2); + if (ids.size() == 2 && px.size() == 2) { + CHECK(ids[0] == "Near"); + CHECK(ids[1] == "Far"); + CHECK(near(px[0], 105.0)); + CHECK(near(px[1], 108.0)); + } +} + +// Stop-limit activation is broker state, not a property that can be +// reconstructed from each shortened scheduler segment. A activates on O->H; +// B fills first on H->L; resuming from B@100 must retain A's activation so its +// limit can fill later at 95. +class StopLimitActivationProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0) { + strategy_entry("A", true, 95.0, 105.0); + strategy_entry("B", true, 100.0, kNaN); + } + } +}; + +void test_stop_limit_activation_survives_segment_split() { + std::printf("test_stop_limit_activation_survives_segment_split\n"); + StopLimitActivationProbe p; + Bar bars[] = { + {102.0, 103.0, 101.0, 102.0, 1000.0, 900'000}, + {102.0, 110.0, 90.0, 100.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + const auto ids = p.open_lot_ids(); + const auto px = p.open_lot_prices(); + CHECK(ids.size() == 2); + CHECK(px.size() == 2); + if (ids.size() == 2 && px.size() == 2) { + CHECK(ids[0] == "B"); + CHECK(ids[1] == "A"); + CHECK(near(px[0], 100.0)); + CHECK(near(px[1], 95.0)); + } +} + +// Candidate discovery may look beyond the broker cursor, but stop-limit +// activation cannot. A's stop sits beyond the last event-budget cursor (105); +// scanning the full O->H segment must not pre-arm it and allow its 95 limit to +// fill on a later bar that never touches the 108 stop. +class StopLimitSpeculationProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0) { + strategy_entry("M0", true); + strategy_entry("M1", true); + strategy_entry("A", true, 95.0, 108.0); + strategy_entry("B103", true, kNaN, 103.0); + strategy_entry("B105", true, kNaN, 105.0); + } + } +}; + +void test_stop_limit_activation_commits_only_through_consumed_cursor() { + std::printf("test_stop_limit_activation_commits_only_through_consumed_cursor\n"); + StopLimitSpeculationProbe p; + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 110.0, 85.0, 100.0, 1000.0, 1'800'000}, + {100.0, 104.0, 90.0, 95.0, 1000.0, 2'700'000}, + }; + p.run(bars, 3); + + CHECK(p.last_error().empty()); + const auto ids = p.open_lot_ids(); + CHECK(ids.size() == 4); + if (ids.size() == 4) { + CHECK(ids[0] == "M0"); + CHECK(ids[1] == "M1"); + CHECK(ids[2] == "B103"); + CHECK(ids[3] == "B105"); + } +} + +// The legacy one-priced-entry-per-bar throttle predates COOF. A priced entry +// born in a fill recalc belongs to the new broker epoch and may itself fill, +// recalc, and place another priced entry on the remaining same-bar segment. +class RecalcPricedEntryCascadeProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT) { + strategy_entry("L0", true); + } else if (bar_index_ == 1 && position_entry_count_ == 1) { + strategy_entry("L1", true, kNaN, 105.0); + } else if (bar_index_ == 1 && position_entry_count_ == 2) { + strategy_entry("L2", true, kNaN, 108.0); + } + } +}; + +void test_fill_recalc_priced_entries_bypass_legacy_bar_throttle() { + std::printf("test_fill_recalc_priced_entries_bypass_legacy_bar_throttle\n"); + RecalcPricedEntryCascadeProbe p; + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 110.0, 90.0, 100.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + const auto ids = p.open_lot_ids(); + const auto px = p.open_lot_prices(); + CHECK(ids.size() == 3); + CHECK(px.size() == 3); + if (ids.size() == 3 && px.size() == 3) { + CHECK(ids[0] == "L0"); + CHECK(ids[1] == "L1"); + CHECK(ids[2] == "L2"); + CHECK(near(px[0], 100.0)); + CHECK(near(px[1], 105.0)); + CHECK(near(px[2], 108.0)); + } +} + +// Recalc origin is an event epoch, not a permanent exemption. A stop emitted +// by a prior bar's fill recalc and carried overnight must re-enter the legacy +// one-priced-entry-per-bar arbitration on the later bar. +class RecalcPricedCarryThrottleProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT) { + strategy_entry("L0", true); + } else if (bar_index_ == 1 && coof_fill_recalc_active_ + && position_entry_count_ == 1) { + strategy_entry("Carry", true, kNaN, 108.0); + } else if (bar_index_ == 1 && !coof_fill_recalc_active_ + && position_entry_count_ == 1) { + strategy_entry("First", true, kNaN, 105.0); + } + } +}; + +void test_recalc_priced_entry_exemption_expires_after_creation_bar() { + std::printf("test_recalc_priced_entry_exemption_expires_after_creation_bar\n"); + RecalcPricedCarryThrottleProbe p; + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 106.0, 90.0, 100.0, 1000.0, 1'800'000}, + {100.0, 110.0, 85.0, 100.0, 1000.0, 2'700'000}, + }; + p.run(bars, 3); + + CHECK(p.last_error().empty()); + const auto ids = p.open_lot_ids(); + CHECK(ids.size() == 2); + if (ids.size() == 2) { + CHECK(ids[0] == "L0"); + CHECK(ids[1] == "First"); + } +} + +// A full close's stale-order cancellation belongs to the position cycle it +// ended. Once New0 opens a fresh cycle, New1/New2 emitted by its recalcs must +// not be mistaken for adds attached to the old closed long merely because all +// events share one historical bar. +class CloseReopenCycleProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT) { + strategy_entry("Old", true); + return; + } + if (bar_index_ == 1 && position_side_ == PositionSide::LONG + && !coof_fill_recalc_active_) { + strategy_close("Old"); + return; + } + if (bar_index_ != 2) return; + if (position_side_ == PositionSide::FLAT) { + strategy_entry("New0", true); + } else if (position_entry_count_ < 3) { + strategy_entry("New" + std::to_string(position_entry_count_), true); + } + } +}; + +void test_close_cleanup_does_not_leak_into_new_position_cycle() { + std::printf("test_close_cleanup_does_not_leak_into_new_position_cycle\n"); + CloseReopenCycleProbe p; + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 101.0, 99.0, 100.0, 1000.0, 1'800'000}, + {100.0, 105.0, 90.0, 95.0, 1000.0, 2'700'000}, + }; + p.run(bars, 3); + + CHECK(p.last_error().empty()); + const auto ids = p.open_lot_ids(); + const auto px = p.open_lot_prices(); + CHECK(ids.size() == 3); + CHECK(px.size() == 3); + if (ids.size() == 3 && px.size() == 3) { + CHECK(ids[0] == "New0"); + CHECK(ids[1] == "New1"); + CHECK(ids[2] == "New2"); + CHECK(near(px[0], 100.0)); + CHECK(near(px[1], 105.0)); + CHECK(near(px[2], 90.0)); + } +} + +// A COOF-created bracket may contain one leg that is already marketable at the +// entry-fill cursor. TradingView suppresses only that wrong-side leg for the +// entry bar: it carries into the next bar, while a correctly-sided sibling +// remains eligible on the entry bar's remaining path. +class RecalcEntryBarBracketProbe final : public CoofBase { +public: + enum class Shape { + WRONG_STOP_ONLY, + WRONG_LIMIT_ONLY, + WRONG_STOP_VALID_LIMIT, + VALID_STOP_WRONG_LIMIT, + }; + + explicit RecalcEntryBarBracketProbe(Shape shape) : shape_(shape) {} + + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT) { + strategy_entry("L", true); + } else if (bar_index_ == 1 && position_side_ == PositionSide::LONG + && coof_fill_recalc_active_) { + switch (shape_) { + case Shape::WRONG_STOP_ONLY: + strategy_exit("X", "L", kNaN, 105.0); + break; + case Shape::WRONG_LIMIT_ONLY: + strategy_exit("X", "L", 95.0, kNaN); + break; + case Shape::WRONG_STOP_VALID_LIMIT: + strategy_exit("X", "L", 110.0, 105.0); + break; + case Shape::VALID_STOP_WRONG_LIMIT: + strategy_exit("X", "L", 90.0, 95.0); + break; + } + } + } + +private: + Shape shape_; +}; + +void test_recalc_wrong_side_entry_bar_legs_carry_to_next_bar() { + std::printf("test_recalc_wrong_side_entry_bar_legs_carry_to_next_bar\n"); + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 105.0, 90.0, 95.0, 1000.0, 1'800'000}, + {104.0, 106.0, 103.0, 105.0, 1000.0, 2'700'000}, + }; + + for (auto shape : { + RecalcEntryBarBracketProbe::Shape::WRONG_STOP_ONLY, + RecalcEntryBarBracketProbe::Shape::WRONG_LIMIT_ONLY, + }) { + RecalcEntryBarBracketProbe p(shape); + p.run(bars, 3); + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(near(t.entry_price, 100.0)); + CHECK(near(t.exit_price, 104.0)); + CHECK(t.entry_bar_index == 1); + CHECK(t.exit_bar_index == 2); + } + } +} + +void test_recalc_wrong_stop_does_not_hide_valid_limit_leg() { + std::printf("test_recalc_wrong_stop_does_not_hide_valid_limit_leg\n"); + RecalcEntryBarBracketProbe p( + RecalcEntryBarBracketProbe::Shape::WRONG_STOP_VALID_LIMIT); + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 112.0, 90.0, 100.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(near(t.entry_price, 100.0)); + CHECK(near(t.exit_price, 110.0)); + CHECK(t.entry_bar_index == 1); + CHECK(t.exit_bar_index == 1); + } +} + +void test_recalc_wrong_limit_does_not_hide_valid_stop_leg() { + std::printf("test_recalc_wrong_limit_does_not_hide_valid_stop_leg\n"); + RecalcEntryBarBracketProbe p( + RecalcEntryBarBracketProbe::Shape::VALID_STOP_WRONG_LIMIT); + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 106.0, 90.0, 100.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(near(t.entry_price, 100.0)); + CHECK(near(t.exit_price, 95.0)); + CHECK(t.entry_bar_index == 1); + CHECK(t.exit_bar_index == 1); + } +} + +// A first fill that occurs inside an OHLC path segment is not a second broker +// tick at that price. A market entry created by its COOF recalc fills at the +// segment's next waypoint. This is distinct from the bar-open exception where +// a carried market fill and the first order it creates may both consume O. +// +// The second short deliberately inherits an already-marketable buy-limit. Its +// entry must be L=90, the limit must remain dormant for that entry bar, and the +// carried limit must exit at 95 on the next bar. +class InteriorExitReentryCarryProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT) { + strategy_entry("S0", false); + return; + } + + if (position_side_ == PositionSide::SHORT) { + if (position_open_bar_ == 1) { + strategy_exit("X0", "S0", 95.0, kNaN); + } else if (position_open_bar_ == 2) { + strategy_exit("X1", "S1", 95.0, kNaN); + } + return; + } + + if (bar_index_ == 2 && coof_fill_recalc_active_) { + strategy_entry("S1", false); + } + } +}; + +void test_interior_fill_recalc_market_entry_waits_for_next_waypoint() { + std::printf("test_interior_fill_recalc_market_entry_waits_for_next_waypoint\n"); + InteriorExitReentryCarryProbe p; + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 102.0, 98.0, 101.0, 1000.0, 1'800'000}, + {100.0, 105.0, 90.0, 92.0, 1000.0, 2'700'000}, + {100.0, 101.0, 90.0, 96.0, 1000.0, 3'600'000}, + }; + p.run(bars, 4); + + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == 2); + if (p.trade_count() == 2) { + const Trade& first = p.get_trade(0); + CHECK(near(first.entry_price, 100.0)); + CHECK(near(first.exit_price, 95.0)); + CHECK(first.entry_bar_index == 1); + CHECK(first.exit_bar_index == 2); + + const Trade& carried = p.get_trade(1); + CHECK(near(carried.entry_price, 90.0)); + CHECK(near(carried.exit_price, 95.0)); + CHECK(carried.entry_bar_index == 2); + CHECK(carried.exit_bar_index == 3); + } +} + +// process_orders_on_close grants the same-tick close shortcut only at the +// bar's actual C execution. At an intrabar fill-recalc cursor, an ordinary +// close waits for the next waypoint; immediately=true remains selective and +// executes at the current cursor. +class PoocCursorTimingProbe final : public CoofBase { +public: + explicit PoocCursorTimingProbe(bool immediate) : immediate_(immediate) { + process_orders_on_close_ = true; + } + + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT) { + strategy_entry("A", true, kNaN, 100.0); + return; + } + if (bar_index_ != 1) return; + if (position_entry_count_ == 1) { + strategy_entry("B", true); + } else if (position_entry_count_ == 2) { + strategy_close("", "", kNaN, kNaN, immediate_); + } + } + +private: + bool immediate_; +}; + +void test_pooc_same_tick_requires_close_cursor_or_immediately() { + std::printf("test_pooc_same_tick_requires_close_cursor_or_immediately\n"); + Bar bars[] = { + {90.0, 95.0, 85.0, 90.0, 1000.0, 900'000}, + {100.0, 105.0, 90.0, 95.0, 1000.0, 1'800'000}, + }; + + PoocCursorTimingProbe ordinary(false); + ordinary.run(bars, 2); + CHECK(ordinary.last_error().empty()); + CHECK(ordinary.trade_count() == 2); + if (ordinary.trade_count() == 2) { + CHECK(near(ordinary.get_trade(0).exit_price, 105.0)); + CHECK(near(ordinary.get_trade(1).exit_price, 105.0)); + } + + PoocCursorTimingProbe immediate(true); + immediate.run(bars, 2); + CHECK(immediate.last_error().empty()); + CHECK(immediate.trade_count() == 2); + if (immediate.trade_count() == 2) { + CHECK(near(immediate.get_trade(0).exit_price, 100.0)); + CHECK(near(immediate.get_trade(1).exit_price, 100.0)); + } +} + +// POOC's legacy same-bar priced-order carve-out applies to orders created by +// the ordinary CLOSE execution. A bracket born in an INTRABAR fill recalc is +// already live at the broker cursor and must use normal remaining-path trigger +// semantics, not the close endpoint's gap/reissue pricing. +class PoocIntrabarBracketProbe final : public CoofBase { +public: + enum class Leg { STOP, LIMIT }; + explicit PoocIntrabarBracketProbe(Leg leg) : leg_(leg) { + process_orders_on_close_ = true; + } + + void on_bar(const Bar&) override { + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT + && trades_.empty()) { + strategy_entry("L", true, kNaN, 105.0); + } else if (position_side_ == PositionSide::LONG) { + if (leg_ == Leg::STOP) { + strategy_exit("X", "L", kNaN, 102.0); + } else { + strategy_exit("X", "L", 109.0, kNaN); + } + } + } + +private: + Leg leg_; +}; + +void test_pooc_intrabar_recalc_priced_order_uses_remaining_path() { + std::printf("test_pooc_intrabar_recalc_priced_order_uses_remaining_path\n"); + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 110.0, 90.0, 100.0, 1000.0, 1'800'000}, + }; + + PoocIntrabarBracketProbe stop(PoocIntrabarBracketProbe::Leg::STOP); + stop.run(bars, 2); + CHECK(stop.last_error().empty()); + CHECK(stop.trade_count() == 1); + if (stop.trade_count() == 1) { + CHECK(near(stop.get_trade(0).entry_price, 105.0)); + CHECK(near(stop.get_trade(0).exit_price, 102.0)); + CHECK(stop.get_trade(0).entry_bar_index == stop.get_trade(0).exit_bar_index); + } + + PoocIntrabarBracketProbe limit(PoocIntrabarBracketProbe::Leg::LIMIT); + limit.run(bars, 2); + CHECK(limit.last_error().empty()); + CHECK(limit.trade_count() == 1); + if (limit.trade_count() == 1) { + CHECK(near(limit.get_trade(0).entry_price, 105.0)); + CHECK(near(limit.get_trade(0).exit_price, 109.0)); + CHECK(limit.get_trade(0).entry_bar_index == limit.get_trade(0).exit_bar_index); + } +} + +// Generated classes own the concrete deep-copy representation. This manual +// analogue pins the engine's lifecycle: snapshot once; restore before every +// historical execution; commit only the last execution. Script state rolls +// back, while position/trades/orders remain live across recalc executions. +class RollbackProbe final : public CoofBase { +public: + int script_scalar = 0; + Series script_series{32}; + std::vector script_collection; + + int snapshot_calls = 0; + int restore_calls = 0; + int commit_calls = 0; + std::vector scalar_before_body; + std::vector body_bar; + std::vector body_isnew; + std::vector body_isconfirmed; + + void on_bar(const Bar&) override { + scalar_before_body.push_back(script_scalar); + body_bar.push_back(bar_index_); + body_isnew.push_back(is_first_tick_); + body_isconfirmed.push_back(is_last_tick_); + + ++script_scalar; + if (history_advances_new_bar()) script_series.push(script_scalar); + else script_series.update(script_scalar); + script_collection.push_back(bar_index_); + + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT + && trades_.empty()) { + strategy_entry("L", true); + } else if (position_side_ == PositionSide::LONG) { + strategy_close("L"); + } + } + +protected: + void snapshot_script_state() override { + ++snapshot_calls; + checkpoint_scalar_ = script_scalar; + checkpoint_series_ = script_series; + checkpoint_collection_ = script_collection; + } + + void restore_script_state() override { + ++restore_calls; + script_scalar = checkpoint_scalar_; + script_series = checkpoint_series_; + script_collection = checkpoint_collection_; + } + + void commit_script_state() override { + ++commit_calls; + checkpoint_scalar_ = script_scalar; + checkpoint_series_ = script_series; + checkpoint_collection_ = script_collection; + } + +private: + int checkpoint_scalar_ = 0; + Series checkpoint_series_{32}; + std::vector checkpoint_collection_; +}; + +void test_historical_barstate_and_committed_state_rollback_hooks() { + std::printf("test_historical_barstate_and_committed_state_rollback_hooks\n"); + RollbackProbe p; + auto bars = standard_feed(); + p.run(bars.data(), 2); + + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == 1); // broker state persisted through rollback + CHECK(p.snapshot_calls == 2); + CHECK(p.commit_calls == 2); + // Every script execution restores its starting checkpoint. The repaired + // scheduler additionally restores the completed ordinary-close checkpoint + // after post-C recalcs so speculative C state cannot become live state. + CHECK(p.restore_calls == static_cast(p.body_bar.size()) + 2); + + // Only one committed mutation per historical bar survives. + CHECK(p.script_scalar == 2); + CHECK(p.script_series.size() == 2); + CHECK(p.script_series[0] == 2); + CHECK(p.script_series[1] == 1); + CHECK(p.script_collection.size() == 2); + if (p.script_collection.size() == 2) { + CHECK(p.script_collection[0] == 0); + CHECK(p.script_collection[1] == 1); + } + + int bar1_executions = 0; + for (std::size_t i = 0; i < p.body_bar.size(); ++i) { + CHECK(p.body_isnew[i]); + CHECK(p.body_isconfirmed[i]); + if (p.body_bar[i] == 1) { + ++bar1_executions; + CHECK(p.scalar_before_body[i] == 1); + } + } + CHECK(bar1_executions == 3); // entry fill, close fill, final close calc +} + +// A fill produced by the ordinary process_orders_on_close pass occurs at the +// historical bar's terminal C tick. There is no later broker tick on which to +// run a fill-triggered body for that bar. In particular, such a body must not +// create a priced order that wakes over the next bar before its ordinary close +// execution can issue the durable order. This is the Fran470 production shape. +class PoocTerminalBracketProbe final : public CoofBase { +public: + explicit PoocTerminalBracketProbe(bool is_long) : is_long_(is_long) { + process_orders_on_close_ = true; + pyramiding_ = 0; + } + + void on_bar(const Bar&) override { + const std::string entry_id = is_long_ ? "L" : "S"; + if (coof_fill_recalc_active_ && coof_cursor_is_bar_close_) { + ++terminal_recalc_calls; + if (position_side_ != PositionSide::FLAT) { + strategy_exit("X", entry_id, + is_long_ ? 105.0 : 95.0, kNaN); + } + return; + } + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT + && trades_.empty()) { + strategy_entry(entry_id, is_long_); + } else if (bar_index_ == 1 + && position_side_ != PositionSide::FLAT) { + strategy_exit("X", entry_id, kNaN, + is_long_ ? 95.0 : 105.0); + } + } + + int terminal_recalc_calls = 0; + +private: + bool is_long_; +}; + +void test_pooc_terminal_close_fill_has_no_recalc_or_c_born_order() { + std::printf( + "test_pooc_terminal_close_fill_has_no_recalc_or_c_born_order\n"); + for (bool is_long : {true, false}) { + PoocTerminalBracketProbe p(is_long); + Bar bars[] = { + {100.0, 110.0, 90.0, 100.0, 1000.0, 900'000}, + {100.0, 106.0, 94.0, 100.0, 1000.0, 1'800'000}, + {100.0, 106.0, 94.0, 100.0, 1000.0, 2'700'000}, + }; + p.run(bars, 3); + + CHECK(p.last_error().empty()); + CHECK(p.terminal_recalc_calls == 0); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(t.entry_bar_index == 0); + CHECK(t.exit_bar_index == 2); + CHECK(near(t.entry_price, 100.0)); + CHECK(near(t.exit_price, is_long ? 95.0 : 105.0)); + } + CHECK(near(p.signed_size(), 0.0)); + } +} + +void test_magnifier_pooc_terminal_close_fill_has_no_recalc_or_c_born_order() { + std::printf( + "test_magnifier_pooc_terminal_close_fill_has_no_recalc_or_c_born_order\n"); + for (bool is_long : {true, false}) { + PoocTerminalBracketProbe p(is_long); + Bar lower[] = { + {100.0, 105.0, 95.0, 102.0, 500.0, 0}, + {102.0, 110.0, 90.0, 100.0, 500.0, 60'000}, + {100.0, 103.0, 97.0, 101.0, 500.0, 120'000}, + {101.0, 106.0, 94.0, 100.0, 500.0, 180'000}, + {100.0, 103.0, 97.0, 101.0, 500.0, 240'000}, + {101.0, 106.0, 94.0, 100.0, 500.0, 300'000}, + }; + p.run(lower, 6, "1", "2", /*bar_magnifier=*/true, + /*magnifier_samples=*/4, MagnifierDistribution::ENDPOINTS); + + CHECK(p.last_error().empty()); + CHECK(p.terminal_recalc_calls == 0); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(t.entry_bar_index == 0); + CHECK(t.exit_bar_index == 2); + CHECK(near(t.entry_price, 100.0)); + CHECK(near(t.exit_price, is_long ? 95.0 : 105.0)); + } + CHECK(near(p.signed_size(), 0.0)); + } +} + +// Per-trade excursion begins at a POOC entry's C fill. A fill-triggered body +// after that terminal tick would call update_per_trade_extremes() with the +// completed entry bar and retroactively count its pre-entry high/low. +class PoocTerminalExcursionProbe final : public CoofBase { +public: + explicit PoocTerminalExcursionProbe(bool is_long) : is_long_(is_long) { + process_orders_on_close_ = true; + pyramiding_ = 0; + } + + void on_bar(const Bar&) override { + if (coof_fill_recalc_active_ && coof_cursor_is_bar_close_) { + ++terminal_recalc_calls; + return; + } + const std::string entry_id = is_long_ ? "L" : "S"; + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT + && trades_.empty()) { + strategy_entry(entry_id, is_long_); + } else if (bar_index_ == 1 + && position_side_ != PositionSide::FLAT) { + strategy_close(entry_id); + } + } + + int terminal_recalc_calls = 0; + +private: + bool is_long_; +}; + +void test_pooc_terminal_fill_does_not_backfill_entry_bar_excursion() { + std::printf( + "test_pooc_terminal_fill_does_not_backfill_entry_bar_excursion\n"); + for (bool is_long : {true, false}) { + PoocTerminalExcursionProbe p(is_long); + Bar bars[] = { + {100.0, 120.0, 80.0, 100.0, 1000.0, 900'000}, + {100.0, 101.0, 99.0, 100.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + CHECK(p.terminal_recalc_calls == 0); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(t.entry_bar_index == 0); + CHECK(t.exit_bar_index == 1); + CHECK(near(t.entry_price, 100.0)); + CHECK(near(t.exit_price, 100.0)); + CHECK(near(t.max_runup, 1.0)); + CHECK(near(t.max_drawdown, 1.0)); + } + } +} + +// Delta control: the ordinary close pass enters at C. Its next ordinary close +// pass closes at the next C; neither terminal fill triggers another body. +class PoocCloseCursorSingleUseProbe final : public CoofBase { +public: + PoocCloseCursorSingleUseProbe() { + process_orders_on_close_ = true; + pyramiding_ = 0; + } + + void on_bar(const Bar&) override { + if (coof_fill_recalc_active_ && coof_cursor_is_bar_close_) { + ++terminal_recalc_calls; + } + if (position_side_ == PositionSide::FLAT) { + strategy_entry("L", true); + } else { + strategy_close("L"); + } + } + + int terminal_recalc_calls = 0; +}; + +void test_delta_pooc_close_fills_are_terminal() { + std::printf("test_delta_pooc_close_fills_are_terminal\n"); + PoocCloseCursorSingleUseProbe p; + Bar bars[] = { + {100.0, 110.0, 90.0, 104.0, 1000.0, 900'000}, + {104.0, 112.0, 98.0, 106.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + CHECK(p.terminal_recalc_calls == 0); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(t.entry_bar_index == 0); + CHECK(t.exit_bar_index == 1); + CHECK(near(t.entry_price, 104.0)); + CHECK(near(t.exit_price, 106.0)); + } +} + +void test_delta_magnifier_pooc_close_fills_are_terminal() { + std::printf( + "test_delta_magnifier_pooc_close_fills_are_terminal\n"); + PoocCloseCursorSingleUseProbe p; + Bar lower[] = { + {100.0, 103.0, 99.0, 101.0, 500.0, 0}, + {101.0, 105.0, 100.0, 104.0, 500.0, 60'000}, + {104.0, 109.0, 103.0, 105.0, 500.0, 120'000}, + {105.0, 110.0, 102.0, 106.0, 500.0, 180'000}, + }; + p.run(lower, 4, "1", "2", /*bar_magnifier=*/true, + /*magnifier_samples=*/4, MagnifierDistribution::ENDPOINTS); + + CHECK(p.last_error().empty()); + CHECK(p.terminal_recalc_calls == 0); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(t.entry_bar_index == 0); + CHECK(t.exit_bar_index == 1); + CHECK(near(t.entry_price, 104.0)); + CHECK(near(t.exit_price, 106.0)); + } +} + +// MrWick control: breakout/daily mutations and its bracket are issued by the +// ordinary C execution. They remain committed without a terminal fill body. +class PoocBreakoutStateScheduleProbe final : public CoofBase { +public: + PoocBreakoutStateScheduleProbe() { + process_orders_on_close_ = true; + pyramiding_ = 0; + } + + void on_bar(const Bar&) override { + if (coof_fill_recalc_active_ && coof_cursor_is_bar_close_) { + ++terminal_recalc_calls; + } + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT + && !first_breakout_seen) { + first_breakout_seen = true; + continuation_taken = true; + breakout_direction = 1; + strategy_entry("L", true); + } + if (bar_index_ == 1 && position_side_ == PositionSide::LONG + && first_breakout_seen && continuation_taken + && breakout_direction == 1) { + strategy_close("L"); + } + } + + bool first_breakout_seen = false; + bool continuation_taken = false; + int breakout_direction = 0; + int terminal_recalc_calls = 0; + +protected: + void snapshot_script_state() override { + checkpoint_first_breakout_seen_ = first_breakout_seen; + checkpoint_continuation_taken_ = continuation_taken; + checkpoint_breakout_direction_ = breakout_direction; + } + void restore_script_state() override { + first_breakout_seen = checkpoint_first_breakout_seen_; + continuation_taken = checkpoint_continuation_taken_; + breakout_direction = checkpoint_breakout_direction_; + } + void commit_script_state() override { + snapshot_script_state(); + } + +private: + bool checkpoint_first_breakout_seen_ = false; + bool checkpoint_continuation_taken_ = false; + int checkpoint_breakout_direction_ = 0; +}; + +void test_mrwick_ordinary_close_state_survives_without_terminal_recalc() { + std::printf( + "test_mrwick_ordinary_close_state_survives_without_terminal_recalc\n"); + PoocBreakoutStateScheduleProbe p; + Bar bars[] = { + {100.0, 110.0, 90.0, 104.0, 1000.0, 900'000}, + {104.0, 112.0, 98.0, 106.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + CHECK(p.terminal_recalc_calls == 0); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(t.entry_bar_index == 0); + CHECK(t.exit_bar_index == 1); + CHECK(near(t.entry_price, 104.0)); + CHECK(near(t.exit_price, 106.0)); + } + CHECK(p.first_breakout_seen); + CHECK(p.continuation_taken); + CHECK(p.breakout_direction == 1); +} + +// Wayward control: a close and opposite entry emitted by the one ordinary C +// execution are siblings at the same live broker epoch and both fill there. +class PoocOrdinaryCloseReversalSiblingProbe final : public CoofBase { +public: + PoocOrdinaryCloseReversalSiblingProbe() { + process_orders_on_close_ = true; + pyramiding_ = 0; + } + + void on_bar(const Bar&) override { + if (coof_fill_recalc_active_ && coof_cursor_is_bar_close_) { + ++terminal_recalc_calls; + } + if (bar_index_ == 0 && position_side_ == PositionSide::FLAT + && trades_.empty()) { + strategy_entry("L", true); + return; + } + if (bar_index_ == 1 && position_side_ == PositionSide::LONG + && !coof_fill_recalc_active_) { + strategy_close("L"); + strategy_entry("S", false); + } + } + + int terminal_recalc_calls = 0; +}; + +void test_pooc_ordinary_close_reversal_siblings_share_live_c() { + std::printf("test_pooc_ordinary_close_reversal_siblings_share_live_c\n"); + PoocOrdinaryCloseReversalSiblingProbe p; + Bar bars[] = { + {100.0, 110.0, 90.0, 104.0, 1000.0, 900'000}, + {104.0, 112.0, 98.0, 106.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + CHECK(p.terminal_recalc_calls == 0); + CHECK(p.trade_count() == 1); + if (p.trade_count() == 1) { + const Trade& t = p.get_trade(0); + CHECK(t.entry_bar_index == 0); + CHECK(t.exit_bar_index == 1); + CHECK(near(t.entry_price, 104.0)); + CHECK(near(t.exit_price, 106.0)); + } + CHECK(near(p.signed_size(), -1.0)); +} + +// An EXPLICIT immediate close produced by the final allowed INTRABAR recalc +// consumes the same four-event historical budget. The carried five-unit fill +// is event 1, so only three one-unit immediate closes may execute; admitting a +// fourth close would silently produce a fifth broker event. This control is +// deliberately non-POOC: its carried entry fills at O, so all recalculations +// occur before the terminal close phase. +class RecalcChainBudgetProbe final : public CoofBase { +public: + void on_bar(const Bar&) override { + if (position_side_ == PositionSide::FLAT && trades_.empty()) { + strategy_entry("L", true, kNaN, kNaN, 5.0); + } else if (position_side_ == PositionSide::LONG) { + strategy_close("L", "", 1.0, kNaN, /*immediately=*/true); + } + } +}; + +void test_intrabar_direct_fill_from_last_recalc_respects_event_budget() { + std::printf( + "test_intrabar_direct_fill_from_last_recalc_respects_event_budget\n"); + RecalcChainBudgetProbe p; + Bar bars[] = { + {100.0, 101.0, 99.0, 100.0, 1000.0, 900'000}, + {100.0, 110.0, 90.0, 104.0, 1000.0, 1'800'000}, + }; + p.run(bars, 2); + + CHECK(p.last_error().empty()); + CHECK(p.trade_count() == 3); + CHECK(near(p.signed_size(), 2.0)); +} + +struct IdentitySnapshot { + std::vector trades; + double signed_size = 0.0; + int body_calls = 0; + int snapshot_calls = 0; + int restore_calls = 0; + int commit_calls = 0; +}; + +class FalsePathProbe final : public CoofBase { +public: + explicit FalsePathProbe(bool enabled) : CoofBase(enabled) {} + + void on_bar(const Bar&) override { + ++body_calls; + if (bar_index_ == 0) strategy_entry("L", true); + if (position_side_ == PositionSide::LONG && bar_index_ >= 1) { + strategy_close("L"); + } + } + + IdentitySnapshot result() const { + IdentitySnapshot out; + for (int i = 0; i < trade_count(); ++i) out.trades.push_back(get_trade(i)); + out.signed_size = signed_position_size(); + out.body_calls = body_calls; + out.snapshot_calls = snapshot_calls; + out.restore_calls = restore_calls; + out.commit_calls = commit_calls; + return out; + } + + int body_calls = 0; + int snapshot_calls = 0; + int restore_calls = 0; + int commit_calls = 0; + +protected: + void snapshot_script_state() override { ++snapshot_calls; } + void restore_script_state() override { ++restore_calls; } + void commit_script_state() override { ++commit_calls; } +}; + +bool identical_trade(const Trade& a, const Trade& b) { + return a.entry_time == b.entry_time && a.exit_time == b.exit_time + && a.entry_bar_index == b.entry_bar_index + && a.exit_bar_index == b.exit_bar_index + && a.is_long == b.is_long && a.entry_id == b.entry_id + && a.exit_id == b.exit_id && a.entry_comment == b.entry_comment + && a.exit_comment == b.exit_comment && a.entry_price == b.entry_price + && a.exit_price == b.exit_price && a.qty == b.qty && a.pnl == b.pnl + && a.pnl_pct == b.pnl_pct && a.max_runup == b.max_runup + && a.max_drawdown == b.max_drawdown && a.commission == b.commission; +} + +void test_false_flag_path_is_legacy_identical_and_never_calls_hooks() { + std::printf("test_false_flag_path_is_legacy_identical_and_never_calls_hooks\n"); + FalsePathProbe default_false(false); + FalsePathProbe explicit_false(false); + auto bars = standard_feed(); + default_false.run(bars.data(), static_cast(bars.size())); + + StrategyOverrides ov; + ov.calc_on_order_fills = 0; + std::unordered_map inputs; + SymInfo sym; + explicit_false.run(bars.data(), static_cast(bars.size()), + "15", "15", inputs, sym, &ov); + + const IdentitySnapshot a = default_false.result(); + const IdentitySnapshot b = explicit_false.result(); + CHECK(a.trades.size() == b.trades.size()); + for (std::size_t i = 0; i < a.trades.size() && i < b.trades.size(); ++i) { + CHECK(identical_trade(a.trades[i], b.trades[i])); + } + CHECK(a.signed_size == b.signed_size); + CHECK(a.body_calls == static_cast(bars.size())); + CHECK(b.body_calls == static_cast(bars.size())); + CHECK(a.snapshot_calls == 0 && a.restore_calls == 0 && a.commit_calls == 0); + CHECK(b.snapshot_calls == 0 && b.restore_calls == 0 && b.commit_calls == 0); +} + +void test_strategy_override_can_enable_and_disable_coof() { + std::printf("test_strategy_override_can_enable_and_disable_coof\n"); + auto bars = standard_feed(); + std::unordered_map inputs; + SymInfo sym; + + MarketCloseProbe enabled_by_override(false); + StrategyOverrides on; + on.calc_on_order_fills = 1; + enabled_by_override.run(bars.data(), static_cast(bars.size()), + "15", "15", inputs, sym, &on); + CHECK(enabled_by_override.coof_enabled()); + CHECK(enabled_by_override.trade_count() == 1); + if (enabled_by_override.trade_count() == 1) { + CHECK(enabled_by_override.get_trade(0).entry_bar_index + == enabled_by_override.get_trade(0).exit_bar_index); + } + + MarketCloseProbe disabled_by_override(true); + StrategyOverrides off; + off.calc_on_order_fills = 0; + disabled_by_override.run(bars.data(), static_cast(bars.size()), + "15", "15", inputs, sym, &off); + CHECK(!disabled_by_override.coof_enabled()); + CHECK(disabled_by_override.trade_count() == 1); + if (disabled_by_override.trade_count() == 1) { + CHECK(disabled_by_override.get_trade(0).exit_bar_index + > disabled_by_override.get_trade(0).entry_bar_index); + } +} + +} // namespace + +int main() { + test_market_close_fills_same_bar_at_entry_price(); + test_recalc_bracket_uses_remaining_path(); + test_historical_refill_is_exact_o_o_near_far_and_capped_at_four(); + test_non_open_endpoint_fill_consumes_point_before_market_add(); + test_magnifier_endpoint_fill_consumes_tick_before_market_add(); + test_real_magnifier_gap_fills_limit_at_fresh_subbar_open(); + test_real_magnifier_gap_fills_stop_at_fresh_subbar_open(); + test_exit_fills_consume_historical_event_budget(); + test_same_segment_priced_orders_fill_nearest_first(); + test_stop_limit_activation_survives_segment_split(); + test_stop_limit_activation_commits_only_through_consumed_cursor(); + test_fill_recalc_priced_entries_bypass_legacy_bar_throttle(); + test_recalc_priced_entry_exemption_expires_after_creation_bar(); + test_close_cleanup_does_not_leak_into_new_position_cycle(); + test_recalc_wrong_side_entry_bar_legs_carry_to_next_bar(); + test_recalc_wrong_stop_does_not_hide_valid_limit_leg(); + test_recalc_wrong_limit_does_not_hide_valid_stop_leg(); + test_interior_fill_recalc_market_entry_waits_for_next_waypoint(); + test_pooc_same_tick_requires_close_cursor_or_immediately(); + test_pooc_intrabar_recalc_priced_order_uses_remaining_path(); + test_historical_barstate_and_committed_state_rollback_hooks(); + test_pooc_terminal_close_fill_has_no_recalc_or_c_born_order(); + test_magnifier_pooc_terminal_close_fill_has_no_recalc_or_c_born_order(); + test_pooc_terminal_fill_does_not_backfill_entry_bar_excursion(); + test_delta_pooc_close_fills_are_terminal(); + test_delta_magnifier_pooc_close_fills_are_terminal(); + test_mrwick_ordinary_close_state_survives_without_terminal_recalc(); + test_pooc_ordinary_close_reversal_siblings_share_live_c(); + test_intrabar_direct_fill_from_last_recalc_respects_event_budget(); + test_false_flag_path_is_legacy_identical_and_never_calls_hooks(); + test_strategy_override_can_enable_and_disable_coof(); + + if (tests_failed == 0) { + std::printf("test_calc_on_order_fills PASSED (%d checks)\n", tests_passed); + return 0; + } + std::printf("test_calc_on_order_fills FAILED (%d failed, %d passed)\n", + tests_failed, tests_passed); + return 1; +}