Skip to content

codegen: hoist history-subscripted lazy-edge ta.* sites; hold-last source clock for change/mom/roc - #121

Merged
luisleo526 merged 5 commits into
mainfrom
round5/hoist-lazy-rhs-ta
Sep 4, 2026
Merged

codegen: hoist history-subscripted lazy-edge ta.* sites; hold-last source clock for change/mom/roc#121
luisleo526 merged 5 commits into
mainfrom
round5/hoist-lazy-rhs-ta

Conversation

@luisleo526

Copy link
Copy Markdown
Contributor

TV rules (all pinned with lab tv on NYSE:F 1D, 39-call cadence tapes; verified independently by two agents)

  • Under a lazy and/or RHS or a ternary arm in top-level scope, a history-subscripted stateful call (ta.highest/lowest/sma/ema(...)[k]) is evaluated every bar ([1] twins: 9/9, 25/25, 23/23, 38/39). A bare call advances only when reached (sma/ema per-execution FIFO/recursion 39/39 each; highest/lowest over the bar-indexed ring 39/39). Every regressed production shape (louislapis9, oliver1002, ycelestine77, quantbyboji on the ETH hard lane) was a bare call; every every-bar tape carried [1].
  • ta.change/mom/roc under a lazy edge run on TV's hold-last source clock (the call's own source series, held on skipped bars, na before the first execution): roc 38/38, change 39/39, mom 39/39 (every-bar 0/38).
  • cum/barssince/valuewhen/cross/crossover/rising/falling/math.sum are per-execution (39/39 each) — unchanged lowering, precalc opt-out.

Commits

334f8a5 every-bar hoist (superseded) → 3e461cd allow-list + source clock → ebb2367 math.sum → 41ce7e7 hoist keyed on the [k] read → c3bdba3 bare-twin pins (tests only). Production effect (Cloud Run, full population, like-for-like): robmagnaye14 v2/v5 and blackelonmusk → excellent on every lane; no hard-lane regression attributable to codegen.

Full suite with engine env: 2710 passed / 2 pre-existing skips / 0 failed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Dwbd239WT9zfcTLYM5UpUU

luisleo526 and others added 5 commits September 4, 2026 00:17
TradingView rule (pinned 2026-09-03 with `lab tv` on NYSE:F 1D, feed
`lab bars NYSE:F 1D --around 2025-10-15 -n 200`): a stateful ta.* call
inside ANY expression operand of a top-level statement -- the RHS of a
Pine v6 lazy `and`/`or`, either ternary arm, nested comparisons -- is
evaluated on EVERY bar. Short-circuiting and branch selection gate only
the value, never the built-in's state, and `[1]` on it is the previous
BAR's value. Pins (TV trades vs model):

  c = bar_index % 7 == 3 and close > ta.highest(high,5)[1]  every-bar 9/9
                                                            (per-call wrong on 8)
  ... and close > ta.sma(close,5)[1]                        25/25 (per-call 28)
  ... and close > ta.ema(close,5)[1]                        23/23 (per-call 27)
  v = bar_index % 7 == 3 ? ta.highest(high,5)[1] : na       38/39 (ring 2/39)

A stateful call inside an if/local-block/function body that does not
execute every bar IS execution-gated on TV (pinned separately); that
lowering is untouched. Production instance: robmagnaye14 `bullMSS =
setupAlive and dir == 1 and close > ta.highest(high, 10)[1]` (+ BOS /
bear variants), diverging on ~12 lanes incl. the ETH hard lane.

Lowering: `_lazy_edge_ta_hoist_plan` (codegen/ta.py) scans every
top-level statement expression (VarDecl value except var/varip,
Assignment target/value, TupleAssign value, ExprStmt, the head `if`
condition) and, for each ta.* site below a lazy edge at any depth, emits
`const auto _pf_every_bar_ta_N = <site>;` BEFORE the statement -- in
dynamic mode (bar magnifier / input_tf / script_tf) too, since it wraps
the existing `_use_precalc ? ... : compute/recompute` lowering. A direct
`[k]` on the hoisted call pushes its `_hist_call_*` Series there as well.
`_visit_func_call` / `_visit_subscript` read the local / Series while the
statement is lowered. Nested sites hoist inner-first.

Not hoisted (existing lowering kept): if/for/while/switch bodies, `else if`
conditions (an `if` inside the else local block), user-function bodies,
`var` initializers, request.security payloads, tuple-returning sites, and
the pinned lazy saturated ta.roc(close, 3) clock
(tests/test_lazy_saturated_roc*.py). Block-local arguments cannot occur
in top-level statements, so no site is skipped for that reason.

