Skip to content

Pine adapter: margin calls at the open, the exact reversal tie, and global exits on a held position, pinned by TradingView controls - #345

Merged
luisleo526 merged 3 commits into
mainfrom
px/wave-r2
Oct 5, 2026
Merged

luisleo526 merged 3 commits into
mainfrom
px/wave-r2

Conversation

@luisleo526

@luisleo526 luisleo526 commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Pine adapter: three TradingView rules pinned by synthetic controls

Three adapter-only changes (src/source/, include/pineforge/source/, tests and docs; no kernel,
grader, metric, tolerance or codegen change). Each rule part sits behind its own test-only switch,
default on, so a regression bisects per rule. One commit per change.

1. Margin calls at the open after a process_orders_on_close fill (detail::MarginOpeningSwitches)

A full-margin, one-lot process_orders_on_close position (percent commission, slippage, lot grid
of at most 1) is called at the open after its close fill on TradingView's schedule:

  • the call is sized at the source close c′ for a long as for a short;
  • its one-unit band and short gate are priced at the open's print;
  • the booked book is then re-checked at the open's mark at that print, gated like any other check:
    • not short there: the lagged follow-up of the close-sized call books at the print;
    • short, and the re-check books nothing (its units vetoed by the gate, or under one restore step):
      the sequence ends, with no follow-up at the print and nothing owed onward;
    • short, and the re-check books: the re-check's own lagged follow-up is owed to the bar's first
      extreme, booked at that point's print or dropped where that point's own check is short (as the
      open's own call already owes its follow-up there).
  • none of these calls is repeated at one price. A long's booked re-check is never short at full margin.

Also:

  • a leveraged book's open check runs after a market add filling at that open, on the book holding it
    and its entry fee (slippage 0, percent commission, not process_orders_on_close);
  • an explicit-quantity long from flat filling at the next open is dropped where
    sig10(sig10(E) / Q) is below the slipped open tick (full margin, with slippage, not
    process_orders_on_close);
  • in the process_orders_on_close path walk the lagged follow-up also runs on whole-share books with
    the one-unit fallback, chains, and is dropped at a point whose own check is short; inside the
    opening-call scope the open's own call owes it to the first extreme, outside it a carried short's
    open call keeps its repeat at one price.

Evidence:

  • 43 TradingView tapes of synthetic controls (lab tv --no-note, two byte-identical exports each,
    predictions written before export), committed as tests/fixtures/margin_open_rules (129 TradingView
    trades): the engine books 43/43 row for row, and a forward stream books the backtest's trades on all
    43. Turning any switch off costs tapes.
  • 14 of those controls pin the gated re-check, the end of the sequence and the owed follow-up: the
    re-check vetoed or rounding to zero (a sequence that ends there, high- or low-first), the re-check
    booking and owing its follow-up to a first extreme (low or high, booked there; dropped where that point
    is short itself), a re-check with no follow-up, and an open where the booked book is not short. With
    them, one population cohort's TradingView trades (OANDA:EURUSD 15, 2025-06-18: 5.52 at the open, the
    re-check vetoed, then 522.36 at the high), also committed as a replica tape.
  • 14 margin_call_rules tapes now match and are asserted (258 of 266, was 244) and the three
    add-fee tapes of margin_schedule_rules (38 of 39, was 35).

2. Rule 2's exact tie beside resting exit legs (MarginRuleSwitches::tie_reversal_beside_exits)

TradingView keeps only the close leg of a default 100 % reversal whose binary64 equity E is under
sig10(Q c′) while its ten-digit money sig10(E) reaches that cost (E < cost <= sig10(E)),
whatever strategy.exit stop or trail legs rest in the book. The engine judged that tie only on a clear
book. It now judges it beside resting stop and trail legs on a whole-unit lot; beside a limit leg, on a
fractional lot and for an explicit reversal quantity the earlier rules stand (no tape decides those).

Evidence: 36 synthetic controls on NYSE:F 15m (three byte-identical exports each, predictions
written before export): long and short, with and without process_orders_on_close, beside no exit, a
resting stop, or stop and trail exits; equity one ulp under the cost, equal to it, and a cent above and
below. TradingView equals the prediction on all 36; the previous engine opened the reversal on the 8
one-ulp ties beside an exit. test_admission_rules_tapes: 111/111 tapes, 216 TradingView rows;
forward == batch on 94 of the 97 NYSE:F tapes (the test names the three it leaves out). The population
case the rule fixes sits 13 ulps under its cost, so the rule does not hang on a one-ulp residue.

