在 GitHub 上查看
ZigZag EA
概述
本策略复刻了 MT5 原版 “ZigZag EA” 的核心思想:等待三个连续的 ZigZag 枢轴点,在前两个枢轴之间的区间两端同时布置买入止损和卖出止损单。该移植使用 StockSharp 的高级 API,只处理已经收盘的 K 线。最近的两个枢轴构成交易通道,最新的枢轴(MQL 源码中的 “room 0”)必须保持在通道内部,策略才会激活挂单。整个流程完全对称,让市场自行选择突破方向。
指标与数据
- Highest / Lowest: 由于 StockSharp 没有内置 ZigZag 指标,策略通过滚动高点与低点来模拟 ZigZag 行为。当价格突破极值、趋势反转时,内部的枢轴缓冲区会像原始 EA 读取 ZigZag 缓冲区一样更新。
- K 线: 可以指定任意蜡烛类型(默认 1 分钟),策略只在蜡烛收盘之后做出决策,以便兼容回测和实盘。
交易逻辑
- 记录最新三个 ZigZag 枢轴。前两个枢轴决定区间高低值(
high/low),最新枢轴必须位于通道内,并且距离上下边界大于经纪商的最小止损距离。
- 检查通道高度是否处于
MinCorridorPips 与 MaxCorridorPips 之间。过窄的区间被视为噪声,过宽的区间会导致过大的止损,因此也被过滤。
- 当通道有效且没有持仓时,同时挂出两个止损单:
- Buy stop:
high + EntryOffsetPips。
- Sell stop:
low - EntryOffsetPips。
- 止损与止盈的计算完全沿用 MQL 版本的斐波那契规则:
FiboStopLoss 按照斐波那契比例放置止损,FiboTakeProfit 先投影目标再扣除通道高度。所有价格都会按照交易品种的最小价位波动幅度进行取整。
- 某一侧触发后,另一侧挂单立即撤销,并登记相应的止损/止盈委托。若启用跟踪止损,价格每向有利方向移动
TrailingStepPips,止损就会重新登记到新的位置。
- 当仓位归零时,策略自动恢复到等待下一个 ZigZag 通道的状态。
风险与订单管理
- 止损与止盈均为真实的委托单,由券商撮合执行,因此可以自然处理跳空与滑点。
- 跟踪止损继承自原始 EA:只有当浮盈超过
TrailingStopPips + TrailingStepPips 时才会激活,此后每当价格再向有利方向移动一个“步长”就重新注册止损。
- 仓位大小由基类
Strategy 的 Volume 参数决定。原版中根据风险百分比调整手数的部分没有移植,因为在 StockSharp 中仓位管理通常由外部组件完成。
交易时段过滤
- 仅在
StartHour:StartMinute 至 StopHour:StopMinute 区间内交易。如果结束时间小于开始时间,则视为跨越午夜的会话。
- 一旦离开交易时段,所有挂单都会被取消,与 MQL 版本保持一致。
参数
| 名称 |
说明 |
默认值 |
CandleType |
用于分析的蜡烛类型。 |
1 分钟蜡烛 |
ZigZagDepth |
识别枢轴所用的回溯长度。 |
12 |
EntryOffsetPips |
挂单相对通道边界的偏移。 |
5 |
MinCorridorPips |
可接受的最小通道高度。 |
20 |
MaxCorridorPips |
可接受的最大通道高度。 |
100 |
FiboStopLoss |
计算止损所用的斐波那契比例。 |
61.8% |
FiboTakeProfit |
计算止盈所用的斐波那契比例。 |
161.8% |
StartHour / StartMinute |
交易窗口起点。 |
00:01 |
StopHour / StopMinute |
交易窗口终点。 |
23:59 |
TrailingStopPips |
跟踪止损的基础距离。 |
5 |
TrailingStepPips |
每次移动止损所需的最小增量。 |
5 |
DrawCorridorLevels |
是否在图表上绘制通道标记。 |
false |
实现说明
- 点值由最小报价单位推算而来。对于 3 位或 5 位报价的品种,会自动将最小步长乘以 10,以重现原始 EA 的
adjusted_point 处理。
- 代码完全使用高层封装的下单方法(
BuyStop、SellStop、SellLimit、BuyLimit),符合项目规范。
- 源码中的注释全部为英文,详细说明在三个 README 文件中分别提供英文、俄文和中文版本。
- 按要求未创建 Python 版本,目录中只有 C# 实现。
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// ZigZag breakout strategy using highest/lowest channels.
/// Buys on breakout above recent high, sells on breakdown below recent low.
/// </summary>
public class ZigZagEAStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _depth;
private decimal? _prevHigh;
private decimal? _prevLow;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int Depth
{
get => _depth.Value;
set => _depth.Value = value;
}
public ZigZagEAStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe", "General");
_depth = Param(nameof(Depth), 20)
.SetGreaterThanZero()
.SetDisplay("Depth", "Channel lookback period", "Indicators");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevHigh = null;
_prevLow = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevHigh = null;
_prevLow = null;
var highest = new Highest { Length = Depth };
var lowest = new Lowest { Length = Depth };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(highest, lowest, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, highest);
DrawIndicator(area, lowest);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal high, decimal low)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
{
_prevHigh = high;
_prevLow = low;
return;
}
if (_prevHigh == null || _prevLow == null)
{
_prevHigh = high;
_prevLow = low;
return;
}
var close = candle.ClosePrice;
// Breakout above channel high
if (close > _prevHigh.Value && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
}
// Breakdown below channel low
else if (close < _prevLow.Value && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
}
_prevHigh = high;
_prevLow = low;
}
}
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
class zig_zag_ea_strategy(Strategy):
def __init__(self):
super(zig_zag_ea_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._depth = self.Param("Depth", 20) \
.SetDisplay("Depth", "Channel lookback period", "Indicators")
self._prev_high = None
self._prev_low = None
@property
def CandleType(self):
return self._candle_type.Value
@property
def Depth(self):
return self._depth.Value
def OnReseted(self):
super(zig_zag_ea_strategy, self).OnReseted()
self._prev_high = None
self._prev_low = None
def OnStarted2(self, time):
super(zig_zag_ea_strategy, self).OnStarted2(time)
self._prev_high = None
self._prev_low = None
highest = Highest()
highest.Length = self.Depth
lowest = Lowest()
lowest.Length = self.Depth
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(highest, lowest, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, highest)
self.DrawIndicator(area, lowest)
self.DrawOwnTrades(area)
def _on_process(self, candle, high_value, low_value):
if candle.State != CandleStates.Finished:
return
hv = float(high_value)
lv = float(low_value)
if self._prev_high is None or self._prev_low is None:
self._prev_high = hv
self._prev_low = lv
return
close = float(candle.ClosePrice)
# Breakout above channel high
if close > self._prev_high and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
# Breakdown below channel low
elif close < self._prev_low and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._prev_high = hv
self._prev_low = lv
def CreateClone(self):
return zig_zag_ea_strategy()