TradingView replaced the arithmetic behind math.sum and ta.sma

Sometime between July 31 and October 29, 2026, TradingView changed how math.sum computes a rolling sum, and with it ta.sma and everything built on them. There was no release note that I could find. The same Pine script, fed the same numbers, now prints different last digits than it did in July.

The differences are small. On ordinary price data they sit in the last bit of a double, one unit in the last place, sometimes a few. For most people that is nothing. For anyone who keeps reference outputs, validates a port of Pine logic against TradingView, or expects a backtest rerun months later to match the old one byte for byte, it is a change in the ground truth with no date attached to it.

I build PyneCore, a Python runtime that runs Pine logic bar for bar and is held to TradingView’s output at the bit level. That is how I noticed, and it is also why I can say exactly what changed. Everything below is measured. I cannot read TradingView’s source. I can only compare its output with mine, bar by bar, and keep doing that until the two agree on every bar.

How it showed up

A ta.sma(close, 3) on CAPITALCOM:BTCUSD, 5 minutes, stopped matching. PyneCore had reproduced TradingView’s math.sum on hundreds of thousands of bars since the summer, including the odd corners: windows that shrink and grow mid-series, na bars, huge values passing through the window. Now plain closes at length 3 were one bit off on a share of the bars.

The first suspect is always your own code. What settled it was a probe I had archived in July. Its source depends on nothing but bar_index, so it produces the same input sequence on any chart, on any day:

//@version=6
indicator("math.sum probe", overlay = false, precision = 16)

// Full-mantissa values in [0.25, 0.75]: every add rounds, no periodic structure.
src = 0.5 + 0.25 * math.sin(bar_index * 12.9898)

plot(src, "SRC")
plot(math.sum(src, 2), "S2")
plot(math.sum(src, 3), "S3")
plot(math.sum(src, 5), "S5")
plot(math.sum(src, 9), "S9")
plot(math.sum(src, 14), "S14")

I ran it again on October 29 and compared the output with the July capture, bar by bar, over 20,880 bars. The SRC column is identical on every bar, so the input did not move. The sums did:

lengthfirst differing barJuly 31October 29exact sum, rounded
2110.52840521358649140.52840521358649130.5284052135864913
381.62469062115891251.62469062115891271.6246906211589127
573.38605080470260863.3860508047026093.386050804702609
9124.2265188987059174.2265188987059184.226518898705918
14206.88713831488084256.8871383148808426.887138314880842

Every first difference is one unit in the last place. Over the whole run, between 40 and 78 percent of the bars differ depending on the length, almost always by one ulp, at most by 18 at length 3. The last column is the window sum computed with exact rational arithmetic and rounded once to a double. The October output equals it on every single bar of this probe. The July output did not.

The old engine

The engine I had matched since July was a fused evict-and-add rolling sum in the Kahan family. Each bar it subtracted the value leaving the window and added the new one in one two-round step, kept the realized entries it had actually added so it could remove exactly those later, and on specific bars abandoned the accumulated state and re-baselined: it recomputed the window as a plain sum, cleared its error register and started again from raw values. The condition for that re-baseline was a rounding test on the incoming value against the error register, and getting it bit-exact took two days of probing in July.

That engine had a quirk. A window that had become all zeros did not report zero. It reported leftover rounding dust, about -2.8e-14. You never saw it on a chart, because Pine’s comparisons are tolerant and the plot rounds, but it was there in the bits.

The new engine

The scheme TradingView runs now is a Neumaier compensated sum, also known as Kahan-Babuška summation. It keeps two doubles, an accumulator s and an error register c. Every add of a value v computes the rounded sum and puts the exact rounding error of that add into c. The reported sum is s + c, rounded. Each bar first evicts the raw value that entered length bars ago, then adds the new one. The pair is never renormalized: c is read out and never added back into s.

In Python, the complete rule for a fixed length and no na bars is this:

def rolling_sum(values, length):
    s = c = 0.0
    out = []
    for i, x in enumerate(values):
        ops = [x] if i < length else [-values[i - length], x]
        for v in ops:
            t = s + v
            if abs(s) >= abs(v):
                c += (s - t) + v
            else:
                c += (v - t) + s
            s = t
        out.append(None if i + 1 < length else s + c)
    return out

Fed the SRC column of the probe above, this function reproduces the October output on all 20,880 bars at every length, bit for bit. ta.sma is that sum divided by the length, with one more rounding.

