DEV Community

Lei Chen
Lei Chen

Posted on Edited on

I Ran 1,909 Real Pine Scripts Through a Translator. Here's What Actually Breaks

Your Pine script compiles. Your Python port runs. And the backtest still disagrees with TradingView. I spent the last few months finding out exactly where that gap lives — not by arguing about it, but by running 1,909 real Pine scripts through a translator and counting what broke, where, and why.

This is the third post in a series where I convert classic Pine strategies to Python line by line. The first two posts walked through one script each. This one steps back and looks at the whole corpus, because the pattern across 1,909 scripts is more useful than any single conversion.

The headline numbers first, because this is the part you can check:

What we measured Result, in our testing What it means
Compile 1,904 / 1,909 = 99.7% Analyzer + emitter both pass
Fully translated (zero diagnostics) 1,879 / 1,909 = 98.4% No unsupported diagnostic at all
Clean run 1,887 / 1,904 = 99.1% Runs bar-by-bar, invariants hold, no violations
Run errors 17 files = 9 unique scripts, 7 families Every one categorized (table below)
Drawing-only scripts 366 No plot(), only drawing objects
Silent scripts 196 Ran fine, nothing triggered on this data

One honesty note before the numbers do any persuading: the corpus is my own library of 1,909 .pine files (1,720 main + 191 restored v6 scripts), and it contains duplicate copies of the same script under different folders. So "1,909" counts files, and wherever the distinction matters I give the unique-script count too. The 5 compile failures, for example, are 3 unique scripts. The 17 run errors are 9. I will keep saying which number is which, because a translator that hides the difference between files and scripts is exactly the kind of tool you should not trust.

The failures that disappeared: 84 → 5

The most useful number in the table is not 99.7%. It is the journey to get there. When this sweep started, 84 scripts failed analysis. By the final baseline it was 5. Here is what those 79 fixes were, because each one is a Pine construct that some converter somewhere still chokes on:

  • request.security() with a tuple literal, and with a scalar expression (17 + 8)
  • Namespace field access, e.g. reading a field off a namespaced value (16)
  • matrix/pinv and UDT method calls reported as unsupported call (12)
  • for...in iterator typing (8), line-continuation and indentation parsing (8)
  • chart.point field reassignment, table.cell_set_text_formatting, tuple-switch tail returns, chained constructor calls (2 each)

None of these is exotic syntax. They are ordinary, working Pine that real authors write every day. The lesson I take from the list: if your converter fails on a script, the first question is not "is my script too clever" — it is "has this converter ever been measured against a corpus this size." Unmeasured converters fail quietly on the boring stuff.

What is left in the 5: 3 unique scripts, and none of them is a capability gap. Two are waiting on external evidence before anyone touches the runtime, and one is waiting on a library import. I would rather ship a tool that says "I can't do this one yet, here is why" than one that silently rewrites it into something else.

The 55 diagnostics: where translation is honest but incomplete

Compiling is not the same as translating completely. 25 files (55 diagnostics) compile with a warning attached — the output exists, runs, but something in it is not a faithful translation, and the converter tells you so instead of staying quiet.

The split matters more than the total:

  • 47 diagnostics: history reads inside module-level helpers. The pattern is a helper function — zigzag, renderStructures, backfill, complete, target_bull are the repeat offenders — that reads high[1] or a parameter's src[i] from inside the helper. The converter emits the code and flags it, because at the call site the helper only receives plain values: there is no way to thread a series' history through a function call in the target runtime. It is a structural limit, not a bug waiting for a fix, and it is the single most common reason a translation is incomplete.
  • 8 diagnostics: input.* edge cases. input.enum with no known parameter type (3), and input.* calls that are not at the top level (5, all in one test-framework script).

Here is one of those 8, reproduced just now on my own machine with a five-line script — the diagnostic, verbatim, exactly as the compiler prints it:

//@version=5
indicator("input.enum diagnostic demo", overlay=true)
x = input.enum("a", options=["a", "b"])
plot(close)
Enter fullscreen mode Exit fullscreen mode
unsupported constructs (1):
  - input.enum has no known Pyne parameter type (line 3)
Enter fullscreen mode Exit fullscreen mode

That is what a good diagnostic looks like: one line, the construct, the location, no hedging. If your converter's idea of a diagnostic is a silent fallback to a default value, you do not have diagnostics — you have a surprise scheduled for later.

The 17 run errors: 7 families, and the fixable bucket is empty

Compiling cleanly and running cleanly are different things, so the sweep also ran every compiled script bar-by-bar. 1,887 of 1,904 ran clean. The 17 that did not sort into 7 families — and here is the part I did not expect when I started: the "emitter can fix this" bucket is empty. Every family is either the script's own bug, a structural limit, or something that is not supposed to pass at all.

