TL;DR: With the Expression node, most of Pine Script's ta.* library has a one-line Trigr equivalent. ta.crossover(a, b) is crosses_above(a, b), close[3] is lag(close, 3), ta.barssince(cond) is bars_since(cond), and request.security(syminfo.tickerid, "240", close) is close@4h. Indicators Pine has built in but Trigr does not, such as the stochastic or a volume-weighted average, can usually be written from their definitions. What still does not translate is anything stateful or procedural: var, loops, arrays, custom functions and order-type tricks.
Why does the Expression node change Pine porting?
Before formulas, porting a Pine strategy to Trigr meant mapping each condition onto a catalog indicator node. Standard setups mapped well, but any custom math (a z-score, a ratio between two markets, a "bars since the last cross" rule) had no home. Our TradingView import guide listed those cases as the main reason a port could fail.
An Expression formula closes most of that gap. It is a single true or false condition with Pine-like functions, evaluated point in time on Trigr's own data. You still translate rather than paste, but the translation is now mostly mechanical.
Which Pine functions map to which Trigr functions?
The table below covers the Pine v6 functions that appear in most strategies. Trigr's indicator functions use the same formulas as its catalog nodes; we verified that close > ema(close, 50) and the EMA indicator node produce identical backtests.
| Pine Script v6 | Trigr formula | Notes |
|---|---|---|
close, open, high, low, volume |
close, open, high, low, volume |
Of the node's market and timeframe |
close[n] |
lag(close, n) |
n of at least 1 |
ta.sma(x, n), ta.ema(x, n) |
sma(x, n), ema(x, n) |
EMA is seeded with an SMA |
ta.rsi(x, n) |
rsi(x, n) |
Wilder smoothing |
ta.macd(x, f, s, sig) |
macd(x, f, s), macd_signal(x, f, s, sig), macd_hist(x, f, s, sig) |
One function per output line |
ta.bb(x, n, k) |
bbands_mid(x, n), bbands_upper(x, n, k), bbands_lower(x, n, k) |
Population standard deviation |
ta.stdev(x, n) |
stdev(x, n) or rolling_std(x, n) |
Both population |
ta.atr(n) |
atr(n) |
Wilder; atr(asset("ETH"), n) for another market |
ta.dmi(n, smooth) ADX output |
adx(n) |
Matches only when smooth equals n; +DI and -DI are not exposed |
ta.crossover(a, b) |
crosses_above(a, b) |
True on the bar a moves from at or below b to above b |
ta.crossunder(a, b) |
crosses_below(a, b) |
|
ta.change(x, n), ta.mom(x, n) |
diff(x, n) |
|
ta.roc(x, n) |
100 * pct_change(x, n) |
pct_change returns a fraction |
ta.highest(x, n), ta.lowest(x, n) |
highest(x, n), lowest(x, n) |
Current bar included |
math.sum(x, n) |
rolling_sum(x, n) |
A condition counts as 1 or 0 |
ta.barssince(cond) |
bars_since(cond) |
Missing, so comparisons are false, if cond was not true in the last 1,000 bars; bars_since(cond, 5000) raises the cap |
ta.percentrank(x, n) |
rank(x, n) |
Trigr returns (0, 1] with the current bar included; close to Pine's 0 to 100 rank, not identical |
math.abs, math.log, math.sign, math.min, math.max |
abs, log, sign, min, max |
|
request.security(syminfo.tickerid, "240", close) |
close@4h |
Closed bars only, see below |
request.security("BINANCE:ETHUSDT", "240", close) |
asset("ETH").close@4h |
Any market in Trigr's registry |
and, or, not |
and, or, not |
Comparisons cannot be chained |
Pine's input.* values become plain numbers. Window lengths must be whole-number literals from 1 to 5,000 (at least 2 for rsi, stdev, zscore, rank, atr, adx and the Bollinger bands), so ta.sma(close, len) with a variable len becomes sma(close, 20) with the number written in.
How do you write indicators Trigr does not have built in?
Many Pine built-ins are short formulas over primitives Trigr does have. Each of these was checked against the live engine before publishing:
| Pine built-in | Trigr formula |
|---|---|
ta.stoch(close, high, low, 14) (0 to 100) |
100 * (close - lowest(low, 14)) / (highest(high, 14) - lowest(low, 14)) |
ta.wpr(14) (Williams %R, minus 100 to 0) |
100 * (close - highest(high, 14)) / (highest(high, 14) - lowest(low, 14)) |
ta.vwma(close, 20) |
rolling_sum(close * volume, 20) / rolling_sum(volume, 20) |
Keltner upper band (ta.kc, EMA basis, ATR range) |
ema(close, 20) + 2 * atr(14) |
| Donchian breakout | close > lag(highest(high, 20), 1) |
| Bollinger bandwidth | (bbands_upper(close, 20, 2) - bbands_lower(close, 20, 2)) / bbands_mid(close, 20) |
On SOLUSDT 1h data, the stochastic-below-20 condition and the Williams-%R-below-minus-80 condition were true on exactly the same 9,017 bars, as the algebra says they should be. That kind of cross-check is worth doing on any indicator you rebuild. The definitions on Wikipedia for the stochastic oscillator and Keltner channels are a good reference.
Some built-ins remain out of reach. Session-anchored ta.vwap, ta.wma and ta.hma (weighted averages), ta.cci (it needs a mean absolute deviation) and ta.supertrend (it carries state from bar to bar) do not reduce to the available functions. For SuperTrend and CCI, check the catalog: several such indicators exist as regular indicator nodes you can combine with formula filters.
What happens to request.security and repainting?
Higher-timeframe data is where Pine strategies most often look better than they should. Called without care, request.security can return the value of a higher-timeframe bar that is still forming, which leaks future price into history. The usual fix in Pine is to request the previous bar's value with lookahead on, so only completed bars are used. TradingView's repainting guide explains the details.
In Trigr there is no setting to get wrong. close@4h on a 1h strategy is the close of the last completed 4h bar, carried forward until the next one completes, and the same rule applies to asset("ETH").close@4h. A formula cannot read a timeframe finer than its own node, so the reverse leak is refused at validation. Multi-timeframe strategies without look-ahead bias covers why this matters.
What does a full port look like?
Here is a small Pine strategy: a pullback entry in a higher-timeframe uptrend, a volatility exit and a fixed stop.
//@version=6
strategy("Trend pullback", overlay = true)
htfUp = request.security(syminfo.tickerid, "240", close[1] > ta.ema(close, 50)[1], lookahead = barmerge.lookahead_on)
if ta.crossover(ta.rsi(close, 14), 40) and htfUp
strategy.entry("L", strategy.long)
if ta.crossunder(close, ta.ema(close, 20) - 2 * ta.atr(14))
strategy.close("L")
strategy.exit("SL", "L", stop = strategy.position_avg_price * 0.97)
The Trigr version uses one TRIGGER formula and two RISK exits:
| Pine | Trigr node and parameter | Value |
|---|---|---|
| Entry condition | TRIGGER long |
crosses_above(rsi(close, 14), 40) and close@4h > ema(close@4h, 50) |
| Long only | RISK direction |
long |
strategy.close condition |
RISK exitLong |
crosses_below(close, ema(close, 20) - 2 * atr(14)) |
| 3% stop from entry | RISK sl |
3 |
| Hold until an exit fires | RISK exitOnFlip |
false |
That last row matters, and it is why the port is long only: with exitOnFlip off, onOpposite must stay unset or ignore. A two-sided port that should reverse on the opposite signal sets exitLong to the short entry condition and exitShort to the long one. The entry formula is an event: it is true on one bar. With the default exitOnFlip on, the trade would close as soon as the event ends, one bar later. Event vs state signals explains why, and why this port turns it off and relies on the exit formula and the stop instead.
What still does not translate?
Be explicit about these, because a port that quietly drops them is a different strategy:
- State that persists across bars.
varvariables, counters that reset on custom events, and trailing values updated byifblocks have no equivalent.bars_sincecovers the most common case. - Loops, arrays, maps and custom functions. A formula is one expression, at most 500 characters.
- Conditional values. There is no ternary operator. Split the logic into separate conditions, or into long and short formulas.
ta.valuewhenand pivot functions.ta.pivothighconfirms a pivot only after later bars arrive, so it cannot be point in time on the pivot bar itself.- Order types. Limit and stop entries, partial exits with
qty_percent, break-even moves and multi-level targets have no direct equivalent. Trigr fills at the next bar's open and its RISK node offers one stop, one target, an ATR trail, a time stop and formula exits. - Intrabar logic.
calc_on_every_tickandprocess_orders_on_closebehaviors do not exist; formulas evaluate on closed bars.
How do you check the port?
Run the Trigr version gross first, then with slippage and funding, and compare trade lists rather than headline returns. Expect fewer, later entries (next-bar-open fills) and lower returns (fees on every trade). Why your Pine Script backtest looks better than it will trade lists each source of difference, and the strategy builder docs define every RISK parameter used above.
If you would rather not translate by hand, paste the Pine code into an assistant connected to Trigr's MCP server and ask for a mapping table first, with every approximation listed. The TradingView import walkthrough shows that session step by step, and Pine's own language reference is the authority on what each ta.* function does.
Backtests are not guarantees, and perps are leveraged instruments that can lose more than expected.