ZigAndZagScalpelStrategy is a StockSharp port of the MetaTrader 4 "ZigAndZag" toolkit (folder 8304).
The original package combines a custom indicator and an expert advisor. Two ZigZag windows are used:
KeelOver – a long lookback swing detector that marks the dominant trend.
Slalom – a short lookback swing detector that defines actionable breakouts.
When the long-term ZigZag flips upward the strategy looks for the next Slalom low and waits for price
to rise a configurable number of points above that pivot. A buy market order is issued once the
breakout distance is met. A symmetrical rule opens a short position when the KeelOver trend turns
down, the Slalom prints a fresh high, and price drops below it. Positions can optionally be closed
as soon as the opposite Slalom pivot is confirmed, mimicking the indicator's limit-arrow removal.
The implementation keeps the daily trade limiter from the expert advisor. Only a configurable number
of trades is allowed per trading day, resetting automatically at midnight (exchange time). This
reproduces the "new day" flag from the original code.
How it works
Subscribe to the primary candle stream defined by CandleType.
Feed two ZigZagIndicator instances:
Depth = KeelOverLength for the trend detector.
Depth = SlalomLength for entry signals.
Track the most recent KeelOver pivot to determine whether the trend is up (last pivot is a low)
or down (last pivot is a high).
When the Slalom indicator publishes a new pivot, arm a breakout in that direction.
Calculate the weighted price (5×Close + 2×Open + High + Low) / 9. If price moves more than
BreakoutDistancePoints (converted into price units) away from the pivot while the trend supports
the move, execute a market order.
Close existing positions when the global trend flips or the opposite Slalom pivot appears and
CloseOnOppositePivot is enabled.
Reset the daily trade counter at every calendar day change.
The parameters DeviationPoints and Backstep are shared between both ZigZag instances so the
swing structure matches the MetaTrader indicator buffers.
Parameters
Name
Default
Description
CandleType
15m
Primary timeframe used to build both ZigZag ladders.
KeelOverLength
55
Long-term ZigZag lookback that defines the trend (original KeelOver).
SlalomLength
17
Short-term ZigZag lookback used for entries (original Slalom).
DeviationPoints
5
Minimum swing size in points before a new ZigZag pivot is confirmed.
Backstep
3
Required bar distance between consecutive pivots.
BreakoutDistancePoints
2
Distance from a pivot (in points) before firing an order.
MaxTradesPerDay
1
Maximum number of entries per calendar day. Mirrors the original newday flag.
CloseOnOppositePivot
true
Close open positions when the Slalom ZigZag produces the opposite swing.
All point-based parameters are converted to price units using Security.PriceStep. If the instrument
has no price step configured, a value of 1 is used to keep the strategy functional during testing.
Usage notes
The strategy operates with market orders (BuyMarket / SellMarket). Attach your own risk rules
or stop-loss helpers if tighter risk management is required.
Because both ZigZag indicators share the same candle stream, make sure the chosen CandleType is
supported by your data adapter.
MaxTradesPerDay = 1 reproduces the "one trade per day" behaviour. Increase the value if you need
multiple entries during the same session.
Set CloseOnOppositePivot = false to keep positions open until the global trend reverses instead of
reacting to every short-term swing.
Differences vs. the MT4 expert advisor
The MetaTrader version placed pending limit arrows. The StockSharp port executes breakouts with
immediate market orders to stay within the high-level API.
Risk management, lot sizing and partial closes are intentionally omitted. Use StockSharp position
sizing helpers if you need advanced capital control.
Indicator buffers 4/5/6 are replaced by direct strategy logic and chart annotations via
DrawIndicator and DrawOwnTrades.
Recommended extensions
Add stop-loss and take-profit parameters tied to ATR or recent ZigZag swings.
Overlay the original indicator with BreakoutDistancePoints = 0 to visualize the raw pivot ladder.
Combine with a session filter (IsFormedAndOnlineAndAllowTrading) to limit trading hours.
namespace StockSharp.Samples.Strategies;
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// ZigAndZagScalpel translation that trades on breakouts from short-term pivots confirmed by a long-term ZigZag trend.
/// </summary>
public class ZigAndZagScalpelStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maxTradesPerDay;
private readonly StrategyParam<bool> _closeOnOppositePivot;
private decimal _previousMajorPivot;
private decimal _lastMajorPivot;
private decimal _previousMinorPivot;
private decimal _lastMinorPivot;
private DateTime _currentDay = DateTime.MinValue;
private int _tradesToday;
private bool _trendUp;
private PivotTypes _lastMinorPivotType = PivotTypes.None;
private bool _minorPivotUsed;
/// <summary>
/// Initializes a new instance of the <see cref="ZigAndZagScalpelStrategy"/> class.
/// </summary>
public ZigAndZagScalpelStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for all calculations", "General");
_maxTradesPerDay = Param(nameof(MaxTradesPerDay), 1)
.SetDisplay("Max Trades Per Day", "Daily limit matching the original expert advisor", "Trading");
_closeOnOppositePivot = Param(nameof(CloseOnOppositePivot), true)
.SetDisplay("Close On Opposite Pivot", "Exit when the entry ZigZag prints the opposite swing", "Risk");
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Maximum number of trades allowed per trading day.
/// </summary>
public int MaxTradesPerDay
{
get => _maxTradesPerDay.Value;
set => _maxTradesPerDay.Value = value;
}
/// <summary>
/// Determines whether open positions should be closed on the opposite entry pivot.
/// </summary>
public bool CloseOnOppositePivot
{
get => _closeOnOppositePivot.Value;
set => _closeOnOppositePivot.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousMajorPivot = 0m;
_lastMajorPivot = 0m;
_previousMinorPivot = 0m;
_lastMinorPivot = 0m;
_currentDay = DateTime.MinValue;
_tradesToday = 0;
_trendUp = false;
_lastMinorPivotType = PivotTypes.None;
_minorPivotUsed = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var majorZigZag = new ZigZag { Deviation = 0.02m };
var minorZigZag = new ZigZag { Deviation = 0.005m };
var subscription = SubscribeCandles(CandleType);
subscription
.BindWithEmpty(majorZigZag, minorZigZag, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, majorZigZag);
DrawIndicator(area, minorZigZag);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal? majorValue, decimal? minorValue)
{
if (candle.State != CandleStates.Finished)
return;
UpdateDailyCounter(candle.OpenTime);
if (majorValue is not null)
UpdateMajorTrend(majorValue.Value);
if (minorValue is not null)
UpdateMinorPivot(minorValue.Value);
if (!IsFormedAndOnlineAndAllowTrading())
return;
ManageExistingPosition();
if (Position != 0)
return;
if (_minorPivotUsed)
return;
if (_lastMinorPivotType == PivotTypes.None)
return;
if (_tradesToday >= MaxTradesPerDay)
return;
var navel = CalculateNavel(candle);
if (_lastMinorPivotType == PivotTypes.Low && _trendUp)
{
if (navel > _lastMinorPivot)
{
BuyMarket();
_minorPivotUsed = true;
_tradesToday++;
}
}
else if (_lastMinorPivotType == PivotTypes.High && !_trendUp)
{
if (navel < _lastMinorPivot)
{
SellMarket();
_minorPivotUsed = true;
_tradesToday++;
}
}
}
private void UpdateDailyCounter(DateTime time)
{
var date = time.Date;
if (date == _currentDay)
return;
_currentDay = date;
_tradesToday = 0;
}
private void UpdateMajorTrend(decimal majorValue)
{
if (_lastMajorPivot == 0m)
{
_lastMajorPivot = majorValue;
_previousMajorPivot = majorValue;
return;
}
if (majorValue == _lastMajorPivot)
return;
_previousMajorPivot = _lastMajorPivot;
_lastMajorPivot = majorValue;
_trendUp = _lastMajorPivot < _previousMajorPivot;
}
private void UpdateMinorPivot(decimal minorValue)
{
if (_lastMinorPivot == 0m)
{
_lastMinorPivot = minorValue;
_previousMinorPivot = minorValue;
_lastMinorPivotType = PivotTypes.Low;
_minorPivotUsed = false;
return;
}
if (minorValue == _lastMinorPivot)
return;
_previousMinorPivot = _lastMinorPivot;
_lastMinorPivot = minorValue;
_lastMinorPivotType = _lastMinorPivot < _previousMinorPivot ? PivotTypes.Low : PivotTypes.High;
_minorPivotUsed = false;
}
private void ManageExistingPosition()
{
if (Position > 0)
{
if (!_trendUp || (CloseOnOppositePivot && _lastMinorPivotType == PivotTypes.High))
SellMarket(Position);
}
else if (Position < 0)
{
if (_trendUp || (CloseOnOppositePivot && _lastMinorPivotType == PivotTypes.Low))
BuyMarket(Position.Abs());
}
}
private static decimal CalculateNavel(ICandleMessage candle)
{
return (5m * candle.ClosePrice + 2m * candle.OpenPrice + candle.HighPrice + candle.LowPrice) / 9m;
}
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, DateTime, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import ZigZag
# Pivot type constants
PIVOT_NONE = 0
PIVOT_LOW = 1
PIVOT_HIGH = 2
class zig_and_zag_scalpel_strategy(Strategy):
def __init__(self):
super(zig_and_zag_scalpel_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Primary timeframe for all calculations", "General")
self._max_trades_per_day = self.Param("MaxTradesPerDay", 1) \
.SetDisplay("Max Trades Per Day", "Daily limit matching the original expert advisor", "Trading")
self._close_on_opposite_pivot = self.Param("CloseOnOppositePivot", True) \
.SetDisplay("Close On Opposite Pivot", "Exit when the entry ZigZag prints the opposite swing", "Risk")
self._previous_major_pivot = Decimal(0)
self._last_major_pivot = Decimal(0)
self._previous_minor_pivot = Decimal(0)
self._last_minor_pivot = Decimal(0)
self._current_day = DateTime.MinValue
self._trades_today = 0
self._trend_up = False
self._last_minor_pivot_type = PIVOT_NONE
self._minor_pivot_used = False
@property
def CandleType(self):
return self._candle_type.Value
@property
def MaxTradesPerDay(self):
return self._max_trades_per_day.Value
@property
def CloseOnOppositePivot(self):
return self._close_on_opposite_pivot.Value
def OnStarted2(self, time):
super(zig_and_zag_scalpel_strategy, self).OnStarted2(time)
major_zigzag = ZigZag()
major_zigzag.Deviation = Decimal(0.02)
minor_zigzag = ZigZag()
minor_zigzag.Deviation = Decimal(0.005)
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindWithEmpty(major_zigzag, minor_zigzag, self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, major_zigzag)
self.DrawIndicator(area, minor_zigzag)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle, major_value, minor_value):
if candle.State != CandleStates.Finished:
return
self._update_daily_counter(candle.OpenTime)
if major_value is not None:
self._update_major_trend(major_value)
if minor_value is not None:
self._update_minor_pivot(minor_value)
if not self.IsFormedAndOnlineAndAllowTrading():
return
self._manage_existing_position()
if self.Position != 0:
return
if self._minor_pivot_used:
return
if self._last_minor_pivot_type == PIVOT_NONE:
return
if self._trades_today >= self.MaxTradesPerDay:
return
navel = self._calculate_navel(candle)
if self._last_minor_pivot_type == PIVOT_LOW and self._trend_up:
if navel > self._last_minor_pivot:
self.BuyMarket()
self._minor_pivot_used = True
self._trades_today += 1
elif self._last_minor_pivot_type == PIVOT_HIGH and not self._trend_up:
if navel < self._last_minor_pivot:
self.SellMarket()
self._minor_pivot_used = True
self._trades_today += 1
def _update_daily_counter(self, time):
date = time.Date
if date == self._current_day:
return
self._current_day = date
self._trades_today = 0
def _update_major_trend(self, major_value):
if self._last_major_pivot == Decimal(0):
self._last_major_pivot = major_value
self._previous_major_pivot = major_value
return
if major_value == self._last_major_pivot:
return
self._previous_major_pivot = self._last_major_pivot
self._last_major_pivot = major_value
self._trend_up = self._last_major_pivot < self._previous_major_pivot
def _update_minor_pivot(self, minor_value):
if self._last_minor_pivot == Decimal(0):
self._last_minor_pivot = minor_value
self._previous_minor_pivot = minor_value
self._last_minor_pivot_type = PIVOT_LOW
self._minor_pivot_used = False
return
if minor_value == self._last_minor_pivot:
return
self._previous_minor_pivot = self._last_minor_pivot
self._last_minor_pivot = minor_value
self._last_minor_pivot_type = PIVOT_LOW if self._last_minor_pivot < self._previous_minor_pivot else PIVOT_HIGH
self._minor_pivot_used = False
def _manage_existing_position(self):
if self.Position > 0:
if not self._trend_up or (self.CloseOnOppositePivot and self._last_minor_pivot_type == PIVOT_HIGH):
self.SellMarket(self.Position)
elif self.Position < 0:
if self._trend_up or (self.CloseOnOppositePivot and self._last_minor_pivot_type == PIVOT_LOW):
self.BuyMarket(abs(self.Position))
def _calculate_navel(self, candle):
return (Decimal(5) * candle.ClosePrice + Decimal(2) * candle.OpenPrice +
candle.HighPrice + candle.LowPrice) / Decimal(9)
def OnReseted(self):
super(zig_and_zag_scalpel_strategy, self).OnReseted()
self._previous_major_pivot = Decimal(0)
self._last_major_pivot = Decimal(0)
self._previous_minor_pivot = Decimal(0)
self._last_minor_pivot = Decimal(0)
self._current_day = DateTime.MinValue
self._trades_today = 0
self._trend_up = False
self._last_minor_pivot_type = PIVOT_NONE
self._minor_pivot_used = False
def CreateClone(self):
return zig_and_zag_scalpel_strategy()