# Family Files Unique What it actually is
1 IndexError: list index out of range 5 3 Script bugs that break on TradingView too. One script loops for i = 0 to math.min(showBull-1, bullish_ob.size()) — when the order-block count is smaller than showBull, TradingView stops with the same error. Another declares var l = array.new<line>(0) and never fills it, then reads l.get(j). The translator is faithfully reproducing a crash.
3 NameError: name 'hi' is not defined 2 1 Structural limit: a method reads hi[1] where hi = high is a main-scope local. History cannot be threaded through a method call (same root cause as the 47 diagnostics above). Flagged, not silently rewritten.
6 RuntimeError: Security process died 2 1 Data-dependent: the 12M feed arrived, then the security subprocess died iterating na. TradingView hangs on the same script.
7 SyntaxError: fill() cannot be passed as a function value 2 1 Runtime structural limit. Deliberately not fixed — fixing it would make the report lie about what the runtime supports.
8 RuntimeError: only available for INDEX:BTCUSD 2 1 By design: the script only runs on one symbol.
9 RuntimeError: Chart timeframe needs to be lower 2 1 By design: same as above.
10 TimezoneNotFoundError: Unknown timezone 'UTC0' 2 1 The script builds 'UTC0' with str.format when the offset is 0. The runtime requires an IANA name or a signed UTC±N. No change without a TradingView-side confirmation first.

(Families 2, 4 and 5 existed in earlier sweeps and are gone now — the numbering is kept so older notes still line up. Family 4 was three strategies fixed by a runtime patch; family 5 was a missing 12M feed.)

Three of the seven families are scripts that are broken on TradingView as well. That is the finding I keep coming back to: a faithful translator does not make your scripts correct. It makes their incorrectness portable. If you port a strategy and the Python version disagrees with TradingView, check whether the Pine version was ever right — on a corpus this size, "the script itself was wrong" is not a rare explanation, it is the single biggest family.

What to do with this if you port scripts by hand

Four checks, in order, each one cheaper than debugging a wrong equity curve:

  1. Count unique scripts, not files. Duplicates inflate every rate you compute. If a vendor quotes you a percentage and cannot tell you the unique count, the percentage is decoration.
  2. Read the diagnostics before you read the code. A conversion with zero diagnostics and a conversion with 55 of them are different products. The diagnostic list is the translator telling you where it stopped being faithful — treat a missing diagnostic list as a missing feature, not as a clean bill of health.
  3. Run it, don't just compile it. 25 scripts compiled with diagnostics and 17 more compiled cleanly and then failed at runtime. Compilation is a syntax check; a bar-by-bar run on real data is the actual test.
  4. Watch for the silent scripts. 196 scripts in this corpus ran fine and produced nothing on the test feed — no plot, no drawing, no trigger. "It ran" and "it works" are different claims, and the silent ones are where the difference hides. If your port produces no output, check the input defaults first: in this corpus, one script's 55 plots were all na because an input.bool defaulted to false.

The honest summary

Pine-to-Python translation, measured rather than asserted: 99.7% of 1,909 real scripts compile, 98.4% translate with zero diagnostics, 99.1% run clean — in our testing, on our own corpus, baseline dated 2026-10-24. The failures that remain are small, named, and mostly not the translator's fault: three scripts that break on TradingView too, two that are not supposed to run on this feed at all, and one structural limit around history inside helper functions that any honest tool has to flag rather than fake.

The number I would put on a wall is not 99.7%. It is 0 — the number of run-error families the emitter can still fix. When the fixable bucket is empty, the remaining work is not in the translator. It is in the scripts, the data, and the runtime contract underneath. That is a better place to be than a higher percentage would suggest.

This conversion work was done by Pine2Py, the tool I build for exactly this job: paste a Pine script, get PyneCore-flavored Python back, diagnostics included when something can't be translated. The runtime is the open-source PyneCore project. We ship a pinned build (PyneCore 6.10.11 plus a small patch set — details on the site) because that's what the output is verified against. The two constructs the patches originally addressed are resolved upstream: ta.* calls inside UDT methods work in stock PyneCore 6.11.0+, and text.valign_* is rejected by TradingView itself. Per-patch upstream status is in PATCHES.md in the download. It's free to try — every account gets 3 conversions, no card — and Pro is $12/month if you end up converting a whole library. No return promises, ever: a translator moves your logic, it doesn't make it profitable.

Next in the series: a teardown — one real script, two engines, two equity curves, and the exact bar where they diverge.


What's the weirdest thing a converter has silently gotten wrong on one of your scripts? I read every comment — the failure stories are what the next posts are built from.

Top comments (0)