ta.py `_ta_site_uses_precalc`: hoisted sites are every-bar, so precalc
(the same evaluation) is valid for them again. The `and`-RHS ta.sma /
lazy-edge ta.ema opt-outs (pf-probe-oliver-dual-vol-sma: "eager precalc
is TV-incorrect", #58) encoded the refuted per-call clock for exactly the
top-level shape the lazyand-sma/-ema tapes measured; they now only reach
a static lazy-edge sma/ema that is not a top-level statement operand (UDT
field defaults). The affected pins in test_codegen_validation_fixes.py
and test_security_lazy_ta_under_conditionals.py::
test_chart_context_conditional_ta_is_inline are re-pinned to the every-bar
rule citing the tapes; the security-evaluator lazy rule is untouched.

tests/golden/matrix_eigen_pca.cpp regenerated: its top-level `if ... and
ta.crossover(lam, lamSma)` / crossunder now hoist (the only diff).

New tests/test_lazy_edge_ta_every_bar.py: generated-C++ structure for the
four pin strategies and the robmagnaye MSS/BOS shape, or/not/nesting/
ternary/if-condition/expression-statement shapes, the not-hoisted set,
compile-only checks against the engine headers, and an executable
synthetic-bars driver (no feed) proving 2 hits per shape in both the
precalc and dynamic runs where the per-call model gives 0.

Full suite with the engine env (PINEFORGE_ENGINE_INCLUDE / EIGEN / LIB,
engine 11e61d4): main 9a3f101 2681 passed / 2 skipped / 0 failed; this
branch 2706 passed / 2 skipped / 0 failed (+25 new tests).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dwbd239WT9zfcTLYM5UpUU
Claude-Session: https://claude.ai/code/session_01FjBPpCZh8ZRPCGq3dfkdyE
…hange/mom/roc

Eight more cadence-7 ternary pins on NYSE:F 1D (lab tv, range 2025-04-01..
2026-05-01; `v = bar_index % 7 == 3 ? <call> : na`, value exposed through
the entry size; plus the lazy-`and` roc probe), scored per call against
three models -- every-bar (A), hold-last source clock (B), ring of
executions (C):

  roc        A 0/38   B 38/38  C 0/38    (+ lazy-and entries A 25 / B 39 / C 27 of 39)
  change     A 0/39   B 39/39  C 1/39    call 1 (first execution): no TV entry = na
  mom        A 0/39   B 39/39  C 1/39
  cum        A 0/39   B 1/39   C 39/39
  barssince  A 26/39  B 33/39  C 39/39
  valuewhen  A 25/39  B 39/39  C 39/39   (B == C for a one-execution lookback)
  crossover  A 31/39  B 39/39  C 39/39
  cross      A 18/39  B 39/39  C 39/39
  rising     A 26/39  B 20/39  C 39/39   (strictly monotonic over executed samples)

None of the eight is every-bar. Measured on Cloud Run against the full
population (2026-09-04, like-for-like vs the same lab bundle), the broad
every-bar hoist of 334f8a5 fixed the pinned shapes (+21 tiers) but cost
170 tiers and 30 hard-lane regressions; 27 of the 31 regressed sources
have ta.crossover/crossunder below a top-level lazy edge, the rest
rising/falling, valuewhen, change/roc, and ta.sum. TradingView's clock
under lazy evaluation is per ta.* family, so the hoist becomes an
ALLOW-LIST:

* LAZY_EVERY_BAR_TA = highest, lowest, sma, ema (pinned every-bar; lowest
  is highest's mirror): hoisted to `_pf_every_bar_ta_N` as in 334f8a5.
* LAZY_SOURCE_CLOCK_TA = change, mom, roc (numeric source): computed from
  the CALL'S OWN `source[length]` history -- written only on bars where
  the call executes, held on skipped bars, na before the first execution
  -- through a generated `_PFLazySourceClock` + per-site
  `Series<double> _pf_lazy_src_hist_N` (held source recorded once per
  chart bar before the script body; `hist[length-1]` is the source at the
  most recent execution at or before bar-length; a runtime length indexes
  the Series at run time). Never precalcs.
* LAZY_PER_EXECUTION_TA = cum, barssince, valuewhen, cross, crossover,
  rising + the exact mirrors crossunder, falling: the native only sees the
  samples of bars where the call executes, which is exactly the
  reached-only inline `compute` codegen already emits; now also opted out
  of precalc (a static `ta.cum` under a lazy edge used to precalc every
  bar in static mode).
* Every other family keeps its existing lowering unchanged (inline compute
  when reached, precalc when static) until a tape pins it;
  `_lazy_edge_ta_hoist_plan()["skipped"]` lists them per script
  (UNPINNED_LAZY_EDGE_REASON). In the 31 regressed sources the only such
  family is ta.sum (3 sites).

The source clock replaces the #64 `_PFLazySaturatedROC3Clock` (literal
`ta.roc(close, 3)` under a plain `and` RHS only). Its saturated branch is
the same clock (B), but its eager chart `close[3]` fallback before the
first execution is refuted by call 1 of the value probes (TV: na, no
entry), and the fallback for cadences shorter than the length is replaced
by the held-source read. Re-pinned and disclosed:
tests/test_lazy_saturated_roc*.py -> tests/test_lazy_source_clock*.py
(the shadowed-close / named-length / or / ternary exclusions were
conservatism of the literal-shape rule and now route too); the three
compile-smoke collision cases follow the new generated names; the
matrix_eigen_pca golden returns to main's text (its `ta.crossover`/
`crossunder` under a top-level `if ... and` are per-execution); the
ta.rsi half of test_chart_context_conditional_ta_is_hoisted_every_bar
stays inline (unpinned family).

