Edgoro
Strategy

Pine Script EMA crossover strategy: a complete v6 example with stops and costs

Build an EMA crossover strategy in Pine Script v6 with a trend filter, ATR stop and target, commission and slippage, then test it without fooling yourself.

The moving-average crossover is the strategy almost everyone codes first. The logic fits in one sentence — buy when the fast EMA crosses above the slow EMA — and Pine Script can express it in five lines. The hard part isn't writing it. It's making the backtest mean something: stops that don't drift, realistic costs, sensible fills and parameters you didn't tune to the past.

This guide builds a complete Pine Script v6 strategy you can paste into TradingView, explains each block, and lists what to check before you believe the report. It's an example to test and learn from, not a recommendation to trade.

Indicator or strategy?

Pine Script has two script types for this job:

  • An indicator() marks crosses on the chart and sends alerts. You decide whether to trade. If that's all you need, use the EMA crossover indicator.
  • A strategy() simulates orders on historical bars and produces a Strategy Tester report: trades, net profit, drawdown. That's what we build here.

A strategy doesn't place real orders on its own. It's a research tool.

The complete script

//@version=6
strategy("EMA crossover strategy (example)", overlay = true, initial_capital = 10000,
  default_qty_type = strategy.percent_of_equity, default_qty_value = 10,
  commission_type = strategy.commission.percent, commission_value = 0.05, slippage = 2)

// Inputs
fastLen    = input.int(20, "Fast EMA", minval = 1)
slowLen    = input.int(50, "Slow EMA", minval = 2)
useTrend   = input.bool(true, "Trade only with the trend EMA")
trendLen   = input.int(200, "Trend EMA", minval = 2)
allowShort = input.bool(true, "Allow short trades")
atrLen     = input.int(14, "ATR length", minval = 1)
atrMult    = input.float(2.0, "Stop distance (x ATR)", minval = 0.5, step = 0.25)
rr         = input.float(2.0, "Target (x stop distance)", minval = 0.5, step = 0.25)

// Indicators and signals, calculated on every bar
fast  = ta.ema(close, fastLen)
slow  = ta.ema(close, slowLen)
trend = ta.ema(close, trendLen)
atr   = ta.atr(atrLen)

crossUp   = ta.crossover(fast, slow)
crossDown = ta.crossunder(fast, slow)
longOk    = not useTrend or close > trend
shortOk   = allowShort and (not useTrend or close < trend)

// Levels are fixed when the signal fires, so they don't drift with ATR
var float longStop    = na
var float longTarget  = na
var float shortStop   = na
var float shortTarget = na

if strategy.position_size == 0
    if crossUp and longOk
        longStop   := close - atrMult * atr
        longTarget := close + rr * atrMult * atr
        strategy.entry("Long", strategy.long)
    else if crossDown and shortOk
        shortStop   := close + atrMult * atr
        shortTarget := close - rr * atrMult * atr
        strategy.entry("Short", strategy.short)

strategy.exit("Long exit", "Long", stop = longStop, limit = longTarget)
strategy.exit("Short exit", "Short", stop = shortStop, limit = shortTarget)

plot(fast, "Fast EMA", color = color.new(color.teal, 0), linewidth = 2)
plot(slow, "Slow EMA", color = color.new(color.orange, 0), linewidth = 2)
plot(useTrend ? trend : na, "Trend EMA", color = color.new(color.gray, 0))

New to adding scripts? How to add a Pine Script indicator to TradingView covers the editor; choose a new Strategy instead of an indicator.

How the script works, block by block

The declaration. initial_capital, default_qty_type and default_qty_value put 10% of equity into each trade. commission_value = 0.05 charges 0.05% per order and slippage = 2 worsens every market and stop fill by two ticks. Set both to match your broker; leaving them at zero is one of the backtesting mistakes that inflate results.

Signals on every bar. ta.crossover(), ta.ema() and ta.atr() are calculated at the top level, outside any if. Functions that keep history should run on every bar. In v6 this matters even more: and and or are lazy, so a ta.* call placed on the right-hand side of an and may be skipped on some bars and return inconsistent values.

