Skip to content

Expressions

Expressions are the formulas you use inside rule conditions, order quantities, price levels, and variable assignments. They let you go beyond simple indicator-to-number comparisons and build dynamic, adaptive trading logic – all without writing code.

You enter expressions in the formula fields throughout the strategy builder. Stratifyre validates them in real time, so you will see an error message immediately if something is not right.

Use standard math to build calculations:

Operator Meaning Example
+ Addition price + 10
- Subtraction price - ATR(14)
* Multiplication account.equity * 0.02
/ Division account.balance / 100
% Modulo (remainder) bar_index % 5 (every 5th bar)

You can combine operators freely and use parentheses to control the order of operations:

(account.equity * 0.02) / (ATR(14) * 2)

This example calculates a risk-based position size: 2% of equity divided by twice the 14-period ATR.

These are used inside conditions to compare two values. (For the full set of 15 condition operators available in the rule builder, see Rules.)

Operator Meaning Example
== Equal to position.open_qty == 0
!= Not equal to position.open_qty != 0
> Greater than RSI(14) > 70
< Less than RSI(14) < 30
>= Greater than or equal volume >= 1000000
<= Less than or equal price <= SMA(200)

Combine multiple comparisons into complex conditions:

Operator Meaning Example
&& AND – both sides must be true RSI(14) < 30 && price > SMA(50)
|| OR – at least one side must be true RSI(14) > 70 || price < SMA(200)
! NOT – inverts true/false !session.is_rth (true outside regular hours)

Use parentheses to make complex logic clear:

(RSI(14) < 30 && price > SMA(50)) || (MACD().histogram > 0 && volume > SMA(20, "volume") * 1.5)

Boolean comparisons in Stratifyre expressions evaluate to numbers — 1 when true, 0 when false. That means you can add them together to count how many of a set of signals are currently active. This unlocks several patterns that would otherwise require nested boolean logic or multiple helper rules.

Suppose you want to enter only when at least 2 of 4 independent signals agree:

(RSI(14) < 30) + (MACD().histogram > 0) + (close > SMA(50)) + (volume > AVG(volume, 20)) >= 2

Each parenthesized comparison produces 1 or 0. Summing them gives the number of true signals, and >= 2 gates the rule on that count. This is a single condition — no helper rules, no nested boolean tree.

Variants:

Goal Pattern
At least one signal true (same as OR) (a) + (b) + (c) >= 1
All signals true (same as AND) (a) + (b) + (c) >= 3
Exactly one signal true (a) + (b) + (c) == 1
Majority of signals true (a) + (b) + (c) + (d) + (e) >= 3

Because the booleans are just numbers, you can weight them to give some signals more influence than others:

3 * (close > SMA(200)) + 2 * (RSI(14) < 30) + 1 * (MACD().histogram > 0) >= 4

Here the long-term trend filter is worth 3 points, RSI oversold is worth 2, and MACD momentum is worth 1. The rule fires when the total score reaches 4. Pure boolean logic cannot express this kind of prioritization — you would need a separate rule for every combination of signals.

Instead of writing a long chain of || operators, counting is often more readable:

// Hard to scan
RSI(14) < 30 || RSI(14) > 70 || ATR(14) > ATR(14)[20] * 1.5 || volume > AVG(volume, 20) * 3
// Easier to reason about
(RSI(14) < 30) + (RSI(14) > 70) + (ATR(14) > ATR(14)[20] * 1.5) + (volume > AVG(volume, 20) * 3) >= 1

Both mean the same thing, but the counted form makes it trivial to change “any one” to “any two” later — just change >= 1 to >= 2.

Stratifyre provides built-in math functions for common calculations:

Function What it does Example
abs(x) Absolute value abs(price - SMA(20))
max(a, b) Larger of two values max(high, high[1])
min(a, b) Smaller of two values min(low, low[1])
round(x) Round to nearest integer round(account.equity * 0.01 / price)
floor(x) Round down floor(account.equity / price)
ceil(x) Round up ceil(quantity * 1.1)
pow(x, n) Raise to a power pow(1.02, 12) (compound 2% monthly)
sqrt(x) Square root sqrt(variance)
log(x) Natural logarithm log(price / price[20]) (log return)

