Strategy: EMA Crossover System
The classic trend system, built end-to-end - signals, arrows, delays and a backtest you can run yourself.
- ·Fast/slow EMA logic
- ·Clean long & short signals
- ·Plotting the system
- ·Backtest setup
- ·Reading its report
- ·Tuning the periods
Every trader's first complete system is some version of this one: two moving averages, one fast and one slow, that take turns crossing each other. When the fast line climbs above the slow line, the trend has likely turned up, so you go long; when it drops back below, you step aside. It is the "hello world" of trading systems - and despite its age, a clean trend-follower like this still forms the backbone of countless professional strategies.
The moving-average crossover is one of the oldest mechanical systems on record - Richard Donchian was trading versions of it in the 1960s. Its unambiguous rules make it the perfect first AFL system.
In this chapter we build it properly, from a bare idea to a system you can paste into AmiBroker and backtest today. Every technique you learned in the system-building module shows up here in one place: clean signals, a one-bar delay so nothing repaints, arrows on the chart, position sizing and trade delays. By the end you will have a single copy-pasteable formula and know exactly how to test and tune it.
The idea in one breath
Two exponential moving averages. Buy when the fast EMA crosses above the slow EMA; Sell when it crosses back below. Optionally flip to the short side at the same moments. That is the whole strategy - everything else is plumbing that turns the idea into a system AmiBroker can trade without tricking us.
We use Cross(a, b), which is true only on the single bar where a rises through b. That keeps each signal to one clean bar instead of staying true for the whole stretch the fast line spends above the slow.
The complete system
Here is the entire strategy. Read the comments top to bottom - it follows the exact lifecycle from inputs, to averages, to signals, to fills.
_SECTION_BEGIN("EMA Crossover System");
// === Chart cosmetics ===
SetChartOptions(0, chartShowArrows | chartShowDates);
_N(Title = StrFormat("{{NAME}} - {{INTERVAL}} {{DATE}} O %g H %g L %g C %g (%.1f%%)",
O, H, L, C, SelectedValue(ROC(C, 1))));
Plot(Close, "Close", colorDefault, styleCandle | styleNoTitle | GetPriceStyle());
// === Inputs (swap Param for Optimize from Chapter 26 to tune) ===
FastLen = Param("Fast EMA", 10, 2, 50, 1);
SlowLen = Param("Slow EMA", 20, 5, 100, 1);
tradeShort = ParamToggle("Trade Short Side?", "No|Yes", 1);
// === The two moving averages ===
Fast = EMA(Close, FastLen);
Slow = EMA(Close, SlowLen);
Plot(Fast, "Fast EMA", colorBlue, styleLine | styleThick);
Plot(Slow, "Slow EMA", colorRed, styleLine | styleThick);
// === Raw signals (true only on the crossing bar) ===
buySig = Cross(Fast, Slow); // fast crosses ABOVE slow
sellSig = Cross(Slow, Fast); // fast crosses BELOW slow
// === Non-repainting: decide on the close, act on the next bar ===
Buy = Ref(buySig, -1);
Sell = Ref(sellSig, -1);
// === Optional short side (default: long-only) ===
Short = Cover = 0;
if (tradeShort)
{
Short = Ref(sellSig, -1);
Cover = Ref(buySig, -1);
}
// === Remove duplicates - keep only the first of each run ===
Buy = ExRem(Buy, Sell);
Sell = ExRem(Sell, Buy);
Short = ExRem(Short, Cover);
Cover = ExRem(Cover, Short);
// === Fills and sizing for the backtest ===
SetTradeDelays(0, 0, 0, 0); // we already shifted by one bar
BuyPrice = ShortPrice = Open;
SellPrice = CoverPrice = Open;
SetPositionSize(100, spsPercentOfEquity); // all-in, one position at a time
// === Arrows on the chart ===
PlotShapes(IIf(Buy, shapeUpArrow, shapeNone), colorLime, 0, Low, -20);
PlotShapes(IIf(Sell, shapeDownArrow, shapeNone), colorOrange, 0, High, 20);
PlotShapes(IIf(Short, shapeHollowDownArrow, shapeNone), colorRed, 0, High, 30);
PlotShapes(IIf(Cover, shapeHollowUpArrow, shapeNone), colorAqua, 0, Low, -30);
_SECTION_END();
Why each guardrail is there
Three lines do the quiet, important work that separates a toy from a tradable system.
The pair Buy = Ref(buySig, -1) shifts every signal back by one bar. Without it, the cross is detected using the current bar's close - a value that is still moving while the bar forms, so the signal could appear and vanish on the same live bar. That flicker is called repainting, and it makes a backtest lie. By acting on the next bar we only ever trade on information that was already final.
ExRem ("remove excess") keeps only the first Buy until a Sell appears, and vice versa. Raw crossover logic can fire several buys in a row before the opposite signal; ExRem collapses each cluster to the one signal that actually matters.
Ref(signal, -1) plus SetTradeDelays(0,0,0,0) is the standard non-repainting recipe. We move the signal back one bar ourselves, so we tell the backtester not to add its own delay. Mixing both - a Ref(-1) and a non-zero trade delay - would push fills two bars late. Choose one method; this course uses the explicit Ref.
Backtesting it
With the formula in the Analysis window, set your symbol (try RELIANCE or NIFTY), a date range of a few years, and a daily timeframe, then press Backtest. AmiBroker walks the history bar by bar, opening and closing trades by your Buy/Sell/Short/Cover arrays, and produces a full report with an equity curve and a trade list.
When you open the report, read it in the order you learned earlier: start with CAR and Max. drawdown to judge whether the return was worth the pain, then CAR/MDD for the risk-adjusted picture, then win rate and payoff ratio. A trend system like this typically wins less than half its trades but makes it back with a few large winners - so a payoff ratio comfortably above 1 matters more than a high win rate.
A crossover system pays a tax in choppy, sideways markets, where the averages cross back and forth and every trade is a small loss. That is normal and expected - the system earns its keep in sustained trends. Judge it over a full cycle that contains both, not over a quiet six months.
Tuning the periods
The 10/20 default is a starting point, not gospel. This is exactly where Chapter 26 pays off: swap the two Param calls for Optimize and sweep the fast and slow lengths.
FastLen = Optimize("Fast EMA", 10, 5, 30, 1);
SlowLen = Optimize("Slow EMA", 20, 20, 80, 2);
Run the optimisation, view the 2D heatmap, and pick a setting from the middle of a green plateau rather than the single tallest spike. Remember the lesson: a broad neighbourhood of decent results is far more trustworthy than one dazzling but isolated peak.
Before you ever route these signals to real orders, run them in sandbox trading - analyzer mode in OpenAlgo - where fills are simulated and nothing reaches the exchange. A backtest is a hypothesis about the past; sandbox trading is how you watch the hypothesis behave on fresh data. Nothing here is investment advice.
Try it yourself
- Run the system long-only first (set the toggle to No), then enable shorts and compare the two reports - which side carried the profit?
- Change the fast EMA to 9 and the slow to 21 and re-run; note how the trade count and drawdown shift.
- Optimise both periods, study the heatmap, and choose a robust pair from a plateau.
- Switch the symbol to BANKNIFTY and see whether the same settings still hold up.
Recap
- The system is two EMAs:
BuyonCross(Fast, Slow),SellonCross(Slow, Fast), with an optional symmetric short side. Ref(signal, -1)withSetTradeDelays(0,0,0,0)makes it non-repainting;ExRemkeeps one clean signal per swing.SetPositionSizeand theBuyPrice/SellPricelines turn the signals into a backtestable system.- Read the report by CAR, drawdown and CAR/MDD first; expect a sub-50% win rate carried by a few big trends.
- Tune the periods with
Optimizeand pick robust settings - then prove them in sandbox before going live.
Next we tackle a system that trails its own stop: the ATR-based Supertrend, built from scratch with a for-loop.