namespace StockSharp.Samples.Strategies;
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;
/// <summary>
/// ZigZag swing strategy that reproduces the ZigZagEvgeTrofi 1 expert advisor.
/// Enters when a fresh ZigZag turning point appears within a limited number of bars.
/// </summary>
public class ZigZagEvgeTrofi1Strategy : Strategy
{
private enum PivotTypes
{
None,
High,
Low
}
private readonly StrategyParam<int> _depth;
private readonly StrategyParam<decimal> _deviation;
private readonly StrategyParam<int> _backstep;
private readonly StrategyParam<int> _urgency;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _volume;
private Highest _highest;
private Lowest _lowest;
private PivotTypes _pivotType;
private decimal _pivotPrice;
private int _barsSincePivot;
private bool _signalHandled;
private decimal _priceStep;
/// <summary>
/// ZigZag depth parameter identical to the original expert advisor.
/// </summary>
public int Depth
{
get => _depth.Value;
set => _depth.Value = value;
}
/// <summary>
/// Minimum deviation expressed in points that confirms a new swing.
/// </summary>
public decimal Deviation
{
get => _deviation.Value;
set => _deviation.Value = value;
}
/// <summary>
/// Minimum bars required before switching to an opposite pivot.
/// </summary>
public int Backstep
{
get => _backstep.Value;
set => _backstep.Value = value;
}
/// <summary>
/// Number of bars after a pivot during which entries are allowed.
/// </summary>
public int Urgency
{
get => _urgency.Value;
set => _urgency.Value = value;
}
/// <summary>
/// Candle data type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Volume applied to each market order.
/// </summary>
public decimal VolumePerTrade
{
get => _volume.Value;
set => _volume.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ZigZagEvgeTrofi1Strategy"/> class.
/// </summary>
public ZigZagEvgeTrofi1Strategy()
{
_depth = Param(nameof(Depth), 17)
.SetGreaterThanZero()
.SetDisplay("Depth", "ZigZag depth parameter", "ZigZag")
.SetOptimize(5, 40, 1);
_deviation = Param(nameof(Deviation), 7m)
.SetGreaterThanZero()
.SetDisplay("Deviation", "Minimum price movement in points", "ZigZag")
.SetOptimize(1m, 20m, 1m);
_backstep = Param(nameof(Backstep), 5)
.SetNotNegative()
.SetDisplay("Backstep", "Bars to wait before switching pivots", "ZigZag")
.SetOptimize(0, 10, 1);
_urgency = Param(nameof(Urgency), 2)
.SetNotNegative()
.SetDisplay("Urgency", "Maximum bars to trade the latest pivot", "Trading")
.SetOptimize(0, 5, 1);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for analysis", "General");
_volume = Param(nameof(VolumePerTrade), 1m)
.SetGreaterThanZero()
.SetDisplay("Volume", "Order volume per trade", "Trading");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highest = null;
_lowest = null;
_pivotType = PivotTypes.None;
_pivotPrice = 0m;
_barsSincePivot = int.MaxValue;
_signalHandled = true;
_priceStep = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_priceStep = GetEffectivePriceStep();
_highest = new Highest { Length = Depth };
_lowest = new Lowest { Length = Depth };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_highest, _lowest, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal highestValue, decimal lowestValue)
{
// React only on completed candles to mirror the original bar-by-bar logic.
if (candle.State != CandleStates.Finished)
return;
// Ensure both indicators collected enough data before generating signals.
if (_highest == null || _lowest == null || !_highest.IsFormed || !_lowest.IsFormed)
return;
// Track how many bars have passed since the latest identified pivot.
if (_pivotType != PivotTypes.None && _barsSincePivot < int.MaxValue)
_barsSincePivot++;
var deviationPrice = GetDeviationPrice();
var canSwitch = _pivotType == PivotTypes.None || _barsSincePivot >= Backstep;
// Detect a new swing high whenever price matches the tracked maximum.
if (candle.HighPrice >= highestValue && highestValue > 0m)
{
var difference = candle.HighPrice - _pivotPrice;
if ((_pivotType != PivotTypes.High && canSwitch) || (_pivotType == PivotTypes.High && difference >= deviationPrice))
SetPivot(PivotTypes.High, candle.HighPrice);
}
// Detect a new swing low whenever price touches the tracked minimum.
else if (candle.LowPrice <= lowestValue && lowestValue > 0m)
{
var difference = _pivotPrice - candle.LowPrice;
if ((_pivotType != PivotTypes.Low && canSwitch) || (_pivotType == PivotTypes.Low && difference >= deviationPrice))
SetPivot(PivotTypes.Low, candle.LowPrice);
}
// Stop if no pivot is available after the checks above.
if (_pivotType == PivotTypes.None)
return;
// Skip if the pivot is already considered stale by the urgency filter.
if (_barsSincePivot > Urgency)
return;
// Avoid firing multiple orders for the same pivot.
if (_signalHandled)
return;
// Confirm that trading permissions and connections are valid.
if (!IsFormedAndOnlineAndAllowTrading())
return;
var volume = VolumePerTrade;
if (volume <= 0m)
{
_signalHandled = true;
return;
}
var isBuySignal = _pivotType == PivotTypes.High;
// Do nothing if a position in the same direction is already open.
if (isBuySignal)
{
if (Position > 0m)
{
_signalHandled = true;
return;
}
}
else
{
if (Position < 0m)
{
_signalHandled = true;
return;
}
}
// Close existing exposure in the opposite direction before reversing.
if (isBuySignal)
{
if (Position < 0m)
{
var closeVolume = Math.Abs(Position);
if (closeVolume > 0m)
BuyMarket(closeVolume);
}
BuyMarket(volume);
}
else
{
if (Position > 0m)
{
var closeVolume = Math.Abs(Position);
if (closeVolume > 0m)
SellMarket(closeVolume);
}
SellMarket(volume);
}
_signalHandled = true;
}
// Update the internal pivot state when a new turning point is registered.
private void SetPivot(PivotTypes type, decimal price)
{
_pivotType = type;
_pivotPrice = price;
_barsSincePivot = 0;
_signalHandled = false;
}
// Convert the deviation parameter expressed in points to an absolute price move.
private decimal GetDeviationPrice()
{
var step = _priceStep > 0m ? _priceStep : 1m;
var deviation = Deviation;
if (deviation <= 0m)
return step;
var value = deviation * step;
return value >= step ? value : step;
}
// Determine the effective price step for transforming point-based inputs.
private decimal GetEffectivePriceStep()
{
if (Security != null)
{
if (Security.PriceStep.HasValue && Security.PriceStep.Value > 0m)
return Security.PriceStep.Value;
}
return 1m;
}
}
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
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import Highest, Lowest
class zig_zag_evge_trofi1_strategy(Strategy):
PIVOT_NONE = 0
PIVOT_HIGH = 1
PIVOT_LOW = 2
def __init__(self):
super(zig_zag_evge_trofi1_strategy, self).__init__()
self._depth = self.Param("Depth", 17) \
.SetDisplay("Depth", "ZigZag depth parameter", "ZigZag")
self._deviation = self.Param("Deviation", 7.0) \
.SetDisplay("Deviation", "Minimum price movement in points", "ZigZag")
self._backstep = self.Param("Backstep", 5) \
.SetDisplay("Backstep", "Bars to wait before switching pivots", "ZigZag")
self._urgency = self.Param("Urgency", 2) \
.SetDisplay("Urgency", "Maximum bars to trade the latest pivot", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe for analysis", "General")
self._volume_param = self.Param("VolumePerTrade", 1.0) \
.SetDisplay("Volume", "Order volume per trade", "Trading")
self._pivot_type = self.PIVOT_NONE
self._pivot_price = 0.0
self._bars_since_pivot = 999999
self._signal_handled = True
self._price_step = 0.0
@property
def Depth(self):
return self._depth.Value
@property
def Deviation(self):
return self._deviation.Value
@property
def Backstep(self):
return self._backstep.Value
@property
def Urgency(self):
return self._urgency.Value
@property
def CandleType(self):
return self._candle_type.Value
@property
def VolumePerTrade(self):
return self._volume_param.Value
def OnStarted2(self, time):
super(zig_zag_evge_trofi1_strategy, self).OnStarted2(time)
self._price_step = self._get_effective_price_step()
self._highest = Highest()
self._highest.Length = self.Depth
self._lowest = Lowest()
self._lowest.Length = self.Depth
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._highest, self._lowest, self.ProcessCandle).Start()
def ProcessCandle(self, candle, highest_value, lowest_value):
if candle.State != CandleStates.Finished:
return
if not self._highest.IsFormed or not self._lowest.IsFormed:
return
highest_value = float(highest_value)
lowest_value = float(lowest_value)
high_price = float(candle.HighPrice)
low_price = float(candle.LowPrice)
if self._pivot_type != self.PIVOT_NONE and self._bars_since_pivot < 999999:
self._bars_since_pivot += 1
deviation_price = self._get_deviation_price()
can_switch = self._pivot_type == self.PIVOT_NONE or self._bars_since_pivot >= self.Backstep
if high_price >= highest_value and highest_value > 0:
difference = high_price - self._pivot_price
if (self._pivot_type != self.PIVOT_HIGH and can_switch) or \
(self._pivot_type == self.PIVOT_HIGH and difference >= deviation_price):
self._set_pivot(self.PIVOT_HIGH, high_price)
elif low_price <= lowest_value and lowest_value > 0:
difference = self._pivot_price - low_price
if (self._pivot_type != self.PIVOT_LOW and can_switch) or \
(self._pivot_type == self.PIVOT_LOW and difference >= deviation_price):
self._set_pivot(self.PIVOT_LOW, low_price)
if self._pivot_type == self.PIVOT_NONE:
return
if self._bars_since_pivot > self.Urgency:
return
if self._signal_handled:
return
volume = float(self.VolumePerTrade)
if volume <= 0:
self._signal_handled = True
return
is_buy_signal = self._pivot_type == self.PIVOT_HIGH
if is_buy_signal:
if self.Position > 0:
self._signal_handled = True
return
else:
if self.Position < 0:
self._signal_handled = True
return
if is_buy_signal:
if self.Position < 0:
close_vol = abs(self.Position)
if close_vol > 0:
self.BuyMarket(close_vol)
self.BuyMarket(volume)
else:
if self.Position > 0:
close_vol = abs(self.Position)
if close_vol > 0:
self.SellMarket(close_vol)
self.SellMarket(volume)
self._signal_handled = True
def _set_pivot(self, pivot_type, price):
self._pivot_type = pivot_type
self._pivot_price = price
self._bars_since_pivot = 0
self._signal_handled = False
def _get_deviation_price(self):
step = self._price_step if self._price_step > 0 else 1.0
deviation = float(self.Deviation)
if deviation <= 0:
return step
value = deviation * step
return value if value >= step else step
def _get_effective_price_step(self):
if self.Security is not None:
ps = self.Security.PriceStep
if ps is not None and float(ps) > 0:
return float(ps)
return 1.0
def OnReseted(self):
super(zig_zag_evge_trofi1_strategy, self).OnReseted()
self._pivot_type = self.PIVOT_NONE
self._pivot_price = 0.0
self._bars_since_pivot = 999999
self._signal_handled = True
self._price_step = 0.0
def CreateClone(self):
return zig_zag_evge_trofi1_strategy()