# 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

```flowscope title="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**:

| 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:` 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.

## Related

- [Strategies & backtesting](/docs/scripting/strategies), [orders](/docs/scripting/strategies/orders), [risk](/docs/scripting/strategies/risk)
- [CVD breakout strategy](/docs/scripting/examples/cvd-breakout-strategy)
