Examples
This page shows complete example strategies and short cookbook recipes. The
examples are written exactly as you'd write them in the app (no imports and no
comments) because the toolkit names (ctx, Signal, np, pd, math) are
already provided for you.
See Strategy authoring for the sandbox rules and SDK reference for the full API surface.
EMA crossover
A long-only momentum strategy that enters when the fast EMA (12) crosses above the slow EMA (26) and exits on the reverse cross. Stop-loss is set 3 % below entry; take-profit is set 6 % above entry.
FAST = 12
SLOW = 26
# The engine withholds evaluate() until this much history exists, so
# no warm-up guard is needed.
HISTORY = {"primary": 53}
def evaluate(ctx):
close = ctx.candles.close
fast = ctx.indicators.ema(close, FAST)
slow = ctx.indicators.ema(close, SLOW)
curr_fast = float(fast.iloc[-1])
curr_slow = float(slow.iloc[-1])
prev_fast = float(fast.iloc[-2])
prev_slow = float(slow.iloc[-2])
price = float(close.iloc[-1])
position = ctx.position
if prev_fast <= prev_slow and curr_fast > curr_slow and position is None:
return Signal(
direction="LONG",
stop_loss=price * 0.97,
take_profit=price * 1.06,
setup_id="EMA_CROSS_LONG",
)
if prev_fast >= prev_slow and curr_fast < curr_slow and position is not None:
return Signal(direction="FLAT", setup_id="EMA_CROSS_EXIT")
return None
EMA crossover with parameters
The same crossover logic, but with the fast and slow EMA lengths declared as
forward-walkable parameters. A module-level PARAMS dict gives each one a
default and a sweep of candidate values, and evaluate reads them via
ctx.params[...]. A plain backtest uses the defaults (12 / 26); forward-walk
optimizes over the declared sweeps. See
Parameters & forward-walk
for the full contract.
PARAMS = {
"fast": {"default": 12, "sweep": [8, 12, 16]},
"slow": {"default": 26, "sweep": [21, 26, 34]},
}
# Cover two periods of the largest swept EMA (34), plus the previous bar
# read by the crossover.
HISTORY = {"primary": 69}
def evaluate(ctx):
fast_n = ctx.params["fast"]
slow_n = ctx.params["slow"]
close = ctx.candles.close
fast = ctx.indicators.ema(close, fast_n)
slow = ctx.indicators.ema(close, slow_n)
curr_fast = float(fast.iloc[-1])
curr_slow = float(slow.iloc[-1])
prev_fast = float(fast.iloc[-2])
prev_slow = float(slow.iloc[-2])
price = float(close.iloc[-1])
position = ctx.position
if prev_fast <= prev_slow and curr_fast > curr_slow and position is None:
return Signal(
direction="LONG",
stop_loss=price * 0.97,
take_profit=price * 1.06,
setup_id="EMA_CROSS_LONG",
)
if prev_fast >= prev_slow and curr_fast < curr_slow and position is not None:
return Signal(direction="FLAT", setup_id="EMA_CROSS_EXIT")
return None
RSI mean reversion
Goes long when RSI(14) dips below 30 and short when it pushes above 70; exits when RSI returns to the neutral 45-55 band. Stop-loss and take-profit are expressed as multiples of ATR so the strategy adapts to current volatility.
PERIOD = 14
HISTORY = {"primary": 100} # several Wilder periods for RSI and ATR convergence
LOWER = 30.0
UPPER = 70.0
EXIT_LOW = 45.0
EXIT_HIGH = 55.0
def evaluate(ctx):
close = ctx.candles.close
high = ctx.candles.high
low = ctx.candles.low
rsi = float(ctx.indicators.rsi(close, PERIOD).iloc[-1])
atr = float(ctx.indicators.atr(high, low, close, PERIOD).iloc[-1])
price = float(close.iloc[-1])
position = ctx.position
if position is None:
if rsi < LOWER:
return Signal(
direction="LONG",
stop_loss=price - 2 * atr,
take_profit=price + 3 * atr,
setup_id="RSI_LONG",
)
if rsi > UPPER:
return Signal(
direction="SHORT",
stop_loss=price + 2 * atr,
take_profit=price - 3 * atr,
setup_id="RSI_SHORT",
)
return None
if EXIT_LOW <= rsi <= EXIT_HIGH:
return Signal(direction="FLAT", setup_id="RSI_EXIT")
return None
Cookbook recipes
The snippets below are illustrative fragments, not complete strategies. Paste
the relevant block into your evaluate function alongside your entry logic.
All identifiers (ctx, Signal, np, pd, math) are pre-injected by the
sandbox; do not add import statements in user-submitted code. When a recipe
reads more history than your strategy already declares, increase HISTORY to
cover it.
Higher-timeframe trend filter
Declare the higher timeframe at module level, then use ctx.get_htf() inside
evaluate. The engine raises KeyError if you call get_htf for a timeframe
that was not declared.
HISTORY = {"primary": 1, "4h": 20}
additional_timeframes = ("4h",)
def evaluate(ctx):
htf = ctx.get_htf("4h")
htf_sma = htf.indicators.sma(htf.candles.close, 20)
if htf.candles.close.iloc[-1] < htf_sma.iloc[-1]:
return None
...
Cooldown after a stop-out
Detect the in-trade → flat transition via ctx.state and block re-entry for
M bars. The engine provides no explicit "stopped out" event, so you track the
previous bar's position yourself.
M = 5
def evaluate(ctx):
prev = ctx.state.get("prev_position")
if prev is not None and ctx.position is None:
ctx.state["cooldown_until"] = ctx.bar_index + M
ctx.state["prev_position"] = ctx.position
if ctx.position is None and ctx.bar_index < ctx.state.get("cooldown_until", 0):
return None
...
Risk-based stop sizing
Use ctx.portfolio.equity to reason about stop width relative to account size.
The engine executes the stop_loss price you pass in the Signal; this
pattern just helps you choose where to place it.
def evaluate(ctx):
price = float(ctx.candles.close.iloc[-1])
atr = float(ctx.indicators.atr(
ctx.candles.high, ctx.candles.low, ctx.candles.close, 14
).iloc[-1])
sl = price - 2 * atr
stop_dist = price - sl
reference_qty = (ctx.portfolio.equity * 0.01) / stop_dist
if ctx.position is None:
return Signal(
direction="LONG",
stop_loss=sl,
take_profit=price + 3 * atr,
setup_id="ATR_RISK_LONG",
)
return None
For the full list of ctx fields, indicator signatures, Signal parameters,
and sandbox constraints see SDK reference and
Strategy authoring.