Repository navigation
fix(engine): stop retro-arming explicit-zero-offset trails from the carried peak - #148
Merged
Merged
Conversation
…arried peak An EXPLICIT trail_offset=0 is TV's one-shot exit-at-activation trail: it fills AT its activation crossing, so it can never survive into a later bar in the armed state — an armed zero-offset trail is an executed one. The per-bar ExitTrailState reconstruction nevertheless derived "armed" from the carried post-entry extreme, so when a strategy re-issues strategy.exit each bar with trail_points derived from close (level_t = entry ± prevBarClose * perc, trail_offset = 0), an activation level refreshed from a NEWER close could drop under a peak set beneath an older, higher level: the stop retro-armed at that stale peak and the position exited at the next bar's open (or at the stale level on an against-direction segment) — an exit TradingView never prints. Scope the carried-best pre-arming away from explicit-zero-offset trails only. An OMITTED trail_offset keeps the carried 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 — pinned by corpus bracket-exit-stop-limit-trail-same-bar-01 2025-08-30 08:45 (entry 4392.08, activation 4392.26, entry-bar high 4396.01, first live bar opens 4392.25 one tick BELOW the level, TV fills at that open, which only a carried armed state produces; the corpus stays byte-identical under this change). Offset>0 trails likewise keep the reconstruction. TV rule for the explicit-zero shape (fitted 219/219 + 147/147 clean trailing exits on the boztilkiserhan serhan1 WMA scalp / ADX tapes, both of which pass trail_offset=0 literally): level_t = entry ± prevBarClose * perc, live from the bar after entry, intrabar cross fills at the level, at the open when the bar opens past it. Discriminating tape: scalp 2025-04-09 — the engine exited at the 14:15 open 1475.99 off a 1501.03 peak under the refreshed 1500.98 level, while TV holds to 16:30 @1501.19 (entry + C[16:15]*1.5%, to the cent). Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
pull Bot
referenced
this pull request
in Malumbo21/pineforge-engine
Sep 4, 2026
Two trailing-exit rules diagnosed from TradingView tapes (TV is ground
truth). Family: winthetrade-ema-9-vwap-strategy-with-atr-trailing-stop
(strategy.exit("x", trail_points = atr*2, trail_offset = atr*2) -- a
price-unit ATR passed as TICKS) on 7 lanes: matchPct 100, entry/exit
timestamps exact, exit PRICE wrong on the same bar, so PnL / equity /
qty diverge from there.
(B) The bar's open is the first price a resting trail observes.
resolve_exit_path_fill only folded prices into the trail state AFTER
each O->L / O->H segment (update_exit_trail_state). With a POOC close
fill the carried best is the entry price itself (apply_market_order_fill
same_bar_close_fill guard), so a trail whose activation level the next
bar's open already sits past stayed dormant through an adverse-first
leg, armed only at the favourable extreme and filled at extreme -/+
offset. TV's emulator arms at the open with best = open and fills the
retrace at open -/+ offset.
OANDA:XAUUSD 15m long 2025-04-04 10:45Z @3110.31; 11:00Z bar
O 3110.40 H 3136.775 L 3109.24 C 3134.46, atr*2 ~ 15 ticks
(activation 3110.325, offset 0.015): TV "Long Exit" 3110.385 =
open - 0.015 (PnL 24.47); engine 3136.76 = high - 0.015 (PnL 8631).
194 of the 195 differing XAUUSD exits fit TV = open -/+ k ticks vs
engine = extreme -/+ the same k, always adverse-leg-first
(|O-L| < |H-O| for longs) with the open beyond the activation.
NASDAQ:AAPL 15m short 2025-04-02 18:45Z @222.93; 04-03 13:30Z bar
O 205.54 L 202.52: TV 205.55 (open + 1 tick), engine 202.53.
NYSE:F 1D long 2025-04-21 @9.47; 04-22 O 9.55 H 9.72: TV 9.54,
engine 9.71.
Fix: after try_exit_open_gap_fill declines (same liveness condition --
path cursor at the open, non-entry bar unless magnifier / cascade
waypoint), observe_exit_trail_open folds bar.open into the running best
and arms the trail iff the OPEN itself is past the activation. It never
arms from the carried best, so the #148 one-shot rule (an explicit zero
offset never retro-arms) is untouched; for exit-at-activation shapes an
open past the level was already gap-filled, so it is a pure best-price
observation there.
(A) A trail_offset in [0, 1) ticks is TV's explicit-zero one-shot trail.
compute_exit_trail_state gated the zero rule on trail_offset != 0.0 and
only then floored, so an offset in (0, 1) became a FINITE zero distance:
exits_at_activation = false and the stop rode exactly on the running
extreme, filling at the bar's best price. TV floors first and applies
the zero-offset rule: fill at the activation crossing, or at the open
when the bar opens past it, and no carried arming.
Pinned with `lab tv` on OANDA:EURUSD 15m (2025-04-01 -> 05-01): the
tapes for strategy.exit("x", "L", trail_points=3, trail_offset=0),
trail_offset=0.5 and trail_offset=0.9 are byte-identical
(190 rows, sha256 36aa80ac...).
EURUSD 15m short 2025-03-31 03:45Z @1.08330, atr*2 ~ 0.0006 "ticks"
(activation ceil -> 1 tick = 1.08329, floor(offset) = 0); exit bar
O 1.08330 L 1.08314: TV 1.08329 (the activation), engine 1.08314.
Fix: floor(trail_offset) first; a zero-tick result takes the existing
explicit-zero path (trail_offset_price NaN, one_shot_zero_offset_trail).
Whole-tick and larger fractional offsets keep the nils123456-orb /
legalrice floor rule unchanged; negative offsets are untouched. The
trail-peak fold in engine_fills.cpp already floors, so it agrees.
Tests: tests/test_trail_open_arm_subtick_offset.cpp (105 checks) pins
the XAUUSD / AAPL / F shapes at the resolver and, for XAUUSD and AAPL,
end to end through a POOC close-fill entry; 0 == 0.5 == 0.9 == 0.6 at
the activation, at a gapped open, and for the #148 no-retro-arm hold
(resolver and engine level, with the omitted-offset durable-arming
control); whole-tick 1.0 / 1.4 keep the floored trailing distance.
Against the pre-fix source 29 of those checks fail, all of them the
tape cells.
One existing pin contradicted the TV rule and is re-pinned, not
dropped: test_path_resolve_extra.cpp fl_sub (0f1f06e, a factorial
round, 2026-08-25) asserted that a 0.6-tick offset "rides the extreme
itself once armed (distinct from an explicit 0)" -- fill at the peak
102.00 -- as an extrapolation of the floor rule with no tape behind
the sub-tick cell. It now expects the activation 101.00 and equality
with the explicit-0 fill, with the `lab tv` evidence in the comment.
Every other trail pin (test_path_resolve_extra, the #148 retro-arm
cells, test_margin_call_trail_exit_chronology D, cascade G4) passes
unchanged. ctest 153/153 (was 152/152 before the new binary), scripts
unittest 102/102.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_014vizwdJMsDkHsjmS4StvZY
pull Bot
referenced
this pull request
in Malumbo21/pineforge-engine
Sep 6, 2026
…rs; re-pin the one-shot fixtures test_zero_offset_trail_rides: 32 `lab tv` tapes (NYSE:F 15m feed 80f404ae85ef, NASDAQ:AAPL 15m feed ae2b03d3736f) replayed end to end through BacktestEngine, each under the sensor's own issue mode (re-issued per bar, once, alongside the entry, or the probe's close*1.5%/mintick re-issue), asserting TV's exit price and bar. Re-pinned to the tape-confirmed rule: test_trail_fill_snap (the explicit-0 AAPL 196.135 open is a path level fill at position 0, still 196.13 end to end), test_path_resolve_extra and test_trail_open_arm_subtick_offset (the #148 fixtures now carry the best family Z actually carries — the issuing close — and HOLD; a carried best past the activation arms every zero-tick offset, the contract the command layer's restart relies on; a gapped open arms the trail and it rides), test_margin_call_trail_exit_ chronology fixture D (the explicit zero arms from the placement close like the omitted offset). ctest 190/190. (cherry picked from commit 5f87ccb)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gate
pr-gatePASS — engine corpus zero regressions, scraped entering excellent+strong >= leaving.Verdict:
/Users/haoliangwen/code/pineforge-lab/pr-gate-verdict.json🤖 Generated with Claude Code