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.
Arithmetic operators
Section titled “Arithmetic operators”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.
Comparison operators
Section titled “Comparison operators”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) |
Logical operators
Section titled “Logical operators”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)
Counting signals
Section titled “Counting signals”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.
“Any N of M” confirmation
Section titled ““Any N of M” confirmation”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)) >= 2Each 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 |
Weighted voting
Section titled “Weighted voting”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) >= 4Here 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.
Cleaner alternatives to OR chains
Section titled “Cleaner alternatives to OR chains”Instead of writing a long chain of || operators, counting is often more readable:
// Hard to scanRSI(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) >= 1Both mean the same thing, but the counted form makes it trivial to change “any one” to “any two” later — just change >= 1 to >= 2.
Math functions
Section titled “Math functions”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) |
Market data
Section titled “Market data”Current bar
Section titled “Current bar”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 |
Previous bars
Section titled “Previous bars”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.
Multi-timeframe data
Section titled “Multi-timeframe data”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.
Percentage changes
Section titled “Percentage changes”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.
Account data
Section titled “Account data”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.
Position data
Section titled “Position data”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.
Time and session data
Section titled “Time and session data”Time fields
Section titled “Time fields”| 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 |
Session fields
Section titled “Session fields”| 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
Order data
Section titled “Order data”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
Section titled “Custom variables”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).
Why variables exist
Section titled “Why variables exist”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 += 1in one rule andvars.pullbackCount == 3in 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.02once 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:
-
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.
-
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.”
-
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 onbar_index - vars.lastStopBarIndex > 10. -
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.
-
Anchored levels — “Trail the stop behind the highest high since entry.” Record
vars.highWaterMark = max(vars.highWaterMark, high)every bar, and compute the stop asvars.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
pullbackSignalorregimeIsTrendingdocuments 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.”
Setting a variable
Section titled “Setting a variable”In a rule’s action, choose Set Variable and provide:
- Name – A descriptive identifier (like
entryPrice,stopLevel, ortradeCount) - Value – Any expression (like
price,price - ATR(14) * 1.5, orvars.tradeCount + 1) - Scope – Local (unique to each instrument) or Global (shared across all instruments)
Reading a variable
Section titled “Reading a variable”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 vs. global variables
Section titled “Local vs. global variables”- 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.
Putting it all together
Section titled “Putting it all together”Here are some practical expression patterns:
ATR-based position sizing
Section titled “ATR-based position sizing”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.
Dynamic stop-loss
Section titled “Dynamic stop-loss”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.
Intraday range filter
Section titled “Intraday range filter”(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.
Equity curve protection
Section titled “Equity curve protection”account.total_drawdown_pct < 0.15
Disables new entries when the account drawdown exceeds 15%.
Consecutive-bar counter
Section titled “Consecutive-bar counter”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.
Re-entry cooldown
Section titled “Re-entry cooldown”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.
High-water-mark trailing stop
Section titled “High-water-mark trailing stop”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.
Portfolio position cap
Section titled “Portfolio position cap”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.
Multi-signal confirmation
Section titled “Multi-signal confirmation”(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 spike detection
Section titled “Volume spike detection”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.
Best practices
Section titled “Best practices”- 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).
