ZigZag EvgeTrofi 策略
ZigZag EvgeTrofi 策略是将经典的 MetaTrader 专家顾问移植到 StockSharp 高级 API 的版本。它追踪 ZigZag 逻辑识别的最近一个摆动点,并在信号仍然有效的时间窗口内迅速执行交易。
策略思路
- 原始顾问会检查 ZigZag 缓冲区中最新的非零点,以判断最后一个摆动是高点还是低点。
- 默认情况下,高点会触发做多。启用 SignalReverse 后,方向会被反转。
- 只有在新摆动仍然“新鲜”时才允许开仓,参数 Urgency 限制了从摆动出现后可以进场的最大 K 线数量。
- 在发送新的订单之前,会先平掉反方向的持仓;在有效窗口内,策略可以在同一方向上逐步加仓。
该移植保持了原策略的反趋势特性:新高触发多单,新低触发空单。
运作流程
Highest与Lowest指标模拟 ZigZag 的深度参数,在最近Depth根 K 线内跟踪极值。- 当价格突破这些极值且幅度超过 Deviation(按价格步长折算)时,就确认新的摆动点。
- 策略记录自摆动出现以来经过的 K 线数量,超过 Urgency 后信号失效。
- 只要仍在时间窗口内,每根收盘的 K 线都会按
VolumePerTrade的数量开仓;先平掉反向仓位,保证顺利翻仓。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
Depth |
17 | 查找摆动高/低点的窗口长度,对应 ZigZag 的深度。 |
Deviation |
7 | 确认新摆动所需的最小价格变动,以价格步长为单位。 |
Backstep |
5 | 在允许切换到反向摆动之前必须经过的最小 K 线数。 |
Urgency |
2 | 摆动出现后允许进场的最大 K 线数。 |
SignalReverse |
false |
翻转高/低点对应的做多和做空信号。 |
CandleType |
5 分钟 K 线 | 用于分析的时间框架,可根据需求调整。 |
VolumePerTrade |
0.10 | 每次进场的下单数量,对应原策略的 Lot。 |
交易提示
- 策略本身不包含止损或止盈,需要结合账户或其他模块进行风险控制。
- 在
Urgency窗口内每根 K 线都可能加仓,趋势行情中仓位会迅速扩大。 - 对于波动较大的标的,建议提高
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()