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
47 changes: 47 additions & 0 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,27 @@ class BacktestEngine {
std::string sb_close_comment_; // surviving order's comment
std::unordered_map<std::string, double> close_reserved_qty_;

// --- KI-64: POOC script-visible position freeze ---
// Under process_orders_on_close, a strategy.close/close_all that fills
// IN-LINE (execute_immediate_close) mutates the broker position mid-on_bar,
// but TradingView keeps the SCRIPT position accessor
// ``strategy.position_size`` (signed_position_size(), and the
// opentrades/position_avg_price na-guards derived from it) reporting the
// PRE-close position until the NEXT bar. While ``pos_view_freeze_bar_ ==
// bar_index_`` the script-facing signed_position_size() returns this frozen
// snapshot; broker / order state and every internal position_qty_ /
// position_side_ read (order sizing, affordability, reversal qty, exit
// sizing) are UNAFFECTED. Armed in strategy_close on the ordinary POOC
// immediate-close path (NOT immediately=true, which is defined to reflect
// its fill at once); cleared at the top of flush_same_bar_close() — the
// first thing after every POOC on_bar — so step-4 and post-run reads see
// the real position. The bar_index_ scoping is a defensive backstop: the
// snapshot auto-expires when the script advances a bar even if a clear site
// is ever missed.
int pos_view_freeze_bar_ = -1;
PositionSide pos_view_frozen_side_ = PositionSide::FLAT;
double pos_view_frozen_qty_ = 0.0;

// --- Strategy parameters (set from strategy() declaration) ---
double initial_capital_ = 1000000.0;
bool process_orders_on_close_ = false;
Expand Down Expand Up @@ -1161,11 +1182,37 @@ class BacktestEngine {

// --- Strategy variable accessors ---
double signed_position_size() const {
// KI-64: while a POOC same-bar in-line close is frozen for this bar,
// the SCRIPT sees the pre-close position (TV defers close visibility to
// the next bar). Broker/internal reads use position_side_/position_qty_
// directly and are unaffected.
if (pos_view_freeze_bar_ == bar_index_) {
if (pos_view_frozen_side_ == PositionSide::LONG) return pos_view_frozen_qty_;
if (pos_view_frozen_side_ == PositionSide::SHORT) return -pos_view_frozen_qty_;
return 0.0;
}
if (position_side_ == PositionSide::LONG) return position_qty_;
if (position_side_ == PositionSide::SHORT) return -position_qty_;
return 0.0;
}

// KI-64: freeze the pre-close position for the script-visible position
// accessor before an ordinary POOC strategy.close/close_all fills in-line
// this bar. Capture-once per on_bar (a second same-bar close keeps the
// FIRST pre-close snapshot). Caller guards process_orders_on_close_ &&
// !immediately; this reads position_side_/position_qty_ while they still
// hold the pre-close values (execute_immediate_close has not run yet).
void freeze_script_position_view() {
if (pos_view_freeze_bar_ == bar_index_) return;
pos_view_freeze_bar_ = bar_index_;
pos_view_frozen_side_ = position_side_;
pos_view_frozen_qty_ = position_qty_;
}
// KI-64: release the freeze so the next script-visible read returns the real
// (post-close) position. Called at the top of flush_same_bar_close(), i.e.
// immediately after every POOC on_bar returns.
void clear_script_position_view() { pos_view_freeze_bar_ = -1; }

double net_profit() const { return net_profit_sum_; }
double gross_profit() const { return gross_profit_sum_; }
double gross_loss() const { return gross_loss_sum_; }
Expand Down
2 changes: 2 additions & 0 deletions src/engine_run.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ uint64_t BacktestEngine::execute_coof_script_body(
is_last_tick_ = true;
history_slot_is_new_ = !coof_checkpoint_contains_current_bar_;
pending_close_qty_in_bar_ = 0.0;
pos_view_freeze_bar_ = -1; // KI-64: recompute re-arms the freeze fresh
_push_source_series();
update_per_trade_extremes();

Expand Down Expand Up @@ -366,6 +367,7 @@ void BacktestEngine::reset_run_state() {
// pyramid_entries_, trail, partial ids
pending_orders_.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;
sb_close_bar_ = -1;
sb_close_calls_ = 0;
Expand Down
15 changes: 15 additions & 0 deletions src/engine_strategy_commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,14 @@ void BacktestEngine::strategy_close(const std::string& id, const std::string& co
if ((pooc_can_fill_at_this_cursor || immediately)
&& !(coof_scheduler_active_
&& coof_direct_fill_events_remaining_ == 0)) {
// KI-64: for an ORDINARY POOC close (not immediately=true, which is
// defined to reflect its fill at once) freeze the script-visible
// position BEFORE execute_immediate_close mutates it, so a later
// strategy.position_size gate in THIS bar still sees the pre-close
// position. Broker/order side effects below are unchanged.
if (process_orders_on_close_ && !immediately) {
freeze_script_position_view();
}
execute_immediate_close(id, comment, qty_to_close, matching_qty,
closes_full_position, closes_fifo_qty, closes_any_qty);
return;
Expand Down Expand Up @@ -513,6 +521,13 @@ void BacktestEngine::enqueue_same_bar_close(const std::string& id,
// the magnifier's last tick), i.e. the same bar and price the immediate
// path used, after the strategy's on_bar has fully run.
void BacktestEngine::flush_same_bar_close() {
// KI-64: on_bar has returned — release any POOC script-visible position
// freeze so the flush below, step-4 order processing, and post-run reads
// all observe the real (post-close) position. Runs on every POOC bar
// (flush is the first call after each POOC on_bar), before the early
// return so a bar with an in-line close but no enqueued survivor still
// clears. Broker reads here use position_side_/position_qty_ directly.
clear_script_position_view();
if (!sb_close_active_) return;
const std::string id = sb_close_id_;
const std::string comment = sb_close_comment_;
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ set(TEST_SOURCES
test_margin_call
test_streaming
test_calc_on_order_fills
test_pooc_position_visibility
)

find_package(Threads REQUIRED)
Expand Down
Loading
Loading