3. A global exit called while a position is held binds to it (ExitBindingRuleSwitches::global_exit_binds_held_position)

A strategy.exit with no from_entry, called while a position is held beside an entry order of the
other side (resting at a level, or placed earlier in the same calculation), took that pending order as
its parent: its limit and stop sat on the wrong side of the leg that closes the held position, so the
exit filled bars late or only on a gapped open, a stop fired a tick late, and a relative profit /
loss leg was never placed. It now binds to the held position, at any margin setting.

Evidence: 21 pairs of synthetic controls (limit, stop and profit legs, long and short,
calc_on_order_fills off and on, the default margin requirement and margins off; predictions written
before export): every script with the resting opposite entry books exactly the trades of the same script
without it, 0 counterexamples. tests/fixtures/cross_side_exit commits 15 of the tapes, run at the
default margin (TradingView's margin-0 exports of the same scripts are byte-identical to the default-margin
ones): 11 equal TradingView row for row, 4 depart at one known row of another rule
(math.round_to_mintick of a half-cent level); with the switch off exactly the 8 opp tapes move; streams
equal backtests with the rule on and off.

Approved re-pin: test_adapter_quiet_bar (12 of 276 rows)

The exit-binding rule moves fills in three random witnesses. The table
(tests/test_adapter_quiet_bar_pinned.inc) is re-harvested with the TU's own harvest switch; nothing
else in the suite moves. Run modes: magnifier off/on × recording on/off (the same 12 run-passes move in
the plain, transcript, probe and ungated builds).

