FlowscopeDocs

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

ema-atr-strategy.fs
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:

OrderDistance from the fill
Stop-lossstopAtr × ATR
Take-profittargetAtr × ATR
Trailing stopActivates 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: and trailOffset: 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.close into state, and only allow longs when the confirmed 4h close is above it. Never read h4.forming for the decision.
  • Fill at the close. Add processOrdersOnClose: true to 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.