Executable synthetic-bars coverage (no feed): cadence-4 lazy edges over
closes 100..122 give roc 7.272727273 / change 8 / mom hits 2 / one na
first execution / cum 330 / barssince 2 in both the precalc and the
dynamic run (every-bar would give 5.769 / 6 / 1090 / 7; a ring would
still be na), and each stream lifecycle starts from the na state.

Full suite with the engine env (PINEFORGE_ENGINE_INCLUDE / EIGEN / LIB,
engine 11e61d4): 2703 passed / 2 skipped / 0 failed (334f8a5: 2706 / 2 /
0; main 9a3f101: 2681 / 2 / 0).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dwbd239WT9zfcTLYM5UpUU
Claude-Session: https://claude.ai/code/session_01FjBPpCZh8ZRPCGq3dfkdyE
…ION_TA)

The last unpinned lazy-edge family in the 31 regressed sources was
`math.sum` (hexatrades, 3 sites; the analyzer maps it to the TA site
`math::Sum _ta_sum_N`, so the classifier reported the family as `sum`).
Cadence-7 ternary pin on NYSE:F 1D (lab tv, `v = bar_index % 7 == 3 ?
math.sum(close, 3) : na`, Size = round(v*100), 37 entries of 39
candidates):

  bar  date        TV      A every  B hold   C ring
    3  2025-04-04  na      29.27    na       na
   10  2025-04-15  na      28.49    28.61    na
   17  2025-04-25  29.07   29.88    28.94    29.07
   24  2025-05-06  29.93   30.89    30.52    29.93
   31  2025-05-15  31.23   32.00    31.63    31.23
   38  2025-05-27  31.50   31.11    31.81    31.50
  totals            A 0/39   B 1/39   C 39/39

TradingView's math.sum below a lazy edge sums the last `length` EXECUTED
samples and is na until it has `length` of them: the per-execution ring,
i.e. the reached-only inline compute codegen already emits. `sum` joins
LAZY_PER_EXECUTION_TA (inline lowering unchanged; opted out of precalc so
static mode does not turn it every-bar). Not every-bar, not source clock.

Full suite with the engine env (engine 11e61d4): 2703 passed / 2 skipped /
0 failed (unchanged count: one family moved sets, one test extended).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dwbd239WT9zfcTLYM5UpUU
Claude-Session: https://claude.ai/code/session_01FjBPpCZh8ZRPCGq3dfkdyE
Cloud Run, engine + 3e461cd vs the corrected pin: +135 / -30 tiers, with
four ETH hard-lane probes going excellent -> weak (louislapis9 100 -> 67.1,
oliver1002 100 -> 75.2, quantbyboji 100 -> 87.5, ycelestine77 100 -> 59.4;
quantbyboji/oliver1002 also on BTC/ES/NQ, miemomo3 BTC countAbsDelta 0 ->
1). Transpiling each source on main 9a3f101 and on the branch, every changed
site is an allow-listed family below a lazy edge WITHOUT a history read:

  louislapis9   isAdxStrong = adxVal > 30 and adxVal > ta.sma(adxVal, 7)
  oliver1002    isSpringTest = ... and (volume < ta.sma(volume, 20))
                isBOS = ta.crossover(...) and ... and (volume > ta.sma(volume, 20) * 1.2)
                (the original pf-probe-oliver-dual-vol-sma pin)
  ycelestine77  baseLong/Short = ta.mom(close, 20) > 0 and ta.change(ta.sma(close, 50)) > 0
  quantbyboji   five ta.ema(close, 200) under `or` (ema_crossover) + two
                ta.ema(close, len) under `and`; plus ta.roc(close, 3) > 0
  miemomo3      time_exit = hold_days >= 3 and ta.highest(high, n) / entry_price > 1.05

