Short answer: Converting Pine Script to Python means re-implementing TradingView's execution model, not just its syntax. Pine runs a script once per bar, and by default a strategy order created at a bar's close fills at the next bar's open. Built-ins map to pandas with specific settings, such as ta.ema to ewm with alpha = 2/(length + 1) and ta.rsi to Wilder's RMA seeded with a simple average. With the same rules and data, a Python backtest should reproduce TradingView's trade list trade by trade, which you can check as shown below.
Teams port Pine strategies to trade from their own servers, use other data, run large parameter studies or test the logic. Pine strategies only simulate orders; the alternative is keeping the rules in Pine and forwarding alerts as a webhook JSON message.
Translations rarely fail on syntax; they fail on timing: when a value is known and when an order fills. This guide follows the Pine Script v6 documentation as of October 2026; every Python sample was run on synthetic data.
Pine Script execution model vs a pandas backtest
Pine executes the whole script once per historical bar, oldest to newest, keeping each variable's past values as a series; pandas computes whole columns at once.
That is harmless if every column is aligned with the moment its value is known: Pine's close[1] is close.shift(1). On the realtime bar, indicators recalculate on every tick while strategies by default calculate only at the close, so a Python bot on closed candles matches a default Pine strategy, not an intrabar indicator alert.
| Pine Script | Python and pandas |
|---|---|
| close[1] | close.shift(1) |
| Script runs once per closed bar | Vectorized columns, each shifted to when it is known |
| strategy.entry() at the close, filled on the next tick | Fill at the next bar's open: open.shift(-1) on the signal bar |
| process_orders_on_close = true | Fill at the signal bar's close |
| pyramiding = 0 (default) | Ignore entries while a position is open |
| strategy.close() | Market exit at the next bar's open |
Strategy defaults that change the numbers
Several strategy() defaults shape results; set them explicitly and copy them into Python.
- Fills. Orders fill on the next available tick, so an order created at a bar's close fills at the next open; process_orders_on_close = true fills it at the close.
- Costs. commission_value defaults to 0 and slippage to 0 ticks, so an unconfigured backtest is cost-free.
- Size and capital. TradingView's Help Center gives a default initial capital of 1,000,000, while the v6 reference lists 100000, so set initial_capital explicitly. The default order size is 100% of equity (strategy.percent_of_equity, default_qty_value 100).
- Limit orders fill as soon as price touches the level, unless backtest_fill_limits_assumption (default 0 ticks) requires price to trade through it.
- Intrabar path. Without lower-timeframe data the emulator assumes open, high, low, close when the open is nearer the high, otherwise open, low, high, close; stops and targets inside a bar depend on it. The bar magnifier, which uses lower-timeframe data, requires Premium or higher.
- Calculation settings. calc_on_every_tick (default false) affects only realtime bars.
ta.sma, ta.ema, ta.rsi and ta.crossover in pandas
The v6 reference defines each built-in precisely:
- ta.sma: rolling(length).mean().
- ta.ema: alpha = 2/(length + 1). The reference's equivalent Pine code starts from the first source value, as pandas ewm(adjust=False) does.
- ta.rma, the moving average used in RSI: alpha = 1/length, but seeded with a simple average of the first length values, which plain ewm does not do.
- ta.rsi: 100 − 100/(1 + RS), where RS is the RMA of upward changes divided by the RMA of downward changes.
- ta.crossover(a, b): a is above b on the current bar and was at or below it on the previous bar; ta.crossunder mirrors it.
import numpy as np
import pandas as pd
def sma(s, n): # ta.sma
return s.rolling(n).mean()
def ema(s, n): # ta.ema: alpha = 2/(n+1), starts at the first value
return s.ewm(alpha=2 / (n + 1), adjust=False).mean()
def rma(s, n): # ta.rma: SMA of the first n values, then alpha = 1/n
s = s.dropna()
seeded = s.iloc[n - 1:].copy()
seeded.iloc[0] = s.iloc[:n].mean()
return seeded.ewm(alpha=1 / n, adjust=False).mean()
def rsi(close, n=14): # ta.rsi: RMA of up moves vs RMA of down moves
d = close.diff()
up, down = rma(d.clip(lower=0), n), rma(-d.clip(upper=0), n)
return (100 - 100 / (1 + up / down)).reindex(close.index)
def crossover(a, b): # ta.crossover
return (a > b) & (a.shift(1) <= b.shift(1))
def crossunder(a, b): # ta.crossunder
return (a < b) & (a.shift(1) >= b.shift(1))
closes = pd.Series([100, 101.5, 101, 102.5, 104, 103, 104.5, 106, 105.5, 107,
108.5, 108, 109.5, 111, 110, 112, 111, 109.5, 110.5, 108])
print(rsi(closes).dropna().round(2).tolist()) # hypothetical closesThe printed values, 79.41, 81.73, 77.06, 70.55, 72.24 and 62.6, match a hand calculation of 14-period Wilder RSI on the same 20 hypothetical closes.
A tested EMA crossover backtest in Python
A hypothetical Pine strategy, not compiled in TradingView, that buys when a 12-bar EMA crosses above a 26-bar EMA and exits on the cross below, with 0.1% commission per side:
//@version=6
strategy("Hypothetical EMA cross", overlay = true, initial_capital = 100000,
commission_type = strategy.commission.percent, commission_value = 0.1)
fast = ta.ema(close, 12)
slow = ta.ema(close, 26)
if ta.crossover(fast, slow)
strategy.entry("Long", strategy.long)
if ta.crossunder(fast, slow)
strategy.close("Long")The Python version continues the previous block on 2,000 synthetic bars, skips a 30-bar warm-up, ignores entries while in a position (pyramiding = 0) and fills at the next open or the signal close.
rng = np.random.default_rng(42) # synthetic, hypothetical prices
n = 2000
close = pd.Series(100 * np.exp(np.cumsum(rng.normal(0, 0.01, n))))
open_ = close.shift(1).fillna(100) * np.exp(rng.normal(0, 0.002, n))
fast, slow = ema(close, 12), ema(close, 26)
buy, sell = crossover(fast, slow), crossunder(fast, slow)
def backtest(fill, fee=0.001):
"""Signal on bar i's close; fill.iloc[i] is the price used for it."""
equity, entry, trades = 1.0, None, 0
for i in range(30, n - 1): # 30-bar warm-up
if buy.iloc[i] and entry is None: # pyramiding = 0
entry, trades = fill.iloc[i] * (1 + fee), trades + 1
elif sell.iloc[i] and entry is not None:
equity *= fill.iloc[i] * (1 - fee) / entry
entry = None
return trades, round(float(equity), 4)
print("next open (Pine default):", backtest(open_.shift(-1)))
print("signal close (process_orders_on_close):", backtest(close))
d = close.diff() # plain ewm without the SMA seed
plain = 100 - 100 / (1 + d.clip(lower=0).ewm(alpha=1 / 14, adjust=False).mean()
/ (-d.clip(upper=0)).ewm(alpha=1 / 14, adjust=False).mean())
gap = (plain - rsi(close)).abs()
print([round(float(gap.iloc[i]), 5) for i in (14, 100, 200)])Output:
next open (Pine default): (35, 0.6065)
signal close (process_orders_on_close): (35, 0.593)
[17.0718, 0.03145, 2e-05]Reading the output (hypothetical data). Both versions make the same 35 trades, but ending equity is 0.6065 with next-open fills and 0.593 with same-bar fills. The series itself loses two-thirds, the crossover loses about 40% and commission cuts ending equity by about 7%: noise, not an edge. The last line shows that seeding matters only early: RSI from plain ewm differs from the ta.rsi-style version by about 17 points at first, 0.03 after 100 bars and 0.00002 after 200.
Reproducing TradingView results within tolerance
Aim for an identical trade list first; net profit alone can match by coincidence.
- Same bars. Use TradingView's chart data export or a feed with the same exchange, session, time zone and adjustments.
- Warm-up. TradingView may load different history than your file, so compare after a few hundred bars.
- Indicators. Compare EMA and RSI bar by bar; after warm-up, differences should be at floating-point level.
- Trades. Compare entry and exit times and prices with the strategy report's trade list; one shifted trade usually means a timing or fill rule differs.
- Sizing. Match capital, order size, costs and rounding to lot and tick sizes.
- Intrabar orders. Stops, limits and trailing exits inside a bar follow the emulator's path assumption; model it or accept small differences.
Pitfalls: request.security lookahead and repainting
Higher-timeframe data is a common source of hidden lookahead in converted strategies.
request.security() with lookahead = barmerge.lookahead_on and no [1] offset returns a higher-timeframe value, such as the day's close, on historical bars before it was known; the reference calls this future leak. The default, lookahead_off, is safe on history but repaints on realtime bars. The non-repainting form requests expression[1] with lookahead_on. In pandas, shift the resampled series before aligning it:
import numpy as np
import pandas as pd
idx = pd.date_range("2026-01-01", periods=72, freq="h") # three days, hourly
hourly = pd.Series(np.arange(72, dtype=float), index=idx) # hypothetical closes
daily = hourly.resample("1D").last()
leaky = daily.reindex(idx, method="ffill") # like lookahead_on without [1]
safe = daily.shift(1).reindex(idx, method="ffill") # like close[1] with lookahead_on
print(hourly.iloc[30], leaky.iloc[30], safe.iloc[30]) # 2 January, 06:00It prints 30.0 47.0 23.0: at 06:00 on 2 January the leaky version already knows that day's 23:00 close, while the safe version uses the previous day's. Also beware: timenow and calc_on_order_fills can leak future data on history, calc_on_every_tick or varip logic relies on intrabar ticks that candles lack, and Heikin Ashi or other non-standard charts typically give unrealistic fills.
From a matched backtest to a Python trading bot
A matched Python version can trade live, but the live loop adds problems no backtest sees.
The bot must act on closed candles only, handle late candles, round orders to exchange precision, confirm fills, reconcile positions and obey a kill switch, as covered in trading bot development. If the strategy should stay in Pine, a TradingView webhook bot is the alternative. A matched backtest proves the translation is faithful, not that the strategy will make money. Read backtesting pitfalls before trusting the numbers, and run the bot on a testnet or paper account before live funds; automated strategies can lose money. Porting and verification are part of the TradingView automation service. If you are hiring someone for the Pine side, Pine Script developer covers what to put in the brief.
TradingView's Terms of Use (opens in a new tab), read on 5 November 2026, describe alerts and webhooks as licensed for display use and prohibit using them for automated trading, including through third-party products or services. Read those terms and take legal advice before relying on this route; it is your agreement with TradingView.
Frequently asked questions
What is the Python equivalent of ta.rsi?
Smooth gains and losses with Wilder's RMA (a simple average of the first 14 values, then alpha = 1/14) and compute 100 − 100/(1 + RS). Plain ewm(alpha=1/14) without the seed gives different early values.
Why does my Python EMA differ from TradingView's?
Usually the series start at different bars, so the seed differs, or the data differs in exchange, session or adjustments. Compare after a warm-up of a few hundred bars.
Does a TradingView strategy fill orders at the close?
Not by default. An order created at a bar's close fills at the next bar's open; setting process_orders_on_close = true makes it fill at the close of the signal bar.
Can a tool translate Pine Script to Python automatically?
Tools can translate syntax, but not guarantee Pine's execution model, fill rules and defaults. Verify any translation by comparing indicator values and trades with TradingView on the same data.
Can I run my Pine strategy as a live Python bot?
Yes, once the logic is re-implemented and verified; the bot then needs its own market data, order handling and risk controls. Alternatively, keep the strategy in Pine and send its alerts to a webhook bridge.
Sources
- TradingView Pine Script v6 Reference Manual (ta.sma, ta.ema, ta.rma, ta.rsi, ta.crossover, strategy, request.security) (opens in a new tab)
- TradingView Pine Script v6 User Manual: Strategies (opens in a new tab)
- TradingView Pine Script v6 User Manual: Execution model (opens in a new tab)
- TradingView Pine Script v6 User Manual: Repainting (opens in a new tab)
- TradingView Pine Script v6 User Manual: Other timeframes and data (opens in a new tab)
- TradingView Help Center: Relative Strength Index (RSI) (opens in a new tab)
- pandas documentation: DataFrame.ewm (opens in a new tab)
- pandas documentation: Series.rolling (opens in a new tab)
- pandas documentation: Series.shift (opens in a new tab)
- pandas documentation: Series.resample (opens in a new tab)
- TradingView Help Center: Initial capital (default value) (opens in a new tab)
- TradingView Help Center: Strategy properties (opens in a new tab)
- TradingView: Terms of Use (read 5 November 2026) (opens in a new tab)
This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the editorial policy.