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
14 changes: 11 additions & 3 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ struct PendingOrder {
// which intentionally survives same-id replacement to keep broker ordering
// stable, incarnation is never copied or reused by a replacement.
uint64_t incarnation = 0;
// True when this pending object was created by reissuing an id that was
// already live. The fresh incarnation above identifies the new call, but
// created_seq intentionally retains the replaced order's broker priority.
// Exact clean-room two-call rules must fail closed on this provenance
// rather than mistaking retained priority for current source order.
bool created_by_same_id_replacement = false;
// 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.
Expand Down Expand Up @@ -916,9 +922,10 @@ class BacktestEngine {
std::vector<Trade> trades_;
std::vector<PendingOrder> pending_orders_;
// Source bars whose otherwise eligible flat MARKET candidate set was
// mutated (same-id replacement/cancel). Even if two orders survive, the
// original bar contained more/different calls and is outside the exact
// two-call oracle, so finalization must leave it ordinary.
// mutated (same-id replacement/cancel) or contained an extra rejected
// call. Even if two orders survive, the original bar contained more or
// different calls and is outside both exact two-call oracles (KI-65 and
// terminal-C POOC+COOF), so finalization must leave it ordinary.
std::unordered_set<int> pending_flat_market_pair_disqualified_bars_;

// strategy.exit partial orders are one-shot per open position for a given id
Expand Down Expand Up @@ -2121,6 +2128,7 @@ class BacktestEngine {
void update_trail_best_for_bar_open(const Bar& bar);
void sort_exit_siblings_by_path_fill(const Bar& bar);
bool pending_flat_market_pair_scope_is_live() const;
void apply_pooc_coof_explicit_flat_market_gross_admission();
void finalize_pending_flat_market_pairs(const Bar& bar);
void sort_orders_by_fill_phase(const Bar& bar);
// TradingView binds a valid, single/full, non-trailing strategy.exit to a
Expand Down
134 changes: 134 additions & 0 deletions src/engine_fills.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,140 @@ bool BacktestEngine::pending_flat_market_pair_scope_is_live() const {
&& !risk_halted_;
}

// TradingView admission for the exact Fran-470 terminal-close shape. Two
// distinct explicit-FIXED opposite MARKET strategy.entry calls are emitted
// from true flat in one ordinary historical evaluation with both POOC and COOF
// enabled. Each own quantity first passes strategy_entry's normal signal-time
// check. Before the terminal-C broker pass, TV additionally costs the LATER
// call as the gross reversal transaction:
//
// (first own qty + later own qty) * signal close * pointvalue * fx * margin
// <= placement equity
//
// If the pair exceeds that budget, the later call is silently declined and the
// first call remains the sole fill. The clean-room N=2 probe separates this
// from order priority and bracket interaction: the duration-one survivor pins
// the second source call for fixed qty=1, while Fran's ~95%-of-equity explicit
// qty keeps only the first, with and without a position-scoped bracket. Those
// controls bracket the behavior below and above budget; they do not pin exact
// gross-equality behavior, which retains the engine's ordinary margin model.
//
// Keep this independent from the established KI-65 pending MARKET pair. KI-65
// owns a non-POOC/non-COOF pyramiding=2 buy-before-sell transaction model; this
// terminal-C shape preserves source order and only adds the gross admission
// fence. The complete-book and default-risk guards deliberately fail closed
// for third entries, OCA/raw/priced/resting siblings, same-direction calls,
// replacement-tainted/cancel-rearmed books, after-close creation, COOF-born
// calls, magnifier, slippage, custom margin, commission, or non-default risk
// policy.
void BacktestEngine::apply_pooc_coof_explicit_flat_market_gross_admission() {
for (auto it = pending_flat_market_pair_disqualified_bars_.begin();
it != pending_flat_market_pair_disqualified_bars_.end();) {
if (*it < bar_index_) {
it = pending_flat_market_pair_disqualified_bars_.erase(it);
} else {
++it;
}
}
const bool source_bar_disqualified =
pending_flat_market_pair_disqualified_bars_.find(bar_index_)
!= pending_flat_market_pair_disqualified_bars_.end();
if (!process_orders_on_close_
|| !calc_on_order_fills_
|| bar_magnifier_enabled_
|| pyramiding_ != 0
|| slippage_ != 0
|| pending_orders_.size() != 2
|| source_bar_disqualified
|| std::abs(margin_long_ - 100.0) >= 1e-12
|| std::abs(margin_short_ - 100.0) >= 1e-12
|| risk_direction_ != RiskDirection::BOTH
|| risk_max_cons_loss_days_ != 0
|| risk_max_drawdown_ > 0.0
|| risk_max_intraday_loss_ > 0.0
|| risk_max_position_size_ > 0.0
|| max_intraday_filled_orders_ > 0
|| risk_halted_) {
return;
}

PendingOrder* first = &pending_orders_[0];
PendingOrder* second = &pending_orders_[1];
if (second->incarnation < first->incarnation) std::swap(first, second);

auto eligible = [&](const PendingOrder& order) {
return order.type == OrderType::MARKET
&& order.explicit_flat_admission_candidate
&& std::isfinite(order.qty)
&& order.qty > kQtyEpsilon
&& (order.qty_type < 0
|| order.qty_type == static_cast<int>(QtyType::FIXED))
&& order.oca_name.empty()
&& order.created_bar == bar_index_
&& order.incarnation > 0
&& !order.created_by_same_id_replacement
&& order.created_position_side == PositionSide::FLAT
&& !order.created_after_position_close_in_bar
&& !order.created_during_coof_recalc
&& !order.coof_born_at_close_recalc
&& !order.coof_born_mid_bar
&& std::isfinite(order.explicit_placement_equity)
&& order.explicit_placement_equity > 0.0
&& std::isfinite(order.explicit_slipped_signal_close)
&& order.explicit_slipped_signal_close > 0.0;
};
if (!eligible(*first)
|| !eligible(*second)
|| first->id == second->id
|| first->is_long == second->is_long
|| first->created_bar != second->created_bar
// No admitted order object may have been created between the two
// calls. Together with the mutation tombstone above, this excludes
// three-call books reduced back to two by replacement/cancel-rearm.
|| second->incarnation != first->incarnation + 1
// A clean pair's retained broker sequence and fresh call identity
// have the same order. Any disagreement is replacement provenance.
|| first->created_seq >= second->created_seq) {
return;
}

const double first_qty = std::abs(apply_qty_step(first->qty));
const double second_qty = std::abs(apply_qty_step(second->qty));
if (!(first_qty > kQtyEpsilon) || !(second_qty > kQtyEpsilon)) return;

const double equity = second->explicit_placement_equity;
const double equity_guard = std::max(1e-9, std::abs(equity) * 1e-12);
if (std::abs(first->explicit_placement_equity - equity) > equity_guard) {
return;
}
const double signal_close = second->explicit_slipped_signal_close;
const double price_guard = std::max(1e-12, std::abs(signal_close) * 1e-12);
if (std::abs(first->explicit_slipped_signal_close - signal_close)
> price_guard) {
return;
}
if (calc_commission(signal_close, first_qty) != 0.0
|| calc_commission(signal_close, second_qty) != 0.0) {
return;
}

const double notional_k = syminfo_.pointvalue * account_currency_fx_;
if (!std::isfinite(notional_k) || !(notional_k > 0.0)) return;
const double required_margin =
(first_qty + second_qty) * signal_close * notional_k;
if (!(required_margin > equity + equity_guard)) return;

const uint64_t rejected_incarnation = second->incarnation;
invalidate_pending_flat_market_pair(second->created_seq);
pending_orders_.erase(
std::remove_if(
pending_orders_.begin(), pending_orders_.end(),
[&](const PendingOrder& order) {
return order.incarnation == rejected_incarnation;
}),
pending_orders_.end());
}

// Finalize the deferred KI-65 MARKET/MARKET candidate set only after on_bar
// has completed and the broker sees every call from that source bar. Each call
// has already passed the ordinary own-qty placement gate. Exactly two eligible
Expand Down
5 changes: 5 additions & 0 deletions src/engine_run.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,11 @@ void BacktestEngine::dispatch_bar_calc_on_order_fills() {
// eligible order remains.
if (process_orders_on_close_) {
const Bar close_point = coof_point_bar(script_bar, cursor);
// The COOF terminal-C loop bypasses process_pending_orders(), so apply
// the exact two-call explicit reversal gross-admission fence once,
// after the ordinary close body has emitted the complete sibling book
// and before either sibling can fill.
apply_pooc_coof_explicit_flat_market_gross_admission();
int c_guard = 0;
while (++c_guard <= kCoofLoopGuard) {
current_bar_ = close_point;
Expand Down
84 changes: 66 additions & 18 deletions src/engine_strategy_commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long,
&& std::isnan(limit_price) && std::isnan(stop_price)
&& oca_name.empty()
&& (qty_type < 0 || qty_type == static_cast<int>(QtyType::FIXED));
const bool pooc_coof_ordinary_flat_market_call =
!std::isnan(qty)
&& std::isnan(limit_price) && std::isnan(stop_price)
&& process_orders_on_close_
&& calc_on_order_fills_
&& !coof_fill_recalc_active_
&& position_side_ == PositionSide::FLAT;
const double paired_flat_market_own_qty = explicit_fixed_market_call
? std::abs(apply_qty_step(qty))
: std::numeric_limits<double>::quiet_NaN();
Expand All @@ -124,6 +131,9 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long,
&& paired_flat_market_own_qty > kQtyEpsilon
&& pending_flat_market_pair_scope_is_live()
&& position_side_ == PositionSide::FLAT;
const bool pooc_coof_explicit_flat_market_candidate =
explicit_fixed_market_call
&& pooc_coof_ordinary_flat_market_call;

// TradingView broker rule: market-entry orders are admitted only
// when ``qty * <signal-bar close> * margin_pct/100 <= equity``.
Expand Down Expand Up @@ -172,6 +182,13 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long,
double available_equity = current_equity();
double epsilon = std::max(1e-9, std::abs(available_equity) * 1e-12);
if (required_margin > available_equity + epsilon) {
// A rejected third call leaves no PendingOrder or incarnation,
// but it still means the source body was not the clean exact
// two-call terminal-C shape. Preserve that invisible history.
if (pooc_coof_ordinary_flat_market_call) {
pending_flat_market_pair_disqualified_bars_.insert(
bar_index_);
}
return;
}
}
Expand All @@ -198,6 +215,13 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long,
if (paired_flat_market_candidate && entry_like) {
pending_flat_market_pair_disqualified_bars_.insert(bar_index_);
}
// The POOC+COOF gross-admission oracle covers two fresh calls,
// not a current call that inherits an older order's broker
// priority. Taint the current source bar even when the replaced
// object came from a prior bar.
if (pooc_coof_explicit_flat_market_candidate) {
pending_flat_market_pair_disqualified_bars_.insert(bar_index_);
}
if (entry_like
&& pending.created_position_side == PositionSide::FLAT) {
pending_flat_market_pair_disqualified_bars_.insert(
Expand Down Expand Up @@ -243,6 +267,7 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long,
order.created_bar = bar_index_;
order.created_seq = preserved_seq > 0 ? preserved_seq : next_order_seq_++;
order.incarnation = next_order_incarnation_++;
order.created_by_same_id_replacement = preserved_seq > 0;
order.created_during_coof_recalc = coof_fill_recalc_active_;
order.coof_born_at_close_recalc =
coof_fill_recalc_active_ && coof_cursor_is_bar_close_;
Expand Down Expand Up @@ -1256,12 +1281,20 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro
void BacktestEngine::strategy_cancel(const std::string& id) {
for (const PendingOrder& order : pending_orders_) {
if (order.id == id) {
if ((order.type == OrderType::MARKET
|| order.type == OrderType::ENTRY
|| order.type == OrderType::RAW_ORDER)
&& order.created_position_side == PositionSide::FLAT) {
pending_flat_market_pair_disqualified_bars_.insert(
order.created_bar);
const bool entry_like =
order.type == OrderType::MARKET
|| order.type == OrderType::ENTRY
|| order.type == OrderType::RAW_ORDER;
if (entry_like) {
if (order.created_position_side == PositionSide::FLAT) {
pending_flat_market_pair_disqualified_bars_.insert(
order.created_bar);
}
if (process_orders_on_close_ && calc_on_order_fills_
&& !coof_fill_recalc_active_) {
pending_flat_market_pair_disqualified_bars_.insert(
bar_index_);
}
}
invalidate_pending_flat_market_pair(order.created_seq);
}
Expand All @@ -1274,12 +1307,19 @@ void BacktestEngine::strategy_cancel(const std::string& id) {

void BacktestEngine::strategy_cancel_all() {
for (const PendingOrder& order : pending_orders_) {
if ((order.type == OrderType::MARKET
|| order.type == OrderType::ENTRY
|| order.type == OrderType::RAW_ORDER)
&& order.created_position_side == PositionSide::FLAT) {
pending_flat_market_pair_disqualified_bars_.insert(
order.created_bar);
const bool entry_like =
order.type == OrderType::MARKET
|| order.type == OrderType::ENTRY
|| order.type == OrderType::RAW_ORDER;
if (entry_like) {
if (order.created_position_side == PositionSide::FLAT) {
pending_flat_market_pair_disqualified_bars_.insert(
order.created_bar);
}
if (process_orders_on_close_ && calc_on_order_fills_
&& !coof_fill_recalc_active_) {
pending_flat_market_pair_disqualified_bars_.insert(bar_index_);
}
}
}
pending_orders_.clear();
Expand All @@ -1300,12 +1340,20 @@ void BacktestEngine::strategy_order(const std::string& id, bool is_long, double
// Remove existing pending order with same id
for (const PendingOrder& pending : pending_orders_) {
if (pending.id == id) {
if ((pending.type == OrderType::MARKET
|| pending.type == OrderType::ENTRY
|| pending.type == OrderType::RAW_ORDER)
&& pending.created_position_side == PositionSide::FLAT) {
pending_flat_market_pair_disqualified_bars_.insert(
pending.created_bar);
const bool entry_like =
pending.type == OrderType::MARKET
|| pending.type == OrderType::ENTRY
|| pending.type == OrderType::RAW_ORDER;
if (entry_like) {
if (pending.created_position_side == PositionSide::FLAT) {
pending_flat_market_pair_disqualified_bars_.insert(
pending.created_bar);
}
if (process_orders_on_close_ && calc_on_order_fills_
&& !coof_fill_recalc_active_) {
pending_flat_market_pair_disqualified_bars_.insert(
bar_index_);
}
}
invalidate_pending_flat_market_pair(pending.created_seq);
}
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ set(TEST_SOURCES
test_stop_decline_continue_path
test_frozen_flat_gap_reject
test_explicit_qty_fill_admission
test_pooc_coof_reversal_gross_admission
test_limit_fill_slippage
test_strategy_commands_extra
test_multi_tier_exit_precedence
Expand Down
Loading
Loading