Supported operators
Complete reference of the operators that declarative mode (entry_conditions / exit_conditions) accepts.
Quick table
| Operator | Meaning | Fires when |
|---|---|---|
crosses_above | Crosses upward | source[t-1] <= target[t-1] AND source[t] > target[t] |
crosses_below | Crosses downward | source[t-1] >= target[t-1] AND source[t] < target[t] |
crosses | Any cross | crosses_above or crosses_below |
> (or greater_than) | Strictly greater | source[t] > target[t] |
>= (or greater_or_equal) | Greater or equal | source[t] >= target[t] |
< (or less_than) | Strictly less | source[t] < target[t] |
<= (or less_or_equal) | Less or equal | source[t] <= target[t] |
== (or equals) | Equal (point-wise) | source[t] == target[t] |
source[t] is the last value of the series; source[t-1] is the value on the previous candle.
Condition shape
{
"name": "Readable name", # optional
"description": "Explanation", # optional -- tooltip in the UI
"source": "series_name", # key in series (required)
"operator": "crosses_above", # required
"target": "other_series", # key in series, OR
"value": 30, # constant numeric value
"action": "buy_to_open", # action fired on trigger
"enabled": True, # if false, the condition is ignored
"message": "Buy signal", # optional -- log when triggered
}Rule: provide either target (series name) or value (constant), not both. If both are provided, the engine prioritizes target.
Crossing operators (discrete)
crosses_above, crosses_below, and crosses fire only on the exact bar of the crossing. Subsequent bars do not fire again (even if the general condition source > target continues to be true).
Example: fast SMA crosses slow upward
{
"name": "Entry Long",
"source": "ma_fast", # series computed in _build_chart
"operator": "crosses_above",
"target": "ma_slow", # another series computed in _build_chart
"action": "buy_to_open",
"enabled": True,
}Fires once on the candle where ma_fast[-1] > ma_slow[-1] and ma_fast[-2] <= ma_slow[-2].
Example: RSI crosses 30 upward (oversold recovering)
{
"name": "RSI Reversal Up",
"source": "rsi",
"operator": "crosses_above",
"value": 30, # constant, not a series
"action": "buy_to_open",
"enabled": True,
}When you use value, the engine compares the series against a fixed value. source[t-1] <= 30 and source[t] > 30.
Continuous comparison operators
>, <, ==, etc. fire on every bar where the condition is true. Useful for filters, but unsuitable for direct entries (they would fire on every bar in which the position is flat).
Correct usage: combined filter
It is not possible to combine multiple conditions within the same entry (it is one per entry). The engine evaluates exit_conditions on every candle; if the condition remains true and sdk.position != 0, the exit fires.
For regime filters, use imperative mode.
Example: close short if RSI goes below 30 (again)
Declarative mode has no "AND". To express "close short when RSI < 30", simply declare:
"exit_conditions": [
{
"name": "Short Cover by RSI",
"source": "rsi",
"operator": "<", # less_than
"value": 30,
"action": "buy_to_cover",
"enabled": True,
},
],Fires on every bar where rsi[-1] < 30, but only while you hold a position: the engine scans exit_conditions whenever sdk.position != 0 and entry_conditions only when flat. Note it does not check whether a condition's action matches the side of the open position -- every condition in the selected list is evaluated, so a buy_to_cover condition in exit_conditions will also fire while long. Coherence between action and position is left to you (and the order layer).
Actions accepted in conditions
Full list (details in canonical actions):
buy_to_open-- opens longsell_short_to_open(orsell_short) -- opens shortsell_to_close-- closes longbuy_to_cover-- closes shortclose_position-- closes any open positionreverse_position-- closes and reverses
Caveat: coherence between condition and action
The engine does not validate whether the action makes sense with the current position. Consider the following case:
# Anti-pattern: sell_to_close in entry_conditions
{
"name": "Sell",
"operator": "crosses_above",
"action": "sell_to_close",
...
}The condition fires, but if there is no open long position, the engine ignores it. sell_to_close in entry_conditions does nothing.
Convention:
entry_conditions->buy_to_openorsell_short_to_open.exit_conditions->sell_to_closeorbuy_to_cover(orclose_positionas a generic).
Complete example: declarative RSI mean reversion
DECLARATION = {
"type": "strategy",
"inputs": [
{"name": "period", "type": "int", "default": 14, "min": 2, "max": 100},
{"name": "oversold", "type": "float", "default": 30, "min": 0, "max": 50},
{"name": "overbought", "type": "float", "default": 70, "min": 50, "max": 100},
],
"plots": [
{"name": "rsi", "source": "rsi", "type": "line", "color": "#A78BFA"},
],
"pane": "new",
"entry_conditions": [
{
"name": "Buy on oversold",
"source": "rsi",
"operator": "crosses_above",
"value": 30,
"action": "buy_to_open",
"enabled": True,
},
{
"name": "Sell on overbought",
"source": "rsi",
"operator": "crosses_below",
"value": 70,
"action": "sell_short_to_open",
"enabled": True,
},
],
"exit_conditions": [
{
"name": "Long close at midline",
"source": "rsi",
"operator": "crosses_above",
"value": 50,
"action": "sell_to_close",
"enabled": True,
},
{
"name": "Short cover at midline",
"source": "rsi",
"operator": "crosses_below",
"value": 50,
"action": "buy_to_cover",
"enabled": True,
},
],
}
def _rsi_series(closes, period):
# Wilder RSI aligned to `closes` (None during warm-up). Self-contained here;
# in a live backtest prefer an incremental RSI kept in sdk.state so you do
# not rescan the full history every bar.
if len(closes) <= period:
return [None] * len(closes)
out = [None] * period
avg_gain = sum(max(closes[i] - closes[i - 1], 0.0) for i in range(1, period + 1)) / period
avg_loss = sum(max(closes[i - 1] - closes[i], 0.0) for i in range(1, period + 1)) / period
out.append(100.0 - 100.0 / (1.0 + (avg_gain / avg_loss if avg_loss else float("inf"))))
for i in range(period + 1, len(closes)):
change = closes[i] - closes[i - 1]
avg_gain = (avg_gain * (period - 1) + max(change, 0.0)) / period
avg_loss = (avg_loss * (period - 1) + max(-change, 0.0)) / period
out.append(100.0 - 100.0 / (1.0 + (avg_gain / avg_loss if avg_loss else float("inf"))))
return out
def _series(closes, params):
period = int((params or {}).get("period", 14))
return {"rsi": _rsi_series(closes, period)}
def _build_chart(df, params):
return {**DECLARATION, "series": _series(list(df["close"]), params)}
def main(df=None, sdk=None, params={}):
params = params or {}
if df is not None:
return _build_chart(df, params) # chart/study: full rsi series
if sdk is not None:
# Declarative fallback path: return the same rsi series so the evaluator
# has something to cross. Requires params["runtime_declarative_fallback"]
# = True. WARNING: recomputing rsi over the full sdk.candles every bar is
# O(n^2) over the backtest — for production keep Wilder avg_gain/avg_loss
# incrementally in sdk.state instead of rescanning the whole history.
return {**DECLARATION, "series": _series([c["close"] for c in sdk.candles], params)}
return DECLARATIONNote the use of a constant value ("value": 30) instead of a series target. There is no on_bar_strategy, but the engine still needs two things you must supply: an accepted entrypoint (main, above — a lone DECLARATION is rejected at init with the Strict Mode ProtocolError) and the rsi series the conditions cross (computed in _build_chart / the runtime branch). And the conditions only fire when you opt in with params["runtime_declarative_fallback"] = True; otherwise the strategy plots but produces 0 trades.
Known limitations
- No composite logic (AND/OR/NOT). One condition = one comparison.
- No condition histogram. It is not possible to express "fire if the condition is true in 3 of the last 5 bars".
- No cooldown. If two conditions fire on two consecutive bars, the engine tries to execute both (the second is usually rejected by
Max Positions = 1). - No filters. It is not possible to express "only fire during business hours" (except through the global
trading_hoursof the DECLARATION).
For these cases, use imperative mode.
Next steps
- When to use entry/exit conditions -- side-by-side comparison with imperative mode.
- DECLARATION shape -- full format of conditions.
- Canonical actions -- details of the fired actions.