Run Trade Before After Class
Random12, magnifier off (2 runs) 2 exit 98.75, P&L 1 exit 96.75 (the stop, touched exactly), P&L −3 pinned mechanism
4, 5 closed by the opposite entry S closed by the exit X known divergence
6 short 2, P&L −7 short 5, P&L −17.5 known divergence
Random12, magnifier on (2 runs) 2 exit 99, P&L 1.5 exit 96.75, P&L −3 pinned mechanism
Random27, magnifier off (2 runs) 18 exit 88.25 on bar 41 by B (reversal), P&L −1.354 exit 85.75 on bar 40 by Y (its stop, touched exactly), P&L −6.349 pinned mechanism
Random27, magnifier on (2 runs) 17 exit 88.25 on bar 41 by B, P&L 1.1485 exit 85.75 on bar 40 by Y, P&L −3.8465 pinned mechanism
Random31, magnifier off (2 runs) 5 exit 106.5 on bar 39 by B, P&L −2.4221 exit 106 on bar 38 by Y (its limit, touched exactly), P&L −3.7118 pinned mechanism
6 qty 3.7559, P&L −3.6677 qty 4.7559 (the opposite limit fills from flat at its placement size), P&L −4.3779 follows from trade 5 (TradingView does this on the exit's own bar; on this later bar it is an inference)
Random31, magnifier on (2 runs) 5 exit 107.25 on bar 39, P&L −2.4618 exit 106 on bar 38, P&L −3.7118 pinned mechanism

Every other witness keeps its trades. 15 transcript-mode run-passes of the quiet_bar battery change their
recorded broker-state hashes with identical trades (unpinned).

Known divergences

  • quiet_bar Random12 trades 4-6 (recorded by decision): the rule's skip at that call is caused by a
    short limit resting since two bars before; the short stop at the exit's own stop is placed after the
    exit; no tape has this exit-first call order.
  • An exit and an opposite entry reached on one bar, entry first (or at one level, or a bar gapping
    through both): TradingView executes both orders, the entry first, and the exit's quantity then opens a
    second position. Neither this engine nor the previous one books those rows.
  • math.round_to_mintick of a half-cent level (4 cross_side_exit tapes, another rule): TradingView
    rounds 9.655 to 9.66, the engine to 9.65.
  • Margin-call shapes kept on the previous behaviour, untaped: the opening-call schedule for margin
    ≠ 100, commission 0 or several lots; the explicit-quantity admission for shorts at a ten-digit tie; the
    open check after an add when the open's own check already called. 8 of the 266 margin_call_rules
    tapes and one margin_schedule_rules tape stay listed known divergences.
  • Reversal tie scope: beside a take-profit limit leg, on a fractional lot and for an explicit
    reversal quantity the earlier admission rules stand until a control decides them.

Release note

Exits without from_entry now act on the position you hold, even while an order for the other side
is waiting.
If a strategy held a position, had a stop or limit entry of the opposite direction
resting, and called strategy.exit with no from_entry, the exit could miss a level the bar touched
exactly and fill bars late or only on a gap, a stop could fire a tick late, and a profit/loss exit
could never be placed, leaving the position open. This happened at any margin setting, including the
default 100 %. Such exits now fill where TradingView fills them. One case still differs: when the exit
and the opposite entry are both reached on one bar and the entry comes first, TradingView executes both
orders.

Verification

  • Full ctest of the head (Release): every row passes. The rule tests: test_margin_open_rules_tapes 43/43
    tapes, forward == backtest 43/43, every switch costs tapes (short_point_drops_owed 9);
    test_admission_rules_tapes 111/111, forward == batch 94/94; test_cross_side_exit_tapes 11 exact + 4
    known; test_margin_call_rules_tapes 258/266 (all switches off: 140, the count before the rules);
    test_margin_schedule_rules_tapes 38/39; test_margin_opposite_tapes 63,504 checks;
    test_adapter_quiet_bar 276/276 re-pinned runs.
  • Interaction check: main's whole test suite run against main, each change alone and the three together, with a
    per-run dump of every trade field (%.17g): every run that one change moves equals that change's run in the
    combined engine (399 runs of the margin-call tape tests, 48 quiet_bar runs), every other run equals main's,
    and no run is moved by two changes. Each change's own new tests give the same trades alone and combined.
  • The process_orders_on_close fence of the open's owed follow-up moves no trade of the population scripts the
    margin-call change moves (same-input replays with and without it; it never decides there).
  • CI profiles (preflight, release, sanitizers, docs, debug, kernel, corpus parity and subset): all passed on the head: release 864/864 tests (35 verify stages), sanitizers 841/841, debug
    845/845, kernel 307/307 with 0 residual findings, Doxygen 0 warnings, corpus parity 312/312 probes
    unchanged (subset 54/54).

Population gate

One whole-population sweep of this head's exact tree (exp-px-wave-r2-gate-20261006, attempt 1; engine
8eb97508, codegen main c1df8a6, population cf857209), graded against the active baseline
pineforge-parity-baseline-20261005-codegen-609b3519:

  • 7,989 probes graded, coverage complete: 4 up (strong → excellent), 0 down, no hard-lane drop.
    • f-15 stevenygabbyperez-fast-scalper-with-stops: the exact reversal tie (change 2);
    • f-15 axealgo-tp-sl-toolkit-axealgo: margin calls at the open (change 1);
    • eurusd-15 job-2942-mjrodriguezorlich69-forex-apex-matrix-v5-hmm-stationarity-core: the open check after an add's fill (change 1);
    • eth-scraped-15 officialjackofalltrades-caldera-meridian-strategy-joat (hard lane): margin calls at the open (change 1).
  • The cross-side exit change moves no population trade: no population script reaches the shape with a different outcome.
  • One hard-lane move, accepted by the merge rule (hard rank sum 4033 → 4034) and filed as a follow-up:
    eth-15 blitz-locked-macd-pullback-sniper-trend-adx-filtered-strategy stays strong. Its first divergence from
    TradingView moves from 2025-05-03 to 2025-06-23, its exact rows rise from 145 to 233 of 588, and its trade
    count comes within two of TradingView's (597 → 579). Match % moves 97.2 → 95.8, from the equity cascade after
    that later divergence: an opening trim that TradingView books at a slipped market short's fill before a
    same-bar priced exit. That rule is not pinned yet.
  • Gate: canonical PASS (merge-rule/v2): comparable 7,989, coverage lost 0, target band 2 +3/−0, hard regressions 0.
  • Candidate snapshot 616c35d250686fe4395e8ea9adfa1bdc3b91d342a958c0db3c200310ce3de5f1 (7,989 graded).

🤖 Generated with Claude Code

🤖 Generated with Claude Code

luisleo526 and others added 3 commits October 6, 2026 00:55
…open check after an add, fill-time admission at the open, the refined lagged follow-up

A full-margin, one-lot process_orders_on_close position (percent commission,
slippage, lot grid <= 1) is called at the open after its close fill on
TradingView's schedule: the call is sized at the source close c' for longs as
for shorts, and its one-unit band and short gate are priced at the open's
print. The booked book is then re-checked at the open's mark at that print,
gated like any other check:
- not short there: the lagged follow-up of the close-sized call books at the
  print;
- short, and the re-check books nothing (its units vetoed by the gate, or
  under one restore step): the sequence ends, with no follow-up at the print
  and nothing owed onward;
