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
5 changes: 5 additions & 0 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,9 @@ struct PendingOrder {
std::numeric_limits<double>::quiet_NaN();
std::string comment; // order comment for trade reporting
bool requested_partial = false; // true iff caller passed qty_percent < 100
// Preserve the original default/full-percent EXIT call before reservation
// normalization can turn a sub-lot partial request into a full-size order.
bool full_percent_exit_request = false;
// Narrow POOC global-full-exit candidate. ``qty`` deliberately keeps the
// normal finite reservation so sibling exits see and respect its capacity.
// At fill time this bit upgrades that one reservation to the full live
Expand Down Expand Up @@ -3795,6 +3798,8 @@ class BacktestEngine {
// apply (mutate engine state with the fill — see apply_*_order_fill
// declarations above).
enum class OrderEligibility { Proceed, Skip, Remove };
double pooc_short_exit_trigger_close(const PendingOrder& order,
const Bar& bar) const;
OrderEligibility classify_order_eligibility(
PendingOrder& order, int opposing_pass,
internal::DualEntryStopPathWinner dual_entry_path,
Expand Down
64 changes: 58 additions & 6 deletions src/engine_fills.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7199,6 +7199,56 @@ void BacktestEngine::mark_position_brackets_dormant_on_declined_reversal(const B
// Returns whether the given pending order should be processed this
// iteration. Walks the chain of TV-empirical "skip" / "cancel" rules
// in source order; the first rule to fire dictates the verdict.
double BacktestEngine::pooc_short_exit_trigger_close(
const PendingOrder& order, const Bar& bar) const {
// Hariss F POOC pins: newly reissued short exits test the broker's tick
// close, while Pine still sees raw OHLC and the order levels stay raw.
// C11.575 ->11.58 skips limit11.576782; C11.695 ->11.70 reaches
// stop11.698693; C12.495 ->12.50 reaches stop12.496973. Both admission
// and fill evaluation must use the same close and never an elapsed wick.
const bool pinned_reissue = process_orders_on_close_
&& !calc_on_order_fills_ && !coof_scheduler_active_
&& !bar_magnifier_enabled_ && !stream_warmup_mode_
&& stream_phase_ == StreamPhase::IDLE
&& position_side_ == PositionSide::SHORT
&& position_open_bar_ >= 0 && position_open_bar_ < bar_index_
&& position_entry_count_ == 1 && pyramiding_ == 0
&& pyramid_entries_.size() == 1
&& order.type == OrderType::EXIT && !order.is_long
&& order.created_bar == bar_index_ && !order.created_during_coof_recalc
&& order.created_by_same_id_replacement
&& order.replaced_exit_order_incarnation != 0
&& order.created_while_in_position && !order.dormant_bracket
&& !order.from_entry.empty()
&& order.from_entry == pyramid_entries_.front().entry_id
&& order.full_percent_exit_request
&& !order.requested_partial && order.qty_percent == 100.0
&& std::isfinite(order.qty)
&& std::abs(order.qty - position_qty_) <= kQtyEpsilon
&& order.oca_name.empty()
&& std::isnan(order.trail_points) && std::isnan(order.trail_price)
&& std::isnan(order.trail_offset)
&& slippage_ == 0 && commission_type_ == CommissionType::PERCENT
&& syminfo_.pointvalue == 1 && account_currency_fx_ == 1
&& account_currency_fx_timestamps_.empty()
&& max_intraday_filled_orders_ == 0
&& risk_max_intraday_loss_ == 0 && risk_max_drawdown_ == 0
&& risk_max_cons_loss_days_ == 0;
if (!pinned_reissue) return bar.close;
for (const PendingOrder& other : pending_orders_) {
if (&other == &order) continue;
// Hariss emits both directional EXITs at every close. An unbound
// sibling is removed by the existing position-cycle liveness gate;
// it cannot compete with this live exit. Entries, RAW orders, global
// exits and any sibling whose parent filled this cycle still exclude.
const bool unbound_exit = other.type == OrderType::EXIT
&& !other.from_entry.empty()
&& cycle_filled_entry_ids_.count(other.from_entry) == 0;
if (!unbound_exit) return bar.close;
}
return tick_grid_price(bar.close);
}

BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility(
PendingOrder& order, int opposing_pass,
internal::DualEntryStopPathWinner dual_entry_path,
Expand Down Expand Up @@ -7464,16 +7514,17 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility(
&& !has_stop_or_trail
&& !std::isnan(order.limit_price);
bool exit_marketable_at_close = false;
const double trigger_close = pooc_short_exit_trigger_close(order, bar);
if (exit_style && std::isnan(order.trail_points) && std::isnan(order.trail_price)) {
if (!std::isnan(order.stop_price)) {
exit_marketable_at_close = order.is_long
? (bar.close <= order.stop_price)
: (bar.close >= order.stop_price);
? (trigger_close <= order.stop_price)
: (trigger_close >= order.stop_price);
}
if (!exit_marketable_at_close && !std::isnan(order.limit_price)) {
exit_marketable_at_close = order.is_long
? (bar.close >= order.limit_price)
: (bar.close <= order.limit_price);
? (trigger_close >= order.limit_price)
: (trigger_close <= order.limit_price);
}
}
if (!pure_limit_entry && !exit_marketable_at_close
Expand Down Expand Up @@ -7653,10 +7704,11 @@ BacktestEngine::FillEvaluation BacktestEngine::evaluate_fill_price(
// against it; the close is the earliest (and only) point in this
// bar it could have interacted with the market.
bool is_long = position_side_ == PositionSide::LONG;
const double trigger_close = pooc_short_exit_trigger_close(order, bar);
bool stop_marketable = has_stop
&& (is_long ? (bar.close <= stop_price) : (bar.close >= stop_price));
&& (is_long ? (trigger_close <= stop_price) : (trigger_close >= stop_price));
bool limit_marketable = has_limit
&& (is_long ? (bar.close >= limit_price) : (bar.close <= limit_price));
&& (is_long ? (trigger_close >= limit_price) : (trigger_close <= limit_price));
if (stop_marketable) {
// Exit stop for a LONG is a SELL (worse execution = lower
// price); for a SHORT it's a BUY (worse = higher price) --
Expand Down
2 changes: 2 additions & 0 deletions src/engine_strategy_commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2164,6 +2164,8 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro
order.qty_type = -1;
order.qty_percent = qp;
order.requested_partial = is_partial;
order.full_percent_exit_request = !has_explicit_qty
&& (std::isnan(qty_percent) || qty_percent == 100.0);
order.pooc_global_full_exit_dynamic_qty =
bind_global_full_exit_dynamic_qty;
order.pooc_global_full_exit_tracks_bound_adds =
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ set(TEST_SOURCES
test_cascade_exit_gapjump
test_coof_market_limit_recross
test_pooc_position_visibility
test_pooc_short_close_tick
test_prearmed_exit_path_cursor
test_prearmed_market_parent_gap_exit
test_prearmed_bracket_fill_bar
Expand Down
160 changes: 160 additions & 0 deletions tests/test_pooc_short_close_tick.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Round16 Hariss F: original TV rows 114/118/363, source and feed pinned in
// r16-20260906/readback-receipt.json. Cloud Run diagnostic captures the new
// stop/limit prices. The broker tests its tick close against the raw level:
// Sep3 C11.575 ->11.58 skips L11.576782; Sep8 C11.695 ->11.70 reaches
// S11.698693; Apr23 C12.495 ->12.50 reaches S12.496973. Existing resting
// levels miss those bars; the newly reissued close-time exit owns the fill.
// Four synthetic bars isolate each event, without loading strategy/feed data.
#include <cmath>
#include <cstdio>
#include <limits>
#include <vector>
#include <pineforge/bar.hpp>
#include <pineforge/engine.hpp>

using namespace pineforge;
static int passed = 0, failed = 0;
#define CHECK(x) do { if (x) ++passed; else { \
++failed; std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); \
} } while (0)

namespace {
constexpr double N = std::numeric_limits<double>::quiet_NaN();
bool near(double a, double b) { return std::abs(a-b) < 1e-8; }
enum class Guard { None, FreshId, Competing, Partial, Long, Coof,
NonPooc, Slip, Fx, EntryBar };

struct Panel {
double entry, old_stop, old_limit, new_stop, new_limit;
double next_stop, next_limit, expected_exit;
int expected_bar;
std::vector<Bar> bars;
};

Panel panel(int n) {
if (n == 0) return {11.69,11.747887,11.574226,11.746609,11.576782,
11.743280,11.583440,11.58,3,{
{11.69,11.69,11.69,11.69,1,1000},
{11.615,11.615,11.595,11.595,1,2000},
{11.595,11.595,11.575,11.575,1,3000},
{11.58,11.58,11.575,11.575,1,4000}}};
if (n == 1) return {11.64,11.700131,11.519738,11.698693,11.522614,
11.69900,11.52200,11.70,2,{
{11.64,11.64,11.64,11.64,1,1000},
{11.685,11.70,11.685,11.69,1,2000},
{11.69,11.70,11.68,11.695,1,3000},
{11.695,11.695,11.66,11.665,1,4000}}};
return {12.41,12.500586,12.228828,12.496973,12.236054,
12.49700,12.23600,12.50,2,{
{12.41,12.41,12.41,12.41,1,1000},
{12.46,12.48,12.45,12.48,1,2000},
{12.48,12.50,12.48,12.495,1,3000},
{12.50,12.515,12.47,12.48,1,4000}}};
}

class CloseTickProbe : public BacktestEngine {
public:
CloseTickProbe(Panel data, Guard guard = Guard::None, bool unbound = false)
: p_(std::move(data)), guard_(guard), unbound_(unbound) {
initial_capital_ = 100000;
margin_long_ = margin_short_ = 100;
pyramiding_ = 0;
qty_step_ = 1;
syminfo_.pointvalue = 1;
set_syminfo_mintick(.01);
commission_type_ = CommissionType::PERCENT;
commission_value_ = .05;
process_orders_on_close_ = guard != Guard::NonPooc;
calc_on_order_fills_ = guard == Guard::Coof;
slippage_ = guard == Guard::Slip ? 1 : 0;
account_currency_fx_ = guard == Guard::Fx ? 2 : 1;
}
void on_bar(const Bar& b) override {
const int seed_bar = guard_ == Guard::EntryBar ? 2 : 0;
if (bar_index_ == seed_bar && position_side_ == PositionSide::FLAT
&& trades_.empty())
strategy_entry("E", guard_ == Guard::Long, N, N, 1);
if (bar_index_ >= 1 && position_side_ != PositionSide::FLAT) {
if (bar_index_ == 2 && guard_ == Guard::FreshId)
strategy_cancel("X");
if (guard_ == Guard::Competing)
strategy_order("Idle", true, 1, N, 1000);
const double stop = bar_index_ == 1 ? p_.old_stop
: (bar_index_ == 2 ? p_.new_stop : p_.next_stop);
const double limit = bar_index_ == 1 ? p_.old_limit
: (bar_index_ == 2 ? p_.new_limit : p_.next_limit);
// The real source issues both directional brackets each close.
// This other parent never opened in this position cycle.
if (unbound_)
strategy_exit("Opposite", "Other", limit, stop);
strategy_exit("X", "E", limit, stop, N, N, N,
guard_ == Guard::Partial ? 50 : 100, "X");
}
seen_close = b.close;
}
double remaining() const { return position_qty_; }
uint64_t fills() const { return broker_fill_event_seq_; }
double seen_close = N;
private:
Panel p_;
Guard guard_;
bool unbound_;
};

void positive(int n, bool unbound) {
const auto d = panel(n);
CloseTickProbe p(d, Guard::None, unbound);
for (int repeat = 0; repeat < 2; ++repeat) {
p.run(d.bars.data(), d.bars.size());
CHECK(p.last_error().empty());
CHECK(p.trade_count() == 1);
CHECK(p.fills() == 2);
CHECK(near(p.seen_close, d.bars.back().close));
if (p.trade_count() != 1) continue;
const auto& t = p.get_trade(0);
CHECK(t.entry_bar_index == 0);
CHECK(t.exit_bar_index == d.expected_bar);
CHECK(near(t.entry_price, d.entry));
CHECK(near(t.exit_price, d.expected_exit));
CHECK(near(t.qty, 1));
CHECK(!t.is_long);
CHECK(t.exit_id == "X");
CHECK(near(t.commission, (d.entry+d.expected_exit)*.0005));
CHECK(near(t.pnl, d.entry-d.expected_exit-t.commission));
}
}

// Signatures are compared with the unchanged parent's matching headers/lib.
// These excluded synthetic inputs characterize existing behavior only.
void guards() {
for (Guard g : {Guard::FreshId, Guard::Competing, Guard::Partial,
Guard::Long, Guard::Coof, Guard::NonPooc, Guard::Slip,
Guard::Fx, Guard::EntryBar}) {
for (int n = 0; n < 3; ++n) {
const auto d = panel(n);
CloseTickProbe p(d, g);
p.run(d.bars.data(), d.bars.size());
CHECK(p.last_error().empty());
std::printf("guard %d panel %d trades %d fills %llu remaining %.9f",
static_cast<int>(g), n, p.trade_count(),
static_cast<unsigned long long>(p.fills()), p.remaining());
for (int i = 0; i < p.trade_count(); ++i) {
const auto& t = p.get_trade(i);
std::printf(" | %d,%d,%.9f,%.9f,%.9f,%.9f",
t.entry_bar_index,t.exit_bar_index,t.entry_price,t.exit_price,t.qty,t.pnl);
}
std::puts("");
}
}
}
} // namespace

int main(int argc, char**) {
if (argc == 1) for (int i = 0; i < 3; ++i) {
positive(i, false);
positive(i, true);
}
guards();
std::printf("%d passed, %d failed\n", passed, failed);
return failed ? 1 : 0;
}
Loading