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
12 changes: 11 additions & 1 deletion include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1627,6 +1627,12 @@ class BacktestEngine {
// PendingOrder::dormant_trail_best).
double trail_best_before_bar_ = std::numeric_limits<double>::quiet_NaN();
int trail_best_before_bar_index_ = -1;
// The ordinary POOC close scan may revisit a retained trail with that
// same pre-bar extreme only while the carried position is unchanged.
// A new cycle, add, reduction or close-time trail restart keeps its own
// established path state instead of inheriting an earlier position's.
int64_t trail_best_before_bar_position_cycle_ = 0;
uint64_t trail_best_before_bar_fill_seq_ = 0;

// --- Intraday fill counter ---
// Counts every fill processed by ``apply_filled_order_to_state`` on
Expand Down Expand Up @@ -3696,7 +3702,11 @@ class BacktestEngine {
// round 8 family R / round 10 family AB: the 10-significant-digit
// margin-call trigger on a margin-100 LONG (process_margin_call; rule
// and pins on tv_money_long_margin_call in engine_fills.cpp).
bool tv_money_long_margin_call(const Bar& bar);
// The POOC extension is called only before the close-time script, with
// no pending broker orders. End-of-bar callers keep it disabled so a
// close/add cannot make earlier prices act on the post-close position.
bool tv_money_long_margin_call(const Bar& bar,
bool carried_pooc_pre_close = false);
// finding-311: mark the live position's standing strategy.exit brackets
// dormant when an in-position reversal entry is declined at fill.
void mark_position_brackets_dormant_on_declined_reversal(const Bar& bar);
Expand Down
131 changes: 126 additions & 5 deletions src/engine_fills.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1381,14 +1381,40 @@ void BacktestEngine::process_margin_call(const Bar& bar) {
// pins in the workflow repo): cash 0.0001 / 0.0002 fire at 1606.17, 0.0003+
// never (the residual there is 0.00029); 07-21 Q 270.621 cash <= 0.0003 fires
// at the fill bar's high 3734.89 (residual 0.00031), 0.0004+ never.
bool BacktestEngine::tv_money_long_margin_call(const Bar& bar) {
// Round 13 D: r12-d-residual exact/default+explicit and +/-0.0001 capital
// controls pin this same rule on a CARRIED POOC long. Q 878945.99 at 1.17987,
// C 1037042.0056329: next bar low 1.17905 produces a 0.0000789 deficit
// and TV closes 1. The Q 878945.98 matched close/stop pair proves a POOC close
// fill must not revisit the entry bar's earlier high. Fresh full/30% closes
// on the trigger bar read PS 878944.99 before sizing their close orders
// (log-20260906t091207z-83d4bea0), so dispatch_bar calls this BEFORE on_bar.
bool BacktestEngine::tv_money_long_margin_call(const Bar& bar,
bool carried_pooc_pre_close) {
if (!margin_call_enabled_) return false;
if (position_side_ != PositionSide::LONG) return false;
if (!std::isfinite(margin_long_)
|| std::abs(margin_long_ / 100.0 - 1.0) >= 1e-12) return false;
if (last_margin_call_event_bar_ == bar_index_) return false;
if (intrabar_exit_margin_call_bar_ == bar_index_) return false;
if (process_orders_on_close_ || calc_on_order_fills_
if (process_orders_on_close_) {
// This extension has no oracle for a pending order racing the money
// trigger, adds, fees/slippage, currency conversion or risk-forced
// exits. Keep their prior POOC behavior. End-of-bar calls stay out
// even when the script merely reduced an older position: its current
// quantity did not exist over this bar's already-traversed path.
if (!carried_pooc_pre_close || position_open_bar_ < 0
|| position_open_bar_ >= bar_index_ || !pending_orders_.empty()
|| opening_affordability_pending_ || pyramiding_ != 0
|| position_entry_count_ != 1 || pyramid_entries_.size() != 1
|| pyramid_entries_.front().entry_bar_index >= bar_index_
|| commission_value_ != 0.0 || slippage_ != 0
|| account_currency_fx_ != 1.0 || max_intraday_filled_orders_ > 0
|| risk_max_intraday_loss_ != 0.0 || risk_max_drawdown_ != 0.0
|| risk_max_cons_loss_days_ > 0) {
return false;
}
}
if (calc_on_order_fills_
|| bar_magnifier_enabled_ || coof_scheduler_active_
|| stream_warmup_mode_ || stream_phase_ != StreamPhase::IDLE) {
return false;
Expand Down Expand Up @@ -1425,6 +1451,7 @@ bool BacktestEngine::tv_money_long_margin_call(const Bar& bar) {
}
double fire_price = std::numeric_limits<double>::quiet_NaN();
double deficit = 0.0;
int fire_path_point = -1;
for (int i = start; i < 4; ++i) {
const double p = path[i];
if (!std::isfinite(p) || !(p > 0.0)) continue;
Expand All @@ -1442,6 +1469,7 @@ bool BacktestEngine::tv_money_long_margin_call(const Bar& bar) {
if (equity + 1e-7 >= value && equity + 1e-7 < rounded_value) {
fire_price = p;
deficit = rounded_value - equity;
fire_path_point = i;
break;
}
}
Expand All @@ -1467,6 +1495,22 @@ bool BacktestEngine::tv_money_long_margin_call(const Bar& bar) {

const double raw_exit_fill_base = bar_fill_price(fire_price);
const size_t trades_before = trades_.size();
if (process_orders_on_close_) {
// The pre-script pass precedes the ordinary full-bar excursion
// sample. Sample only the traversed waypoint prefix for this slice:
// the low-trigger pin includes the preceding high (MFE 0.00015),
// while the open-trigger control must not inherit that future high.
// update_per_trade_extremes is an arithmetic-only, non-throwing walk.
const Bar script_bar = current_bar_;
current_bar_.high = current_bar_.low = path[0];
for (int i = 1; i <= fire_path_point; ++i) {
current_bar_.high = std::max(current_bar_.high, path[i]);
current_bar_.low = std::min(current_bar_.low, path[i]);
}
current_bar_.close = fire_price;
update_per_trade_extremes();
current_bar_ = script_bar;
}
if (qty_liq >= qty - kQtyEpsilon) {
execute_market_exit(raw_exit_fill_base);
} else {
Expand Down Expand Up @@ -2238,6 +2282,8 @@ void BacktestEngine::update_trail_best_for_bar_open(const Bar& bar) {
if (first_fold_this_bar) {
trail_best_before_bar_ = trail_best_price_;
trail_best_before_bar_index_ = bar_index_;
trail_best_before_bar_position_cycle_ = position_cycle_seq_;
trail_best_before_bar_fill_seq_ = broker_fill_event_seq_;
}
if (position_side_ == PositionSide::LONG) {
if (std::isnan(trail_best_price_) || bar.high > trail_best_price_)
Expand Down Expand Up @@ -4318,6 +4364,9 @@ void BacktestEngine::apply_filled_order_to_state(
// reversals are admitted on their actual fill, and paired reentries may
// fill from flat despite having been placed from a live position.
bool admitted_flat_on_frozen_sizing_price = false;
// This call only: a true-flat positive gap admitted on rounded price
// still needs its existing opening-margin checkpoint after the fill.
bool admitted_flat_on_price_gap_band = false;

if (order.type == OrderType::MARKET || order.type == OrderType::ENTRY) {
PositionSide requested = order.is_long ? PositionSide::LONG : PositionSide::SHORT;
Expand Down Expand Up @@ -4778,6 +4827,45 @@ void BacktestEngine::apply_filled_order_to_state(
std::isfinite(order.sizing_fx) && order.sizing_fx > 0.0
? order.sizing_fx
: active_account_currency_fx();
// Round 13 taro BTC, also pinned on ETH: for ordinary zero-fee
// default 100% market orders, a positive close-to-open gap compares
// the fill price with sig10(sig10(E_s) / Q), not exact Q*fill with E_s.
// BTC offsets -.00030 admit / -.00032 drop distinguish BOTH rounds.
// Keep the existing cost decision outside this directly pinned scope;
// in particular this does not widen tv_money_scope for other rules.
const bool price_gap_scope =
order.type == OrderType::MARKET
&& std::isnan(order.qty)
&& std::abs(default_qty_value_ - 100.0) < 1e-12
&& std::isfinite(margin_pct)
&& std::abs(margin_pct - 100.0) < 1e-12
&& qty_step_ > 0.0 && qty_step_ < 1.0
&& syminfo_.pointvalue == 1.0 && sizing_fx == 1.0
&& account_currency_fx_timestamps_.empty()
&& commission_type_ == CommissionType::PERCENT
&& commission_value_ == 0.0 && slippage_ == 0
&& !process_orders_on_close_ && !calc_on_order_fills_
&& !bar_magnifier_enabled_ && !coof_scheduler_active_
&& !stream_warmup_mode_ && stream_phase_ == StreamPhase::IDLE
&& !order.created_during_coof_recalc
&& !order.created_after_position_close_in_bar
&& std::isfinite(order.sizing_equity)
&& std::isfinite(order.frozen_default_qty)
&& std::isfinite(order.sizing_price)
&& std::isfinite(order.sizing_mark)
&& std::isfinite(fill_price) && fill_price > order.sizing_price
&& ((position_side_ == PositionSide::FLAT
&& order.created_position_side == PositionSide::FLAT
&& !pending_flat_market_pair_is_live(order))
|| (reversal && order.created_position_side == position_side_
&& order.created_position_cycle_seq == position_cycle_seq_
&& pyramid_entries_.size() == 1));
const auto price_gap_affordable = [&]() {
const double affordable_price = tv_money_round(
tv_money_round(order.sizing_equity) / order.frozen_default_qty);
return std::isfinite(affordable_price)
&& affordable_price >= apply_fill_slippage(fill_price, order.is_long);
};
// Gap-reject (design-cntvxiao-gap-reject, PANEL-CLEARED; widened to
// commissioned entries by the round-7 family-H market-entry-admission
// pin, below): a high-level strategy.entry with omitted qty, sized
Expand Down Expand Up @@ -4884,8 +4972,12 @@ void BacktestEngine::apply_filled_order_to_state(
const double float_guard =
std::max(1e-9, std::abs(order.sizing_equity) * 1e-12);
if (gap_notional > order.sizing_equity + float_guard) {
decline_and_cancel();
return;
if (price_gap_scope && price_gap_affordable()) {
admitted_flat_on_price_gap_band = true;
} else {
decline_and_cancel();
return;
}
}
}
// A same-direction add (fractional OR all-in) IS gated, against
Expand Down Expand Up @@ -5003,7 +5095,10 @@ void BacktestEngine::apply_filled_order_to_state(
* sizing_fx
* (margin_pct / 100.0));
}
if (required_margin > free_funds + epsilon) {
const bool price_band_admitted_reversal =
reversal && price_gap_scope && price_gap_affordable();
if (required_margin > free_funds + epsilon
&& !price_band_admitted_reversal) {
// design-declined-reversal-close-leg: ONLY the reversal decline
// triggers close-leg suppression (admit_price == slipped fill,
// MARKET). The same_dir add decline (probe65 shape) and the
Expand Down Expand Up @@ -5681,6 +5776,11 @@ void BacktestEngine::apply_filled_order_to_state(
&& !order.created_after_position_close_in_bar
&& position_side_before_fill == PositionSide::FLAT
&& admitted_flat_on_frozen_sizing_price
// Newly price-band-admitted positive gaps can have a real
// fill deficit on either side (BTC/ETH flat MC1 tapes).
// Use the existing event/quantizer; exact-affordable fills
// keep the historical exemption and no persistent flag.
&& !admitted_flat_on_price_gap_band
&& std::isfinite(new_opening_commission)
&& new_opening_commission == 0.0;

Expand Down Expand Up @@ -7414,6 +7514,27 @@ BacktestEngine::FillEvaluation BacktestEngine::evaluate_fill_price(
path_start_position = earliest_parent;
}
}
// Ordinary POOC scans retained orders before and after on_bar.
// The second call's trail_best_path_state already contains this
// bar's favorable extreme. Replaying O/H/L with that value can
// retroactively gap-fill a trail that only activated later on the
// path (Nils AAPL 2025-03-31: H 220.58 first arms the long, but
// the earlier O 219.56 was incorrectly reused as its exit).
// Rewalk a retained trail from the SAME pre-bar best on both scans.
// New/reissued orders, entry bars, changed position state, close
// restarts and the dedicated dormant/COOF/magnifier paths retain
// their existing state and chronology.
if (has_trail && order.type == OrderType::EXIT
&& process_orders_on_close_ && !calc_on_order_fills_
&& !bar_magnifier_enabled_ && !order.dormant_bracket
&& !is_entry_bar && order.created_bar < bar_index_
&& trail_close_restart_bar_ != bar_index_
&& trail_best_before_bar_index_ == bar_index_
&& position_cycle_seq_ != 0
&& trail_best_before_bar_position_cycle_ == position_cycle_seq_
&& trail_best_before_bar_fill_seq_ == broker_fill_event_seq_) {
trail_best_path_state = trail_best_before_bar_;
}
ExitPathFill exit_fill = resolve_exit_path_fill(
bar,
tick_bar,
Expand Down
12 changes: 12 additions & 0 deletions src/engine_run.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,20 @@ void BacktestEngine::dispatch_bar() {
// inlines its own on_bar call and pushes there instead.
_push_source_series();
if (process_orders_on_close_) {
const bool no_pending_broker_orders = pending_orders_.empty();
const uint64_t fills_before_pending = broker_fill_event_seq_;
process_pending_orders(current_bar_); // step 1: old stop/limit
evaluate_max_intraday_loss_over_path(current_bar_);
// Round 13 D: the carried 1x-long rounded-money event belongs before
// the close-time script. TV's full/30% close pins read the already
// reduced position here; an end-of-bar check would see the script's
// flattened/reduced state instead. The helper refuses pending-order
// interactions and every fresh entry, so it cannot replay a close
// fill's past path or move an existing broker fill across the event.
if (no_pending_broker_orders
&& broker_fill_event_seq_ == fills_before_pending) {
tv_money_long_margin_call(current_bar_, /*carried_pooc_pre_close=*/true);
}
update_per_trade_extremes(); // step 2: update before strategy reads
invoke_chart_on_bar(current_bar_); // step 3: strategy logic
flush_same_bar_close(); // step 3b: surviving strategy.close fill
Expand Down
3 changes: 3 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ set(TEST_SOURCES
test_close_percent_calltime_basis
test_famag_close_first_admission
test_famag_opening_money
test_taro_price_gap_admission
test_live_position_market_gross_admission
test_lower_tf_parse_extra
test_ta_ma_warmup_extra
Expand All @@ -158,6 +159,7 @@ set(TEST_SOURCES
test_margin_call_trail_exit_chronology
test_margin_call_1x_long_entry_fill
test_tv_money_long_margin_call_eth
test_tv_money_carried_pooc
test_margin_call_gap_open
test_entry_bar_margin_path
test_m_admission_36
Expand Down Expand Up @@ -186,6 +188,7 @@ set(TEST_SOURCES
test_zero_offset_trail_rides
test_trail_ref_entry_bar_extreme
test_trail_close_restart_no_fold
test_pooc_retained_trail_path
test_famx_declined_reversal_trail_leg
test_famae_lot_sizing_ten_digit_equity
test_famae_declined_reversal_trail_gap
Expand Down
Loading
Loading