EMA and ATR strategy
A risk-sized EMA crossover strategy with an RSI filter, ATR-based stop and target, and a trailing stop.
This strategy follows the trend in both directions. A crossover of a fast and a slow EMA proposes a trade, RSI confirms that momentum agrees, and the ATR at signal time sets the stop, the target and the trailing stop. Each trade risks a fixed share of equity, capped by the margin your leverage allows.
It is a good template for your own ideas: the signal block is a few lines, while sizing, brackets, plots and the HUD can stay as they are.
The script
script "EMA + ATR strategy"
input (
fastLen = input.int(21, title: "Fast EMA", min: 2, max: 200)
slowLen = input.int(55, title: "Slow EMA", min: 3, max: 400)
rsiLen = input.int(14, title: "RSI length", min: 2, max: 50)
rsiLong = input.float(52.0, title: "Min RSI for longs", min: 0.0, max: 100.0)
rsiShort = input.float(48.0, title: "Max RSI for shorts", min: 0.0, max: 100.0)
atrLen = input.int(14, title: "ATR length", min: 1, max: 100)
stopAtr = input.float(1.5, title: "Stop (ATR)", min: 0.2)
targetAtr = input.float(3.5, title: "Target (ATR)", min: 0.5)
trailStartAtr = input.float(1.5, title: "Trail activates after (ATR)", min: 0.1)
trailAtr = input.float(1.0, title: "Trail distance (ATR)", min: 0.1)
riskPct = input.float(1.0, title: "Risk per trade (% equity)", min: 0.1, max: 5.0)
lev = input.float(3.0, title: "Leverage", min: 1.0, max: 20.0)
allowLongs = input.bool(true, title: "Allow longs")
allowShorts = input.bool(true, title: "Allow shorts")
)
strategy(
initialCapital: 10000.0,
commissionType: strategy.commission.percent,
commissionValue: 0.04,
slippage: 1,
leverage: lev,
fundingCosts: true
)
data chart = subscribe(data.ohlcv)
pane osc = pane(title: "RSI", height: 0.2)
plot (
fastLine = plot.line(title: "Fast EMA", color: color.blue, width: 2)
slowLine = plot.line(title: "Slow EMA", color: color.amber, width: 2)
stopLine = plot.line(title: "Stop", color: color.red, style: linestyle.step)
targetLine = plot.line(title: "Target", color: color.green, style: linestyle.step)
rsiLine = plot.line(title: "RSI", color: color.purple, on: osc)
rsiMid = plot.line(title: "50", color: color.gray, style: linestyle.dashed, on: osc)
)
state (
pendingDir = 0.0
pendingAtr = 0.0
activeAtr: float? = null
hud = entities.labelPool(max: 1, anchor: anchor.topRight)
)
on chart.close {
// 1. Indicators: always evaluated, every bar.
let fast = ta.ema(chart.close, fastLen)
let slow = ta.ema(chart.close, slowLen)
let rsi = ta.rsi(chart.close, rsiLen)
let atr = chart.atr(atrLen)
let crossUp = ta.crossover(fast, slow)
let crossDown = ta.crossunder(fast, slow)
fastLine.plot(fast)
slowLine.plot(slow)
rsiLine.plot(rsi)
rsiMid.plot(50.0)
// 2. Promote the signal ATR once the matching position exists.
let pos = strategy.positionSize
if pendingDir != 0.0 && pos * pendingDir > 0.0 {
activeAtr = pendingAtr
pendingDir = 0.0
}
if pos == 0.0 && pendingDir == 0.0 {
activeAtr = null
}
// 3. Signals.
let r = rsi ?? 50.0
let longSignal = allowLongs && crossUp && r >= rsiLong
let shortSignal = allowShorts && crossDown && r <= rsiShort
let price = chart.close
if atr != null && price != null && atr > 0.0 {
// 4. Size: risk budget, capped by 90 % of the margin leverage allows.
let dist = atr * stopAtr
let riskQty = strategy.equity * riskPct / 100.0 / dist
let capQty = strategy.equity * 0.9 * lev / price
let size = math.min(riskQty, capQty)
// 5. Bracket distances in ticks, fixed at signal time.
let lossTicks = math.round(dist / context.tickSize)
let profitTicks = math.round(atr * targetAtr / context.tickSize)
let trailOn = math.round(atr * trailStartAtr / context.tickSize)
let trailOff = math.round(atr * trailAtr / context.tickSize)
if longSignal && pos <= 0.0 {
strategy.entry("Long", strategy.long, qty: size, comment: str.format("RSI {0:.0}", r))
strategy.exit("Long exit", "Long", loss: lossTicks, profit: profitTicks,
trailPoints: trailOn, trailOffset: trailOff)
pendingDir = 1.0
pendingAtr = atr
} else if shortSignal && pos >= 0.0 {
strategy.entry("Short", strategy.short, qty: size, comment: str.format("RSI {0:.0}", r))
strategy.exit("Short exit", "Short", loss: lossTicks, profit: profitTicks,
trailPoints: trailOn, trailOffset: trailOff)
pendingDir = -1.0
pendingAtr = atr
}
}
// 6. Signal exits when no reversal was placed.
if pos > 0.0 && crossDown && !shortSignal {
strategy.close("Long", comment: "Cross down")
}
if pos < 0.0 && crossUp && !longSignal {
strategy.close("Short", comment: "Cross up")
}
// 7. Stop and target rails for the open position.
let avg = strategy.positionAvgPrice
let eAtr = activeAtr
if avg != null && eAtr != null && pos != 0.0 {
let sign = pos > 0.0 ? 1.0 : -1.0
stopLine.plot(avg - sign * eAtr * stopAtr)
targetLine.plot(avg + sign * eAtr * targetAtr)
}
// 8. HUD.
let n = strategy.closedTrades
let winRate = n > 0 ? float(strategy.winTrades) / float(n) * 100.0 : 0.0
let side = pos > 0.0 ? "LONG" : pos < 0.0 ? "SHORT" : "FLAT"
let mode = chart.isRealtime ? "PAPER (live)" : "BACKTEST"
let text = str.format("{0} {1}\nEquity {2:,.2}\nOpen P&L {3:,.2}\nTrades {4} Win {5:.1}%", mode, side, strategy.equity, strategy.openProfit, n, winRate)
hud.get("status").set(12.0, 12.0, text, font: font.mono)
}Open it on a liquid perpetual such as BTCUSDT on Binance, 1h. The Strategy Tester opens at the bottom with the backtest; after the last historical bar the strategy continues as paper trading.
How it works
Entry rules
A long needs three things on the same confirmed bar: the fast EMA crosses above the
slow EMA, RSI is at or above rsiLong, and longs are allowed. Shorts mirror this
with rsiShort. The defaults leave a small neutral zone between 48 and 52, so a
cross in a flat, directionless market is skipped.
All ta.* calls run at the top of the handler on every bar. Putting them behind an
if would freeze their state on bars where the condition is false.
strategy.entry reverses: a long signal while short closes the short and opens the
long in one fill at the next bar’s open. The pos <= 0.0 and pos >= 0.0 checks
prevent re-sending the same entry while already in that direction.
Position size
riskQty is the quantity at which a stop-out costs riskPct of equity. capQty is
the largest quantity that fits into 90 % of the margin available at the chosen
leverage. The smaller one wins and is passed as qty: in contracts (base units). The cap keeps
a very tight ATR from requesting an unaffordable position.
Stop, target and trailing stop
The bracket is defined in ticks, computed from the ATR at signal time:
| Order | Distance from the fill |
|---|---|
| Stop-loss | stopAtr × ATR |
| Take-profit | targetAtr × ATR |
| Trailing stop | Activates after trailStartAtr × ATR of profit, then trails trailAtr × ATR behind the best price |
All three belong to one bracket, so they are one-cancels-other: whichever fills first closes the trade and removes the others. With the defaults, a trade that moves 1.5 ATR in your favour can no longer lose more than about half an ATR, because the trail then sits 1 ATR behind the best price.
Signal exits
A cross against the position closes it at market even if neither stop nor target has been reached, unless the opposite signal already placed a reversal. Closing the entry cancels its bracket automatically.
Plots and HUD
The stop and target rails are drawn from strategy.positionAvgPrice and the ATR that
was current when the trade was signalled. Because the entry fills on the bar after
the signal, the script keeps that ATR as pending and only promotes it when the
position’s sign matches. This keeps the rails from jumping during a reversal. The HUD,
a label anchored to the top-right corner, shows whether the strategy is backtesting or
paper trading live, plus equity, open P&L, trade count and win rate.
Costs
The declaration charges 0.04 % per fill, one tick of slippage on market and stop fills, and real historical funding while a position is open. Check the Funding paid metric in the Strategy Tester: on higher timeframes with long holding times it can be a meaningful share of the result.
Variations
- Long only. Set Allow shorts off. On markets with a persistent uptrend this often improves the Long column and removes the drag of counter-trend shorts.
- No trailing stop. Remove
trailPoints:andtrailOffset:to compare a pure stop/target bracket against the trailing version. - Higher-timeframe filter. Subscribe to a 4h series, compute its EMA in
on h4.closeinto state, and only allow longs when the confirmed 4h close is above it. Never readh4.formingfor the decision. - Fill at the close. Add
processOrdersOnClose: trueto the declaration to fill at the signal bar’s close instead of the next open, and compare the results. - Breakeven move. Once open profit exceeds one ATR, call
strategy.exit("Long exit", "Long", stop: avg)again to move the stop to the entry price.