On ordinary price data the result is, for practical purposes, the correctly rounded window sum. The error register captures what the accumulator loses, and reading out s + c gives it back.

After a huge value passes through the window the picture changes, and TradingView’s output shows the same thing. A 1e15 spike entering and leaving leaves a residue in c that does not go away, and the reported sums sit one to three ulp off the exact window sum for a long time afterwards. A version that renormalizes the pair, or reads out s alone, drifts off within a few hundred bars.

The rest is unchanged. A length that changes from bar to bar still walks the window the way it did before, na bars are still skipped and not stored, and ta.cum is still a plain accumulator with no compensation at all.

Why the last bit reaches your indicators

A one-ulp change in a sum is 1e-16 relative, and it survives into the indicator because many indicators subtract two nearly equal quantities built from such sums.

Bollinger %b and bandwidth divide a small difference by another small difference. CCI divides the distance from a mean by a mean deviation. RVI, the trend strength index and a correlation coefficient all go through variances and covariances, which are differences of large sums. In each of these the last bit of the sum comes out around 1e-9 relative in the indicator. Still invisible on a chart, but well above the tolerance you would pick for a regression test.

Technical Ratings is the extreme case. It counts votes, and a vote is a comparison. When the compared values differ by one ulp, a vote can flip, and a discrete rating value moves by a whole step.

The PyneComp test corpus holds reference outputs captured from TradingView for about 180 indicators and strategies. After the change, nine of them failed at a 1e-9 tolerance: BBTrend, Bollinger %b, Bollinger bandwidth, CCI, Woodies CCI, RVI, the trend strength index, the correlation coefficient and Technical Ratings. All nine encoded the old engine and had to be captured again. The 80 strategy references passed unchanged.

What this means if you depend on the numbers

If you only read charts, nothing. The lines are where they were.

If you store TradingView output as a reference, the reference now has an implicit version. A capture from before the change and a capture from after it disagree in the last bits, and in cancellation-heavy indicators they disagree at 1e-9. There is no field on the platform that tells you which engine produced a given number, and no option to pin the old one.

If you validate a port of Pine logic, a disagreement that appears after months of agreement can originate on either side. My own first hour went into looking for the bug in PyneCore. The archived probe is what made the question answerable: same input, different output, same platform, three months apart.

If you compare a strategy’s backtest with one you ran in the summer, the trades will almost certainly match, because order logic runs on prices and tolerant comparisons. A strategy that thresholds a CCI or a %b at a round number can cross on a different bar now. None of the strategies in the corpus did, so this is a corner, but the mechanism is there.

What an open implementation does with the same change

PyneCore reproduced the old engine bit for bit until it was replaced. It reproduces the new one bit for bit now, validated on about 400,000 TradingView bars across lengths 2 to 300, with spikes, na bars and lengths that move every bar. The whole machine is one file, core/rolling_sum.py, and its compiled twin.

That is the part of this story I care about. The rolling sum is an ordinary function whose formula you can read, the switch from the old scheme to the new one is a commit with a date and a message that says what was measured, and the test references that encoded the old engine were recaptured in the same change, so the diff shows exactly which numbers moved. If TradingView changes the engine again, the probe will show it again, and the fix will be another commit you can read.

On the platform itself the same change is invisible. It happened between two dates, it reached every script that uses math.sum or ta.sma, and the only way to know is to have kept the old output and compared.

Why it changed

I don’t know, and nothing I can measure tells me. The new scheme is simpler: it has no realized-entry bookkeeping and no occasional re-baseline, which was a full pass over the window. That is consistent with a cleanup for accuracy and robustness, and it is equally consistent with making the engine cheaper to run. Other parts of the Pine pipeline changed in the same week, which points to a broader release rather than a one-line fix. If I had to guess, I would guess the rewrite was about correctness, because on normal data the new output is the correctly rounded sum and the old one was not.

Reproducing it

Paste the probe into the Pine editor on any symbol and timeframe. The source depends only on bar_index, so the values do not depend on the chart; I used CAPITALCOM:BTCUSD, 5 minutes, in July and again in October. At bar 7 the length-5 sum reads 3.386050804702609 today. If you kept output from before the change, the same bar reads 3.3860508047026086. If you did not, the Python function above gives you today’s values from the SRC column, and exact rational arithmetic gives you the same numbers, which is the simplest way to say what the new engine is: the sum you would have computed by hand, rounded once, as long as nothing enormous has passed through the window.