Access the current bar’s price and volume data:

Field Description
price Current price (same as close)
open Open price of the current bar
high High price of the current bar
low Low price of the current bar
close Close price of the current bar
volume Volume of the current bar

Append [N] to any value to look back N bars:

Expression Meaning
close[1] Previous bar’s close
high[2] High from two bars ago
volume[1] Previous bar’s volume
RSI(14)[1] RSI value from the previous bar
SMA(20)[3] SMA value from three bars ago

This lookback syntax works with any field or indicator – just add square brackets with the number of bars to look back.

Access data from a different timeframe than your strategy’s primary resolution with the canonical bars['[<InstrumentId>~]<Interval>'].<field>[<index>] shape:

Expression Meaning
bars['D'].close[1] Previous day’s close for current instrument
bars['D'].high[0] Current day’s high for current instrument
bars['1h'].volume[1] Previous hour’s volume for current instrument
bars['W'].open[2] Open from two weeks ago for current instrument
bars['15m'].low[1] Previous 15-minute bar’s low for current instrument
bars['EQ:NASDAQ:QQQ~1m'].close Current 1-minute close for QQQ
bars['EQ:NASDAQ:QQQ~D'].high[1] Previous daily high for QQQ

Inside an instrument-scoped rule, bars['5m'] means “5-minute bars for instrument currently being evaluated.” In portfolio-level rules with no current instrument, use the explicit instrument form such as bars['EQ:NASDAQ:QQQ~5m'].

You can also reference indicators on a different timeframe by passing a timeframe parameter. See Multi-Timeframe Strategies for details.

Built-in fields for common percentage-change calculations:

Field Description
pct_change_day Percentage change since the start of the current day
pct_change_week Percentage change since the start of the current week
pct_change_month Percentage change since the start of the current month
pct_change_year Percentage change since the start of the current year

You can also calculate custom percentage changes:

(close - close[5]) / close[5] * 100 – Percentage change over the last 5 bars.

Access your account’s financial information:

Field Description
account.balance Current account balance
account.equity Balance plus unrealized profit and loss
account.available_margin Margin available for new positions
account.used_margin Margin currently in use
account.total_realized_pnl Total realized profit and loss
account.total_unrealized_pnl Total unrealized profit and loss
account.total_commission_paid Total commissions paid
account.total_drawdown_pct Maximum drawdown as a percentage

Common use: Risk-based position sizing that adapts to your current equity:

account.equity * 0.02 / ATR(14) – Risk 2% of equity per trade, with stop distance based on ATR.

Access information about the current position for the instrument being evaluated:

Field Description
position.open_qty Open quantity (positive = long, negative = short, zero = flat)
position.market_value Current market value of the position
position.unrealized_pnl Unrealized profit or loss
position.realized_pnl Realized profit or loss
position.margin Margin requirement for the position
position.positionReturnPct Return as a percentage of position size
position.accountReturnPct Return as a percentage of account equity
position.drawdown Position drawdown from peak
position.commission_paid Commissions paid for this position

Common use: Profit-target exit:

position.positionReturnPct > 5 – Exit when the position is up 5%.

Common use: Stop-loss exit:

position.unrealized_pnl < -(account.equity * 0.01) – Exit when loss exceeds 1% of equity.

Field Description
time.min_since_midnight Minutes elapsed since midnight
time.day_of_week Day of the week (0 = Sunday, 6 = Saturday)
time.min_since_rth_open Minutes since regular trading hours opened
time.min_since_eth_open Minutes since extended trading hours opened
Field Description
session.is_rth True during regular trading hours
session.is_eth True during extended trading hours

Common use: Only trade during the first two hours of the session:

time.min_since_rth_open < 120

Common use: Avoid trading on Mondays:

time.day_of_week != 1

Access information about your pending orders:

Field Description
orders.pending_buy_qty Total quantity of pending buy orders
orders.pending_sell_qty Total quantity of pending sell orders
orders.stop_loss_price Price of the active stop-loss order
orders.take_profit_price Price of the active take-profit order
orders.limit_buy_price Best (highest) pending limit buy price
orders.limit_sell_price Best (lowest) pending limit sell price

