Why Most ZigZag EAs Are Broken
If you've ever backtested a ZigZag-based Expert Advisor, you've likely seen equity curves that look too good to be true. That's because they are. The built-in ZigZag indicator in MetaTrader repaints—it recalculates historical swing points as new price data arrives, making past signals look perfect. In live trading, those same signals disappear or shift, destroying the strategy.
But the core idea of trading breakouts from swing highs and lows is still sound. The solution is to build a non-repainting Swing Point EA that identifies structure using fixed lookback logic, not the repainting ZigZag. In this post, I'll show you exactly how to code such an EA in MQL4, how to set its parameters for robustness, and where the hidden risks still lurk.
What a Non-Repainting Swing Point Actually Is
A swing point is a local high or low that forms when price reverses by a minimum percentage or number of pips. The key difference from ZigZag: once a swing point is confirmed, it never changes. This means your EA sees the same structure in real-time that it would in backtesting—no signal distortion.
We define a swing high as a bar where both the left and right neighbors (or a configurable number of bars) have lower highs. A swing low is the mirror image. This is the same logic used in classic pivot point identification, and it's deterministic: given the same price history, you always get the same swing points.
The Core Algorithm in Plain English
- Scan the last N bars to find the highest high and lowest low.
- Confirm a swing high when the central bar's high exceeds both neighbors by at least a minimum pip distance.
- Store confirmed swing levels in arrays that are only updated when a new bar opens.
- On each new tick, check if price breaks above the most recent swing high (for longs) or below the most recent swing low (for shorts).
- Execute entry only on the first tick of a new bar to avoid whipsaw.
This approach gives you a stable, backtestable structure that behaves identically in forward testing.
Practical MQL4 Implementation
Let's build the EA step by step. I'll assume you have basic MQL4 familiarity—this isn't a beginner tutorial, but the code is straightforward enough to adapt.
Step 1: Define Input Parameters
These are the knobs you'll tune. Resist the urge to over-optimize; I'll explain sensible ranges afterward.
| Parameter | Type | Default | Description |
|---|---|---|---|
| SwingBars | int | 3 | Number of bars on each side to confirm a swing point. |
| MinSwingPips | int | 20 | Minimum pip distance from neighbors to confirm swing. |
| BreakoutPips | int | 5 | Extra pips beyond swing level to confirm breakout. |
| RiskPercent | double | 1.0 | Percent of account risk per trade. |
| StopLossPips | int | 40 | Fixed stop loss in pips. |
Step 2: The Swing Detection Function
Here's the heart of the EA. This function returns true if the bar at index shift is a confirmed swing high.
//+------------------------------------------------------------------+
//| Check if bar at shift is a swing high |
//+------------------------------------------------------------------+
bool IsSwingHigh(int shift)
{
double currentHigh = High[shift];
double minDist = MinSwingPips * Point * 10; // account for 5-digit brokers
// Check left and right neighbors
for(int i = 1; i <= SwingBars; i++)
{
if(High[shift + i] >= currentHigh - minDist) return(false);
if(High[shift - i] >= currentHigh - minDist) return(false);
}
return(true);
}
Notice the minDist calculation. On 5-digit brokers, 1 pip equals 10 points. This ensures your 20-pip minimum works across all brokers. The same logic applies for swing lows using Low[].
Step 3: Entry Logic on New Bar
We execute trades only at the open of a new bar to avoid noise. Here's a simplified version of the OnTick() logic:
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
static datetime lastBarTime = 0;
if(Time[0] == lastBarTime) return; // only trade on new bar
lastBarTime = Time[0];
// Find the most recent confirmed swing high/low
int swingShiftHigh = FindLastSwingHigh(10); // look back 10 bars
int swingShiftLow = FindLastSwingLow(10);
double swingHigh = High[swingShiftHigh] + BreakoutPips * Point * 10;
double swingLow = Low[swingShiftLow] - BreakoutPips * Point * 10;
// Long entry
if(Ask >= swingHigh && CountTrades(OP_BUY) == 0)
{
double lotSize = CalculateLotSize(StopLossPips);
OrderSend(Symbol(), OP_BUY, lotSize, Ask, 3,
Ask - StopLossPips * Point * 10, 0, "SwingBreakout", 0, 0, Green);
}
// Short entry - mirror logic
if(Bid <= swingLow && CountTrades(OP_SELL) == 0)
{
double lotSize = CalculateLotSize(StopLossPips);
OrderSend(Symbol(), OP_SELL, lotSize, Bid, 3,
Bid + StopLossPips * Point * 10, 0, "SwingBreakout", 0, 0, Red);
}
}
The FindLastSwingHigh() function iterates backward from the current bar to find the most recent confirmed swing point. I limit the lookback to 10 bars to keep the EA responsive to recent structure—adjust based on your timeframe.
Step 4: Position Sizing with ATR
Fixed stop loss in pips is naive. A better approach uses Average True Range (ATR) to adapt to volatility. Here's a quick sizing function:
double CalculateLotSize(int stopPips)
{
double atr = iATR(NULL, 0, 14, 0);
double stopDistance = atr * 1.5; // 1.5x ATR as dynamic stop
double riskAmount = AccountBalance() * RiskPercent / 100;
double lotSize = riskAmount / (stopDistance / Point * 10 * MarketInfo(Symbol(), MODE_TICKVALUE));
return(NormalizeDouble(lotSize, 2));
}
This scales your position so that each trade risks the same percentage of your account, regardless of market volatility. It's the single most important risk control you can add.
Pros, Cons, and Risks
The Good
- No repainting: Your backtest results match live trading behavior.
- Clear structure: Swing points are objective and easy to visualize on the chart.
- Trend-following bias: Breakouts in the direction of the larger trend have a statistical edge.
- Customizable: You can adjust swing sensitivity for any timeframe from M5 to H4.
The Bad
- False breakouts: Price often pokes above a swing high only to reverse. The
BreakoutPipsfilter helps but doesn't eliminate them. - Lag: The EA waits for confirmation, meaning you enter after the initial move. In fast markets, you may enter near the top.
- Parameter sensitivity: Small changes to
SwingBarsorMinSwingPipsdramatically change signal frequency. Over-optimization is a real danger.
The Ugly
- Trending markets only: This strategy bleeds in ranging conditions. You must add a trend filter (e.g., 200 EMA slope) or disable trading during low volatility.
- Gap risk: Breakouts often occur on market open gaps, where your stop may be filled at a worse price than calculated.
Worked Walkthrough: EURUSD on H1
Let me walk through a real scenario. You set SwingBars=3, MinSwingPips=20, BreakoutPips=5 on EURUSD H1. On January 15, 2024, price forms a swing high at 1.0950. Three bars later, the high is confirmed because the bars on each side have highs below 1.0930 (20 pips lower).
Your EA now has a reference level at 1.0955 (1.0950 + 5 pips). On the next bar, price opens at 1.0952 and within an hour reaches 1.0960. The EA enters long at 1.0955 with a stop at 1.0915 (40 pips). Price continues to 1.1020 before reversing—a 65-pip winner.
But the next trade is a loser. Price breaks above a swing high at 1.0980, you enter at 1.0985, and price immediately reverses to 1.0940, hitting your stop. This happens about 40% of the time in choppy markets. The key is that winners are larger than losers on average, giving you a positive expectancy over many trades.
Key Takeaways for Your Swing Point EA
- Non-repainting logic is non-negotiable for reliable backtesting. Use fixed lookback, not ZigZag.
- Add a trend filter to avoid trading against the dominant direction. A simple 200-period moving average works.
- Risk per trade consistently using ATR-based position sizing. This protects your account during volatile drawdowns.
- Test on multiple instruments before trusting the results. A strategy that works on EURUSD may fail on GBPJPY due to different volatility patterns.
- Expect 30-50% win rates with 2:1 or better risk-reward. If you see 70% win rates in backtest, you've likely over-optimized.
Final Thoughts
The Swing Point EA I've described here is not a holy grail—no strategy is. But it's a solid, honest framework that lets you participate in trend continuations with a clear edge. The code is simple enough to audit, the logic is transparent, and the risks are manageable when you respect position sizing.
Your job as a developer is not to eliminate losses but to ensure the system survives them. Build the EA, test it on at least 10 years of data across 3-4 pairs, and then run it on a demo for three months. Only then consider live trading. That discipline separates profitable algo traders from those who burn accounts chasing backtest perfection.
Frequently Asked Questions
Does this Swing Point EA work on all timeframes?
Yes, but it performs best on H1 and H4. Lower timeframes like M5 produce too






