Skip to content

Three bugs, three passing tests, none caught

ETHUSDT perpetual 4 hour chart with two lookback windows drawn side by side, one of 23 bars and one of 22 bars, the length the Pine source computes and the length the Python rebuild computed from the same formula.
The same lookback, computed twice. Pine rounds 22.5 away from zero and gets 23. Python 3 rounds it to the even number and gets 22. Nothing in either language warns you that the convention changed.

If you have ever ported an indicator into Python to backtest it, or trusted a script because it came with a verification, this is about the gap those two things leave open.

We rebuilt three of our own indicators so a backtest could run over them. All three rebuilds had a wrong constant in them. All three passed their validation. Not one was caught by a test.

Why rebuild an indicator at all

A Pine script runs on your chart. It does not run over six years of history in a loop, and it does not answer whether a signal actually pays. To ask that, you rebuild it in something you can run thousands of times.

That rebuild is where an indicator quietly stops being the same indicator.

The bug that cost the most, and it is one bar

One of the lengths inside Volume Oracle is derived from the timeframe:

target bars   = 36 hours × 60 / 240 minutes = 9
regime length = round(9 × 2.5) = round(22.5)

Pine’s math.round breaks ties away from zero. It gives 23. Python 3’s round() uses banker’s rounding, which breaks ties toward the even number. It gives 22.

Both are correct implementations of rounding. They are simply different conventions, and nothing warns you that you have crossed from one to the other.

Every lookback derived from that number moved with it. The result on the best supported row of a 36 signal sweep:

version48h edgewin raten
rebuilt, length 22+0.423%511216
correct, length 23+0.260%501223

A 38% haircut from a rounding convention.

We then recorded the lesson wrong

The note we wrote said it happened at 4H only. It was written from the one timeframe that happened to be under test, and it stayed that way for two months.

Running the same arithmetic across every timeframe:

timeframeregime lengthflow lengthresult
5m45 vs 4523 vs 22diverges
2H45 vs 4523 vs 22diverges
4H23 vs 2212 vs 11diverges
8H53 vs 5227 vs 26diverges
1D18 vs 189 vs 9ties, agrees
3D, 1W15 vs 158 vs 8ties, agrees

Four of sixteen, not one.

Two things fall out of that table that no amount of reasoning produced.

A tie is not enough. 1D, 3D and 1W all land exactly on .5 and agree. Banker’s rounding only moves a tie whose round half up answer is odd, so round(17.5) is 18 either way. Roughly half of all ties are harmless, which is precisely why spot checking one proves nothing.

The divergence can arrive one step late. At 5m and 2H the first length agrees at 45. The next line, round(45 × 0.5), is another 22.5, and that one splits. If you check the first constant and move on, you clear both timeframes wrongly.

The other two, briefly

Harmonic normalises an oscillator by clipping it to a rolling band. In the source, each bar is clipped by its own band. The rebuild clipped an entire 200 bar window using the current bar’s band, which is a different function. Measured difference across 11,497 bars: mean 0.47 points. A real transcription error with no material effect.

We also inflated that one before measuring it. It was announced as almost certainly the 9% drift, from reading the code. The number was 0.47. A real bug and a material bug are different claims, and the magnitude gets measured before the impact gets named.

Plutus Flow averages volume over 50 bars to detect spikes. The rebuild used 20, because that was the number we thought we knew. Effect: under a tenth of a percentage point, no sign changes. The verdict did not move, but the number was wrong and is now right, which is the only standard that matters.

The part that is actually the lesson

Here is what each rebuild’s validation reported at the time.

rebuildits own checkwhat that meant
Volume Oracle3 of 3 before, 3 of 3 aftercould not tell them apart
Harmonic6 of 8 before, 5 of 8 afterscored the buggy version higher
Plutus Flownever run

The middle row is the one to sit with. The validation preferred the wrong code. If we had let the check decide, it would have argued us back into the bug.

A validation that compares one output value cannot detect a wrong input constant. The output is too coarse a sieve. Many different wrong parameter sets produce the same regime label, the same crossover state, the same colour on the chart today. They differ on bars you are not currently looking at, which is exactly where a backtest lives.

What to do instead

Print every derived parameter. Every length, every threshold, every multiplier. Diff them against the source line by line, before you grade anything.

Keep the output check. It catches a different class of error and it is necessary. It is just nowhere near sufficient, and all three of these walked straight past it.

The honest scope

The Pine was correct in all three cases. What was wrong was the apparatus built to test it, which is the more uncomfortable version of the problem: the instrument drifted, and the instrument’s own self check said it had not.

The 38% figure is one row, one sweep, one timeframe, in sample, and is quoted from our own record rather than re run. The rounding result is reproducible in a single command, and correcting it is what turned up the fact that our own note about it had been wrong for two months.


Past measurement of our own tooling, described after the fact. Nothing here is a recommendation or an indication of future results.