Repository navigation
Bind an exit without from_entry to the market entry placed before it in the same execution - #769
Conversation
…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]>
There was a problem hiding this comment.
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
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]>
Summary
A
strategy.exitgiven in ticks (profit, loss) and called withoutfrom_entrythat follows a market entry in the same execution now arms when that entry fills, as the exit with an explicitfrom_entryalready does. Before, its legs took effect with the next call ofstrategy.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
from_entry.materialize_relative_exits, which turns the definitions of an entry into live brackets when the entry fills, only took definitions whosefrom_entryequals the opening entry. It now also takes the definitions with an emptyfrom_entrywhen the opening is a market opening (no limit and no stop in the opening snapshot) and the run does not usecalc_on_order_fills, after the named ones and after the adoptability check, and re-runsexit()with their profit and loss ticks (the named ones keep passingNaN, as before).from_entrycalled with no position and no working entry order, not undercalc_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.CHANGELOG.mdhas 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_fillsoff, five scenarios): an explicitfrom_entryevery 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.calc_on_order_fillsexits, 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.What is not covered
calc_on_order_fillsnothing 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.ci/cr150-source-reading.tsv, 10 oftests/heap_census_report.md, comments inscripts/ci_verify.pyand a few test files) cite lines ofsrc/source/pine_adapter.cppandtests/CMakeLists.txtthat this change moved; they were not moved and no test reads them.🤖 Generated with Claude Code