Стратегия ZigZag EvgeTrofi
Стратегия ZigZag EvgeTrofi представляет собой перенос оригинального советника MetaTrader на высокоуровневый API StockSharp. Она отслеживает последний пивот, найденный по логике ZigZag, и стремится открыть позицию, пока сигнал остаётся актуальным.
Идея
- Исходный советник анализирует первый ненулевой элемент буфера ZigZag и определяет, был ли последний подтверждённый экстремум максимумом или минимумом.
- Максимум порождает длинный вход по умолчанию. Параметр SignalReverse инвертирует направление сделок.
- Сделки разрешены только пока новый пивот считается «свежим». Параметр Urgency ограничивает число баров после пивота, когда допускается открытие позиции.
- Противоположные позиции закрываются немедленно перед постановкой новых заявок. В течение окна актуальности возможно наращивание позиции в том же направлении.
Таким образом перенос сохраняет контртрендовую природу оригинала: новые максимумы дают лонги, свежие минимумы — шорты.
Как работает
- Два скользящих индикатора (
Highest и Lowest) имитируют выборку ZigZag с глубиной Depth.
- Когда цена формирует новый экстремум выше/ниже этих уровней и движение превышает Deviation (в шагах цены), фиксируется новый пивот.
- Алгоритм считает число баров, прошедших с момента пивота. После превышения Urgency сигнал истекает.
- На каждом закрытом баре в пределах окна стратегия входит объёмом
VolumePerTrade. Сначала закрываются обратные позиции, поэтому перевороты происходят корректно.
Параметры
| Параметр |
Значение по умолчанию |
Описание |
Depth |
17 |
Количество баров для поиска экстремумов, аналог глубины ZigZag. |
Deviation |
7 |
Минимальный сдвиг цены в шагах цены, необходимый для подтверждения нового пивота. |
Backstep |
5 |
Минимальное число баров, которое должно пройти перед сменой направления пивота. |
Urgency |
2 |
Максимальное количество баров после пивота, когда допускаются входы. |
SignalReverse |
false |
Инвертирует соответствие максимумов/минимумов и длинных/коротких сигналов. |
CandleType |
5-минутные свечи |
Таймфрейм анализа, подберите под используемый график. |
VolumePerTrade |
0.10 |
Объём заявки при каждом входе, совпадает с параметром Lot оригинала. |
Особенности торговли
- Логика не содержит стопов или тейк-профитов — управление риском необходимо добавлять отдельно.
- Стратегия может наращивать позицию на каждом баре в окне
Urgency, поэтому размер позиции быстро растёт при сильных движениях.
- Для волатильных инструментов увеличивайте
Depth, чтобы отфильтровать шум, и уменьшайте для более агрессивной торговли.
- При включённом SignalReverse стратегия становится пробойной: максимумы открывают шорт, минимумы — лонг.
Файлы
CS/ZigZagEvgeTrofiStrategy.cs — реализация стратегии на C#.
- Версия на Python сознательно не предоставляется.
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>
/// ZigZag pivot strategy based on the original ZigZagEvgeTrofi expert advisor.
/// Reacts to the most recent zigzag swing and enters within a limited number of bars.
/// </summary>
public class ZigZagEvgeTrofiStrategy : 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<bool> _signalReverse;
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 decimal _priceStep;
/// <summary>
/// ZigZag depth parameter controlling the swing detection window.
/// </summary>
public int Depth
{
get => _depth.Value;
set => _depth.Value = value;
}
/// <summary>
/// Minimum deviation in price steps required to confirm a new pivot.
/// </summary>
public decimal Deviation
{
get => _deviation.Value;
set => _deviation.Value = value;
}
/// <summary>
/// Minimum number of bars between opposite pivot updates.
/// </summary>
public int Backstep
{
get => _backstep.Value;
set => _backstep.Value = value;
}
/// <summary>
/// Maximum number of bars after a pivot when entries are allowed.
/// </summary>
public int Urgency
{
get => _urgency.Value;
set => _urgency.Value = value;
}
/// <summary>
/// Reverses the direction of the generated signals.
/// </summary>
public bool SignalReverse
{
get => _signalReverse.Value;
set => _signalReverse.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Trading volume submitted on every entry.
/// </summary>
public decimal VolumePerTrade
{
get => _volume.Value;
set => _volume.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ZigZagEvgeTrofiStrategy"/> class.
/// </summary>
public ZigZagEvgeTrofiStrategy()
{
_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)
.SetGreaterThanZero()
.SetDisplay("Backstep", "Bars to lock a pivot before switching", "ZigZag")
.SetOptimize(1, 15, 1);
_urgency = Param(nameof(Urgency), 2)
.SetNotNegative()
.SetDisplay("Urgency", "Maximum bars to use the latest signal", "Trading")
.SetOptimize(0, 5, 1);
_signalReverse = Param(nameof(SignalReverse), false)
.SetDisplay("Signal Reverse", "Flip long and short entries", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for analysis", "General");
_volume = Param(nameof(VolumePerTrade), 0.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;
_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)
{
// Skip unfinished candles to ensure decisions are made on closed bars only.
if (candle.State != CandleStates.Finished)
return;
// Wait until both indicators are fully formed before reacting.
if (_highest == null || _lowest == null || !_highest.IsFormed || !_lowest.IsFormed)
return;
// Increment the bar counter that measures freshness of the latest pivot.
if (_pivotType != PivotTypes.None && _barsSincePivot < int.MaxValue)
_barsSincePivot++;
var deviationPrice = Math.Max(GetDeviationInPrice(), _priceStep);
var canSwitch = _pivotType == PivotTypes.None || _barsSincePivot >= Backstep;
// Detect a fresh swing high if price pushes above 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 fresh swing low when price dips under 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);
}
if (_pivotType == PivotTypes.None)
return;
var isBuySignal = _pivotType == PivotTypes.High ? !SignalReverse : SignalReverse;
// Close opposite exposure before entering in the new direction.
if (isBuySignal)
{
if (Position < 0)
{
var closeVolume = Math.Abs(Position);
if (closeVolume > 0m)
BuyMarket(closeVolume);
}
}
else
{
if (Position > 0)
{
var closeVolume = Math.Abs(Position);
if (closeVolume > 0m)
SellMarket(closeVolume);
}
}
// Enter the market while the pivot is still considered fresh.
if (_barsSincePivot > Urgency)
return;
var volume = VolumePerTrade;
if (volume <= 0m)
return;
if (isBuySignal)
BuyMarket(volume);
else
SellMarket(volume);
}
// Update the stored pivot information when a new swing is confirmed.
private void SetPivot(PivotTypes type, decimal price)
{
_pivotType = type;
_pivotPrice = price;
_barsSincePivot = 0;
}
// Convert the deviation input expressed in points to a price value.
private decimal GetDeviationInPrice()
{
return Deviation * _priceStep;
}
// Determine the effective price step for translating point-based parameters.
private decimal GetEffectivePriceStep()
{
if (Security?.PriceStep is > 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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
# Pivot type constants
PIVOT_NONE = 0
PIVOT_HIGH = 1
PIVOT_LOW = 2
class zig_zag_evge_trofi_strategy(Strategy):
def __init__(self):
super(zig_zag_evge_trofi_strategy, self).__init__()
self._depth = self.Param("Depth", 17)
self._deviation = self.Param("Deviation", 7.0)
self._backstep = self.Param("Backstep", 5)
self._urgency = self.Param("Urgency", 2)
self._signal_reverse = self.Param("SignalReverse", False)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._highest = None
self._lowest = None
self._pivot_type = PIVOT_NONE
self._pivot_price = 0.0
self._bars_since_pivot = 999999
self._price_step = 1.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@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 SignalReverse(self):
return self._signal_reverse.Value
def OnStarted2(self, time):
super(zig_zag_evge_trofi_strategy, self).OnStarted2(time)
sec = self.Security
if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0:
self._price_step = float(sec.PriceStep)
else:
self._price_step = 1.0
self._highest = Highest()
self._highest.Length = self.Depth
self._lowest = Lowest()
self._lowest.Length = self.Depth
self._pivot_type = PIVOT_NONE
self._pivot_price = 0.0
self._bars_since_pivot = 999999
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._highest, self._lowest, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, highest_v, lowest_v):
if candle.State != CandleStates.Finished:
return
if not self._highest.IsFormed or not self._lowest.IsFormed:
return
if self._pivot_type != PIVOT_NONE and self._bars_since_pivot < 999999:
self._bars_since_pivot += 1
hv = float(highest_v)
lv = float(lowest_v)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
deviation_price = max(self.Deviation * self._price_step, self._price_step)
can_switch = self._pivot_type == PIVOT_NONE or self._bars_since_pivot >= self.Backstep
if high >= hv and hv > 0:
difference = high - self._pivot_price
if (self._pivot_type != PIVOT_HIGH and can_switch) or (self._pivot_type == PIVOT_HIGH and difference >= deviation_price):
self._set_pivot(PIVOT_HIGH, high)
elif low <= lv and lv > 0:
difference = self._pivot_price - low
if (self._pivot_type != PIVOT_LOW and can_switch) or (self._pivot_type == PIVOT_LOW and difference >= deviation_price):
self._set_pivot(PIVOT_LOW, low)
if self._pivot_type == PIVOT_NONE:
return
is_buy_signal = (self._pivot_type != PIVOT_HIGH) if self.SignalReverse else (self._pivot_type == PIVOT_HIGH)
if is_buy_signal:
if self.Position < 0:
self.BuyMarket()
else:
if self.Position > 0:
self.SellMarket()
if self._bars_since_pivot > self.Urgency:
return
if is_buy_signal:
self.BuyMarket()
else:
self.SellMarket()
def _set_pivot(self, pivot_type, price):
self._pivot_type = pivot_type
self._pivot_price = price
self._bars_since_pivot = 0
def OnReseted(self):
super(zig_zag_evge_trofi_strategy, self).OnReseted()
self._highest = None
self._lowest = None
self._pivot_type = PIVOT_NONE
self._pivot_price = 0.0
self._bars_since_pivot = 999999
self._price_step = 1.0
def CreateClone(self):
return zig_zag_evge_trofi_strategy()