Pine Script moving average crossover: code it, then see where it lags
The moving average crossover is often the first idea people code. It's short, it's clear, and it teaches a lot. It also shows, very plainly, why a signal that looks neat on a chart needs testing.
The idea in one line
Draw 2 Moving average lines: a fast one over a few candles, and a slow one over more. When the fast line crosses above the slow line, recent prices are rising faster than older ones. When it crosses below, the opposite.
That's it. It's a simple way to describe direction. It doesn't predict anything. More on the averages themselves in moving averages explained.
New to Pine Script? Start with Pine Script for beginners.
The script
This indicator draws 2 EMA lines (averages that lean toward the newest candles), marks each cross, and skips crosses that go against a longer 200-candle average:
//@version=6
indicator("MA crossover", overlay=true)
fastLen = input.int(20, "Fast length", minval=1)
slowLen = input.int(50, "Slow length", minval=2)
useFilter = input.bool(true, "Only with the 200 average")
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
trendLine = ta.ema(close, 200)
crossUp = ta.crossover(fast, slow)
crossDown = ta.crossunder(fast, slow)
upOK = crossUp and (not useFilter or close > trendLine)
downOK = crossDown and (not useFilter or close < trendLine)
plot(fast, "Fast", color=color.orange)
plot(slow, "Slow", color=color.blue)
plot(useFilter ? trendLine : na, "200 average", color=color.gray)
plotshape(upOK and barstate.isconfirmed, "Cross up", style=shape.triangleup, location=location.belowbar, color=color.green, size=size.small)
plotshape(downOK and barstate.isconfirmed, "Cross down", style=shape.triangledown, location=location.abovebar, color=color.red, size=size.small)
alertcondition(upOK, "Cross up", "{{ticker}}: fast average crossed above slow")
alertcondition(downOK, "Cross down", "{{ticker}}: fast average crossed below slow")
Every line, in plain words
- Line 1 says this is Pine Script version 6.
- Line 2 names the script and puts it on the price chart.
- Lines 3 and 4 make both lengths settings. See Pine Script inputs.
- Line 5 is a tick box for the 200-candle filter.
- Lines 6 to 8 work out the fast, slow and 200-candle averages.
- Line 9 is true on the candle where fast crosses above slow.
- Line 10 is true where fast crosses below slow.
- Line 11 keeps an up-cross only if the filter is off, or the close is above the 200 average. The brackets matter: they group the "or" part.
- Line 12 does the same for down-crosses, below the 200 average.
- Lines 13 to 15 draw the 3 lines. The 200 line hides when the filter is off.
- Lines 16 and 17 draw a triangle on each kept cross. barstate.isconfirmed means only on closed candles. See Pine Script plotshape.
- Lines 18 and 19 add 2 choices to TradingView's alert menu. {{ticker}} becomes the symbol's name. See Pine Script alerts.
Why the filter?
A cross against the bigger direction is a cross into the wind. The filter is a simple way to follow one of our rules: check the bigger picture first, and trade with it.
A 200-candle average is a blunt tool for that. A more direct read is higher highs and higher lows. Or check a bigger timeframe with Pine Script higher-timeframe trend.
Where crossovers struggle
Every honest page about crossovers needs this part.
1. They're late
An average is built from past candles. So a cross happens after the move has already started. By the time 20 crosses 50, price may have run a long way. Exits come late too.
2. They whipsaw in sideways markets
When price drifts sideways, the 2 lines tangle. They cross up, then down, then up again. Each cross looks like a fresh start. Most of them go nowhere.
Scroll back on almost any chart and you'll find stretches like this. Count the marks there.
3. The settings tempt you
10 and 30? 20 and 50? 50 and 200? On any one chart, some pair will look great in hindsight. That's overfitting: settings tuned to old candles that won't repeat.
| Problem | Why it happens | What helps |
|---|---|---|
| Late entries | Averages use past prices | Accept it, or use shorter lengths (and get more false starts) |
| Whipsaws | Sideways price tangles the lines | A trend filter, or skip flat markets |
| Overfitting | Too many settings tried | Pick lengths for a reason, test on other charts |
Does it repaint?
The averages don't change once a candle closes. But on a live candle, the close keeps moving. The lines move with it, and a cross can appear, then vanish.
That's why lines 16 and 17 use barstate.isconfirmed. For the alerts, pick Once per bar close when you create them. Then live and history agree.
That matches a rule we use for every level: only a candle body closing past it counts.
A cross isn't a plan
A crossover tells you when something changed. It doesn't tell you:
- Where the stop goes. Use the chart: the last place price turned around. See how to set a stop loss.
- How big to trade. Size from the stop. See Pine Script position size.
- When to stop for the day. A daily loss limit, like 2 losses and done.
Test it before you trust it. Turn it into a strategy and read the results honestly, with costs included. Pine Script strategy backtest shows how, and why those results are hypothetical.
Try it today
- Paste the script into the Pine Editor on a computer. Save and add it to a daily chart.
- Find one trending stretch and one sideways stretch.
- Count the marks in each. Note how far price moved before each mark appeared.
- Untick the 200 filter. How many extra marks appear? Were they better or worse?
- Then step through a few months with bar replay, so the future stays hidden.
Change one thing at a time. Swap EMA for SMA, or 20 for 10, but not both. Otherwise you won't know which change mattered. See EMA vs SMA.
Learning from a real script
A crossover is a fine first script. Reading a bigger one teaches how levels, settings and alerts fit together. Our course includes the full source code of the StructureX chart tool to learn from and customize. It's source code, not a ready-made trading bot.
Common mistakes
- Treating every cross as a trade. Most crosses in sideways markets go nowhere.
- Marking the live candle. Use barstate.isconfirmed and Once per bar close.
- Hunting for the perfect pair of lengths. That's overfitting.
- No stop and no size. The script finds a moment, not a trade.
Questions
How do I code a moving average crossover in Pine Script?
Work out 2 averages with ta.ema() or ta.sma(), then use ta.crossover(fast, slow) for a cross up and ta.crossunder(fast, slow) for a cross down. Mark them with plotshape().
What is the best moving average crossover?
There isn't one best pair. 20/50 and 50/200 are common, but any pair looks best on some charts in hindsight. Pick lengths for a reason, then test them on many charts.
Why does my crossover signal disappear?
On a live candle the averages move with price, so a cross can undo itself before the close. Add barstate.isconfirmed to only mark closed candles.
Do moving average crossovers work?
They describe direction simply, but they're late and they whipsaw in sideways markets. Results vary by market and period, and losses are part of any rule.
Want the next step? We'll walk you through it.
Four free lessons by email, one every few days — each with a 10-minute exercise on a real chart.
Send Me the Free Lessons 4 short emails · No spam · Unsubscribe anytimeOr see the whole method, step by step:
See What’s Inside the Course Try everything free first. Decide when you're ready. · Open the free chart tool (works in the TradingView phone app)One chart idea a day, in plain English. Follow along: