Skip to content

Bind an exit without from_entry to the market entry placed before it in the same execution - #769

Merged
luisleo526 merged 4 commits into
mainfrom
exit-binds-to-entry
Nov 7, 2026
Merged

luisleo526 merged 4 commits into
mainfrom
exit-binds-to-entry

Conversation

@luisleo526

Copy link
Copy Markdown
Collaborator

Summary

A strategy.exit given in ticks (profit, loss) and called without from_entry that follows a market entry in the same execution now arms when that entry fills, as the exit with an explicit from_entry already does. Before, its legs took effect with the next call of strategy.exit (one bar late) or, when it was called only on the signal bar, never. TradingView binds such an exit to the pending entry, so the legs can fill on the entry's own bar. Refs #694 and the report in pineforge-codegen-oss#295 (item 5).

What changed

  • A relative exit (profit and loss in ticks) that cannot be priced until its entry fills is kept as a pending definition under its exit id and from_entry. materialize_relative_exits, which turns the definitions of an entry into live brackets when the entry fills, only took definitions whose from_entry equals the opening entry. It now also takes the definitions with an empty from_entry when the opening is a market opening (no limit and no stop in the opening snapshot) and the run does not use calc_on_order_fills, after the named ones and after the adoptability check, and re-runs exit() with their profit and loss ticks (the named ones keep passing NaN, as before).
  • A definition remembers whether the call was void under the adapter's existing rule (an exit without from_entry called with no position and no working entry order, not under calc_on_order_fills). A void definition is never adopted. The flag lives on the definition because the shadow rows of the calling execution are cleared at the next bar, before the entry fills.
  • The state digest folds the flag only when it is set. The flag is set exactly by calls of the void kind, so a run that never makes such a call keeps its digest, and a run that makes one gets a different digest with identical trades.
  • CHANGELOG.md has one bullet. The source allocation manifest gets one entry for the new local list (ceiling 1403 to 1404) and its site inventory the three records the gate proposes; the count pin moves by one. 118 documentation citations that the added lines moved were re-anchored.

Tests

  • tests/test_exit_without_from_entry_same_execution.cpp (public, synthetic bars, calc_on_order_fills off, five scenarios): an explicit from_entry every bar, an unnamed exit every bar, an unnamed exit on the signal bar only, an unnamed exit called before the entry statement (void, unchanged), and an explicit exit on the signal bar only. The expected trade is computed in the test from the feed and the documented rule, not from the engine. Before the change the second and third scenarios fail (the exit lands on bar 2 at 102.25 instead of bar 1 at 102.00, or never); after it all five pass.
  • Nine existing rows around exits and exit tapes (per-entry, same-side, cross-side, bracket re-lowering, exit binding, margin open prints and rules, the fill-bar calc_on_order_fills exits, the exit queue) ran together with the new test, with the private tapes overlaid: before the change the nine passed and the new test failed; after it all ten pass. The private tapes are not part of the public CI.
  • The reporter's script on a one-hour BTCUSDT feed: 32 trades and none closed on its entry bar before, 33 trades and 9 closed on their entry bar after (the reporter's TradingView list: 33 and 9); the bar 87 entry closes on bar 87 at 43976.07 after, on bar 88 at 44051.10 before.

What is not covered

  • Two market entries that fill at the same open with one unnamed exit were run on synthetic bars: both close on the entry bar at the +200 tick level of their common fill, as two named exits do, whether the exit is called once or on every bar. The unnamed definition is consumed at the first market fill, so an entry that fills on a later bar (pyramiding) is armed as before, by a later call of the exit made while a position is held. No TradingView tape pins these shapes, and the change adds no test for them.
  • Priced (limit or stop) entries are not changed: the definition is adopted only for a market opening. A stop entry followed by an unnamed exit called only on the signal bar still never arms, while the explicit form does.
  • Under calc_on_order_fills nothing changes: the unnamed definitions are not adopted, as before. No tape says what TradingView does for an unnamed relative exit there, so the change does not reach it.
  • No performance claim: the change adds a boolean field to a pending definition and a branch in an existing loop at a market fill; nothing was timed.
  • No parity sweep of the corpus has been run on this change. It moves trades on purpose for strategies that use this shape.
  • The engine's whole test suite (the hosted CI runs it) was not run by the author.
  • Citations in comments and data outside the pages the anchor checker scans (136 rows of ci/cr150-source-reading.tsv, 10 of tests/heap_census_report.md, comments in scripts/ci_verify.py and a few test files) cite lines of src/source/pine_adapter.cpp and tests/CMakeLists.txt that this change moved; they were not moved and no test reads them.

🤖 Generated with Claude Code

luisleo526 and others added 2 commits November 7, 2026 01:22
…e-execution entry

TradingView binds a strategy.exit without from_entry to a market entry placed
before it in the same execution: the exit legs are armed when the entry fills
and may fill on the entry's own bar. A report on the code generator
(pineforge-codegen-oss#295, case 5, which ties it to pineforge-engine#694) runs
a strategy whose unconditional strategy.exit with relative profit and loss legs
follows an entry in an `if`: TradingView closes 9 of its 33 trades on their
entry bar, and the engine closes none of its trades on their entry bar.

The new test runs five scenarios on a five-bar synthetic feed with the source
host: the exit named and unnamed, called on every bar or only with the entry,
and an unnamed call made before the entry statement (which the adapter documents
as void). The expected trade of each scenario is computed in the test from the
feed, the tick and the relative legs, never from the engine; each scenario
prints what it observed and what it expected.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…in the same execution

A strategy.exit with relative legs (profit or loss in ticks) and no from_entry
that follows a market entry in the same execution was kept as a pending
relative definition under the empty entry id. The step that turns an entry's
relative definitions into live brackets at its fill only took the definitions
named after that entry, so the unnamed exit took effect with its next call, one
bar late, and an exit called once never did. TradingView arms it when the entry
fills, so its legs may fill on the entry's own bar.

materialize_relative_exits now also takes the unnamed relative definitions when
a market entry fills and passes their profit and loss ticks on to the re-run of
exit(), which arms the legs from the fill of that entry. An entry that fills on
a later bar is armed as before, by a later call of exit() made while a position
is held. The step is not taken under calc_on_order_fills, where the previous
behaviour is kept: no tape covers an unnamed relative exit there.
A call that TradingView voids (an exit without from_entry made while no position
was held and no entry order was working) is not taken: the definition keeps
that fact in a field of its own, because the shadow rows of the calling
execution are gone when the entry fills. The field is folded into the state
digest only when set. A named exit, a void call and an exit called while a
position is held take the paths they took before.

The test of the previous commit (five scenarios on a synthetic feed, with
calc_on_order_fills off) covers it: the unnamed exit called every bar and called
once now close on the entry bar, the named exits and the call made before the
entry statement are unchanged.

The source allocation manifest has entries for the new local list and the site
inventory lists its new sites; the documentation citations of lines that the
added lines moved (src/source/pine_adapter.cpp and the headers) are
re-anchored: 118 citations, each mapped through the lines this change added.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.

Not checked: tests were not run; formal verification proved 0 of 1 changed function(s) (1 not verified).

Formal verification. PR-changed functions: 0/1 verified (0 proven, 0 may-equivalent, 0 distinguished) · 1 not verified (1 unsupported).

Not verified on this run: materialize\_relative\_exits (unsupported).


Graphify review — findings

Fixes an exit given in ticks without from_entry that follows a market entry in the same execution: it now takes effect when that entry fills, the way a named exit already does. materialize_relative_exits collects these unnamed relative exit definitions, one per exit id and from_entry pair, when a market entry fills, and moves them into the pending list for that call. The rest of the diff is housekeeping: a changelog line, one new entry in the CR150 source-bounds baseline for the new local vector, regenerated line-number citations in the ADR and docs, and new tests covering the same-execution exit price.

Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2220 functions depend on the 2089 functions this change touches.

Health — grade A; 10 existing hotspot(s) in the area this change touches (pre-existing, not introduced here):

  • consume_matched_request — 2 callers, 43 callees (high)
  • source_trigger_threshold() — 12 callers, 7 callees (high)
  • match_path — 2 callers, 35 callees (high)
  • execute_current — 2 callers, 24 callees (high)
  • invoke_callback — 4 callers, 12 callees (high)
  • deliver_confirmed_script — 2 callers, 23 callees (high)
  • pump_batch — 4 callers, 11 callees (high)
  • find() — 10 callers, 4 callees (high)
  • …and 2 more

Verification — 2220 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2171 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify materialize\_relative\_exits.

The verifier did not have enough to check materialize\_relative\_exits, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: unsupported: member function — receiver/self (`this`) not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver

luisleo526 and others added 2 commits November 7, 2026 13:00
Plain merge of main into this branch. Main now has the run option
series_origin_ms and the series-length percentile change, which edit the same
lines of the bound, citation and parity files. Five files conflict and are
resolved by keeping both sides' changes: CHANGELOG.md keeps both entries;
ci/cr150-source-bounds.yaml takes the summed ceiling and all four entries;
docs/design/native-feature-parity.md takes this branch's adapter line for the
open-bar row and main's host line for the equity-recording row;
scripts/test_cr150_source_gate.py takes the combined group count; and
tests/private_fixture_reads.evidence takes main's cited lines plus the one row
this branch adds above them, each re-read from the merged tests/CMakeLists.txt.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Plain merge of main into this branch, with no conflicts. Main now has the
hosted release and notify-hub jobs and the glibc comparison with the v1.10.0
archive (#768), which edit the release workflow, the CI documents and the
preflight and glibc scripts; this branch changes none of those files.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant