ZigAndZag Trader Strategy
Overview
The ZigAndZag Trader Strategy is the StockSharp port of the MetaTrader expert ZigAndZag_trader.mq4. The system layers two ZigZag-inspired swing detectors:
- A long-term ZigZag (configured by
TrendDepth) tracks the primary trend by marking major swing highs and lows. - A short-term ZigZag (configured by
ExitDepth) identifies the latest swing pivot inside that trend and monitors the weighted price ((5×Close + 2×Open + High + Low) / 9).
The robot opens trades only when price moves away from the latest swing pivot in the direction of the dominant trend and closes positions when the weighted price breaks back through that pivot against the trend. This reproduces the behaviour of the original MetaTrader expert that read buffers 4–6 of the custom ZigAndZag indicator.
Trading Logic
- Trend detection – when the long-term ZigZag confirms a new low the trend is considered up; a new high flips it to down.
- Swing tracking – each short-term pivot resets the internal state and stores the weighted price of that swing.
- Entry conditions
- Uptrend + last pivot is a low: buy when the weighted price rises above the stored pivot by at least one pip.
- Downtrend + last pivot is a high: sell when the weighted price falls below the stored pivot by at least one pip.
- Exit condition – if price moves back through the stored pivot while the trend disagrees with the active swing, all open positions are closed.
- Order throttling – the total absolute position size is capped by
MaxOrders × Volume. Additional signals are ignored once that cap is reached.
Parameters
| Parameter | Default | Description |
|---|---|---|
CandleType |
1 Minute |
Candle type used for both ZigZag evaluations. |
Lots |
0.1 |
Requested trade size in lots. The final volume is aligned to the instrument volume step. |
TrendDepth |
3 |
Lookback (in candles) of the long-term ZigZag that defines the trend. |
ExitDepth |
3 |
Lookback (in candles) of the short-term ZigZag that produces swing entries and exits. |
MaxOrders |
1 |
Maximum number of simultaneous orders/position units. |
StopLossPips |
0 |
Protective stop-loss distance in pips (0 disables the stop). |
TakeProfitPips |
0 |
Take-profit distance in pips (0 disables the target). |
Risk Management
StartProtection is enabled automatically. When the stop-loss or take-profit distance is set to a value greater than zero, fixed protective orders are attached to every market order using the provided pip distance and the instrument tick size.
Visualisation
The strategy draws candlesticks and executed trades on the default chart area. No custom indicator is plotted because the entry and exit logic uses internal ZigZag trackers.
Notes
- The weighted price formula is identical to the MetaTrader indicator and avoids direct indicator buffer access.
- The breakout threshold is equal to one instrument pip, mirroring the original code that required the move to exceed the current spread.
- The port keeps all comments and logging in English as required by the project guidelines.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Replica of the MetaTrader ZigAndZag trader that follows a long-term ZigZag trend and short-term swings.
/// </summary>
public class ZigAndZagTraderStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _lots;
private readonly StrategyParam<int> _trendDepth;
private readonly StrategyParam<int> _exitDepth;
private readonly StrategyParam<int> _maxOrders;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private Lowest _longTermLow = null!;
private Highest _longTermHigh = null!;
private Lowest _shortTermLow = null!;
private Highest _shortTermHigh = null!;
private decimal _pipSize;
private decimal _volumeStep;
private decimal _breakoutThreshold;
private decimal? _lastTrendLow;
private decimal? _lastTrendHigh;
private decimal? _lastShortLow;
private decimal? _lastShortHigh;
private decimal? _lastSlalomZig;
private decimal? _lastSlalomZag;
private bool _trendUp;
private bool _prevTrendUp;
private bool _buyArmed;
private bool _sellArmed;
private bool _limitArmed;
private PivotTypes _lastPivot;
private int _cooldown;
private const int CooldownBars = 500;
/// <summary>
/// Trading candles.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Requested trade volume in lots.
/// </summary>
public decimal Lots
{
get => _lots.Value;
set => _lots.Value = value;
}
/// <summary>
/// Depth of the long-term ZigZag that defines the prevailing trend.
/// </summary>
public int TrendDepth
{
get => _trendDepth.Value;
set => _trendDepth.Value = value;
}
/// <summary>
/// Depth of the short-term ZigZag that produces swing entries and exits.
/// </summary>
public int ExitDepth
{
get => _exitDepth.Value;
set => _exitDepth.Value = value;
}
/// <summary>
/// Maximum number of simultaneously open orders.
/// </summary>
public int MaxOrders
{
get => _maxOrders.Value;
set => _maxOrders.Value = value;
}
/// <summary>
/// Stop loss distance in pips (0 disables the stop).
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance in pips (0 disables the target).
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public ZigAndZagTraderStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Candles used for swing detection", "General");
_lots = Param(nameof(Lots), 0.1m)
.SetDisplay("Lots", "Requested trade size in lots", "Trading")
.SetGreaterThanZero();
_trendDepth = Param(nameof(TrendDepth), 3)
.SetDisplay("Trend Depth", "Lookback for the long-term ZigZag", "ZigZag")
.SetGreaterThanZero()
;
_exitDepth = Param(nameof(ExitDepth), 3)
.SetDisplay("Exit Depth", "Lookback for the short-term swing ZigZag", "ZigZag")
.SetGreaterThanZero()
;
_maxOrders = Param(nameof(MaxOrders), 1)
.SetDisplay("Max Orders", "Maximum simultaneous positions", "Trading")
.SetGreaterThanZero();
_stopLossPips = Param(nameof(StopLossPips), 0m)
.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk")
.SetNotNegative();
_takeProfitPips = Param(nameof(TakeProfitPips), 0m)
.SetDisplay("Take Profit (pips)", "Profit target distance", "Risk")
.SetNotNegative();
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_longTermLow = null;
_longTermHigh = null;
_shortTermLow = null;
_shortTermHigh = null;
_lastTrendLow = null;
_lastTrendHigh = null;
_lastShortLow = null;
_lastShortHigh = null;
_lastSlalomZig = null;
_lastSlalomZag = null;
_trendUp = false;
_prevTrendUp = false;
_buyArmed = false;
_sellArmed = false;
_limitArmed = false;
_lastPivot = PivotTypes.None;
_pipSize = 0;
_volumeStep = 0;
_breakoutThreshold = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = Security?.PriceStep ?? 0.0001m;
_volumeStep = Security?.VolumeStep ?? 1m;
if (_volumeStep <= 0m)
_volumeStep = 1m;
_breakoutThreshold = _pipSize;
var rawVolume = Lots > 0m ? Lots : _volumeStep;
if (rawVolume < _volumeStep)
rawVolume = _volumeStep;
var steps = Math.Max(1L, (long)Math.Ceiling((double)(rawVolume / _volumeStep)));
Volume = steps * _volumeStep;
StartProtection(
takeProfit: TakeProfitPips > 0m ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : null,
stopLoss: StopLossPips > 0m ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : null,
useMarketOrders: true);
_longTermLow = new Lowest { Length = TrendDepth };
_longTermHigh = new Highest { Length = TrendDepth };
_shortTermLow = new Lowest { Length = ExitDepth };
_shortTermHigh = new Highest { Length = ExitDepth };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_cooldown > 0)
_cooldown--;
var t = candle.CloseTime;
var longLow = _longTermLow.Process(candle.LowPrice, t, true).ToDecimal();
var longHigh = _longTermHigh.Process(candle.HighPrice, t, true).ToDecimal();
var shortLow = _shortTermLow.Process(candle.LowPrice, t, true).ToDecimal();
var shortHigh = _shortTermHigh.Process(candle.HighPrice, t, true).ToDecimal();
var longFormed = _longTermLow?.IsFormed == true && _longTermHigh?.IsFormed == true;
var shortFormed = _shortTermLow?.IsFormed == true && _shortTermHigh?.IsFormed == true;
var navel = (5m * candle.ClosePrice + 2m * candle.OpenPrice + candle.HighPrice + candle.LowPrice) / 9m;
if (longFormed)
{
if (candle.LowPrice == longLow && (_lastTrendLow == null || longLow != _lastTrendLow))
{
_trendUp = true;
_lastTrendLow = longLow;
}
if (candle.HighPrice == longHigh && (_lastTrendHigh == null || longHigh != _lastTrendHigh))
{
_trendUp = false;
_lastTrendHigh = longHigh;
}
}
if (_trendUp != _prevTrendUp)
{
_buyArmed = false;
_sellArmed = false;
_limitArmed = false;
_prevTrendUp = _trendUp;
}
if (shortFormed)
{
if (candle.LowPrice == shortLow && (_lastShortLow == null || shortLow != _lastShortLow))
{
_lastPivot = PivotTypes.Low;
_lastShortLow = shortLow;
_lastSlalomZig = navel;
_buyArmed = false;
_sellArmed = false;
_limitArmed = false;
}
if (candle.HighPrice == shortHigh && (_lastShortHigh == null || shortHigh != _lastShortHigh))
{
_lastPivot = PivotTypes.High;
_lastShortHigh = shortHigh;
_lastSlalomZag = navel;
_buyArmed = false;
_sellArmed = false;
_limitArmed = false;
}
}
if (!longFormed || !shortFormed)
return;
var buySignal = false;
var sellSignal = false;
var closeSignal = false;
switch (_lastPivot)
{
case PivotTypes.Low when _lastSlalomZig != null:
{
if (_trendUp)
{
var shouldBuy = navel - _lastSlalomZig.Value >= _breakoutThreshold;
if (shouldBuy && !_buyArmed)
{
_buyArmed = true;
buySignal = true;
}
else if (!shouldBuy && _buyArmed && navel <= _lastSlalomZig.Value)
{
_buyArmed = false;
}
if (_limitArmed && navel <= _lastSlalomZig.Value)
_limitArmed = false;
}
else
{
var shouldClose = navel > _lastSlalomZig.Value;
if (shouldClose && !_limitArmed)
{
_limitArmed = true;
closeSignal = true;
}
else if (!shouldClose && _limitArmed)
{
_limitArmed = false;
}
_buyArmed = false;
}
break;
}
case PivotTypes.High when _lastSlalomZag != null:
{
if (!_trendUp)
{
var shouldSell = _lastSlalomZag.Value - navel >= _breakoutThreshold;
if (shouldSell && !_sellArmed)
{
_sellArmed = true;
sellSignal = true;
}
else if (!shouldSell && _sellArmed && navel >= _lastSlalomZag.Value)
{
_sellArmed = false;
}
if (_limitArmed && navel >= _lastSlalomZag.Value)
_limitArmed = false;
}
else
{
var shouldClose = _lastSlalomZag.Value > navel;
if (shouldClose && !_limitArmed)
{
_limitArmed = true;
closeSignal = true;
}
else if (!shouldClose && _limitArmed)
{
_limitArmed = false;
}
_sellArmed = false;
}
break;
}
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
ExecuteSignals(buySignal, sellSignal, closeSignal);
}
private void ExecuteSignals(bool buySignal, bool sellSignal, bool closeSignal)
{
if (_cooldown > 0)
return;
var volume = Volume;
if (volume <= 0m || MaxOrders <= 0)
return;
var maxVolume = MaxOrders * volume;
if (buySignal)
{
var currentLong = Position > 0m ? Position : 0m;
var available = maxVolume - currentLong;
if (available > 0m)
{
var tradeVolume = Math.Min(volume, available);
BuyMarket(tradeVolume);
_cooldown = CooldownBars;
return;
}
}
if (sellSignal)
{
var currentShort = Position < 0m ? -Position : 0m;
var available = maxVolume - currentShort;
if (available > 0m)
{
var tradeVolume = Math.Min(volume, available);
SellMarket(tradeVolume);
_cooldown = CooldownBars;
return;
}
}
if (closeSignal && Position != 0m)
{
if (Position > 0)
SellMarket(Position);
else
BuyMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
}
private enum PivotTypes
{
None,
Low,
High
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, UnitTypes, Unit
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import Lowest, Highest
from indicator_extensions import *
class zig_and_zag_trader_strategy(Strategy):
PIVOT_NONE = 0
PIVOT_LOW = 1
PIVOT_HIGH = 2
def __init__(self):
super(zig_and_zag_trader_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Candles used for swing detection", "General")
self._lots = self.Param("Lots", 0.1).SetDisplay("Lots", "Requested trade size", "Trading")
self._trend_depth = self.Param("TrendDepth", 3).SetDisplay("Trend Depth", "Lookback for long-term ZigZag", "ZigZag")
self._exit_depth = self.Param("ExitDepth", 3).SetDisplay("Exit Depth", "Lookback for short-term ZigZag", "ZigZag")
self._max_orders = self.Param("MaxOrders", 1).SetDisplay("Max Orders", "Maximum simultaneous positions", "Trading")
self._stop_loss_pips = self.Param("StopLossPips", 0.0).SetDisplay("Stop Loss pips", "Protective stop distance", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 0.0).SetDisplay("Take Profit pips", "Profit target distance", "Risk")
self._pip_size = 0.0001
self._volume_step_val = 1.0
self._breakout_threshold = 0.0
self._last_trend_low = None
self._last_trend_high = None
self._last_short_low = None
self._last_short_high = None
self._last_slalom_zig = None
self._last_slalom_zag = None
self._trend_up = True
self._prev_trend_up = True
self._buy_armed = False
self._sell_armed = False
self._limit_armed = False
self._last_pivot = self.PIVOT_NONE
self._long_term_low = None
self._long_term_high = None
self._short_term_low = None
self._short_term_high = None
@property
def CandleType(self): return self._candle_type.Value
@property
def Lots(self): return self._lots.Value
@property
def TrendDepth(self): return self._trend_depth.Value
@property
def ExitDepth(self): return self._exit_depth.Value
@property
def MaxOrders(self): return self._max_orders.Value
@property
def StopLossPips(self): return self._stop_loss_pips.Value
@property
def TakeProfitPips(self): return self._take_profit_pips.Value
def OnStarted2(self, time):
super(zig_and_zag_trader_strategy, self).OnStarted2(time)
ps = self.Security.PriceStep if self.Security is not None else None
self._pip_size = float(ps) if ps is not None and float(ps) > 0 else 0.0001
vs = self.Security.VolumeStep if self.Security is not None else None
self._volume_step_val = float(vs) if vs is not None and float(vs) > 0 else 1.0
self._breakout_threshold = self._pip_size
import math
raw_vol = float(self.Lots) if float(self.Lots) > 0 else self._volume_step_val
if raw_vol < self._volume_step_val:
raw_vol = self._volume_step_val
steps = max(1, int(math.ceil(raw_vol / self._volume_step_val)))
self.Volume = steps * self._volume_step_val
tp_p = float(self.TakeProfitPips)
sl_p = float(self.StopLossPips)
tp_unit = Unit(tp_p * self._pip_size, UnitTypes.Absolute) if tp_p > 0 else None
sl_unit = Unit(sl_p * self._pip_size, UnitTypes.Absolute) if sl_p > 0 else None
self.StartProtection(takeProfit=tp_unit, stopLoss=sl_unit, useMarketOrders=True)
self._long_term_low = Lowest()
self._long_term_low.Length = self.TrendDepth
self._long_term_high = Highest()
self._long_term_high.Length = self.TrendDepth
self._short_term_low = Lowest()
self._short_term_low.Length = self.ExitDepth
self._short_term_high = Highest()
self._short_term_high.Length = self.ExitDepth
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
t = candle.CloseTime
long_low = float(process_float(self._long_term_low, candle.LowPrice, t, True))
long_high = float(process_float(self._long_term_high, candle.HighPrice, t, True))
short_low = float(process_float(self._short_term_low, candle.LowPrice, t, True))
short_high = float(process_float(self._short_term_high, candle.HighPrice, t, True))
long_formed = self._long_term_low.IsFormed and self._long_term_high.IsFormed
short_formed = self._short_term_low.IsFormed and self._short_term_high.IsFormed
cl = float(candle.ClosePrice)
op = float(candle.OpenPrice)
hi = float(candle.HighPrice)
lo = float(candle.LowPrice)
navel = (5.0 * cl + 2.0 * op + hi + lo) / 9.0
if long_formed:
if lo == long_low and (self._last_trend_low is None or long_low != self._last_trend_low):
self._trend_up = True
self._last_trend_low = long_low
if hi == long_high and (self._last_trend_high is None or long_high != self._last_trend_high):
self._trend_up = False
self._last_trend_high = long_high
if self._trend_up != self._prev_trend_up:
self._buy_armed = False
self._sell_armed = False
self._limit_armed = False
self._prev_trend_up = self._trend_up
if short_formed:
if lo == short_low and (self._last_short_low is None or short_low != self._last_short_low):
self._last_pivot = self.PIVOT_LOW
self._last_short_low = short_low
self._last_slalom_zig = navel
self._buy_armed = False
self._sell_armed = False
self._limit_armed = False
if hi == short_high and (self._last_short_high is None or short_high != self._last_short_high):
self._last_pivot = self.PIVOT_HIGH
self._last_short_high = short_high
self._last_slalom_zag = navel
self._buy_armed = False
self._sell_armed = False
self._limit_armed = False
if not long_formed or not short_formed:
return
buy_signal = False
sell_signal = False
close_signal = False
if self._last_pivot == self.PIVOT_LOW and self._last_slalom_zig is not None:
if self._trend_up:
sb = navel - self._last_slalom_zig >= self._breakout_threshold
if sb and not self._buy_armed:
self._buy_armed = True
buy_signal = True
elif not sb and self._buy_armed and navel <= self._last_slalom_zig:
self._buy_armed = False
if self._limit_armed and navel <= self._last_slalom_zig:
self._limit_armed = False
else:
sc = navel > self._last_slalom_zig
if sc and not self._limit_armed:
self._limit_armed = True
close_signal = True
elif not sc and self._limit_armed:
self._limit_armed = False
self._buy_armed = False
elif self._last_pivot == self.PIVOT_HIGH and self._last_slalom_zag is not None:
if not self._trend_up:
ss = self._last_slalom_zag - navel >= self._breakout_threshold
if ss and not self._sell_armed:
self._sell_armed = True
sell_signal = True
elif not ss and self._sell_armed and navel >= self._last_slalom_zag:
self._sell_armed = False
if self._limit_armed and navel >= self._last_slalom_zag:
self._limit_armed = False
else:
sc = self._last_slalom_zag > navel
if sc and not self._limit_armed:
self._limit_armed = True
close_signal = True
elif not sc and self._limit_armed:
self._limit_armed = False
self._sell_armed = False
self._execute_signals(buy_signal, sell_signal, close_signal)
def _execute_signals(self, buy_signal, sell_signal, close_signal):
volume = self.Volume
if volume <= 0 or int(self.MaxOrders) <= 0:
return
max_vol = int(self.MaxOrders) * float(volume)
if buy_signal:
cur_long = float(self.Position) if self.Position > 0 else 0.0
avail = max_vol - cur_long
if avail > 0:
self.BuyMarket(min(float(volume), avail))
if sell_signal:
cur_short = -float(self.Position) if self.Position < 0 else 0.0
avail = max_vol - cur_short
if avail > 0:
self.SellMarket(min(float(volume), avail))
if close_signal and self.Position != 0:
if self.Position > 0:
self.SellMarket(self.Position)
else:
self.BuyMarket(abs(self.Position))
def OnReseted(self):
super(zig_and_zag_trader_strategy, self).OnReseted()
self._long_term_low = None
self._long_term_high = None
self._short_term_low = None
self._short_term_high = None
self._last_trend_low = None
self._last_trend_high = None
self._last_short_low = None
self._last_short_high = None
self._last_slalom_zig = None
self._last_slalom_zag = None
self._trend_up = True
self._prev_trend_up = True
self._buy_armed = False
self._sell_armed = False
self._limit_armed = False
self._last_pivot = self.PIVOT_NONE
def CreateClone(self):
return zig_and_zag_trader_strategy()