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
2 changes: 1 addition & 1 deletion corpus
Submodule corpus updated 2 files
+3 βˆ’3 run_manifest.json
+1 βˆ’1 validation_report.md
35 changes: 33 additions & 2 deletions src/engine_path_resolve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -567,11 +567,42 @@ ExitTrailState compute_exit_trail_state(bool is_long, double trail_points,
if (!std::isnan(trail_offset) && trail_offset != 0.0) {
s.trail_offset_price = std::ceil(trail_offset) * syminfo_mintick;
}
if (!std::isnan(s.best_price)) {
s.exits_at_activation = std::isnan(s.trail_offset_price);
// An EXPLICIT trail_offset=0 is TV's one-shot exit-at-activation trail:
// it FILLS at the activation crossing itself, so it can never survive
// into a later bar in the armed state β€” an armed zero-offset trail is an
// executed one. Deriving "armed" for it from the running post-entry
// extreme RETRO-ARMED the stop whenever an activation level refreshed
// from a newer close (strategy.exit re-issued per bar, e.g.
// trail_points = close * perc / syminfo.mintick, trail_offset = 0)
// dropped under a peak set beneath an older, higher level; the phantom
// stop then exited at the next bar's open (or at the stale level on an
// against-direction segment) β€” an exit TradingView never prints. TV rule
// (fitted 219/219 + 147/147 clean trailing exits on the boztilkiserhan
// WMA scalp/ADX tapes, both of which pass trail_offset=0 explicitly):
// level_t = entry +- prevBarClose*perc, live from the bar after entry,
// intrabar cross fills AT the level, at the open when the bar already
// opens past it. Both fill routes stay reachable below without any
// pre-arming (open-gap shortcut + with-direction segment cross), so
// only the retro-arming path is removed for this shape.
//
// An OMITTED trail_offset keeps the carried-best arming even though it
// shares the exit-at-activation FILL rule: TV treats its activation as
// durable order state measured against the position's running extreme.
// Discriminator (corpus bracket-exit-stop-limit-trail-same-bar-01,
// 2025-08-30 08:45): entry 4392.08, activation 4392.26 from
// trail_points=atr with NO offset argument, entry-bar high 4396.01
// crossed the level before the order's first live bar, whose open
// 4392.25 sits one tick BELOW the level β€” TV fills at that open, which
// only a carried armed state can produce. Offset>0 trails likewise keep
// the reconstruction: activation is durable for every trail that keeps
// running after it activates.
const bool one_shot_zero_offset_trail =
!std::isnan(trail_offset) && trail_offset == 0.0;
if (!one_shot_zero_offset_trail && !std::isnan(s.best_price)) {
s.trail_active = is_long ? (s.best_price >= s.activation_level)
: (s.best_price <= s.activation_level);
}
s.exits_at_activation = std::isnan(s.trail_offset_price);
return s;
}

Expand Down
61 changes: 61 additions & 0 deletions tests/test_fills_edge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,66 @@ static void test_trailing_activation_is_actionable_but_offset_only_is_inert() {
CHECK(pending_exits_for(ExitActionForm::InfiniteTrailPoints) == 1);
}

// ─────────────────────────────────────────────────────────────────────
// A per-bar re-issued exit-at-activation trail (trail_points refreshed from
// close, trail_offset = 0 β€” the boztilkiserhan serhan1 WMA scalp shape) must
// NOT retro-arm off the carried post-entry peak when a refreshed (lower)
// activation undercuts it. TV rule, fitted 219/219 + 147/147 clean trailing
// exits on those tapes: level_t = entry Β± prevBarClose*perc, live from the
// bar after entry, fill AT the level on the intrabar cross (open if gapped).
// Bars below are the real 2025-04-09 discriminating tape: the engine used to
// exit at the 14:15 open 1475.99 while TV holds to 16:30 @1501.19.
// ─────────────────────────────────────────────────────────────────────
class ReissuedTrailRetroArmProbe : public BacktestEngine {
public:
ReissuedTrailRetroArmProbe() {
initial_capital_ = 1'000'000;
default_qty_type_ = QtyType::FIXED;
default_qty_value_ = 1.0;
slippage_ = 0; commission_value_ = 0; pyramiding_ = 1;
syminfo_mintick_ = 0.01;
}
double exit_px(int i) const { return closed_trade_exit_price(i); }
int exit_bar_index(int i) const { return closed_trade_exit_bar_index(i); }
void on_bar(const Bar& bar) override {
if (bar_index_ == 0)
strategy_entry("Long", true, kNaN, kNaN, 1.0, "L");
// Pine: if strategy.position_size > 0 -> strategy.exit(trail_points =
// close * trailPerc / syminfo.mintick, trail_offset = 0), every bar.
if (position_side_ == PositionSide::LONG) {
strategy_exit("Exit Long", "Long", kNaN, kNaN,
/*trail_points=*/bar.close * 0.015 / 0.01,
/*trail_offset=*/0.0);
}
}
};

static void test_reissued_trail_holds_to_tv_exit_no_retro_arm() {
std::printf("test_reissued_trail_holds_to_tv_exit_no_retro_arm\n");
ReissuedTrailRetroArmProbe p;
Bar bars[5] = {
// signal bar; entry fills next open @1478.84
{1478.84, 1478.84, 1478.84, 1478.84, 1000, kT0_UTC + 0 * k15m_ms},
// entry bar; close 1486.70 -> next level ceil(2230.05)t = 1501.15
{1478.84, 1487.00, 1475.00, 1486.70, 1000, kT0_UTC + 1 * k15m_ms},
// "14:00": peak 1501.03 < level 1501.15 -> hold. close 1475.99
// refreshes the level to ceil(2213.985)t = 1500.98 < carried peak.
{1486.69, 1501.03, 1473.27, 1475.99, 1000, kT0_UTC + 2 * k15m_ms},
// "14:15": retro-arm bar. Nothing crosses 1500.98; the old code
// pre-armed off the 1501.03 peak and gap-filled at the open 1475.99.
// close 1489.89 -> level ceil(2234.835)t = 1501.19.
{1475.99, 1491.82, 1475.89, 1489.89, 1000, kT0_UTC + 3 * k15m_ms},
// "16:30": high 1509.00 crosses 1501.19 -> TV exit AT the level.
{1489.88, 1509.00, 1488.10, 1497.10, 1000, kT0_UTC + 4 * k15m_ms},
};
p.run(bars, 5);
CHECK(p.trade_count() == 1);
if (p.trade_count() == 1) {
CHECK(near(p.exit_px(0), 1501.19));
CHECK(p.exit_bar_index(0) == 4);
}
}

// strategy.close is still an ordinary deferred market close when POOC is off.
class ExplicitMarketClose : public BacktestEngine {
public:
Expand Down Expand Up @@ -834,6 +894,7 @@ int main() {
test_inert_exit_has_no_qty_or_oca_reservation();
test_absolute_and_relative_exit_forms_are_actionable();
test_trailing_activation_is_actionable_but_offset_only_is_inert();
test_reissued_trail_holds_to_tv_exit_no_retro_arm();
test_strategy_close_market_behavior_remains();

std::printf("\n%d passed, %d failed\n", g_pass, g_fail);
Expand Down
117 changes: 114 additions & 3 deletions tests/test_path_resolve_extra.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,10 @@ static void test_resolve_exit_trail_fills() {
CHECK(boz_long_zero.should_fill == true);
CHECK(near(boz_long_zero.fill_price, 1918.59));

// LONG trail no-offset, ALREADY armed via best_start, bar opens past the
// activation level -> gap-fill at the open (exits-at-activation gap arm).
// activation=101; best_start=101.5 (>=activation so armed); open=102>=101.
// LONG trail no-offset, bar opens PAST the activation level -> gap-fill
// at the open (exits-at-activation gap rule; no pre-arming involved β€”
// an exit-at-activation trail is never carried across bars armed).
// activation=101; open=102>=101.
Bar gap_nooff = mk(102, 103, 101, 102);
ExitPathFill g_nooff = resolve_exit_path_fill(
gap_nooff, PositionSide::LONG, kNaN, kNaN,
Expand Down Expand Up @@ -360,6 +361,115 @@ static void test_resolve_exit_trail_fills() {
CHECK(near(fl_tp.fill_price, 101.0));
}

// ── explicit-zero-offset trails never retro-arm from the carried best ──
//
// An EXPLICIT trail_offset=0 trail FILLS at its activation crossing, so in
// TV it can never be carried into a later bar already armed. When the exit is
// re-issued each bar with trail_points derived from close (level_t =
// entry Β± prevBarClose*perc), a refreshed activation can drop UNDER a peak
// that was set beneath an older, higher level; deriving "armed" from that
// carried peak retro-armed a phantom stop that gap-filled at the next bar's
// open. An OMITTED trail_offset keeps the carried arming (see the omitted
// control below β€” TV fills those at the open off the carried peak).
// Real discriminating bars from the boztilkiserhan-serhan1 WMA+RSI
// scalp tape (long entered 2025-04-09 00:15 @1478.84, trailPerc 1.5%,
// trail_offset passed as literal 0):
//
// 14:00 bar peaks at 1501.03 under that bar's level 1501.15
// (trail_points = 1486.70*1.5/0.01 = 2230.05 -> ceil 2231t) β€” no cross.
// 14:15 order refreshes from close 1475.99 -> 2213.985 -> ceil 2214t
// -> activation 1500.98 < carried peak 1501.03.
// TV HOLDS through 16:30 (level from close 1489.89 -> 2235t -> 1501.19,
// crossed by high 1509.00, TV row px 1501.19). The engine used to
// retro-arm at the peak and exit at the 14:15 open 1475.99.
static void test_zero_offset_trail_never_retro_arms() {
std::printf("test_zero_offset_trail_never_retro_arms\n");

// 14:15 bar: nothing crosses 1500.98 intrabar and the open is BELOW the
// activation β€” the position must HOLD (previously: gap-fill at open).
Bar serhan_hold = mk(1475.99, 1491.82, 1475.89, 1486.23);
ExitPathFill retro = resolve_exit_path_fill(
serhan_hold, PositionSide::LONG, kNaN, kNaN,
/*trail_points=*/2213.985, /*trail_price=*/kNaN,
/*trail_offset=*/0.0, /*entry=*/1478.84,
/*best_start=*/1501.03, false, false, kMintick);
CHECK(retro.should_fill == false);

// OMITTED offset control: the same bars with trail_offset = na keep the
// carried arming β€” the pre-armed level gap-fills at the open. This is
// the TV-pinned omitted-offset behavior (see the probe pin below); only
// the explicit literal 0 is the one-shot activation exit.
ExitPathFill retro_nan_off = resolve_exit_path_fill(
serhan_hold, PositionSide::LONG, kNaN, kNaN,
/*trail_points=*/2213.985, /*trail_price=*/kNaN,
/*trail_offset=*/kNaN, /*entry=*/1478.84,
/*best_start=*/1501.03, false, false, kMintick);
CHECK(retro_nan_off.should_fill == true);
CHECK(near(retro_nan_off.fill_price, 1475.99));

// TV pin for the OMITTED-offset carried arming, real corpus bars
// (bracket-exit-stop-limit-trail-same-bar-01, 2025-08-30 08:45 UTC):
// entry 4392.08, trail_points = atr(08:30) = 17.6225 -> ceil 18t ->
// activation 4392.26; the ENTRY bar's high 4396.01 crossed it before
// this first live bar, whose open 4392.25 is one tick BELOW the level.
// TV fills at that open β€” only a carried armed state produces this.
Bar probe_bar = mk(4392.25, 4399.90, 4385.55, 4393.34);
ExitPathFill probe_omitted = resolve_exit_path_fill(
probe_bar, PositionSide::LONG, kNaN, kNaN,
/*trail_points=*/17.6225, /*trail_price=*/kNaN,
/*trail_offset=*/kNaN, /*entry=*/4392.08,
/*best_start=*/4396.01, false, false, kMintick);
CHECK(probe_omitted.should_fill == true);
CHECK(near(probe_omitted.fill_price, 4392.25));

// ... and the TV exit: the 16:30 bar's refreshed level 1501.19 is crossed
// on the rising leg -> fill AT the level (the TV row price).
Bar serhan_tv_exit = mk(1489.88, 1509.00, 1488.10, 1497.10);
ExitPathFill tvx = resolve_exit_path_fill(
serhan_tv_exit, PositionSide::LONG, kNaN, kNaN,
/*trail_points=*/2234.835, /*trail_price=*/kNaN,
/*trail_offset=*/0.0, /*entry=*/1478.84,
/*best_start=*/1501.03, false, false, kMintick);
CHECK(tvx.should_fill == true);
CHECK(near(tvx.fill_price, 1501.19));

// SHORT dual: refreshed activation 99.02 rises ABOVE the carried trough
// 99.00; the bar opens above it and never falls to it -> HOLD
// (previously: retro-armed and gap-filled at the open 100.50).
Bar short_hold = mk(100.50, 100.80, 99.60, 100.10);
ExitPathFill s_retro = resolve_exit_path_fill(
short_hold, PositionSide::SHORT, kNaN, kNaN,
/*trail_points=*/98, /*trail_price=*/kNaN,
/*trail_offset=*/0.0, /*entry=*/100,
/*best_start=*/99.00, false, false, kMintick);
CHECK(s_retro.should_fill == false);

// Control: the "open gapped past the activation" TV rule is untouched β€”
// no pre-arming is needed for it. activation 101, open 102 -> open fill.
Bar gap_past = mk(102, 103, 101.2, 102);
ExitPathFill gap = resolve_exit_path_fill(
gap_past, PositionSide::LONG, kNaN, kNaN,
/*trail_points=*/100, /*trail_price=*/kNaN,
/*trail_offset=*/0.0, /*entry=*/100,
/*best_start=*/101.03, false, false, kMintick);
CHECK(gap.should_fill == true);
CHECK(near(gap.fill_price, 102.0));

// Control: an OFFSET trail keeps the carried-best arming β€” activation is
// durable order state for a trail that keeps running after activation.
// best_start=102 >= activation 101, offset 50t=0.5 -> level 101.5;
// open 101 <= 101.5 -> gap-fill at the open (same as the established
// gap_off pin inside test_resolve_exit_trail_fills).
Bar off_gap = mk(101, 101.5, 100, 100.5);
ExitPathFill off_armed = resolve_exit_path_fill(
off_gap, PositionSide::LONG, kNaN, kNaN,
/*trail_points=*/100, /*trail_price=*/kNaN,
/*trail_offset=*/50, /*entry=*/100,
/*best_start=*/102, false, false, kMintick);
CHECK(off_armed.should_fill == true);
CHECK(near(off_armed.fill_price, 101.0));
}

// ── exit_order_earliest_path_metric_no_trail: entry-bar wrong-side block ──
//
// On the entry bar a no-trail EXIT whose stop/limit lies on the wrong side of
Expand Down Expand Up @@ -432,6 +542,7 @@ int main() {
test_exit_order_touch_gap_arms();
test_path_cross_kind_priority_order();
test_resolve_exit_trail_fills();
test_zero_offset_trail_never_retro_arms();
test_entry_bar_blocks_no_trail_exit();
std::printf("\n%d passed, %d failed\n", tests_passed, tests_failed);
return tests_failed == 0 ? 0 : 1;
Expand Down
Loading