- short, and the re-check books: a short's re-check owes its own lagged
  follow-up to the bar's first extreme, where the walk books it at that
  point's print or drops it when the point's own check is short (as the
  open's own call already owes its follow-up there). Outside the walk's
  scope the follow-up books at the open's print, as before.
None of these calls is repeated at one price. The gated re-check, the end of
the sequence and the owed follow-up are pinned by 14 synthetic TradingView
controls and one population cohort's TradingView trades (OANDA:EURUSD 15,
2025-06-18: 5.52 at the open, the re-check vetoed, then 522.36 at the high).
A long's booked re-check is never short at full margin, so those rules act on
shorts only.

A leveraged book's open check runs after a market add filling there, on the
book holding it and its entry fee (slippage 0, percent commission, not
process_orders_on_close). An explicit-quantity long from flat filling at the
next open is dropped where sig10(sig10(E) / Q) is below the slipped open tick
(full margin, with slippage, not process_orders_on_close). In the POOC path
walk the lagged follow-up also runs on whole-share books with the one-unit
fallback, chains, and is dropped at a point whose own check is short; inside
the opening-call scope the open's own call owes it to the first extreme,
while outside it a carried short's open call keeps its repeat at one price.

Each rule part sits behind its own test-only switch in
detail::MarginOpeningSwitches, default on. The controls are committed as
tests/fixtures/margin_open_rules with test_margin_open_rules_tapes (43/43
tapes, 129 TradingView trades, forward == backtest 43/43, each switch costs a
tape); margin_schedule_rules' add-fee tapes and fourteen margin_call_rules
tapes (the short-s5 family, inherited/eth-slip-5, schedule-q9p1305-s4) now
match and are asserted.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
TradingView keeps only the close leg of a default 100 % reversal whose
binary64 equity E is under sig10(Q c') while its ten-digit money sig10(E)
reaches that cost, whatever strategy.exit stop or trail legs rest in the
book: the reversed position's stop, its stop with a trail, or the new
entry's own exit. The engine judged rule 2 only on a clear book and
opened the reversal beside a resting exit.

unified_admission_terms now judges that exact tie, E < cost <= sig10(E),
beside resting exit stop and trail legs on a whole-unit lot (switch
tie_reversal_beside_exits, on). Off the tie, beside a limit leg and on a
fractional lot such a book keeps the earlier rules: no tape decides
rule 5, the fill re-check or those books there.

Pinned by 36 lab tv controls on NYSE:F 15m (admission_rules/
reversal_tie, 3 byte-identical exports each, predictions written before
export): long and short, with and without process_orders_on_close,
beside no exit, a resting stop, or stop and trail exits; equity one ulp
under the cost, equal to it, and a cent above and below. TradingView
equals the prediction on all 36. The engine now books all 36; with the
switch off the eight one-ulp ties beside exits open the reversal, as on
the parent. The population case the rule fixes sits 13 ulps under its
cost, so the rule does not hang on a one-ulp residue.

Documentation anchors into the two edited files are carried by an exact
line map from the parent.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A strategy.exit with no from_entry, called while a position is held beside an
entry order of the other side (resting at a level, or placed earlier in the
same calculation), took that pending order as its parent (observe_staged_parent
in exit()). With the order's side, the exit's limit and stop thresholds sat
half a tick on the wrong side of the leg that closes the held position: the
leg settled at its source level outside its immutable trigger, the kernel
answered MatchRejected InvalidTerms, the request was withdrawn and re-issued,
and the exit filled bars late or only on a gapped open; a stop fired a tick
late. With the order's price basis, a relative profit or loss leg waited for
that order's fill, which never came. Reachable on main with
calc_on_order_fills off, at any margin setting.

TradingView binds such an exit to the held position. In 21 pairs of synthetic
`lab tv --no-note` controls on NYSE:F 15m (limit, stop and profit legs, long
and short, calc_on_order_fills off and on, the default margin requirement and
margins off; every prediction written before its export), each script with the
resting opposite entry books exactly the trades of the same script without it,
row for row: 0 counterexamples. The adapter now skips a pending opposite entry
as the parent of a global exit while a position is held, so the thresholds
rest on the held side and a relative leg resolves against the held position.
Kernel untouched; no stream gate.

The rule is ExitBindingRuleSwitches::global_exit_binds_held_position (on; tests
clear it). It acts whatever the margin setting, as the controls bind the same
with the default margin requirement and without one.

Approved re-pin: the rule ships unscoped, and the three synthetic witnesses it
moves are re-pinned. tests/test_adapter_quiet_bar_pinned.inc is re-harvested
with that TU's own switch (12 of 276 rows, Random12, 27 and 31 under the four
run modes). Random27 trade 18 (17 under the magnifier), Random31 trade 5 and
Random12 trade 2 are the pinned mechanism: the exit's level touched exactly on
the held side, the opposite entry not reached on that bar. Random31 trade 6
follows from trade 5: the opposite limit then fills from flat at its placement
size, which TradingView does on the exit's own bar in the pin's controls; on
that later bar it is an inference. Random12 trades 4-6 (magnifier off) are a
known divergence, recorded by decision: the rule's skip at that call is caused
by a short limit resting since two bars before, the short stop at the exit's
own stop is placed after the exit, and no tape has this exit-first call order
(tests/fixtures/cross_side_exit/README.md). Per-run-pass dumps of the whole
suite, rule on against off: trades move only in those 12 quiet_bar run-passes
(and their probe and ungated variants) and the new tapes; every other witness
keeps its trades, and 15 transcript-mode run-passes of the quiet_bar battery
change their recorded broker-state hashes with identical trades (unpinned).

