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
27 changes: 27 additions & 0 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,12 @@ class BacktestEngine {
// it cannot rewrite otherwise matching trade tapes.
bool margin_zero_cover_full_liquidation_ = false;
int max_intraday_filled_orders_ = 0; // 0 = unlimited
// Default-off validation candidates for independently testable intraday-
// cap broker semantics. They ride the existing metadata channel
// so corpus execution remains byte-identical unless explicitly enabled.
bool intraday_cap_skip_noop_market_fills_ = false;
bool intraday_cap_defer_pooc_close_ = false;
bool intraday_cap_count_pooc_full_close_fills_ = false;
bool close_entries_rule_any_ = false; // true = "ANY", false = "FIFO" (default)
// Percentage of margin required to open a long/short position. Default
// 100 = 1x leverage (no leverage). TradingView's strategy() takes these
Expand Down Expand Up @@ -951,6 +957,15 @@ class BacktestEngine {
int intraday_fill_count_ = 0;
int intraday_day_ = -1; // day key (dayofmonth*100+month) for reset
bool intraday_cap_hit_ = false; // latched once per chart-day; reset on day rollover
// State, not configuration: a POOC MARKET fill reached the cap at the
// signal close and its risk-generated flatten is due at the next broker
// boundary (the next ordinary bar open).
bool intraday_cap_deferred_close_pending_ = false;
// An ordinary POOC strategy.close plus one co-queued opposite MARKET is
// materialized as two engine operations even when TV reports one reversal
// fill. The close spends the quota immediately; this fresh order identity
// may inherit that already-spent slot if it survives every fill-time gate.
uint64_t intraday_cap_pooc_close_inheritor_incarnation_ = 0;

// True iff the intraday cap is currently latched on the CURRENT bar's
// chart-day. Performs a lazy day-rollover reset so callers outside the
Expand Down Expand Up @@ -2531,6 +2546,18 @@ class BacktestEngine {
margin_zero_cover_full_liquidation_ =
std::isfinite(value) && value > 0.0;
}
if (key == "intraday_cap_skip_noop_market_fills") {
intraday_cap_skip_noop_market_fills_ =
std::isfinite(value) && value > 0.0;
}
if (key == "intraday_cap_defer_pooc_close") {
intraday_cap_defer_pooc_close_ =
std::isfinite(value) && value > 0.0;
}
if (key == "intraday_cap_count_pooc_full_close_fills") {
intraday_cap_count_pooc_full_close_fills_ =
std::isfinite(value) && value > 0.0;
}
// "qty_step" is the per-instrument lot increment used by the forced-
// liquidation quantizer. Route it onto the dedicated member so the
// codegen run(const Bar*, int) path (which never overwrites it) keeps
Expand Down
75 changes: 73 additions & 2 deletions src/engine_fills.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,15 @@ void BacktestEngine::apply_filled_order_to_state(
uint64_t& exit_closed_from_incarnation,
bool& exit_closed_was_long,
std::vector<size_t>& filled_indices) {
const bool inherits_pooc_close_fill =
intraday_cap_count_pooc_full_close_fills_
&& order.incarnation != 0
&& order.incarnation
== intraday_cap_pooc_close_inheritor_incarnation_;
auto decline_and_cancel = [&]() {
if (inherits_pooc_close_fill) {
intraday_cap_pooc_close_inheritor_incarnation_ = 0;
}
invalidate_pending_flat_market_pair(order.created_seq);
filled_indices.push_back(order_index);
};
Expand Down Expand Up @@ -1745,6 +1753,27 @@ void BacktestEngine::apply_filled_order_to_state(
}
}

// A same-direction MARKET can reach the fill kernel while the live
// position is already at its pyramiding cap (a later-bar reissue, or a
// same-tick sibling after an earlier entry opened the position). The
// established dispatch consumes it but mutates no position
// (add_to_pyramid_market's cap branch); opt-in factor A consumes it here
// before risk-cap accounting so an attempt that never filled does not
// spend max_intraday_filled_orders quota. Classify against LIVE state at
// fill time so close-then-reentry and reversals remain real fills.
if (intraday_cap_skip_noop_market_fills_
&& max_intraday_filled_orders_ > 0
&& order.type == OrderType::MARKET
&& position_side_ != PositionSide::FLAT) {
const PositionSide requested =
order.is_long ? PositionSide::LONG : PositionSide::SHORT;
if (position_side_ == requested
&& position_entry_count_ >= pyramiding_) {
decline_and_cancel();
return;
}
}

// Check max_intraday_filled_orders limit.
//
// TV's broker emulator (LATCH-TILL-DAY-ROLLOVER semantics):
Expand Down Expand Up @@ -1780,14 +1809,25 @@ void BacktestEngine::apply_filled_order_to_state(
intraday_fill_count_ = 0;
intraday_cap_hit_ = false; // RESET LATCH on chart-day rollover
}
if (intraday_cap_hit_) {
// A POOC close+opposite-entry reversal is split by the engine into a
// close operation followed by a MARKET operation. Factor C counted
// the close synchronously, before this order could be rejected or
// cancelled. Only the exact surviving incarnation may continue the
// same broker event without spending a second slot; all ordinary
// orders still obey the latch. Every pre-account admission failure
// above clears the inheritance while retaining the real close count.
if (intraday_cap_hit_ && !inherits_pooc_close_fill) {
// Latched: drop this pending order and skip dispatch.
// Removing from pending_orders_ matches TV's behaviour of
// silently consuming/rejecting fills past the daily cap.
decline_and_cancel();
return;
}
intraday_fill_count_++;
if (inherits_pooc_close_fill) {
intraday_cap_pooc_close_inheritor_incarnation_ = 0;
} else {
intraday_fill_count_++;
}
will_trigger_cap =
(intraday_fill_count_ >= max_intraday_filled_orders_);
}
Expand Down Expand Up @@ -1943,6 +1983,17 @@ void BacktestEngine::apply_filled_order_to_state(
|| pyramid_entries_.size() != pyramid_lots_before_fill
|| trades_.size() != trades_before;

// The POOC close's quota identity can transfer only to the first broker
// operation that immediately continues that close as its designated
// reversal MARKET. If any other order actually fills first, broker order
// has moved on: expire the token before OCA effects and before this fill's
// cap close/latch. A matched-but-rejected or zero-effect attempt is not a
// broker fill and intentionally leaves the exact inheritor eligible.
if (primary_fill_applied && !inherits_pooc_close_fill
&& intraday_cap_pooc_close_inheritor_incarnation_ != 0) {
intraday_cap_pooc_close_inheritor_incarnation_ = 0;
}

// Bounded POOC global-exit growth. Only MARKET adds that were already
// pending when the one tracking EXIT was armed carry this relation bit.
// Grow its ordinary finite reservation by the actual same-side quantity
Expand Down Expand Up @@ -2164,6 +2215,26 @@ void BacktestEngine::apply_filled_order_to_state(
// only on chart-day rollover (see top of this function).
if (will_trigger_cap) {
if (position_side_ != PositionSide::FLAT) {
// Opt-in factor B is deliberately narrow: an ordinary historical
// POOC run, no COOF/magnifier scheduler, and a MARKET created on
// this bar. TradingView accepts that entry at the signal close
// but emits the cap flatten at the next broker boundary. Latch
// immediately below; dispatch_bar performs the pending close at
// the next bar's open before any other broker work.
const bool defer_pooc_market_close =
intraday_cap_defer_pooc_close_
&& process_orders_on_close_
&& !calc_on_order_fills_
&& !bar_magnifier_enabled_
&& !stream_warmup_mode_
&& stream_phase_ == StreamPhase::IDLE
&& order.type == OrderType::MARKET
&& order.created_bar == bar_index_;
if (defer_pooc_market_close) {
intraday_cap_deferred_close_pending_ = true;
intraday_cap_hit_ = true;
return;
}
// TV cap-close exit price empirics (probe 97 stop-entry +
// cap composition):
//
Expand Down
32 changes: 32 additions & 0 deletions src/engine_run.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,35 @@ void BacktestEngine::dispatch_bar() {
return;
}

// A C-factor inheritance is same-ordinary-bar state. A candidate erased
// by replacement/OCA/cancel never reaches the fill kernel, so discard any
// stale identity before starting the next broker batch.
intraday_cap_pooc_close_inheritor_incarnation_ = 0;

// Opt-in POOC intraday-cap candidate: the cap-triggering MARKET entry
// filled at the prior signal close, while the broker-generated flatten is
// due at this next broker boundary. Run it before resting orders and
// before the current bar's path is sampled so the exit is exactly at open.
if (intraday_cap_deferred_close_pending_) {
intraday_cap_deferred_close_pending_ = false;
if (position_side_ != PositionSide::FLAT) {
const size_t trades_before = trades_.size();
const PositionSide side_before = position_side_;
const double qty_before = position_qty_;
execute_market_exit(current_bar_.open);
if (position_side_ != side_before
|| std::abs(position_qty_ - qty_before) > kQtyEpsilon
|| trades_.size() != trades_before) {
++broker_fill_event_seq_;
}
for (size_t ti = trades_before; ti < trades_.size(); ++ti) {
trades_[ti].exit_comment =
"Close Position (Max number of filled orders in one day)";
trades_[ti].exit_id = "";
}
}
}

// Advance native source-series history before strategy logic so
// get_input_source()'s returned series is current for this bar. Covers
// the simple run() loop, run_simple_bar_loop, and the no-magnifier
Expand All @@ -67,6 +96,7 @@ void BacktestEngine::dispatch_bar() {
invoke_chart_on_bar(current_bar_); // step 3: strategy logic
flush_same_bar_close(); // step 3b: surviving strategy.close fill
process_pending_orders(current_bar_); // step 4: new market orders
intraday_cap_pooc_close_inheritor_incarnation_ = 0;
} else {
process_pending_orders(current_bar_);
update_per_trade_extremes();
Expand Down Expand Up @@ -513,6 +543,8 @@ void BacktestEngine::reset_run_state() {
intraday_day_ = -1;
intraday_cap_hit_ = false;
intraday_fill_count_ = 0;
intraday_cap_deferred_close_pending_ = false;
intraday_cap_pooc_close_inheritor_incarnation_ = 0;
broker_fill_event_seq_ = 0;
coof_scheduler_active_ = false;
coof_fill_recalc_active_ = false;
Expand Down
56 changes: 56 additions & 0 deletions src/engine_strategy_commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,62 @@ void BacktestEngine::flush_same_bar_close() {
|| trades_.size() != trades_before;
if (close_filled) {
++broker_fill_event_seq_;
// Default-off follow-up to the no-op MARKET candidate: this broker
// fill bypasses apply_filled_order_to_state, but TradingView's
// max_intraday_filled_orders counts filled close orders too. Without
// this accounting, removing phantom no-op fills merely unmasks the
// opposite under-count and can admit an extra late-day trade cycle.
// Scope to the ordinary POOC same-bar batch exercised by Regime; other
// immediate/COOF close variants remain unchanged pending their own
// oracle.
if (intraday_cap_count_pooc_full_close_fills_
&& process_orders_on_close_
&& !calc_on_order_fills_
&& !coof_scheduler_active_
&& !bar_magnifier_enabled_
&& !stream_warmup_mode_
&& stream_phase_ == StreamPhase::IDLE
&& !close_entries_rule_any_
&& closes_full_position
&& max_intraday_filled_orders_ > 0) {
// Count the close now, never from the mere presence of a queued
// opposite order. If one co-queued opposite MARKET survives to
// its fill-time admission point, the earliest broker-order
// incarnation inherits this already-spent slot. A rejection,
// cancellation, replacement, or later no-op therefore cannot
// erase the real close fill; the marker simply expires.
const PendingOrder* inheritor = nullptr;
for (const PendingOrder& pending : pending_orders_) {
if (pending.type != OrderType::MARKET
|| pending.created_bar != bar_index_
|| pending.is_long
== (side_before == PositionSide::LONG)) {
continue;
}
if (inheritor == nullptr
|| pending.created_seq < inheritor->created_seq) {
inheritor = &pending;
}
}
intraday_cap_pooc_close_inheritor_incarnation_ =
inheritor == nullptr ? 0 : inheritor->incarnation;

BarTime bt = _decompose_bar_time_chart_tz();
const int cur_day = bt.dayofmonth * 100 + bt.month;
if (cur_day != intraday_day_) {
intraday_day_ = cur_day;
intraday_fill_count_ = 0;
intraday_cap_hit_ = false;
}
if (!intraday_cap_hit_) {
++intraday_fill_count_;
if (intraday_fill_count_ >= max_intraday_filled_orders_) {
// The close already left the position flat, so no
// synthetic risk close is needed; only latch the day.
intraday_cap_hit_ = true;
}
}
}
if (coof_scheduler_active_ && coof_direct_fill_events_remaining_ > 0) {
--coof_direct_fill_events_remaining_;
}
Expand Down
Loading
Loading