Converting Pine Script v5/v6 to Python without errors
Translating Pine to Python is easy for the first 80 % of a script and treacherous for the last 20 %. The syntax is not the problem. The problem is that Pine is a language where every variable is secretly a time series, and Python is not. Here are the four places conversions break, with the exact rules IndicatorSmith's converter applies.
1. Every Pine variable is a series
In Pine, x = close - open does not compute one number; it defines x for every bar, and x[1] is the value on the previous bar. A naive translation to Python computes a scalar and loses the history. The correct translation treats x as a NumPy array aligned to the bar index and turns x[1] into a shift:
The subtle part is functions that take a series and are themselves stateful. ta.ema(close, 20) is not a moving window over an array; it is a recurrence whose seed matters. The converter emits the exact Pine seeding rule (first value = first close, then the standard recurrence) so that the converted EMA matches the Pine reference to the last decimal instead of drifting for the first 100 bars.
2. var and varip persistence
Pine's var keyword declares a variable that is initialised once and then keeps its value across bars unless reassigned. It is how people write trailing stops, counters and state machines. In vectorised Python there is no "previous iteration" to keep state in, so the converter has two strategies:
- If the
varblock is a recognised pattern — running maximum, running count, latch-until-condition — it emits the equivalent vectorised expression (np.maximum.accumulate,cumsum, forward-fill). - If the logic is arbitrary, it emits an explicit
forloop over bars using the@statefuldecorator. Slower, but correct, and clearly marked in the conversion report as approximated: loop.
varip (intrabar persistence) has no meaning on historical bars, so it is converted to var with a warning. On live data the engine re-evaluates on each tick, which reproduces the intended behaviour.
3. na is not NaN — except when it is
Pine's na propagates through arithmetic like NaN, but comparisons and nz() have Pine-specific rules, and in v6 booleans can no longer be na at all. The converter follows the version tag in your script: v5 scripts get na-aware boolean handling; v6 scripts get strict booleans and a per-line error if a boolean expression could be undefined. That difference alone is responsible for most "works in v5, breaks in v6" reports we receive.
4. request.security and lookahead
This is the one that costs people money. request.security(sym, "D", close) on an intraday chart pulls the daily close — and by default it pulls the daily close of the current, unfinished daily bar, which on historical data means a value from the future. Strategies that look brilliant in the original platform's strategy tester and fall apart live are very often doing this without knowing it.
The converter refuses to guess. Every request.security call becomes a request_security(...) call with an explicit lookahead argument and a mandatory comment:
# Python
htf_close = request_security(bars, "D", lambda b: b.close,
lookahead=False) # Pine default was lookahead_off → uses last *completed* daily bar
# ⚠ if the Pine source used barmerge.lookahead_on, the converter
# emits lookahead=True and flags the line as REPAINTING in the report
Historical bars therefore always use the last completed higher-timeframe bar, exactly as a live chart would have. If you deliberately want the repainting behaviour (there are legitimate visual uses), you get it — with a warning you have to acknowledge before the script can be added to your library.
request.security, suspect lookahead first. Our docs have the full note including how lookahead_on interacts with [1] offsets.Verifying a conversion
Never trust a conversion you have not tested. The workflow we recommend, and the one built into IndicatorSmith:
- Export 500 bars of the indicator's plotted values from the platform that runs your Pine script (its chart-data export).
- Convert the script. Read the per-line report; anything marked approximated or unsupported needs a human look.
- Run the regression test against the export. Warm-up bars are excluded; everything after must match to 1e-9 for pure arithmetic and 1e-6 for functions with floating-point accumulation (EMA, RMA, VWAP).
- Replay the last 50 bars with bar replay and watch the indicator update — this is where
varipandbarstateassumptions show up.
For a strategy() script, add a fifth step: run the quick backtest in IndicatorSmith and compare trade count with the original platform's strategy tester on the same date range. Small differences in fills are normal; differences in trade count point to a signal-timing problem.
What does not convert
The converter covers about 1,900 functions and namespaces, including ta, math, str, array, matrix, map, strategy and request. It does not convert request.seed (custom GitHub data feeds), polyline, most table cell styling, and runtime.error message formatting. Each is reported per line rather than silently dropped; the unsupported functions table lists workarounds.
Once a strategy is converted and verified, the Python module is the same file IndicatorSmith's quick backtester runs and the same file you can drop into your own research stack — one implementation, nothing to keep in sync.
Trading involves risk. Backtests are hypothetical results on historical data and do not predict future results. Nothing in this post is financial advice.