Custom variables let you store values in one rule and use them in another. This is how you build logic that spans multiple rules — for example, recording your entry price and then referencing it in a trailing-stop calculation.

Variables are not just a convenience feature in Stratifyre — they are the central abstraction that lets a deliberately simple rule model express arbitrarily complex trading logic. Understanding why they were designed this way will save you from reaching for features that don’t exist (and don’t need to).

Most rule-based trading platforms eventually hit a wall: users want to express something that doesn’t fit a single condition — sequential patterns, multi-step confirmation, state that persists across bars, cross-instrument coordination, counters, latches. The usual response is to keep bolting features onto the rule model: nested boolean trees, sub-rules, scripting escape hatches, DSL extensions. Every addition makes the engine harder to reason about, harder to debug, harder for LLMs to generate correctly, and harder for new users to learn.

Stratifyre took the opposite path. Rather than growing the rule model, we gave you persistent named state (SET_LOCAL_VAR and SET_GLOBAL_VAR) and made it a first-class citizen of the expression language. This one primitive collapses a surprising amount of complexity:

  • Sequential logic — “X happens, then Y happens relative to X” becomes two rules connected by a latch variable.
  • Counters — “enter on the third pullback of the day” is vars.pullbackCount += 1 in one rule and vars.pullbackCount == 3 in another.
  • Latches and state machines — “arm on breakout, disarm on reversal” is literally two rules that flip a flag.
  • Cross-instrument coordination — a global variable lets AAPL’s rule react to what MSFT’s rule did.
  • Derived intermediate values — store riskBudget = account.equity * 0.02 once and reference it everywhere instead of re-computing it in every condition.
  • Grouped boolean logic — compute (A AND B) as one flag and (C AND D) as another, then OR them in a consumer rule. (See Expressing complex boolean logic in the Rules guide.)

What variables unlock that the rule model alone cannot

Section titled “What variables unlock that the rule model alone cannot”

Some things genuinely cannot be expressed with conditions and operands, no matter how sophisticated. Variables are the only path:

  1. Ordering — “RSI crossed 30 before price broke the prior high.” Boolean logic has no notion of before and after. A latch variable records that the first event happened, and a second rule watches for the follow-up.

  2. Accumulated state — “Enter only after 3 consecutive bars of rising volume” needs a counter that increments when the condition holds and resets when it doesn’t. There is no expression that reaches back an arbitrary number of bars to test “were the last N all true.”

  3. Cross-rule memory — “Don’t re-enter for 10 bars after a stop-out.” The exit rule stamps vars.lastStopBarIndex, and the entry rule gates on bar_index - vars.lastStopBarIndex > 10.

  4. Portfolio-level coordination — “Stop all new entries once 5 positions are open across the strategy.” A global counter incremented on entry and decremented on exit, checked in every entry rule’s conditions.

  5. Anchored levels — “Trail the stop behind the highest high since entry.” Record vars.highWaterMark = max(vars.highWaterMark, high) every bar, and compute the stop as vars.highWaterMark - 2 * ATR(14).

None of these are “workarounds for missing features” — they are the intended way to do stateful trading logic. A nested boolean tree couldn’t express any of them.

Why this design scales better than alternatives

Section titled “Why this design scales better than alternatives”

A few concrete reasons the variable-based model outperforms bigger, more complex rule languages in practice:

  • Inspectable state. When a strategy misfires, you can look at each variable’s current value and see exactly which flag was set, which counter was at what value, and which latch was armed. A 40-line nested boolean expression offers none of that — you see only its final true/false.
  • Named intent. A variable called pullbackSignal or regimeIsTrending documents what the strategy is trying to do. A bare (RSI(14) < 30 && close > SMA(200)) only documents what it’s checking. Over time, named flags turn into a glossary of the strategy’s concepts.
  • LLM-friendly. Most Stratifyre users describe their strategies in natural language and let an LLM generate the rule graph. LLMs are notoriously poor at deeply nested parentheses and precedence, but very good at breaking a description into discrete steps — which is exactly what the variable model rewards.
  • Composable. A flag set by one rule is a building block for the next. You can layer three rules to build a three-step confirmation sequence without rewriting any of them. Nested expressions are atomic — changing one sub-clause often means re-architecting the whole thing.
  • Zero new concepts. Everything in the variable system uses the same expression language you already know. There is no second syntax, no sub-DSL, no “when you need more power, drop into scripting mode.”

