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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Port of the ExpertZZLWA MetaTrader strategy with three operation modes.
/// </summary>
public class ExpertZzlwaStrategy : Strategy
{
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<bool> _useMartingale;
private readonly StrategyParam<decimal> _martingaleMultiplier;
private readonly StrategyParam<decimal> _maximumVolume;
private readonly StrategyParam<StrategyModes> _mode;
private readonly StrategyParam<TermLevels> _termLevel;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<DataType> _candleType;
private Highest _highest;
private Lowest _lowest;
private SmoothedMovingAverage _slowMa;
private SimpleMovingAverage _fastMa;
private bool _pendingBuySignal;
private bool _pendingSellSignal;
private bool _originalBuyReady;
private bool _originalSellReady;
private int _zigZagDirection;
private decimal _prevSlow;
private decimal _prevFast;
private decimal _trackedPosition;
private decimal _averageEntryPrice;
private decimal _lastClosedVolume;
private bool _lastTradeLoss;
/// <summary>
/// Operation modes reproduced from the original expert.
/// </summary>
public enum StrategyModes
{
Original,
ZigZagAddition,
MovingAverageTest,
}
/// <summary>
/// ZigZag sensitivity presets available in addition mode.
/// </summary>
public enum TermLevels
{
ShortTerm,
MediumTerm,
LongTerm,
}
/// <summary>
/// Protective stop size in price points.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Profit target size in price points.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Base order volume used by the strategy.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Enable martingale style position sizing.
/// </summary>
public bool UseMartingale
{
get => _useMartingale.Value;
set => _useMartingale.Value = value;
}
/// <summary>
/// Multiplier applied after a losing trade when martingale is active.
/// </summary>
public decimal MartingaleMultiplier
{
get => _martingaleMultiplier.Value;
set => _martingaleMultiplier.Value = value;
}
/// <summary>
/// Maximum allowed order volume.
/// </summary>
public decimal MaximumVolume
{
get => _maximumVolume.Value;
set => _maximumVolume.Value = value;
}
/// <summary>
/// Selected trading mode.
/// </summary>
public StrategyModes Mode
{
get => _mode.Value;
set => _mode.Value = value;
}
/// <summary>
/// ZigZag term preset for addition mode.
/// </summary>
public TermLevels ZigZagTerm
{
get => _termLevel.Value;
set => _termLevel.Value = value;
}
/// <summary>
/// Period of the slow smoothed moving average used in MA test mode.
/// </summary>
public int SlowMaPeriod
{
get => _slowMaPeriod.Value;
set => _slowMaPeriod.Value = value;
}
/// <summary>
/// Period of the fast simple moving average used in MA test mode.
/// </summary>
public int FastMaPeriod
{
get => _fastMaPeriod.Value;
set => _fastMaPeriod.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpertZzlwaStrategy"/> class.
/// </summary>
public ExpertZzlwaStrategy()
{
_stopLossPoints = Param(nameof(StopLossPoints), 600)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (points)", "Protective stop in points", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 700)
.SetGreaterThanZero()
.SetDisplay("Take Profit (points)", "Profit target in points", "Risk");
_baseVolume = Param(nameof(BaseVolume), 0.01m)
.SetGreaterThanZero()
.SetDisplay("Base Volume", "Default order volume", "Trading");
_useMartingale = Param(nameof(UseMartingale), false)
.SetDisplay("Use Martingale", "Enable martingale sizing", "Trading");
_martingaleMultiplier = Param(nameof(MartingaleMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Martingale Multiplier", "Multiplier applied after a loss", "Trading");
_maximumVolume = Param(nameof(MaximumVolume), 10m)
.SetGreaterThanZero()
.SetDisplay("Maximum Volume", "Upper cap for order size", "Trading");
_mode = Param(nameof(Mode), StrategyModes.MovingAverageTest)
.SetDisplay("Mode", "Operating mode", "General");
_termLevel = Param(nameof(ZigZagTerm), TermLevels.LongTerm)
.SetDisplay("ZigZag Term", "Sensitivity preset for ZigZag", "Indicators");
_slowMaPeriod = Param(nameof(SlowMaPeriod), 150)
.SetGreaterThanZero()
.SetDisplay("Slow MA Period", "Smoothed MA length", "Indicators");
_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Fast MA Period", "Simple MA length", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Time frame to analyse", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highest = null;
_lowest = null;
_slowMa = null;
_fastMa = null;
_pendingBuySignal = false;
_pendingSellSignal = false;
_originalBuyReady = true;
_originalSellReady = true;
_zigZagDirection = 0;
_prevSlow = 0m;
_prevFast = 0m;
_trackedPosition = 0m;
_averageEntryPrice = 0m;
_lastClosedVolume = BaseVolume;
_lastTradeLoss = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
StartProtection(
stopLoss: new Unit(StopLossPoints * GetPriceStep(), UnitTypes.Absolute),
takeProfit: new Unit(TakeProfitPoints * GetPriceStep(), UnitTypes.Absolute));
_originalBuyReady = true;
_originalSellReady = true;
_pendingBuySignal = false;
_pendingSellSignal = false;
_trackedPosition = 0m;
_averageEntryPrice = 0m;
_lastClosedVolume = BaseVolume;
_lastTradeLoss = false;
var subscription = SubscribeCandles(CandleType);
switch (Mode)
{
case StrategyModes.Original:
subscription.Bind(ProcessOriginalCandle).Start();
break;
case StrategyModes.ZigZagAddition:
_highest = new Highest { Length = GetZigZagDepth(ZigZagTerm) };
_lowest = new Lowest { Length = GetZigZagDepth(ZigZagTerm) };
subscription.Bind(_highest, _lowest, ProcessAdditionCandle).Start();
break;
case StrategyModes.MovingAverageTest:
_slowMa = new SmoothedMovingAverage { Length = SlowMaPeriod };
_fastMa = new SimpleMovingAverage { Length = FastMaPeriod };
subscription.Bind(_slowMa, _fastMa, ProcessMovingAverageCandle).Start();
break;
default:
throw new NotSupportedException($"Unsupported mode {Mode}.");
}
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
switch (Mode)
{
case StrategyModes.ZigZagAddition:
DrawIndicator(area, _highest);
DrawIndicator(area, _lowest);
break;
case StrategyModes.MovingAverageTest:
DrawIndicator(area, _slowMa);
DrawIndicator(area, _fastMa);
break;
}
DrawOwnTrades(area);
}
}
private void ProcessOriginalCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (Position == 0)
{
if (_originalBuyReady)
{
ExecuteTrade(Sides.Buy);
_originalBuyReady = false;
_originalSellReady = true;
}
else if (_originalSellReady)
{
ExecuteTrade(Sides.Sell);
_originalSellReady = false;
_originalBuyReady = true;
}
}
}
private void ProcessAdditionCandle(ICandleMessage candle, decimal highest, decimal lowest)
{
if (candle.State != CandleStates.Finished)
return;
if (!_highest.IsFormed || !_lowest.IsFormed)
return;
// Detect fresh ZigZag pivots similar to the original indicator buffers.
if (candle.HighPrice >= highest && _zigZagDirection != 1)
{
_pendingSellSignal = true;
_pendingBuySignal = false;
_zigZagDirection = 1;
}
else if (candle.LowPrice <= lowest && _zigZagDirection != -1)
{
_pendingBuySignal = true;
_pendingSellSignal = false;
_zigZagDirection = -1;
}
DispatchSignals();
}
private void ProcessMovingAverageCandle(ICandleMessage candle, decimal slow, decimal fast)
{
if (candle.State != CandleStates.Finished)
return;
if (!_slowMa.IsFormed || !_fastMa.IsFormed)
return;
// Reproduce cross checks from the MQL version.
var crossDown = _prevSlow > _prevFast && slow < fast;
var crossUp = _prevSlow < _prevFast && slow > fast;
_prevSlow = slow;
_prevFast = fast;
if (crossUp)
{
_pendingBuySignal = true;
_pendingSellSignal = false;
}
else if (crossDown)
{
_pendingSellSignal = true;
_pendingBuySignal = false;
}
DispatchSignals();
}
private void DispatchSignals()
{
if (_pendingBuySignal)
{
ExecuteTrade(Sides.Buy);
_pendingBuySignal = false;
_pendingSellSignal = false;
}
else if (_pendingSellSignal)
{
ExecuteTrade(Sides.Sell);
_pendingSellSignal = false;
_pendingBuySignal = false;
}
}
private void ExecuteTrade(Sides side)
{
var volume = GetOrderVolume();
if (volume <= 0)
return;
if (side == Sides.Buy)
BuyMarket(volume);
else
SellMarket(volume);
}
private decimal GetOrderVolume()
{
if (!UseMartingale)
return BaseVolume;
if (!_lastTradeLoss)
return BaseVolume;
var nextVolume = _lastClosedVolume * MartingaleMultiplier;
return nextVolume > MaximumVolume ? BaseVolume : nextVolume;
}
private int GetZigZagDepth(TermLevels level)
{
return level switch
{
TermLevels.ShortTerm => 12,
TermLevels.MediumTerm => 24,
_ => 48,
};
}
private decimal GetPriceStep()
{
return Security?.PriceStep ?? 1m;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
if (trade?.Order == null)
return;
var side = trade.Order.Side;
var volume = trade.Trade.Volume;
var price = trade.Trade.Price;
var previousPosition = _trackedPosition;
if (side == Sides.Buy)
{
if (previousPosition >= 0)
{
// Building or creating a long position.
var newPosition = previousPosition + volume;
_averageEntryPrice = newPosition == 0m
? 0m
: (_averageEntryPrice * previousPosition + price * volume) / newPosition;
_trackedPosition = newPosition;
}
else
{
// Closing part or all of a short position.
var closingVolume = Math.Min(volume, Math.Abs(previousPosition));
var profit = (_averageEntryPrice - price) * closingVolume;
var remaining = previousPosition + volume;
if (remaining >= 0m)
{
RegisterClosedTrade(closingVolume, profit);
if (remaining > 0m)
{
// Flip into a new long position with leftover quantity.
_trackedPosition = remaining;
_averageEntryPrice = price;
}
else
{
_trackedPosition = 0m;
_averageEntryPrice = 0m;
}
}
else
{
_trackedPosition = remaining;
// Average price of the remaining short stays unchanged.
}
}
}
else
{
if (previousPosition <= 0)
{
// Building or creating a short position.
var newPosition = previousPosition - volume;
var absPrev = Math.Abs(previousPosition);
var absNew = Math.Abs(newPosition);
_averageEntryPrice = absNew == 0m
? 0m
: (_averageEntryPrice * absPrev + price * volume) / absNew;
_trackedPosition = newPosition;
}
else
{
// Closing part or all of a long position.
var closingVolume = Math.Min(volume, previousPosition);
var profit = (price - _averageEntryPrice) * closingVolume;
var remaining = previousPosition - volume;
if (remaining <= 0m)
{
RegisterClosedTrade(closingVolume, profit);
if (remaining < 0m)
{
_trackedPosition = remaining;
_averageEntryPrice = price;
}
else
{
_trackedPosition = 0m;
_averageEntryPrice = 0m;
}
}
else
{
_trackedPosition = remaining;
// Average entry price is preserved for the reduced long position.
}
}
}
}
private void RegisterClosedTrade(decimal volume, decimal profit)
{
_lastClosedVolume = volume;
_lastTradeLoss = profit < 0m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import (
Highest, Lowest, SmoothedMovingAverage, SimpleMovingAverage
)
from StockSharp.Algo.Strategies import Strategy
class expert_zzlwa_strategy(Strategy):
def __init__(self):
super(expert_zzlwa_strategy, self).__init__()
self._stop_loss_points = self.Param("StopLossPoints", 600)
self._take_profit_points = self.Param("TakeProfitPoints", 700)
self._base_volume = self.Param("BaseVolume", 0.01)
self._use_martingale = self.Param("UseMartingale", False)
self._martingale_multiplier = self.Param("MartingaleMultiplier", 2.0)
self._maximum_volume = self.Param("MaximumVolume", 10.0)
self._mode = self.Param("Mode", 2)
self._term_level = self.Param("ZigZagTerm", 2)
self._slow_ma_period = self.Param("SlowMaPeriod", 150)
self._fast_ma_period = self.Param("FastMaPeriod", 10)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._highest = None
self._lowest = None
self._slow_ma = None
self._fast_ma = None
self._pending_buy = False
self._pending_sell = False
self._original_buy_ready = True
self._original_sell_ready = True
self._zigzag_direction = 0
self._prev_slow = 0.0
self._prev_fast = 0.0
self._last_closed_volume = 0.01
self._last_trade_loss = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def BaseVolume(self):
return self._base_volume.Value
@property
def UseMartingale(self):
return self._use_martingale.Value
@property
def MartingaleMultiplier(self):
return self._martingale_multiplier.Value
@property
def MaximumVolume(self):
return self._maximum_volume.Value
@property
def Mode(self):
return self._mode.Value
@property
def ZigZagTerm(self):
return self._term_level.Value
@property
def SlowMaPeriod(self):
return self._slow_ma_period.Value
@property
def FastMaPeriod(self):
return self._fast_ma_period.Value
def OnStarted2(self, time):
super(expert_zzlwa_strategy, self).OnStarted2(time)
step = self._get_price_step()
self.StartProtection(
Unit(self.TakeProfitPoints * step, UnitTypes.Absolute),
Unit(self.StopLossPoints * step, UnitTypes.Absolute))
self._original_buy_ready = True
self._original_sell_ready = True
self._pending_buy = False
self._pending_sell = False
self._last_closed_volume = self.BaseVolume
self._last_trade_loss = False
subscription = self.SubscribeCandles(self.CandleType)
if self.Mode == 0:
subscription.Bind(self._process_original).Start()
elif self.Mode == 1:
depth = self._get_zigzag_depth()
self._highest = Highest()
self._highest.Length = depth
self._lowest = Lowest()
self._lowest.Length = depth
subscription.Bind(self._highest, self._lowest, self._process_addition).Start()
else:
self._slow_ma = SmoothedMovingAverage()
self._slow_ma.Length = self.SlowMaPeriod
self._fast_ma = SimpleMovingAverage()
self._fast_ma.Length = self.FastMaPeriod
subscription.Bind(self._slow_ma, self._fast_ma, self._process_ma).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
if self.Mode == 1 and self._highest is not None:
self.DrawIndicator(area, self._highest)
self.DrawIndicator(area, self._lowest)
elif self.Mode == 2 and self._slow_ma is not None:
self.DrawIndicator(area, self._slow_ma)
self.DrawIndicator(area, self._fast_ma)
self.DrawOwnTrades(area)
def _process_original(self, candle):
if candle.State != CandleStates.Finished:
return
if self.Position == 0:
if self._original_buy_ready:
self._execute_trade(True)
self._original_buy_ready = False
self._original_sell_ready = True
elif self._original_sell_ready:
self._execute_trade(False)
self._original_sell_ready = False
self._original_buy_ready = True
def _process_addition(self, candle, highest, lowest):
if candle.State != CandleStates.Finished:
return
if not self._highest.IsFormed or not self._lowest.IsFormed:
return
hv = float(highest)
lv = float(lowest)
if float(candle.HighPrice) >= hv and self._zigzag_direction != 1:
self._pending_sell = True
self._pending_buy = False
self._zigzag_direction = 1
elif float(candle.LowPrice) <= lv and self._zigzag_direction != -1:
self._pending_buy = True
self._pending_sell = False
self._zigzag_direction = -1
self._dispatch_signals()
def _process_ma(self, candle, slow, fast):
if candle.State != CandleStates.Finished:
return
if not self._slow_ma.IsFormed or not self._fast_ma.IsFormed:
return
sv = float(slow)
fv = float(fast)
cross_down = self._prev_slow > self._prev_fast and sv < fv
cross_up = self._prev_slow < self._prev_fast and sv > fv
self._prev_slow = sv
self._prev_fast = fv
if cross_up:
self._pending_buy = True
self._pending_sell = False
elif cross_down:
self._pending_sell = True
self._pending_buy = False
self._dispatch_signals()
def _dispatch_signals(self):
if self._pending_buy:
self._execute_trade(True)
self._pending_buy = False
self._pending_sell = False
elif self._pending_sell:
self._execute_trade(False)
self._pending_sell = False
self._pending_buy = False
def _execute_trade(self, is_buy):
vol = self._get_order_volume()
if vol <= 0:
return
if is_buy:
self.BuyMarket()
else:
self.SellMarket()
def _get_order_volume(self):
if not self.UseMartingale:
return self.BaseVolume
if not self._last_trade_loss:
return self.BaseVolume
nv = self._last_closed_volume * self.MartingaleMultiplier
return nv if nv <= self.MaximumVolume else self.BaseVolume
def _get_zigzag_depth(self):
if self.ZigZagTerm == 0:
return 12
elif self.ZigZagTerm == 1:
return 24
return 48
def _get_price_step(self):
sec = self.Security
return float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
def OnReseted(self):
super(expert_zzlwa_strategy, self).OnReseted()
self._highest = None
self._lowest = None
self._slow_ma = None
self._fast_ma = None
self._pending_buy = False
self._pending_sell = False
self._original_buy_ready = True
self._original_sell_ready = True
self._zigzag_direction = 0
self._prev_slow = 0.0
self._prev_fast = 0.0
self._last_closed_volume = self.BaseVolume
self._last_trade_loss = False
def CreateClone(self):
return expert_zzlwa_strategy()