Indicators
Indicators are computed once per bar and feed into your trading logic. The transpiler enforces three rules: declare them at the top of execute(), give every one a source, and only chain onto single-value outputs.
Declaration
All indicator constructions must appear at the top of execute(), before any other statement. The transpiler hoists them so each indicator receives every bar’s data — not just the bars where a conditional branch fires.
class Declarations(Strategy): fast_period = Param(7, min=2, max=50) slow_period = Param(21, min=10, max=200) trade = Symbol() sizing = FixedCapital()
def execute(self): # ✅ All indicators first fast = SMA(self.fast_period, on=self.trade) slow = SMA(self.slow_period, on=self.trade) rsi = RSI(14) # implicit source: trade symbol smooth = EMA(5, on=rsi) # chained — EMA of RSI high_sma = SMA(20, on=self.trade.high) # specific OHLCV field
# Logic starts AFTER all indicators self.stop_loss(2.0) if fast.crossed_above(slow) and smooth.value < 70 and high_sma.value > 0: self.buy(self.trade, reason="Fast crossed slow")Putting an indicator inside an if or for block emits RC2_IND_IN_BODY.
on= source
The on= keyword names the indicator’s input:
| Pattern | What it means |
|---|---|
SMA(20, on=self.trade) |
Close price of the trade symbol. |
SMA(20, on=self.spy) |
Close price of a data feed. |
SMA(20, on=self.trade.high) |
A specific OHLCV field. |
EMA(5, on=rsi) |
Chain — input is another indicator’s output. |
SMA(20) |
Omitted — defaults to the trade symbol’s close. |
Explicit on=self.trade is recommended even when optional — it makes intent obvious to a reader (and to a code review six months from now).
Built-in indicators
| Name | Values | Notes |
|---|---|---|
SMA |
Single decimal | Simple Moving Average. |
EMA |
Single decimal | Exponential Moving Average. |
WMA |
Single decimal | Weighted Moving Average. |
RSI |
Single decimal | Relative Strength Index. |
MACD |
.line, .signal, .histogram |
Trend momentum (EMA difference). |
BollingerBands |
.upper, .mid, .lower |
SMA ± stddev envelope. |
ATR |
Single decimal | Average True Range. |
CCI |
Single decimal | Commodity Channel Index. |
ROC |
Single decimal | Rate of Change (percentage momentum). |
Momentum |
Single decimal | Price momentum (close − close N bars ago). |
StdDev |
Single decimal | Rolling standard deviation. |
Aroon |
.up, .down, .oscillator |
Trend strength. |
See the Indicator Catalog for parameters, formulas, and warmup requirements.
Reading values
fast.value # current value (explicit)fast > slow # comparison reads .value implicitly on both sidesfast > 100 # comparison with a numberfast.value_at(1) # value from 1 bar agofast.value_at(5) # any past bar (within the history window)value_at(0) is equivalent to .value. Negative offsets are not supported.
Crossover detection
# Indicator vs indicatorfast.crossed_above(slow) # True for the single bar fast goes from ≤ slow to > slowfast.crossed_below(slow) # True for the single bar fast goes from ≥ slow to < slow
# Indicator vs numeric thresholdrsi.crossed_above(30)rsi.crossed_below(self.overbought)Both sides must be single-value decimals. Multi-value indicators (Aroon, MACD, BollingerBands) can’t use crossover helpers directly — extract a field first:
aroon = Aroon(14, on=self.trade)aroon.crossed_above(50) # ❌ RC2_CROSS_MULTIVALUEaroon.up.crossed_above(50) # ❌ also refused — field-level crossover is not implementedBoth forms are refused today. Express a multi-value crossover manually instead:
aroon = Aroon(14, on=self.trade)if aroon.up > 70 and aroon.down < 30: ...Chaining
rsi = RSI(14, on=self.trade)smooth_rsi = EMA(5, on=rsi) # chain a single-value indicatorhigh_ema = EMA(10, on=self.trade.high) # chain onto an OHLCV fieldChaining onto a multi-value indicator is rejected, in two different ways — and the first message asks for the second, which is also refused:
EMA(5, on=aroon)→RC2_IND_ON_MULTIVALUE, whose message tells you to name an output.EMA(5, on=aroon.up)→RC2_IND_ON_MULTIVALUE_CHAIN— naming the output is the right spelling, but the runtime support for it has not landed.
So there is no way to chain onto a multi-value indicator right now. Compute the field into a local and compare it directly, or reproduce the smoothing yourself in persistent state.
Warmup
Each indicator declares the number of bars it needs before producing valid output. The platform automatically prefills history so execute() only runs once every indicator is ready. You never see the warmup phase — your first execute() call already has fully-warmed indicators.
See Warmup → for the full lifecycle.
Common diagnostics
| Code | What it means |
|---|---|
RC2_IND_IN_BODY |
Indicator declared inside a conditional or loop. |
RC2_IND_ON |
The indicator needs on= because no trading symbol is declared. |
RC2_IND_ON_UNKNOWN |
on= refers to an unknown symbol slot or indicator. |
RC2_IND_ON_FIELD |
The OHLCV field doesn’t exist on the source. |
RC2_IND_ON_MULTIVALUE |
on= points at a multi-value indicator without naming an output. |
RC2_IND_ON_MULTIVALUE_CHAIN |
Chained onto a named output of a multi-value indicator. |
RC2_IND_PERIOD |
No first positional argument. (It fires even for the one indicator that has no parameters — see Volume.) |
RC2_IND_CYCLE |
An indicator references itself through on=. |
RC2_UNKNOWN_INDICATOR |
Unknown indicator name. |
RC2_CROSS_MULTIVALUE |
crossed_above / crossed_below on a multi-value indicator. |