In a rule’s action, choose Set Variable and provide:

  • Name – A descriptive identifier (like entryPrice, stopLevel, or tradeCount)
  • Value – Any expression (like price, price - ATR(14) * 1.5, or vars.tradeCount + 1)
  • ScopeLocal (unique to each instrument) or Global (shared across all instruments)

Reference your variable anywhere in an expression using vars.:

Expression Meaning
vars.entryPrice The stored entry price
vars.stopLevel The stored stop-loss level
vars.tradeCount A counter you are maintaining
price - vars.entryPrice How far price has moved from entry
  • Local variables are specific to each instrument. If your strategy trades AAPL and MSFT, each has its own vars.entryPrice.
  • Global variables are shared across all instruments in the strategy. Use these for portfolio-level logic, like tracking the total number of open positions or a shared risk budget.

See Multi-Instrument Strategies for detailed examples of local and global variable usage.

Here are some practical expression patterns:

account.equity * 0.02 / ATR(14)

Allocates 2% of equity per trade, scaled by the 14-period ATR. When volatility is high, you trade fewer shares; when volatility is low, you trade more.

vars.entryPrice - (1.5 * ATR(14))

Sets the stop loss at 1.5 times ATR below the entry price. Store this in a variable so your exit rule can reference it.

(high - low) / open * 100 > 2

Only triggers when the current bar’s range exceeds 2% of the open – useful for filtering out low-volatility periods.

account.total_drawdown_pct < 0.15

Disables new entries when the account drawdown exceeds 15%.

In one rule with condition volume > volume[1], action: Set Variable risingVolumeBars = vars.risingVolumeBars + 1. In a second rule with condition volume <= volume[1], action: Set Variable risingVolumeBars = 0. A third rule gates on vars.risingVolumeBars >= 3 to fire on the third consecutive rising-volume bar. This pattern — increment-on-match, reset-on-miss, consume-on-threshold — handles any “N in a row” requirement.

Exit rule action: Set Variable lastExitBar = bar_index. Entry rule condition: bar_index - vars.lastExitBar > 10. Prevents re-entering for 10 bars after any exit, without needing a dedicated “cooldown” feature.

On every bar, a rule with no conditions (or a simple position.open_qty > 0 gate) sets vars.highWaterMark = max(vars.highWaterMark, high). An exit rule then uses price < vars.highWaterMark - 2 * ATR(14) as its condition. The stop trails the highest price reached since entry — anchored state that no pure expression can express.

On entry: Set Variable (global) openPositions = globals.openPositions + 1. On exit: globals.openPositions - 1. Entry rule gates on globals.openPositions < 5. Caps total open positions across all instruments in the strategy.

(RSI(14) < 30) + (close > SMA(200)) + (MACD().histogram > 0) >= 2

Fires when at least 2 of 3 independent signals agree — trend, momentum, and mean-reversion all voting together. See Counting signals for more patterns.

volume > SMA(20, "volume") * 2

True when the current bar’s volume is more than double the 20-period average – a common filter for confirming breakouts.

  • Use parentheses generously. Even when the order of operations is technically correct, parentheses make complex expressions much easier to read and verify.
  • Break long expressions into variables. Instead of one giant formula, use Set Variable actions to compute intermediate values, then reference them in your conditions.
  • Watch for division by zero. If a denominator could ever be zero (like an indicator value), add a guard condition to your rule.
  • Test with historical data. After writing an expression, backtest to make sure it evaluates the way you expect across different market conditions.
  • Use round() for position sizing. When calculating share quantities, wrap the result in round() to get whole numbers: round(account.equity * 0.02 / price).