Tests: tests/fixtures/cross_side_exit holds 15 of the tapes with their frozen
generated strategies (test_cross_side_exit_tapes), run at the default margin;
TradingView's margin-0 exports of the same scripts are byte-identical to the
default-margin ones (the pin's evidence): 11 equal TradingView row for row; 4 depart
at one known row of another rule (math.round_to_mintick of a half-cent level),
the same in each control and its opp twin. With the switch off exactly the 8
opp tapes move; streams equal backtests with it on and off; the
calc_on_order_fills opp tape is refused by a stream. test_exit_binding_tapes
registers the part (no exit_binding tape has the shape). Docs: the part's row
and scope in pine-adapter-kernel-notes and the exit_binding README;
documentation anchors moved by an exact line map.

Population: the shape is reached by 9 probes of one source (a market reversal
on the exit's bar, where the reversal cancels the exit anyway); a census found
no other run reaching it, and the rule moved no population trade set.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
sys.path.insert(0, str(scripts))
from run_strategy import Strategy, _check_abi, _load_bars, inputs_run_kwargs # noqa: E402

class TapeStrategy(Strategy):
_check_abi(self.lib)
self._setup_signatures()

class StreamStrategy(Strategy):
@luisleo526
luisleo526 marked this pull request as ready for review October 5, 2026 18:41
@luisleo526
luisleo526 merged commit a9544d3 into main Oct 5, 2026
16 of 17 checks passed
@luisleo526
luisleo526 deleted the px/wave-r2 branch October 5, 2026 23:45
luisleo526 added a commit that referenced this pull request Oct 6, 2026
…se's owed follow-up, open-marketable exits first, and same-side global exits, pinned by TradingView controls (#347)

* Pine adapter: a re-issued exit filled at its call keeps the pass's position view

Under process_orders_on_close, exit() fills a re-issued strategy.exit of a
one-lot short whose stop or limit is already through the bar's close at the
call (its current-close path, execute_current). Unlike an ordinary close_all
(KI-64), it did not freeze the script's position view, so the rest of the
pass read strategy.position_size == 0 and a reset strategy.position_avg_price.
A late observer, strategy.position_size[1] < 0 and strategy.position_size ==
0, then fired one bar early, and an exit cooldown blocked the next bar's
entry. The scraped job-2947 probe on BINANCE:ETHUSDT.P 15 lost its
2026-01-08 23:30 UTC short and every later trade this way.

TradingView fills that exit at the close, after the pass: the pass keeps the
pre-fill size and average. The adapter now freezes the script's position view
before it executes the exit, as close_all does. Once the fill has left the
book flat, the generated strategy.position_avg_price read
(position_entry_price_ behind a non-zero signed_position_size()) keeps the
pre-fill average until the pass ends. PineStrategyHost::
hold_script_average_price writes it, and release_script_average_price
restores the flat book's 0 right after on_source_bar, before the orders the
pass placed settle. No codegen change.

Each part has its own switch, PoocCloseFillViewSwitches::
current_exit_keeps_position_view and current_exit_keeps_average_price (on;
tests clear them). Both act only at that call site. close_all's own average
read and the intraday-cap close at the call are unchanged.

Pinned by 15 synthetic lab tv --no-note controls on BINANCE:ETHUSDT.P 15m,
2026-01-08..10. Each was exported twice, the two exports are byte-identical,
rangeProof is covered on both, and every prediction was written before its
export: 0 TradingView counterexamples. tests/fixtures/pooc_close_fill_view
holds 11 of them with their frozen generated strategies (codegen c1df8a6):
- Nine equal TradingView row for row, entry ids included. These are the
  re-issued stop exit's size read, its cooldown observer, its ATR-stop
  cooldown at 100 % of equity, its average read and its limit leg, plus the
  long mirror, strategy.close, close_all and a first-issue exit, which never
  moved.
- Two are known divergences the rule leaves open. close_all's average read
  is AVGPRE on TradingView and AVGOTHER in the engine. After the re-issued
  exit, TradingView keeps the pre-fill strategy.opentrades, closedtrades,
  openprofit, netprofit and equity; the engine reads them after the fill.

test_pooc_close_fill_view_tapes clears each part and requires the departures
recorded in switch_off_departures.json. The average part moves 2 tapes; the
view part moves the 5 re-issued short tapes. It also requires every tape's
stream to equal its backtest with both parts on and off. Last, it records
every tape's per-bar broker-state hash with the average part on and off:
position_entry_price_ is hashed, so the hashes must be equal wherever the
trades are, and otherwise first differ after the exit bar's fold. A hold
that outlived its pass fails it: with the release_script_average_price()
call removed, the trades stay the same and the hashes of the re-issued exit
tapes differ from the exit bar's fold on. The four cooldown controls of
tests/fixtures/margin_call_rules stay equal.

Fail-before: the test module does not compile against the parent tree
(pooc_close_fill_view_switches is not a member of detail). With both parts
cleared, the five re-issued short tapes depart from TradingView, as on the
parent.

Whole-suite per-run trade dumps (%.17g, every field) against the parent: the
dumps cover 670 processes and 56,558 runs. Every pre-existing run-pass keeps
its trades with both parts on and with both off. The only differences are
the new test, the repetition counts of timing-calibrated loops and the run
order of a test that runs two hosts on two threads; their distinct runs are
identical. No witness re-pin.

The transient flag is waived in broker_state_hash_waivers.txt: it is false
at every fold. Documentation anchors into the four edited files are carried
by an exact line map from the parent (245 moved, 0 stuck).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Pine adapter: a POOC close's owed follow-up booked after the script, frozen close sizes and their over-fill lot, and the open's lot-by-lot follow-up

R-A (MarginScheduleSwitches::close_follow_up_after_script): a carried
process_orders_on_close short's lagged follow-up owed to its bar's close is
held through the close's own check (rule 7 still drops it where the close is
short) and booked after the script, ahead of the close's fills, at the same
print. A strategy.close(id) of the whole one-lot book (flush_pending_closes)
or a close_all() placed there keeps the size the script saw: a transaction
that crosses zero and opens the excess as a lot of its own, under the close
order's label. That lot is credited to the close ledger and the pyramiding
records ahead of the entry beside it, so the next open's nameless margin
calls take it first in first out, as the kernel does from the book, and a
later strategy.close of that entry closes all of the entry's units. A
reversal entry's excess joins its own lot (apply_frozen_reversal_close).
Partial closes, closes of several lots and the twin outside
process_orders_on_close keep today's sizing (margin_schedule_rules
frozen/close-id stays a known divergence). No tape runs pyramiding above 1,
so the pyramiding-record credit is unexercised.