whereas every every-bar tape (highest/sma/ema `[1]` under `and`, the
ternary `ta.highest(high, 5)[1]`) and the robmagnaye production shape read
`[k]` on the call. TradingView therefore keeps a lazily executed call on
the reached-only clock unless the call's own history is referenced, in
which case the built-in is evaluated on every bar (the history must be
consistent). The hoist now requires BOTH the LAZY_EVERY_BAR_TA family and a
direct `Subscript` on the call; other allow-listed sites are listed in
`_lazy_edge_ta_hoist_plan()["skipped"]` under NO_HISTORY_READ_REASON and
keep their existing lowering (inline compute when reached, precalc when
static, the sma/ema opt-outs of main).

quantbyboji's roc: the new source clock replaces #64 exactly in the
saturated regime and, per the lead's call, keeps #64's eager chart
`source[length]` read between executions closer than `length` bars, which
no tape distinguishes -- a paired `_pf_lazy_src_chart_N` Series records the
chart source once per bar for unshadowed bar-builtin sources
(`previous_source(held, eager, length, bar)`; computed sources read the held
history there). The only remaining behavioural delta vs main on this probe
is the tape-pinned na before a site's first execution (#64 used the eager
value; call 1 of every value probe has no TV entry).

Result on the five sources: louislapis9, oliver1002, miemomo3 now transpile
byte-identically to main; ycelestine77 differs only by the change clock
(length 1 == the previous execution == main's ring); quantbyboji only by
the roc clock above. robmagnaye keeps its four hoists.

Tests: the no-`[k]` re-pins in test_codegen_validation_fixes.py return to
main's assertions (docstrings cite both observations); the `[k]` shapes
keep the every-bar pins; test_lazy_edge_ta_every_bar.py shapes read `[1]`,
plus a test of the five probe shapes staying inline;
test_lazy_source_clock*.py pin previous_source / the chart Series /
collisions and an executable gap<length case (5.660377358).

Full suite with the engine env (engine 11e61d4): 2706 passed / 2 skipped /
0 failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dwbd239WT9zfcTLYM5UpUU
Claude-Session: https://claude.ai/code/session_01FjBPpCZh8ZRPCGq3dfkdyE
…n change)

Four more cadence-7 probes on NYSE:F 1D (lab tv, default range) are the
subscript-free twins of the every-bar tapes, scored per call against the
every-bar (A), hold-last (B) and ring-of-executions (C) models:

  c = %7==3 and close > ta.sma(close, 5)   A 23/39  B 25/39  C 39/39  (26 entries; [1] twin A 39/39)
  c = %7==3 and close > ta.ema(close, 5)   A 26/39           C 39/39  (28 entries; [1] twin A 39/39)
  v = %7==3 ? ta.sma(close, 5) : na        A  2/39  B  1/39  C 39/39  (35 entries: na until five samples)
  v = %7==3 ? ta.highest(high, 5) : na     A 11/39  B 25/39  C 38/39  (38 entries: TV na with one executed
                                                                       sample, then the partial maximum)

So the discriminator of 41ce7e7 holds on tape too: a bare call below a lazy
edge runs per execution (reached-only inline compute, #58's precalc
opt-out); only a history-subscripted call is materialised as a continuous
series and evaluated every bar. robmagnaye (four `[1]` hoists) and
blackelonmusk (`close > ta.highest(high, 8)[1]`, `ta.highest(high, 10)[1]`,
`ta.lowest(low, 8)[1]`: three hoists) keep hoisting.

Generated code is byte-identical to 41ce7e7 (comment-only change in
codegen/ta.py). The four bare scripts are pinned structurally in
tests/test_lazy_edge_ta_every_bar.py. Engine note, not codegen:
`ta::Highest` returns na until its window is full, while TV's lazily
executed `ta.highest` returns the partial maximum from the second executed
sample on (calls 2-4 of the bare highest tape).

Quick suite 1839 passed / 560 skipped; engine-env suite unchanged from
41ce7e7 (2706 / 2 / 0) since codegen did not change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dwbd239WT9zfcTLYM5UpUU
Claude-Session: https://claude.ai/code/session_01FjBPpCZh8ZRPCGq3dfkdyE
@luisleo526
luisleo526 merged commit c3bdba3 into main Sep 4, 2026
9 checks passed
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