TL;DR: The Expression node turns any trading condition into a short formula. Instead of picking one indicator from a list, you write close > ema(close, 200) and close@1d > ema(close@1d, 50) or pct_change(close, 168) > pct_change(asset("BTC").close, 168) and the engine evaluates it point in time. Formulas can be a strategy's entry trigger, a filter, a regime rule or an exit. They read other timeframes, other markets, point-in-time data sources and, for research, your own uploaded signals. A strategy built this way backtests, gets a paper track and runs in Agents like any other.
What is the Expression node?
Until now, every condition in a Trigr strategy came from the catalog: pick an indicator, set its length, choose a comparator and a threshold. That covers a lot of ground, but it breaks down as soon as an idea combines things. "RSI under 30, but only when the daily trend is up and BTC is not dumping" needed three nodes and a combine rule, and "price stretched two standard deviations below its 50-bar average" needed an indicator that did not exist.
The Expression node removes that limit. It is a node whose only setting is a formula: a plain-ASCII condition, up to 500 characters, that evaluates to true or false on every bar. The backtesting service parses it, type-checks it, and refuses it with the exact character position if something is wrong, before any credits are spent on a run.
A formula is still a research hypothesis like any other condition. It makes strategies easier to express, not more likely to work, so the usual discipline about backtest overfitting applies with more force, not less.
What can a formula read?
A formula can read five kinds of input:
| Input | Syntax | Example |
|---|---|---|
| The node's market and timeframe | open, high, low, close, volume |
close > open |
| Another timeframe (same or coarser) | close@4h, high@1d |
close@4h > ema(close@4h, 50) |
| Another market | asset("ETH").close |
asset("ETH").close / asset("BTC").close |
| Another market on another timeframe | asset("ETH").close@4h |
asset("ETH").close@4h > ema(asset("ETH").close@4h, 50) |
| A point-in-time data source | api("source_id") |
api("funding_rate") < 0 |
A sixth input, upload("name"), reads a series you computed yourself in Python or a notebook. It has its own rules, covered in backtesting your own Python signal.
Operators are the ones you would expect: + - * /, the comparisons < <= > >= == !=, and and, or, not with parentheses. Numbers and true/false are valid operands, but every formula must read at least one market input; long: true on its own is refused. Comparisons cannot be chained, so write 30 < rsi(close, 14) and rsi(close, 14) < 70 rather than 30 < rsi(close, 14) < 70. The error message tells you exactly that.
What functions are available?
More than thirty functions, grouped by what they do. Window lengths are whole-number literals from 1 to 5,000 (at least 2 for rsi, stdev, rolling_std, zscore, rank, atr, adx and the Bollinger bands).
| Group | Functions |
|---|---|
| Indicators | sma, ema, rsi, macd, macd_signal, macd_hist, stdev, bbands_mid, bbands_upper, bbands_lower, atr, adx |
| Windows | lag, diff, pct_change, rolling_mean, rolling_std, rolling_min, rolling_max, rolling_sum, highest, lowest, zscore, rank, bars_since |
| Crosses | crosses_above, crosses_below |
| Element-wise | abs, log, sign, min, max, clip |
| Inputs | api("source_id"), upload("name") |
The indicators use the same math as the matching catalog nodes. We checked this directly: a strategy whose trigger is the formula close > ema(close, 50) and the same strategy built with the EMA indicator node produced identical results on SOLUSDT 1h, trade for trade. atr and adx also take a market or timeframe as their first argument, as in atr(asset("ETH"), 14) or adx(tf("4h"), 14).
Because functions compose, many indicators that are not built in can be written in one line. A stochastic oscillator is (close - lowest(low, 14)) / (highest(high, 14) - lowest(low, 14)), a Keltner band is ema(close, 20) + 2 * atr(14), and a rolling VWAP is rolling_sum(close * volume, 24) / rolling_sum(volume, 24). The recipes article has 25 of these.
Where does a formula go in a strategy?
A Trigr strategy is a node graph: one TRIGGER, optional FILTERs, a SIGNAL that combines them, a RISK node and an EXECUTE node. The no-code strategy builder guide explains each stage. Formulas plug into four of those places:
| Place | Parameters | What it does |
|---|---|---|
| TRIGGER | long, short, optional delivery |
Entry conditions per side. A bar where both are true is flat. |
| FILTER | expr, or long and/or short |
Gates entries. expr applies to both sides exactly as written; it is never mirrored for shorts. With the default settings a filter that stops passing also closes an open trade; set RISK filtersCloseTrades to false to gate entries only. |
| RISK regime rule | Same as FILTER | Scales or blocks sizing by regime. |
| RISK exits | exitLong, exitShort |
When true at a bar close, every open position on that side closes (exit reason formula_exit), and new entries on that side wait until it turns false. |
Every expression node also takes tf, the timeframe it is evaluated on. A filter can run on 4h while the strategy trades 1h bars.
Two settings matter more with formulas than with indicator nodes. First, a TRIGGER's delivery is level by default, which means the entry signal repeats on every bar the formula is true. Set it to rising_edge and the trigger emits one pulse when the formula turns from false to true. Second, with the RISK default exitOnFlip on, a trigger that stops being true closes the trade. Combine an event formula such as crosses_above with that default and every trade lasts one bar. When you turn exitOnFlip off, leave onOpposite unset or ignore (reverse is refused, because nothing closes on an opposite signal any more); to reverse anyway, set exitLong to the short entry condition and exitShort to the long one. Event vs state signals walks through the numbers and the fix, and the backtest result now warns you when a strategy is set up this way.
How does Trigr keep formulas point in time?
A formula engine is only useful if it cannot quietly cheat. These rules are enforced by the grammar, not by convention:
- No negative lag.
lag(close, n)requires n of at least 1. There is no way to reference a future bar. - Every function looks backward. Rolling windows, z-scores, ranks and
bars_sinceinclude the current bar and earlier ones only. - Coarser timeframes count only once closed. At 13:00 on a 1h strategy,
close@4his the close of the 4h bar that ended at 12:00, carried forward until the next one completes. Weekly values change only at the week boundary. We verified that aclose@1wstrategy flips only on Mondays at 00:00 UTC. - No finer timeframes. A formula on a 1h node cannot read
close@15m; that is refused, in exit formulas too. - Missing values block. During warm-up, or when a data source has a gap, comparisons are unknown and an unknown condition is false.
sma(close, 5000)cannot trade until 5,000 bars exist. - Cross-asset coverage clips the window. If a formula reads a market with shorter history, the backtest starts where that market's data starts, and the result says so.
This is the same discipline described in point-in-time backtesting, applied to arbitrary formulas instead of fixed indicators.
What does this unlock?
The practical difference is how many ideas you can test without waiting for a new node:
- Indicators that are not in the catalog: stochastic, Williams %R, Keltner channels, rolling VWAP, rate of change, volume-weighted averages.
- Relative strength and ratios: "SOL outperformed BTC over the last week", or "the ETH/BTC ratio is above its 50-bar average".
- Regimes in one line: volatility percentile with
rank(atr(14) / close, 500), trend strength withadx(14) > 25. - Cooldowns and patterns: "no entries for 12 bars after a 5% drop" is
rolling_sum(pct_change(close, 1) < -0.05, 13) == 0. - Formula exits: leave a long when the 4h trend breaks, or when price closes two ATRs under the 20 EMA.
- Ports from other platforms: most of Pine Script's
ta.*library now has a direct equivalent. See converting Pine Script to Trigr formulas. - Your own research signals, through
upload("name"), for exploratory backtests.
Where can a formula strategy run?
Formulas run in backtests, on the marketplace paper track, and in Paper, Hyperliquid and Propr Agents. Operators can switch formulas off per venue; if that happens, Agent creation refuses and names the venue.
There are three limits to know about:
- ML optimization and ML training do not accept formula nodes yet.
- Build-on-top composition from another creator's strategy does not accept formula nodes yet.
- Uploaded series are research only. A strategy that reads
upload("name")can be backtested, but it can never be published, get a marketplace paper track or be deployed to an Agent, Paper included, because Trigr cannot verify when your values were knowable.
volume is refused on CFD-sourced markets such as SPX, where the data provider's volume is tick activity rather than traded volume. Formulas that need more history than live evaluation loads, such as very long windows, are refused when an Agent is created, even if they backtest fine.
How do you build one?
You can write formulas in Trigr Studio or have an AI assistant write them for you over MCP. The Claude Code and Codex setup guide covers connecting an assistant. Either way, a minimal trend strategy looks like this as a graph:
{
"market": "SOLUSDT",
"timeframe": "1H",
"nodes": [
{ "id": "entry", "kind": "TRIGGER", "source": "expr", "title": "EMA cross in a 4h uptrend",
"params": [
{ "k": "tf", "v": "1H" },
{ "k": "long", "v": "crosses_above(ema(close, 12), ema(close, 26)) and close@4h > ema(close@4h, 50)" }
] },
{ "id": "signal", "kind": "SIGNAL", "title": "Entry", "params": [], "combine": { "mode": "all" } },
{ "id": "risk", "kind": "RISK", "title": "Risk",
"params": [
{ "k": "size", "v": "10% eq" }, { "k": "lev", "v": "1x" }, { "k": "sl", "v": "3" },
{ "k": "direction", "v": "long" },
{ "k": "exitLong", "v": "ema(close, 12) < ema(close, 26) or rsi(close, 14) > 75" },
{ "k": "exitOnFlip", "v": "false" }
] },
{ "id": "execute", "kind": "EXECUTE", "title": "Paper", "params": [], "venue": "paper" }
]
}
The cross opens the trade, and the exit formula and the stop close it. Backtest it gross first, then with slippage and funding as described in slippage and funding in perp backtests, and read the trade log before trusting the summary.
Next steps
Pick one idea you could never express with a single indicator node and write it as a formula. Start from the formula recipes, check the strategy builder docs for every parameter, and keep each variation as a labelled experiment on one strategy so your trial count stays honest. External references for the underlying math: the Bollinger Bands and average true range entries on Wikipedia.
Backtests are not guarantees, and perps are leveraged instruments that can lose more than expected.