One position at a time. Entries are only considered when strategy.position_size == 0. That keeps the example readable: no pyramiding, no reversals.

Levels fixed at entry. The stop and target are stored in var variables when the signal fires. Without var, they'd be recalculated from the current ATR on every bar and your stop would wander. Long and short levels use separate variables so an exit order for one side can never pick up the other side's prices.

Exits on every bar. strategy.exit() is called unconditionally. While a position is open it keeps the stop and limit orders in place; while flat it does nothing. For more on exits, including break-even and trailing stops, read strategy.exit stop loss and take profit.

When the orders fill

By default, a strategy evaluates its logic when a bar closes and the resulting market order fills at the next bar's open. That's realistic: you can't trade at a close you only know once the bar is finished. A few settings change this behaviour, and each has a cost:

SettingWhat it doesWatch out for
process_orders_on_close = trueFills market orders at the signal bar's closeOptimistic unless you really can trade the close
calc_on_every_tick = trueRecalculates on every live tickLive behaviour differs from the backtest, which only sees closed bars
calc_on_order_fills = trueRuns the script again after each fillExtra intrabar logic that history can only approximate
Bar Magnifier (where your plan offers it)Uses lower-timeframe data to resolve fills inside a barSlower tests, more realistic results

For a first test, keep the defaults.

Choosing lengths without overfitting

The fastest way to fool yourself with this strategy is to search for the "best" pair of lengths. With enough combinations, one of them will have fitted the past beautifully.

A healthier routine:

  1. Start with round, common pairs: 9 / 21, 20 / 50, 50 / 200.
  2. Look at neighbours. If 20 / 50 looks fine, 18 / 45 and 22 / 55 should look broadly similar. A lone spike is noise.
  3. Split the history. Pick settings on the older part, then run them untouched on the most recent part.
  4. Try other symbols in the same asset class. A rule that only works on one chart is fragile.

Reading the Strategy Tester report

Don't stop at net profit. Check these first:

  • Number of trades. A few dozen trades can't tell you much. Crossovers on daily charts trade rarely, so you may need many years or several symbols.
  • Maximum drawdown. Could you sit through it with real money? The drawdown calculator shows what it means for your balance.
  • Average trade versus costs. If the average trade is barely larger than commission plus slippage, small changes in costs will flip the result.
  • Longs versus shorts. Many markets behave differently in each direction. The allowShort input lets you test them separately.
  • Results over time. Use the list of trades to see whether the profit came from one period or several. Crossovers do well in long trends and give back in ranges.

Then plug your after-cost average win, average loss and win rate into the expectancy calculator.

Variations worth testing

  • Exit on the opposite cross instead of a fixed target: call strategy.close("Long") when crossDown is true. Trades last longer and the win rate usually changes a lot.
  • A trend-strength filter: only enter when ADX is above 20 or 25, to skip some of the sideways chop.
  • A higher-timeframe filter: require the daily close to be above the daily 200 EMA, without repainting — see multi-timeframe Pine Script with request.security().
  • Risk-based sizing: risk a fixed percentage of equity per trade instead of a fixed share of equity, as in position sizing and the 1% rule.

Change one thing at a time and keep notes, so you know which change did what.

Sources and further reading

Try it with Edgoro

Describe your version — "20/50 EMA crossover on the 4-hour chart, longs only above the daily 200 EMA, stop at 2 × ATR, exit on the opposite cross, 0.05% commission" — and Edgoro writes the Pine Script v6 strategy for you, or the same logic as an MT5 Expert Advisor. Build yours with Edgoro.

This strategy is an example for testing and education, not a recommendation. Backtested results are hypothetical, and trading involves substantial risk of loss.

Build it with Edgoro

Describe your strategy in plain English and get a TradingView indicator in Pine Script v6 or a MetaTrader 5 Expert Advisor in minutes. Every first purchase is covered by a 7-day money-back guarantee.

Build your indicator with Edgoro