v6.8.11
Six measurements, mostly taken while running the wild corpus: how TradingView shares state across the iterations of a loop, what its ta.rsi, ta.alma, ta.highest/ta.lowest, math.log and math.sum machines actually compute, and how a strategy splits an order's commission and marks an open position. Plus one broker sync fix from a live session.
Function isolation — one instance per loop call site
- Loop iterations share one instance. A loop-shaped call site no longer creates a child state per iteration. Every iteration now uses one shared instance, matching TradingView's measured per-call-site semantics: user
varstate accumulates across the iterations of a bar, while the builtin machines in the callee's subtree are rolled back to their bar-start state before each same-bar re-execution. The transformer emits__loop_state__/__bind_loop__, so the per-invocation counters, the hoisted child lists and their prologue are gone along with the__grow__,__bind_any_loop__,__next_state__and__bind_next__helpers; comprehension-iterable sites need no separate loop variant, since the new forms are walrus-free. - A machine that advances per call on TradingView opts out of the rollback. The rollback boundary follows the
compactedlayout flag (the@pyne libmarker), andta.percentile_linear_interpolation,ta.percentile_nearest_rank,ta.highest,ta.lowestandmath.randomcarry aper_callflag that shields them from it. Measured onta.highestread from inside aforloop: the per-call machine reproduces all 143815 loop-read values, a bar-start rollback misses 43746 of them. SeriesImpl._restore_barrestores a bar-start snapshot in O(changed slots), falling back to the full restore on any buffer shape it cannot prove. Root snapshot/restore carries a loop cell's bar tracking, so a discarded re-execution finds the same rollback baseline, and anchored dispatchers expose__pyne_cache__/__pyne_impls__so the rollback can reach per-implementation state vectors and layouts.
ta and math — TradingView exactness
ta.rsisaturates instead of dividing. The documented100 - 100/(1 + rma_u/rma_d)is only what the reference snippet computes. The native machine first zeroes eachrmaside under the language's 1e-10 absolute tolerance, and a zeroed side saturates the result. The down side is tested first, so a pair with both sides dormant reports 100, not 0 and notna. Measured on 28519 bars: a weeklyfixnansource held constant for days decaysrma_dto ~1e-11 whilerma_ustays at ~10, where the quotient form returns 99.99999999915303 and TradingView returns exactly 100.ta.highest/ta.lowestage in bars and reset onna. The kept extreme's index advances once per bar, not once per call, so a window read many times on one bar no longer over-ages the index and delays the rescan. Annainput resets the machine outright — window and kept extreme forgotten, the value form returnsna, the bars form 0 — and the rescan after a reset is bounded to the slots actually available since it.ta.almapremultiplies its weights. Each Gaussian weight is divided by the norm up front and the dot product accumulates oldest sample first with no final division. Bit-exact on all four measured parameter sets, where the sum-then-divide form and every summation-order variant of it miss thousands of bars by 1-3 ULP.math.logis correctly rounded. It now goes through a natural logarithm incore/pine_math— 128-bin table reduction, Decimal-verified rounding — instead of the platform libm.math.sumandta.smasplit length changes by regime. Both are ring-buffer window machines that walk a changed series length one compensated step at a time, so the clamped-length idiom (ta.sma(src, math.min(bar_index + 1, n))) matches bit-exactly and the length-1 shortcut that contradicted probe behaviour is gone. On top of that, an isolated length change evicts the raw source values rather than the stored realized entries, which reproduces TradingView bit-for-bit on the measured shrink events (8->1, 100->1, 300->150, 610->100, 5->3), while a running length — one that changes on consecutive bars, as ata.barssinceramp or sawtooth does — takes a separate path that re-baselines to the newest-first raw linear sum with cleared compensation, bounding the divergence at one window's summation error instead of letting the walk drift without bound.input()with a non-source default no longer crashes. A genericinput()default is only resolved as a source name when it is a builtin price series;input(color/number/string constants)previously crashed the lib attribute lookup.input.source()still resolves any shape.
Strategy engine
- An order's commission splits over the order's own quantity and over the legs' exact lot counts. The split base is the order's total quantity rather than the running leg sum, so the closing leg of a reversal no longer absorbs the whole rounding; and because the ratio is taken over integer 1e-8 unit counts instead of the materialized contract doubles, a half-half reversal resolves its tie upward. Only the per-leg attribution moves — the order total is the same either way — but that attribution is what each closed trade reports as its own fee.
- A closed trade takes its two commission legs off the gross P&L one at a time, which only diverges on a converted account, where the two legs convert at different daily rates.
strategy.netprofitsubtracts the publishedgrosslossin one step. - An open position is marked on the fill tick grid, not on the bar's OHLC quantization. The two forms are algebraically equal and differ by one ULP of the price, which the position size scales into an offset that never washes out — and
strategy.equityis built on it. strategy.entry()reverses on a quantity that snaps to zero lots. The opposite position closes and nothing opens, except when the call only modifies an unfilled order it placed on the same bar — a re-placement under an existing id modifies that order with the raw quantity, and the reversal flip is not computed again.
With these, strategy.grossprofit, grossloss, netprofit, openprofit and equity reproduce the TradingView reference of the corpus' "Built-in Kelly ratio" strategy on all 28777 bars bit-exact.
Broker plugins
- A close the plugin declines is retried once per bar, with a wall-clock cap. A script-emitted close declined as
close_already_in_flightornothing_to_closeleft no marker on the new-dispatch path, so event-driven sync passes re-dispatched it on every venue event — thousands of no-op round trips per bar, measured live. The new bar gate arms on the decline and allows one attempt per bar per intent key. Both this gate and the reversal-close stale-redispatch gate are now capped at 60 seconds: on intraday charts the bar boundary arrives first and behaviour is unchanged, while on hourly or daily bars a still-needed close retries within a minute instead of being parked until the next bar.