Beside that lot only a market entry filling at the close it was placed at is
exempt: from the pyramiding-0 refusal, and a default-sized one from the
fill-time affordability check too (a scraped OANDA:EURUSD 15 probe, job-2550,
2026-04-09 01:45 UTC: TradingView fills a default-sized "LONG" of 18624.97
beside the 0.56 over-fill lot). Priced entries and later fills keep every
gate; no tape decides those shapes. One of them changes against the parent
tree, which opens no over-fill lot: an explicit stop entry placed at that
close at pyramiding 0 fills on the next bar there, and is refused here as a
same-side add beside the lot.

R-B (MarginOpeningSwitches::lot_by_lot_open_follow_up): on a fractional lot
grid, the close-sized call at the next open takes a book of several lots the
close's fills opened, first in first out, and its follow-up is checked lot by
lot (open_print_lot_follow_up_units). One-lot books keep
open_print_follow_up_units, and whole-share books of several lots keep the
previous rules. On a fractional book of several lots a short's booked
re-check now runs as well (the parent tree never entered this schedule for
several lots); nothing follows it, since no tape decides that follow-up.

Tests: tests/fixtures/margin_open_rules lots/ holds 10 synthetic TradingView
controls (62 trades), each exported twice with identical output and
predicted before export. All 10 match row for row and stream to the
backtest's trades; the parent tree matches none of the first 8. Two of them
close the entry by id after the open's calls (strategy.close("L"), with an
explicit and with a default-sized L). Off alone, close_follow_up_after_script
loses 7 tapes and lot_by_lot_open_follow_up 9. margin_open_rules: 53 tapes,
all match. The job-2550 probe's trades now equal TradingView's 3761 rows.
Whole-suite per-run trade dumps (%.17g, every field) with both switches on
and off: only the lots/ tapes move. Documentation: both switches' rows, the
counts, and the anchor digits carried by an exact line map.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Pine adapter: a market entry's opening check at its open fill follows the exit legs marketable there

