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
38 changes: 38 additions & 0 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,20 @@ struct PendingOrder {
// 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;
// For a strategy.exit replacement, the unique incarnation of the exact
// matching (id, from_entry) EXIT object it replaced. Zero for a fresh
// child. This correlates retained broker priority with a concrete prior
// child rather than a replacement-shaped call sequence created later.
uint64_t replaced_exit_order_incarnation = 0;
// Incarnation of the live priced ENTRY removed by strategy.cancel(id)
// earlier in the same source evaluation, copied only onto the first fresh
// same-id strategy.entry call and then consumed. Zero means there is no
// exact named-cancel -> fresh-recreate provenance.
uint64_t recreated_after_named_cancelled_entry_incarnation = 0;
// Incarnation of the unique attached EXIT child that was still live when
// the parent above was named-cancelled. Copied with the parent cancel
// token and consumed by the same fresh recreate call.
uint64_t named_cancel_surviving_exit_incarnation = 0;
// 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 @@ -698,6 +712,15 @@ class BacktestEngine {
// Historical fill-triggered recalculation is strictly opt-in. The false
// branch in dispatch_bar remains the legacy control path.
bool calc_on_order_fills_ = false;
// Narrow ordinary-POOC broker ordering rule for one exact book shape:
// while truly flat, a single reissued from_entry bracket can retain an
// older sequence slot than its same-source-bar pure-stop parent after the
// prior parent was explicitly cancelled and freshly recreated.
// That exact two-order book scans the parent first so the child can inspect
// the post-entry path before the close-time script body. The metadata key
// remains as an explicit A/B override; ordinary execution enables the
// TV-pinned rule by default.
bool flat_retained_child_fresh_parent_order_ = true;
QtyType default_qty_type_ = QtyType::FIXED;
double default_qty_value_ = 1.0;
int pyramiding_ = 1; // max additional entries in same direction
Expand Down Expand Up @@ -950,6 +973,16 @@ class BacktestEngine {
// two-call book.
std::unordered_set<int>
default_flat_market_gross_disqualified_bars_;
// Evaluation-scoped tombstones for live priced ENTRY objects actually
// removed by strategy.cancel(id). invoke_chart_on_bar clears the map
// before each script execution; the first fresh same-id strategy.entry
// consumes the unique cancelled incarnation.
struct NamedEntryCancelContext {
uint64_t entry_incarnation = 0;
uint64_t surviving_exit_incarnation = 0;
};
std::unordered_map<std::string, NamedEntryCancelContext>
named_entry_cancelled_incarnation_in_current_eval_;

// strategy.exit partial orders are one-shot per open position for a given id
std::unordered_set<std::string> consumed_partial_exit_ids_;
Expand Down Expand Up @@ -2292,6 +2325,7 @@ class BacktestEngine {
const std::string& from_entry,
bool has_trail_request,
int64_t& preserved_seq_out,
uint64_t& replaced_incarnation_out,
double& preserved_reserved_qty_out);
bool compute_exit_reserved_qty(const std::string& from_entry,
double preserved_reserved_qty,
Expand Down Expand Up @@ -2585,6 +2619,10 @@ class BacktestEngine {
margin_zero_cover_full_liquidation_ =
std::isfinite(value) && value > 0.0;
}
if (key == "flat_retained_child_fresh_parent_order") {
flat_retained_child_fresh_parent_order_ =
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;
Expand Down
64 changes: 64 additions & 0 deletions src/engine_fills.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,55 @@ void BacktestEngine::sort_orders_by_fill_phase(const Bar& bar) {
live_flat_market_pair_seqs.insert(order.created_seq);
}
}

// Default-on retained-child / fresh-parent rule. A child retained across
// an explicit parent cancellation can keep an older broker slot than the
// freshly recreated parent; reissuing the child on that same source bar
// preserves those unequal priorities even though created_bar is now equal. The
// ordinary sequence tie-break then scans the child while flat (Skip) and
// the parent afterwards, so POOC on_bar observes a transient position
// before the second broker pass can revisit the child.
//
// Keep the comparator edge acyclic by admitting only an exact two-object
// book and recording immutable incarnation ids before stable_sort. Every
// provenance predicate below is intentional: ordinary POOC, no COOF or
// magnifier scheduler, one prior-bar freshly recreated pure-stop ENTRY, one
// same-source-bar flat-born full non-trailing EXIT child with older broker
// priority, and no OCA grouping.
uint64_t retained_child_parent_first_incarnation = 0;
uint64_t retained_child_later_incarnation = 0;
int64_t retained_child_parent_effective_seq = 0;
int64_t retained_child_later_effective_seq = 0;
if (pending_orders_.size() == 2) {
const PendingOrder* parent = nullptr;
const PendingOrder* child = nullptr;
for (const PendingOrder& order : pending_orders_) {
if (order.type == OrderType::ENTRY) {
parent = &order;
} else if (order.type == OrderType::EXIT) {
child = &order;
}
}
const RetainedChildFreshParentOrderContext context{
flat_retained_child_fresh_parent_order_,
position_side_ == PositionSide::FLAT,
process_orders_on_close_,
calc_on_order_fills_,
coof_scheduler_active_,
bar_magnifier_enabled_,
stream_warmup_mode_,
stream_phase_ == StreamPhase::IDLE,
pending_orders_.size(),
bar_index_,
};
if (retained_child_fresh_parent_order_pair(
context, parent, child)) {
retained_child_parent_first_incarnation = parent->incarnation;
retained_child_later_incarnation = child->incarnation;
retained_child_parent_effective_seq = child->created_seq;
retained_child_later_effective_seq = parent->created_seq;
}
}
// A single relative strategy.exit armed while FLAT has no concrete
// stop/limit until its earlier same-bar from_entry parent fills. It
// otherwise looks like a phase-0 market order and sorts before a non-gap
Expand Down Expand Up @@ -1397,6 +1446,21 @@ void BacktestEngine::sort_orders_by_fill_phase(const Bar& bar) {
int pa = fill_phase(a);
int pb = fill_phase(b);
if (pa != pb) return pa < pb;
if (retained_child_parent_first_incarnation != 0) {
auto effective_seq = [&](const PendingOrder& order) {
if (order.incarnation
== retained_child_parent_first_incarnation) {
return retained_child_parent_effective_seq;
}
if (order.incarnation == retained_child_later_incarnation) {
return retained_child_later_effective_seq;
}
return order.created_seq;
};
const int64_t a_seq = effective_seq(a);
const int64_t b_seq = effective_seq(b);
if (a_seq != b_seq) return a_seq < b_seq;
}
auto is_entry_same_as_current_position = [&](const PendingOrder& o) {
return (o.type == OrderType::MARKET || o.type == OrderType::ENTRY)
&& ((position_side_ == PositionSide::LONG && o.is_long)
Expand Down
100 changes: 100 additions & 0 deletions src/engine_internal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#include <pineforge/engine.hpp>

#include <cmath>
#include <limits>
#include <string>
#include <vector>
Expand Down Expand Up @@ -55,6 +56,105 @@ inline constexpr double kPathPosEps = 1e-12;
inline constexpr double kSegmentDenomEps = 1e-15;
inline constexpr double kPathTimeEps = 1e-12;

struct RetainedChildFreshParentOrderContext {
bool enabled = false;
bool broker_flat = false;
bool process_orders_on_close = false;
bool calc_on_order_fills = false;
bool coof_scheduler_active = false;
bool bar_magnifier_enabled = false;
bool stream_warmup_mode = false;
bool stream_idle = false;
std::size_t pending_count = 0;
int bar_index = -1;
};

// Exact lifecycle predicate for the ordinary-POOC ordering exception where a
// retained strategy.exit child has an older broker slot than the pure-STOP
// parent explicitly cancelled and freshly recreated in the same evaluation.
// Keeping the full predicate runtime-private and side-effect-free makes every
// exclusion independently unit-testable; the comparator only swaps two
// immutable scalar sequence keys after this returns true.
inline bool retained_child_fresh_parent_order_pair(
const RetainedChildFreshParentOrderContext& ctx,
const PendingOrder* parent,
const PendingOrder* child) {
if (!ctx.enabled
|| !ctx.broker_flat
|| !ctx.process_orders_on_close
|| ctx.calc_on_order_fills
|| ctx.coof_scheduler_active
|| ctx.bar_magnifier_enabled
|| ctx.stream_warmup_mode
|| !ctx.stream_idle
|| ctx.pending_count != 2
|| parent == nullptr
|| child == nullptr) {
return false;
}

const uint64_t cancelled_incarnation =
parent->recreated_after_named_cancelled_entry_incarnation;
const uint64_t surviving_exit_incarnation =
parent->named_cancel_surviving_exit_incarnation;
const bool parent_is_exact_fresh_stop =
parent->type == OrderType::ENTRY
&& parent->created_position_side == PositionSide::FLAT
&& !parent->created_by_same_id_replacement
&& cancelled_incarnation != 0
&& cancelled_incarnation < parent->incarnation
&& cancelled_incarnation != child->incarnation
&& surviving_exit_incarnation > cancelled_incarnation
&& surviving_exit_incarnation < parent->incarnation
&& parent->created_bar == ctx.bar_index - 1
&& std::isnan(parent->qty)
&& !parent->created_during_coof_recalc
&& !parent->created_after_position_close_in_bar
&& !parent->over_pyramiding_cap_at_placement
&& !parent->stop_limit_activated
&& std::isfinite(parent->stop_price)
&& std::isnan(parent->limit_price)
&& std::isnan(parent->trail_points)
&& std::isnan(parent->trail_price)
&& std::isnan(parent->trail_offset)
&& parent->oca_name.empty()
&& parent->oca_type == 0;
const double child_qp = std::isnan(child->qty_percent)
? 100.0 : child->qty_percent;
const bool child_is_exact_retained_bracket =
child->type == OrderType::EXIT
&& !child->from_entry.empty()
&& child->created_by_same_id_replacement
&& child->replaced_exit_order_incarnation
== surviving_exit_incarnation
&& !child->created_while_in_position
&& child->created_position_side == PositionSide::FLAT
&& child->created_bar == ctx.bar_index - 1
&& !child->created_during_coof_recalc
&& !child->created_after_position_close_in_bar
&& !child->requested_partial
&& std::isnan(child->qty)
&& child_qp >= 100.0 - kFullPercentEps
&& std::isfinite(child->stop_price)
&& std::isfinite(child->limit_price)
&& std::isnan(child->profit_ticks)
&& std::isnan(child->loss_ticks)
&& std::isnan(child->trail_points)
&& std::isnan(child->trail_price)
&& std::isnan(child->trail_offset)
&& child->oca_name.empty()
&& child->oca_type == 0;
return parent_is_exact_fresh_stop
&& child_is_exact_retained_bracket
&& child->from_entry == parent->id
&& child->created_seq < parent->created_seq
&& child->incarnation != 0
&& parent->incarnation != 0
&& parent->incarnation
< std::numeric_limits<uint64_t>::max()
&& child->incarnation == parent->incarnation + 1;
}

// Among flat pending opposite ENTRY stop-only orders, which stop price is touched
// first on the synthesized OHLC path (exactly one long + one short, both touched).
// Forward-declared in <pineforge/engine.hpp> with the same underlying type
Expand Down
2 changes: 2 additions & 0 deletions src/engine_run.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ void BacktestEngine::invoke_chart_on_bar(const Bar& bar) {
}
} scope(chart_ema_na_warmup_);

named_entry_cancelled_incarnation_in_current_eval_.clear();
on_bar(bar);
}

Expand Down Expand Up @@ -511,6 +512,7 @@ void BacktestEngine::reset_run_state() {
pending_orders_.clear();
pending_flat_market_pair_disqualified_bars_.clear();
default_flat_market_gross_disqualified_bars_.clear();
named_entry_cancelled_incarnation_in_current_eval_.clear();
pending_close_qty_in_bar_ = 0.0;
pos_view_freeze_bar_ = -1; // KI-64: fresh run starts with no frozen view
sb_close_active_ = false;
Expand Down
48 changes: 46 additions & 2 deletions src/engine_strategy_commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long,
int qty_type) {
if (!trading_is_active(current_bar_.timestamp, trade_start_time_, script_tf_seconds_)) return;

NamedEntryCancelContext named_cancel_context;
const auto named_cancel =
named_entry_cancelled_incarnation_in_current_eval_.find(id);
if (named_cancel
!= named_entry_cancelled_incarnation_in_current_eval_.end()) {
named_cancel_context = named_cancel->second;
named_entry_cancelled_incarnation_in_current_eval_.erase(named_cancel);
}

// TradingView intraday-cap freeze gate (Pine docs:
// ``strategy.risk.max_intraday_filled_orders``):
// "If the limit is reached during the day, the strategy is closed
Expand Down Expand Up @@ -311,6 +320,12 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long,
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;
if (preserved_seq == 0) {
order.recreated_after_named_cancelled_entry_incarnation =
named_cancel_context.entry_incarnation;
order.named_cancel_surviving_exit_incarnation =
named_cancel_context.surviving_exit_incarnation;
}
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 @@ -1021,9 +1036,11 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro
// return follows matching-EXIT removal but precedes all sizing and
// reservation work. trail_offset alone is intentionally insufficient.
int64_t discarded_seq = 0;
uint64_t discarded_incarnation = 0;
double discarded_reserved_qty = std::numeric_limits<double>::quiet_NaN();
clear_existing_exit_order(id, from_entry, /*has_trail_request=*/false,
discarded_seq, discarded_reserved_qty);
discarded_seq, discarded_incarnation,
discarded_reserved_qty);
return;
}
bool has_explicit_qty = !std::isnan(qty);
Expand Down Expand Up @@ -1062,9 +1079,11 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro
}

int64_t preserved_seq = 0;
uint64_t replaced_incarnation = 0;
double preserved_reserved_qty = std::numeric_limits<double>::quiet_NaN();
clear_existing_exit_order(id, from_entry, has_trail_request,
preserved_seq, preserved_reserved_qty);
preserved_seq, replaced_incarnation,
preserved_reserved_qty);

double reserved_qty = std::numeric_limits<double>::quiet_NaN();
bool bind_global_full_exit_dynamic_qty = false;
Expand Down Expand Up @@ -1274,6 +1293,8 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro
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.replaced_exit_order_incarnation = replaced_incarnation;
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 @@ -1375,6 +1396,15 @@ void BacktestEngine::strategy_cancel(const std::string& id) {
order.created_bar);
}
}
uint64_t surviving_exit_incarnation = 0;
int surviving_exit_count = 0;
for (const PendingOrder& order : pending_orders_) {
if (order.type == OrderType::EXIT && order.from_entry == id) {
++surviving_exit_count;
surviving_exit_incarnation = order.incarnation;
}
}
uint64_t removed_priced_entry_incarnation = 0;
for (const PendingOrder& order : pending_orders_) {
if (order.id == id) {
const bool entry_like =
Expand All @@ -1392,9 +1422,20 @@ void BacktestEngine::strategy_cancel(const std::string& id) {
bar_index_);
}
}
if (order.type == OrderType::ENTRY && order.incarnation != 0) {
removed_priced_entry_incarnation = order.incarnation;
}
invalidate_pending_flat_market_pair(order.created_seq);
}
}
if (removed_priced_entry_incarnation != 0
&& surviving_exit_count == 1
&& surviving_exit_incarnation != 0) {
named_entry_cancelled_incarnation_in_current_eval_[id] =
{removed_priced_entry_incarnation, surviving_exit_incarnation};
} else if (removed_priced_entry_incarnation != 0) {
named_entry_cancelled_incarnation_in_current_eval_.erase(id);
}
pending_orders_.erase(
std::remove_if(pending_orders_.begin(), pending_orders_.end(),
[&](const PendingOrder& o) { return o.id == id; }),
Expand Down Expand Up @@ -1835,14 +1876,17 @@ void BacktestEngine::clear_existing_exit_order(const std::string& id,
const std::string& from_entry,
bool has_trail_request,
int64_t& preserved_seq_out,
uint64_t& replaced_incarnation_out,
double& preserved_reserved_qty_out) {
bool had_existing_order = false;
preserved_seq_out = 0;
replaced_incarnation_out = 0;
preserved_reserved_qty_out = std::numeric_limits<double>::quiet_NaN();
for (const auto& o : pending_orders_) {
if (o.type == OrderType::EXIT && o.id == id && o.from_entry == from_entry) {
had_existing_order = true;
preserved_seq_out = o.created_seq;
replaced_incarnation_out = o.incarnation;
if (!std::isnan(o.qty)) {
preserved_reserved_qty_out = o.qty;
}
Expand Down
Loading
Loading