Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,19 @@ struct PendingOrder {
// FLAT before a paired reentry is placed, but that reentry is not an
// independent true-flat opening for KI-61 affordability purposes.
bool created_after_position_close_in_bar = false;
// True when this SAME-direction MARKET/ENTRY order was OVER the pyramiding
// cap at PLACEMENT — i.e. the position was already held in this order's
// direction with position_entry_count_ >= pyramiding_ at the moment it was
// placed. Snapshotted at every entry placement site so it mirrors the
// fill-time pyramiding gate (add_to_pyramid_market / the strategy.order add
// gate) exactly. The post-full-close same-direction wipe reads this to
// distinguish a TV-admissible (within-cap) co-queue — which survives a
// deferred full close that flattens on the fill bar — from one TradingView
// rejects at placement (over cap), which must still be cancelled even though
// the co-queued close zeroed position_entry_count_ before the add's fill-time
// gate ran. See classify_order_eligibility / compact_filled_pending_orders
// and test_close_all_coqueued_entry.cpp.
bool over_pyramiding_cap_at_placement = false;
// Snapshot of the position's quantity at the moment this order was
// PLACED (0 if placed from flat). Used by execute_market_entry's
// flat branch to apply TradingView's deferred-flip growth rule:
Expand Down
34 changes: 33 additions & 1 deletion src/engine_fills.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -696,8 +696,20 @@ void BacktestEngine::compact_filled_pending_orders(
&& pending_orders_[read].created_bar < bar_index_
&& !std::isnan(pending_orders_[read].limit_price)
&& std::isnan(pending_orders_[read].stop_price);
// Mirror classify_order_eligibility's M1v2 narrowed co-queue exemption
// (they MUST stay in lockstep): a same-direction entry co-queued on the
// close's own call bar survives ONLY if it was within the pyramiding cap
// at placement. A co-queued STOP that does NOT fill on the close bar
// reaches compaction without ever entering filled_indices, so without
// this term it would be wiped here even though classify spared it (the
// reverted M1 hit exactly this — R-KEEP-stop failed under a classify-only
// fix). Over-cap co-queues and prior-bar carries are still compacted away.
bool coqueued_within_cap =
pending_orders_[read].created_bar == exit_closed_from_bar
&& !pending_orders_[read].over_pyramiding_cap_at_placement;
bool stale_same_direction_entry_after_exit =
exit_closed_from_bar >= 0
&& !coqueued_within_cap
&& (pending_orders_[read].type == OrderType::ENTRY
|| pending_orders_[read].type == OrderType::MARKET)
&& pending_orders_[read].is_long == exit_closed_was_long
Expand Down Expand Up @@ -1904,11 +1916,31 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility(
&& order.created_bar < bar_index_
&& !std::isnan(order.limit_price)
&& std::isnan(order.stop_price);
// M1v2 narrowed co-queue exemption (pyramid-deferred-flip-close-all-01):
// a same-direction entry co-queued on the close's OWN call bar
// (order.created_bar == exit_closed_from_bar, where exit_closed_from_bar is
// the close order's created_bar — see apply_exit_order_fill) SURVIVES the
// full close, but ONLY if it was within the pyramiding cap at placement. A
// DEFERRED close_all created on bar N fills at bar N+1's open, so an entry
// co-queued on bar N is a "same on_bar as the fired exit" placement TV keeps
// (a market fills at the next open; a stop fires when later touched). But an
// add placed OVER the pyramiding cap is one TV rejects at placement, and the
// fill-time gate misses it because the co-queued close zeroes
// position_entry_count_ first — so over_pyramiding_cap_at_placement keeps it
// in the wipe. PRIOR-bar carries (created_bar < the close's call bar) are
// always cancelled — the deferred-flip carry this wipe exists for
// (test_deferred_flip_carry_close_only.cpp, probes 72/80/93). The reverted
// M1 used the created_bar term alone and un-cancelled over-cap co-queues
// (probe65 732→1463; the composite bracket fell below strong).
bool coqueued_within_cap =
order.created_bar == exit_closed_from_bar
&& !order.over_pyramiding_cap_at_placement;
if (exit_closed_from_bar >= 0
&& (order.type == OrderType::MARKET || order.type == OrderType::ENTRY)
&& order.is_long == exit_closed_was_long
&& order.created_position_side == closed_side
&& !resting_limit_entry_carry) {
&& !resting_limit_entry_carry
&& !coqueued_within_cap) {
return OrderEligibility::Remove;
}

Expand Down
23 changes: 23 additions & 0 deletions src/engine_strategy_commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,18 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long,
order.created_position_side = position_side_;
order.created_after_position_close_in_bar =
pending_close_qty_in_bar_ > kQtyEpsilon;
// Snapshot the placement-time over-pyramiding-cap status, mirroring the
// fill-time gate (add_to_pyramid_market, engine_orders.cpp) EXACTLY: a
// SAME-direction add against a position already at/over the pyramiding cap
// is one TradingView never admits. Same-id re-placement rebuilds this
// PendingOrder wholesale, so the snapshot naturally refreshes each call.
// The post-full-close same-direction wipe reads this flag to keep drop-
// ping over-cap co-queues even when a co-queued full close zeroes
// position_entry_count_ before the add's fill-time gate runs.
order.over_pyramiding_cap_at_placement =
position_side_ != PositionSide::FLAT
&& position_side_ == (is_long ? PositionSide::LONG : PositionSide::SHORT)
&& position_entry_count_ >= pyramiding_;
// TradingView empirical rule (probe 52 trade 113): the deferred-flip
// carry is the position size at THIS placement, not the original.
// ``strategy.entry`` with the same id replaces the pending order
Expand Down Expand Up @@ -804,6 +816,17 @@ void BacktestEngine::strategy_order(const std::string& id, bool is_long, double
order.created_position_side = position_side_;
order.created_after_position_close_in_bar =
pending_close_qty_in_bar_ > kQtyEpsilon;
// Same placement-time over-cap snapshot as strategy_entry, mirroring the
// strategy.order add gate (engine_fills.cpp same-direction RAW add). A
// strategy.order market/priced order is a RAW_ORDER and is not currently a
// target of the same-direction post-full-close wipe (which keys on
// MARKET/ENTRY), so this flag is inert for the RAW path today; it is
// captured here for consistency so the provenance stays correct if the
// wipe is ever widened to RAW_ORDER adds.
order.over_pyramiding_cap_at_placement =
position_side_ != PositionSide::FLAT
&& position_side_ == (is_long ? PositionSide::LONG : PositionSide::SHORT)
&& position_entry_count_ >= pyramiding_;
order.tv_carry_qty = position_qty_;

bool has_limit = !std::isnan(limit_price);
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ set(TEST_SOURCES
test_same_tick_multi_entry_race
test_full_close_while_pyramiding
test_deferred_flip_carry_close_only
test_close_all_coqueued_entry
test_lower_tf_parse_extra
test_ta_ma_warmup_extra
test_ta_osc_edge
Expand Down
Loading
Loading