TradingView, pinned on 25 synthetic controls (each exported twice,
byte-identical, every prediction written before its export): at a market
entry that opens the book from flat at a bar's open, the exit legs it
released that are already marketable at that open -- a short's stop at or
below the open's tick or its limit at or above it, a long's mirror -- fill
there first, and the opening margin check then runs on the book they leave
(nothing after a whole exit). A leg the bar reaches only later on its path,
one tick after the open included, never preempts: the check's call books at
the fill first. Both sides. Four more controls make the strategy.exit global
(no from_entry) and place it with the entry: TradingView books each exactly
as its bound control.

ab9714b's rule -- a 1x short skips the opening trim when an exit leg touches
anywhere on the entry bar -- is refuted by TradingView on its own shape
(test_margin_call_1x_long_entry_fill scenario E's settings and bar: 0.008 ETH
called at the open, then the stop closes 3.492).

For a market entry opening the book from flat at the open (one lot, no add,
no book of several entries, neither process_orders_on_close nor
calc_on_order_fills) the adapter asks the legs that fill released whether
they are marketable at the open (priced_exit_precedes_opening). The released
legs (opening_released_leg) are those bound to the entry's request, those
bound to its id and materialized at the fill on the book's side, and a
global leg placed while the book was flat whose request follows the entry's.
Every other leg (a strategy.exit without from_entry placed before the entry
or while a position was held, a leg carried from an earlier bar or lot) is
still asked ab9714b's question, and every other fill (stop and limit
entries, reversals, adds, books of several entries,
process_orders_on_close, calc_on_order_fills) keeps that rule. The scope is
narrower than the pin's wording because the wider form (every market fill at
the open, every live leg) moved the state hashes of two synthetic witnesses,
test_adapter_quiet_bar (18 runs) and test_publication_witness (10 runs):
hash-only, every trade field equal in a whole-suite dump, through legs the
kernel never fills at that open, whose owed check then lingered. Which
exclusion each of the last 8 runs needed was not bisected.

Known divergences of the kept rule:
- Two are committed as tapes the test lists in kKnownDivergences:
  TradingView calls a short reversal (out of a 0.001 long) and a short stop
  entry gapped at the open (a sell stop above the close) before their stop
  on the path, as it does the market entry; the engine books the stop 3.5
  with no call.
- A global strategy.exit placed before the entry, while the book is flat,
  has no tape. TradingView voids an exit called while no position is held
  and no entry order is working (its exit-binding pins), so, by inference,
  it calls such a short at the open print. The engine keeps a leg there
  that never fills, at the open or later, and still asks ab9714b's
  question of it: the short's opening call is skipped whenever the bar
  touches the leg's stop, and the short is called at the path's extreme
  instead, the same with the switch off. The leg stays out of the released
  legs for an engine-only reason: counted, it defers the check to a fill
  that never comes, which drops the check, a long's too (in
  test_adapter_quiet_bar Random32 that moved only the state hash).
- Adds, books of several entries, limit entries, process_orders_on_close
  and calc_on_order_fills stay on that rule without a tape.

A partial exit at the open owes the check after its own fill
(open_exit_first_bar_: 0.0032 called after a 0.0004 stop, as TradingView
books it); the record is cleared at the next bar open if no released leg
fills. The opening check's body moves unchanged into run_opening_checkpoint
so both points run the same branch.

