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
2 changes: 2 additions & 0 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2151,6 +2151,8 @@ class BacktestEngine {
uint64_t& exit_closed_from_incarnation,
bool& exit_closed_was_long,
std::vector<size_t>& filled_indices);
bool stop_entry_margin_admission_declines(
const PendingOrder& order, double fill_price, const Bar& bar) const;
// design-declined-reversal-close-leg: called at the KI-54 reversal-decline
// site with the just-declined MARKET reversal entry. Flags every pending
// FULL close that was co-queued after it on the same bar against the held
Expand Down
120 changes: 104 additions & 16 deletions src/engine_fills.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,70 @@ bool preserves_same_id_stop_across_deferred_close_all(
&& !order.stop_limit_activated;
}

// TradingView continues along the historical OHLC path after the first
// member of this exact dual-stop book is declined by margin admission. Keep
// the exception on the independently-proven shape: two same-signal,
// true-flat, unlinked strategy.entry pure STOPs and no competing entry-like
// orders. EXIT orders are harmless while flat and retain ordinary cleanup.
bool is_true_flat_unlinked_stop_pair(
const std::vector<PendingOrder>& orders,
DualEntryStopPathWinner winner) {
if (winner != DualEntryStopPathWinner::LongFirst
&& winner != DualEntryStopPathWinner::ShortFirst) {
return false;
}

int pure_stop_entries = 0;
int source_bar = 0;
bool have_source_bar = false;
for (const PendingOrder& order : orders) {
const bool entry_like = order.type == OrderType::ENTRY
|| order.type == OrderType::MARKET
|| order.type == OrderType::RAW_ORDER;
if (!entry_like) continue;

const bool pure_stop = order.type == OrderType::ENTRY
&& std::isfinite(order.stop_price)
&& std::isnan(order.limit_price)
&& std::isnan(order.trail_points)
&& std::isnan(order.trail_price)
&& std::isnan(order.trail_offset)
&& !order.stop_limit_activated;
if (!pure_stop
|| order.created_position_side != PositionSide::FLAT
|| order.created_after_position_close_in_bar
|| !order.oca_name.empty()
|| order.oca_type != 0) {
return false;
}
if (!have_source_bar) {
source_bar = order.created_bar;
have_source_bar = true;
} else if (order.created_bar != source_bar) {
return false;
}
++pure_stop_entries;
}
return pure_stop_entries == 2;
}

} // namespace


bool internal::dual_stop_margin_decline_can_continue_path(
const std::vector<PendingOrder>& orders,
DualEntryStopPathWinner winner,
bool process_orders_on_close,
bool calc_on_order_fills,
bool bar_magnifier) {
return winner != DualEntryStopPathWinner::None
&& !process_orders_on_close
&& !calc_on_order_fills
&& !bar_magnifier
&& is_true_flat_unlinked_stop_pair(orders, winner);
}


// strategy_entry / strategy_close / strategy_close_all / strategy_exit
// moved to engine_strategy_commands.cpp.
void BacktestEngine::process_pending_orders(const Bar& bar) {
Expand Down Expand Up @@ -79,6 +140,10 @@ void BacktestEngine::process_pending_orders(const Bar& bar) {
dual_entry_path_ = dual_entry_stop_path_winner(
bar, pending_orders_, bar_index_);
}
const bool continue_after_stop_margin_decline_scope =
dual_stop_margin_decline_can_continue_path(
pending_orders_, dual_entry_path_, process_orders_on_close_,
calc_on_order_fills_, bar_magnifier_enabled_);

for (int opposing_pass = 0; opposing_pass < 2; ++opposing_pass) {
// Pass 1 only re-evaluates orders pass 0 deferred into the skip set;
Expand Down Expand Up @@ -114,12 +179,27 @@ void BacktestEngine::process_pending_orders(const Bar& bar) {
continue;
}

const bool path_winner_stop_margin_decline =
continue_after_stop_margin_decline_scope
&& ((dual_entry_path_ == DualEntryStopPathWinner::LongFirst
&& order.is_long)
|| (dual_entry_path_ == DualEntryStopPathWinner::ShortFirst
&& !order.is_long))
&& check_risk_allow_entry(order.is_long)
&& stop_entry_margin_admission_declines(
order, fill.fill_price, bar);
apply_filled_order_to_state(
order, i, fill.fill_price, fill.is_limit_fill, bar,
trail_best_path_state,
exit_closed_from_bar, exit_closed_from_incarnation,
exit_closed_was_long,
filled_indices);
if (path_winner_stop_margin_decline) {
// The path winner never became a broker fill. Releasing only the
// path-winner fence lets the already-deferred, path-later stop
// face every ordinary eligibility and admission rule in turn.
dual_entry_path_ = DualEntryStopPathWinner::None;
}
materialize_relative_exit_prices_for_live_position();
}
compact_filled_pending_orders(
Expand Down Expand Up @@ -1327,6 +1407,27 @@ void BacktestEngine::compact_filled_pending_orders(
}


bool BacktestEngine::stop_entry_margin_admission_declines(
const PendingOrder& order, double fill_price, const Bar& bar) const {
if (order.type != OrderType::ENTRY
|| std::isnan(order.stop_price)
|| !std::isnan(order.limit_price)) {
return false;
}
const double margin_pct = order.is_long ? margin_long_ : margin_short_;
if (!(margin_pct > 0.0) || !std::isfinite(bar.open) || bar.open <= 0.0) {
return false;
}
const double fill_qty =
std::abs(calc_qty_for_type(fill_price, order.qty, order.qty_type));
const double required = fill_qty * bar.open * syminfo_.pointvalue
* account_currency_fx_ * (margin_pct / 100.0);
const double available = current_equity();
const double eps = std::max(1e-9, std::abs(available) * 1e-12);
return fill_qty > 0.0 && required > available + eps;
}


// Apply a successfully matched fill to engine state. Dispatches by
// order.type to the appropriate execute_* method, updates trailing-stop
// best price, handles risk gating + intraday-fill caps + OCA group
Expand Down Expand Up @@ -1406,22 +1507,9 @@ void BacktestEngine::apply_filled_order_to_state(
// byte-identical when margin_pct==0). The qty is exactly the fill kernel's
// (calc_qty_for_type at the fill price) so the gate cannot diverge from the
// fill it guards.
if (order.type == OrderType::ENTRY
&& !std::isnan(order.stop_price)
&& std::isnan(order.limit_price)) {
const double margin_pct = order.is_long ? margin_long_ : margin_short_;
if (margin_pct > 0.0 && std::isfinite(bar.open) && bar.open > 0.0) {
const double fill_qty =
std::abs(calc_qty_for_type(fill_price, order.qty, order.qty_type));
const double required = fill_qty * bar.open * syminfo_.pointvalue
* account_currency_fx_ * (margin_pct / 100.0);
const double available = current_equity();
const double eps = std::max(1e-9, std::abs(available) * 1e-12);
if (fill_qty > 0.0 && required > available + eps) {
decline_and_cancel();
return;
}
}
if (stop_entry_margin_admission_declines(order, fill_price, bar)) {
decline_and_cancel();
return;
}

// A fixed-default MARKET entry can change role between placement and fill:
Expand Down
11 changes: 11 additions & 0 deletions src/engine_internal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,17 @@ DualEntryStopPathWinner dual_entry_stop_path_winner(const Bar& bar,
int current_bar_index = -1);


// Exact scope for continuing the historical path after a dual-stop winner is
// declined by fill-time margin admission. Kept runtime-private so focused
// tests can pin the POOC/COOF/magnifier and order-book fences directly.
bool dual_stop_margin_decline_can_continue_path(
const std::vector<PendingOrder>& orders,
DualEntryStopPathWinner winner,
bool process_orders_on_close,
bool calc_on_order_fills,
bool bar_magnifier);


// For OCA exit siblings (e.g., separate TP and SL strategy.order calls),
// compute first-touch position on OHLC path for a single-priced order.
bool exit_order_touch_position(const Bar& bar,
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_margin_admission_gate
test_short_reversal_emission
test_margin_stop_admission
test_stop_decline_continue_path
test_frozen_flat_gap_reject
test_explicit_qty_fill_admission
test_limit_fill_slippage
Expand Down
Loading
Loading