Skip to content

Commit 099cd5d

Browse files
authored
Merge pull request #57 from pineforge-4pass/integration/full-validation-pass
fix(fills): resting pure-limit entries survive full close (TV GTC semantics)
2 parents ea6cb37 + 8286e27 commit 099cd5d

1 file changed

Lines changed: 44 additions & 3 deletions

File tree

src/engine_fills.cpp

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,12 +419,21 @@ void BacktestEngine::compact_filled_pending_orders(
419419
exit_closed_was_long ? PositionSide::LONG : PositionSide::SHORT;
420420
size_t write = 0;
421421
for (size_t read = 0; read < pending_orders_.size(); ++read) {
422+
// Mirror classify_order_eligibility's carve-out: a resting pure-limit
423+
// entry (a GTC limit order from a prior bar, no stop/trail) survives a
424+
// full close — see the rationale there (3commas DCA safety orders).
425+
bool resting_limit_entry_carry =
426+
pending_orders_[read].type == OrderType::ENTRY
427+
&& pending_orders_[read].created_bar < bar_index_
428+
&& !std::isnan(pending_orders_[read].limit_price)
429+
&& std::isnan(pending_orders_[read].stop_price);
422430
bool stale_same_direction_entry_after_exit =
423431
exit_closed_from_bar >= 0
424432
&& (pending_orders_[read].type == OrderType::ENTRY
425433
|| pending_orders_[read].type == OrderType::MARKET)
426434
&& pending_orders_[read].is_long == exit_closed_was_long
427-
&& pending_orders_[read].created_position_side == closed_side;
435+
&& pending_orders_[read].created_position_side == closed_side
436+
&& !resting_limit_entry_carry;
428437
if (!is_filled(read)
429438
&& !stale_same_direction_entry_after_exit) {
430439
if (write != read) pending_orders_[write] = std::move(pending_orders_[read]);
@@ -1026,8 +1035,24 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility(
10261035
bool pre_armed_opposite_sibling =
10271036
order.created_position_side != PositionSide::FLAT
10281037
&& order.created_position_side != requested;
1038+
// A RESTING pure-limit entry carried from a PRIOR bar (a GTC limit
1039+
// sitting in the book, not one freshly (re-)armed this bar) fills on
1040+
// its own touch even when another priced entry already filled this
1041+
// bar: TradingView sweeps the whole bar path against every resting
1042+
// limit order, filling each at its own limit price. The per-bar
1043+
// throttle models TV's treatment of freshly (re-)placed priced orders,
1044+
// not resting book orders — a 3commas DCA bot fills a deal's own SO1
1045+
// and a prior deal's carried-over deep SO limit on the SAME bar when
1046+
// the drop sweeps through both (pullback-sniper deal #15: SO1 @2495.21
1047+
// and the carried SO4 @2471.04 both fill on one bar). Restricted to
1048+
// pure limits (no stop/trail) created on an earlier bar so the
1049+
// same-bar stop-entry throttle (probes 80/92) is untouched.
1050+
bool resting_limit_entry =
1051+
order.created_bar < bar_index_
1052+
&& !std::isnan(order.limit_price)
1053+
&& std::isnan(order.stop_price);
10291054
if (!flat_armed_opposite_close && !flat_armed_same_dir_pyramid
1030-
&& !pre_armed_opposite_sibling) {
1055+
&& !pre_armed_opposite_sibling && !resting_limit_entry) {
10311056
return OrderEligibility::Skip;
10321057
}
10331058
}
@@ -1039,10 +1064,26 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility(
10391064
// position (created_position_side matches the closed direction).
10401065
PositionSide closed_side =
10411066
exit_closed_was_long ? PositionSide::LONG : PositionSide::SHORT;
1067+
// Carve-out: a RESTING pure-limit entry (a GTC limit order sitting in the
1068+
// book since a PRIOR bar, no stop/trail leg) is NOT cancelled by a full
1069+
// close. TradingView leaves pending strategy.entry() orders in the book
1070+
// across strategy.close_all() until they fill or are explicitly cancelled
1071+
// (strategy.cancel); such an order fills in a later deal when its limit is
1072+
// next touched. The same-direction cancel below targets MARKET adds and
1073+
// freshly (re-)armed priced entries tied to the just-closed position
1074+
// (deferred-flip carries — probes 72/80/93), NOT resting limit book
1075+
// orders such as a 3commas DCA bot's unfilled deep safety orders
1076+
// (pullback-sniper: an SO limit placed one deal fills the next).
1077+
bool resting_limit_entry_carry =
1078+
order.type == OrderType::ENTRY
1079+
&& order.created_bar < bar_index_
1080+
&& !std::isnan(order.limit_price)
1081+
&& std::isnan(order.stop_price);
10421082
if (exit_closed_from_bar >= 0
10431083
&& (order.type == OrderType::MARKET || order.type == OrderType::ENTRY)
10441084
&& order.is_long == exit_closed_was_long
1045-
&& order.created_position_side == closed_side) {
1085+
&& order.created_position_side == closed_side
1086+
&& !resting_limit_entry_carry) {
10461087
return OrderEligibility::Remove;
10471088
}
10481089

0 commit comments

Comments
 (0)