Switch MarginOpeningSwitches::open_marketable_exit_first, default on; off
restores the previous rule (every deterministic run-pass of the suite books
the parent tree's trades, field for field).

Tests: the 31 controls are tapes in tests/fixtures/margin_open_rules
exit-order/ (53 trades). margin_open_rules now holds 84 tapes and 244
trades: 82 match, the 2 listed divergences still differ, forward ==
backtest on all 84, and the switch off loses 17 (15 of the 25 pinned
controls, which the parent tree misses, and the two global controls that
tell the rules apart). Scenario E of test_margin_call_1x_long_entry_fill
(twin and base copy) books TradingView's shape, 0.1996 @100 "Margin call"
then 99.7504 @105; its twin-parity rewrite entry is extended. Docs: the
switch row and counts, and every anchor into the moved files re-anchored by
an exact line map.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Pine adapter: a global exit called while a position is held binds to it beside a same-side add too

A strategy.exit with no from_entry, called while a position is held beside an
entry order of the HELD side (an add resting at a limit or a stop, placed
earlier in the same calculation, or queued by a fill recalculation), still
took that pending add as its parent (observe_staged_parent in exit()): #345
skipped only an entry order of the other side. With the add's price basis, a
profit or trail leg rested at the add's limit plus its ticks and filled at the
next open, so a script booked thousands of one-bar trades; a loss leg, and any
leg beside a stop add, waited for the add's fill, which never came, and the
position ran to the end of the range. A market add at the pyramiding cap never
became a pending parent.

TradingView binds such an exit to the held position whatever the side of the
order beside it. In 36 pairs of synthetic `lab tv --no-note` controls on
NYSE:F 15m (a resting limit add, a resting stop add, or a market add at the
cap; profit, loss, profit and loss, and trail legs; long and short;
calc_on_order_fills off and on; every prediction written before its export),
each add script's List of Trades is byte-identical to its control's: 0
counterexamples. The skip now drops its side term, so the exit keeps the held
position's side and entry price.

Under calc_on_order_fills one more path departed: the profit-and-loss bracket
the exit stages in the entry's fill recalculation (the chart-tick scope)
counted the resting add as a competing order of the source book, which took
the bracket's chart-tick reach (its threshold moved half a tick beyond a level
the on-grid test misreads by one ULP), so a limit the fill bar's high reaches
exactly filled one bar late. flush_pending_bracket_legs now leaves the entry
orders of the held side out of the census of a held global exit's leg.

Two switches (ExitBindingRuleSwitches, on; tests clear them):
global_exit_binds_held_position (extended, sizeof unchanged) and the new
held_exit_bracket_ignores_same_side_entries (sizeof 5 -> 6). Kernel untouched;
no stream gate. Not in scope: an add that fills while the exit works
(TradingView gives each entry its own exit levels and books FIFO; the engine
books one exit at the average price) is a separate rule.

Tests: tests/fixtures/same_side_exit holds the 36 add tapes and their 20
controls (exact Pine bytes, TradingView's List of Trades, provenance, frozen
generated C++ of codegen c1df8a69); test_same_side_exit_tapes requires 38
tapes equal to TradingView row for row and the other 18 to depart exactly at
their recorded row (the same row in a control and its add tapes: the trailing
leg and calc_on_order_fills re-entries, another rule's); with
global_exit_binds_held_position off exactly the 32 resting-add tapes move,
with held_exit_bracket_ignores_same_side_entries off exactly the 4
calc_on_order_fills profit-and-loss add tapes; no control and no cap tape
moves. The calc_on_order_fills add tapes are refused by a forward stream;
every other add tape streams (1 and 30 bars of history, every run of the feed
between two nights) to the backtest's trades. test_exit_binding_tapes sets
the new switch in every_part() and lists it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
luisleo526 added a commit that referenced this pull request Oct 6, 2026
* docs: release notes and version scoping for v1.3.0

CHANGELOG 1.3.0: confirmed-bar admission in pineforge-live (#342), the Pine
adapter changes of #341, #345 and #347, tests and docs (#343, #344); no
report key changes; the state-hash recipe additions of #341 and #347, domain
tags and the v19 epoch unchanged; migration.

Version scoping: README, install.md and public-contract.md name v1.3.0, its
tarballs, the engine1.3.0-codegen1.3.0 image and the 1.3.0 pair. The
confirmed-bar receipt and its admission, marked unreleased after 1.2.0 by
#342, ship in 1.3.0 (README, strategy-capabilities.md, runner/README.md).

Codegen license lines (README, LEGAL.md, docs/pages/index.md): the PineForge
Source License 1.2 from codegen 1.3.0; 1.2.0 keeps 1.1, releases up to 1.1.0
the PolyForm Noncommercial terms.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs: release 1.3.0's grades in the README and the changelog, from the facts tokens

The README's release sentence moves to the releases[1.3.0] tokens, and its
main-scoreboard lines re-render the values defe41f (#343) rendered at
pineforge-release 3e39d64, and the 1.3.0 changelog says which registry
baseline grades the release and why that baseline is the release's code,
all rendered from pineforge-release facts/facts.json at ff21de0 (#32). The
changelog sentence extends the last line of the "What a script author sees"
bullet that 0e1ced7 added.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[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