ZigAndZag Scalpel 策略
概述
ZigAndZagScalpelStrategy 是 MetaTrader 4 "ZigAndZag" 方案(文件夹 8304)的 StockSharp 版本。 原始组合包含指标和智能交易系统,两组 ZigZag 协同运作:
- KeelOver – 长周期 ZigZag,用于识别主要趋势。
- Slalom – 短周期 ZigZag,用于寻找入场突破。
当长周期 ZigZag 转向上升时,策略跟踪最新的 Slalom 低点,等待价格向上突破该枢纽点
若干点位后买入。相反方向:趋势向下、Slalom 出现新高并且价格跌破该高点时开空。
启用 CloseOnOppositePivot 后,一旦出现相反的 Slalom 枢纽点即平仓,复刻了原始指标
移除限价箭头的行为。
策略保留了专家顾问中的“新交易日”限制。每天的交易次数受 MaxTradesPerDay 控制,
午夜会自动重置,行为与 MQL 代码中的 newday 标志一致。
工作流程
- 订阅
CandleType指定的主时间框蜡烛。 - 启动两条
ZigZagIndicator:- 深度 =
KeelOverLength,用于确定趋势方向。 - 深度 =
SlalomLength,用于捕捉入场枢纽点。
- 深度 =
- 根据最新的 KeelOver 枢纽判断趋势是向上(低点)还是向下(高点)。
- Slalom 给出新枢纽时,记录该方向并等待突破。
- 计算加权价格
(5×Close + 2×Open + High + Low) / 9。当价格相对枢纽超过BreakoutDistancePoints(换算为实际价格单位)且趋势同向时,发出市价单。 - 若趋势反转或出现相反的 Slalom 枢纽且
CloseOnOppositePivot为真,立即平掉持仓。 - 每次跨日时重置日内交易计数器。
DeviationPoints 与 Backstep 为两条 ZigZag 共用,确保与原 MT4 指标的缓冲区结构一致。
参数
| 名称 | 默认值 | 说明 |
|---|---|---|
CandleType |
15m |
构建两条 ZigZag 所用的主时间框。 |
KeelOverLength |
55 |
趋势 ZigZag 的回溯长度(原 KeelOver)。 |
SlalomLength |
17 |
入场 ZigZag 的回溯长度(原 Slalom)。 |
DeviationPoints |
5 |
认定新枢纽所需的最小点差。 |
Backstep |
3 |
相邻枢纽之间最少的条数。 |
BreakoutDistancePoints |
2 |
相对枢纽的突破距离(点)。 |
MaxTradesPerDay |
1 |
每日最大开仓次数,对应原始 newday 限制。 |
CloseOnOppositePivot |
true |
出现相反 Slalom 枢纽时是否立即平仓。 |
所有“点”参数都会乘以 Security.PriceStep 转成价格单位;若没有价格步长,默认使用 1
以便在测试环境中运行。
使用建议
- 策略只发送市价单(
BuyMarket/SellMarket)。如需止损或目标管理,可在外部增加 风险控制模块。 - 两条 ZigZag 使用同一串蜡烛,请确保数据源支持所选
CandleType。 - 保持
MaxTradesPerDay = 1即可复制原策略的“每日一次”逻辑。需要更多机会时可调大 限制。 - 将
CloseOnOppositePivot设为false可以让持仓持续到趋势真正改变,而不是响应每个 短期摆动。
与 MT4 版本的差异
- 原 EA 通过限价箭头排队等待突破;本移植使用高层 API 的市价单直接入场。
- 未移植自动仓位管理、止盈止损等逻辑。可结合 StockSharp 的风控组件自行扩展。
- 指标缓冲区 4/5/6 的功能由策略逻辑直接处理,并通过
DrawIndicator/DrawOwnTrades在图表上呈现。
推荐扩展
- 新增基于 ATR 或 ZigZag 枢纽的止损、止盈参数。
- 将
BreakoutDistancePoints设为 0,可观察原始枢纽阶梯效果。 - 如需限定交易时间,可结合
IsFormedAndOnlineAndAllowTrading等会话过滤器。
namespace StockSharp.Samples.Strategies;
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// ZigAndZagScalpel translation that trades on breakouts from short-term pivots confirmed by a long-term ZigZag trend.
/// </summary>
public class ZigAndZagScalpelStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maxTradesPerDay;
private readonly StrategyParam<bool> _closeOnOppositePivot;
private decimal _previousMajorPivot;
private decimal _lastMajorPivot;
private decimal _previousMinorPivot;
private decimal _lastMinorPivot;
private DateTime _currentDay = DateTime.MinValue;
private int _tradesToday;
private bool _trendUp;
private PivotTypes _lastMinorPivotType = PivotTypes.None;
private bool _minorPivotUsed;
/// <summary>
/// Initializes a new instance of the <see cref="ZigAndZagScalpelStrategy"/> class.
/// </summary>
public ZigAndZagScalpelStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for all calculations", "General");
_maxTradesPerDay = Param(nameof(MaxTradesPerDay), 1)
.SetDisplay("Max Trades Per Day", "Daily limit matching the original expert advisor", "Trading");
_closeOnOppositePivot = Param(nameof(CloseOnOppositePivot), true)
.SetDisplay("Close On Opposite Pivot", "Exit when the entry ZigZag prints the opposite swing", "Risk");
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Maximum number of trades allowed per trading day.
/// </summary>
public int MaxTradesPerDay
{
get => _maxTradesPerDay.Value;
set => _maxTradesPerDay.Value = value;
}
/// <summary>
/// Determines whether open positions should be closed on the opposite entry pivot.
/// </summary>
public bool CloseOnOppositePivot
{
get => _closeOnOppositePivot.Value;
set => _closeOnOppositePivot.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousMajorPivot = 0m;
_lastMajorPivot = 0m;
_previousMinorPivot = 0m;
_lastMinorPivot = 0m;
_currentDay = DateTime.MinValue;
_tradesToday = 0;
_trendUp = false;
_lastMinorPivotType = PivotTypes.None;
_minorPivotUsed = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var majorZigZag = new ZigZag { Deviation = 0.02m };
var minorZigZag = new ZigZag { Deviation = 0.005m };
var subscription = SubscribeCandles(CandleType);
subscription
.BindWithEmpty(majorZigZag, minorZigZag, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, majorZigZag);
DrawIndicator(area, minorZigZag);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal? majorValue, decimal? minorValue)
{
if (candle.State != CandleStates.Finished)
return;
UpdateDailyCounter(candle.OpenTime);
if (majorValue is not null)
UpdateMajorTrend(majorValue.Value);
if (minorValue is not null)
UpdateMinorPivot(minorValue.Value);
if (!IsFormedAndOnlineAndAllowTrading())
return;
ManageExistingPosition();
if (Position != 0)
return;
if (_minorPivotUsed)
return;
if (_lastMinorPivotType == PivotTypes.None)
return;
if (_tradesToday >= MaxTradesPerDay)
return;
var navel = CalculateNavel(candle);
if (_lastMinorPivotType == PivotTypes.Low && _trendUp)
{
if (navel > _lastMinorPivot)
{
BuyMarket();
_minorPivotUsed = true;
_tradesToday++;
}
}
else if (_lastMinorPivotType == PivotTypes.High && !_trendUp)
{
if (navel < _lastMinorPivot)
{
SellMarket();
_minorPivotUsed = true;
_tradesToday++;
}
}
}
private void UpdateDailyCounter(DateTime time)
{
var date = time.Date;
if (date == _currentDay)
return;
_currentDay = date;
_tradesToday = 0;
}
private void UpdateMajorTrend(decimal majorValue)
{
if (_lastMajorPivot == 0m)
{
_lastMajorPivot = majorValue;
_previousMajorPivot = majorValue;
return;
}
if (majorValue == _lastMajorPivot)
return;
_previousMajorPivot = _lastMajorPivot;
_lastMajorPivot = majorValue;
_trendUp = _lastMajorPivot < _previousMajorPivot;
}
private void UpdateMinorPivot(decimal minorValue)
{
if (_lastMinorPivot == 0m)
{
_lastMinorPivot = minorValue;
_previousMinorPivot = minorValue;
_lastMinorPivotType = PivotTypes.Low;
_minorPivotUsed = false;
return;
}
if (minorValue == _lastMinorPivot)
return;
_previousMinorPivot = _lastMinorPivot;
_lastMinorPivot = minorValue;
_lastMinorPivotType = _lastMinorPivot < _previousMinorPivot ? PivotTypes.Low : PivotTypes.High;
_minorPivotUsed = false;
}
private void ManageExistingPosition()
{
if (Position > 0)
{
if (!_trendUp || (CloseOnOppositePivot && _lastMinorPivotType == PivotTypes.High))
SellMarket(Position);
}
else if (Position < 0)
{
if (_trendUp || (CloseOnOppositePivot && _lastMinorPivotType == PivotTypes.Low))
BuyMarket(Position.Abs());
}
}
private static decimal CalculateNavel(ICandleMessage candle)
{
return (5m * candle.ClosePrice + 2m * candle.OpenPrice + candle.HighPrice + candle.LowPrice) / 9m;
}
private enum PivotTypes
{
None,
Low,
High
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, DateTime, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import ZigZag
# Pivot type constants
PIVOT_NONE = 0
PIVOT_LOW = 1
PIVOT_HIGH = 2
class zig_and_zag_scalpel_strategy(Strategy):
def __init__(self):
super(zig_and_zag_scalpel_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Primary timeframe for all calculations", "General")
self._max_trades_per_day = self.Param("MaxTradesPerDay", 1) \
.SetDisplay("Max Trades Per Day", "Daily limit matching the original expert advisor", "Trading")
self._close_on_opposite_pivot = self.Param("CloseOnOppositePivot", True) \
.SetDisplay("Close On Opposite Pivot", "Exit when the entry ZigZag prints the opposite swing", "Risk")
self._previous_major_pivot = Decimal(0)
self._last_major_pivot = Decimal(0)
self._previous_minor_pivot = Decimal(0)
self._last_minor_pivot = Decimal(0)
self._current_day = DateTime.MinValue
self._trades_today = 0
self._trend_up = False
self._last_minor_pivot_type = PIVOT_NONE
self._minor_pivot_used = False
@property
def CandleType(self):
return self._candle_type.Value
@property
def MaxTradesPerDay(self):
return self._max_trades_per_day.Value
@property
def CloseOnOppositePivot(self):
return self._close_on_opposite_pivot.Value
def OnStarted2(self, time):
super(zig_and_zag_scalpel_strategy, self).OnStarted2(time)
major_zigzag = ZigZag()
major_zigzag.Deviation = Decimal(0.02)
minor_zigzag = ZigZag()
minor_zigzag.Deviation = Decimal(0.005)
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindWithEmpty(major_zigzag, minor_zigzag, self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, major_zigzag)
self.DrawIndicator(area, minor_zigzag)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle, major_value, minor_value):
if candle.State != CandleStates.Finished:
return
self._update_daily_counter(candle.OpenTime)
if major_value is not None:
self._update_major_trend(major_value)
if minor_value is not None:
self._update_minor_pivot(minor_value)
if not self.IsFormedAndOnlineAndAllowTrading():
return
self._manage_existing_position()
if self.Position != 0:
return
if self._minor_pivot_used:
return
if self._last_minor_pivot_type == PIVOT_NONE:
return
if self._trades_today >= self.MaxTradesPerDay:
return
navel = self._calculate_navel(candle)
if self._last_minor_pivot_type == PIVOT_LOW and self._trend_up:
if navel > self._last_minor_pivot:
self.BuyMarket()
self._minor_pivot_used = True
self._trades_today += 1
elif self._last_minor_pivot_type == PIVOT_HIGH and not self._trend_up:
if navel < self._last_minor_pivot:
self.SellMarket()
self._minor_pivot_used = True
self._trades_today += 1
def _update_daily_counter(self, time):
date = time.Date
if date == self._current_day:
return
self._current_day = date
self._trades_today = 0
def _update_major_trend(self, major_value):
if self._last_major_pivot == Decimal(0):
self._last_major_pivot = major_value
self._previous_major_pivot = major_value
return
if major_value == self._last_major_pivot:
return
self._previous_major_pivot = self._last_major_pivot
self._last_major_pivot = major_value
self._trend_up = self._last_major_pivot < self._previous_major_pivot
def _update_minor_pivot(self, minor_value):
if self._last_minor_pivot == Decimal(0):
self._last_minor_pivot = minor_value
self._previous_minor_pivot = minor_value
self._last_minor_pivot_type = PIVOT_LOW
self._minor_pivot_used = False
return
if minor_value == self._last_minor_pivot:
return
self._previous_minor_pivot = self._last_minor_pivot
self._last_minor_pivot = minor_value
self._last_minor_pivot_type = PIVOT_LOW if self._last_minor_pivot < self._previous_minor_pivot else PIVOT_HIGH
self._minor_pivot_used = False
def _manage_existing_position(self):
if self.Position > 0:
if not self._trend_up or (self.CloseOnOppositePivot and self._last_minor_pivot_type == PIVOT_HIGH):
self.SellMarket(self.Position)
elif self.Position < 0:
if self._trend_up or (self.CloseOnOppositePivot and self._last_minor_pivot_type == PIVOT_LOW):
self.BuyMarket(abs(self.Position))
def _calculate_navel(self, candle):
return (Decimal(5) * candle.ClosePrice + Decimal(2) * candle.OpenPrice +
candle.HighPrice + candle.LowPrice) / Decimal(9)
def OnReseted(self):
super(zig_and_zag_scalpel_strategy, self).OnReseted()
self._previous_major_pivot = Decimal(0)
self._last_major_pivot = Decimal(0)
self._previous_minor_pivot = Decimal(0)
self._last_minor_pivot = Decimal(0)
self._current_day = DateTime.MinValue
self._trades_today = 0
self._trend_up = False
self._last_minor_pivot_type = PIVOT_NONE
self._minor_pivot_used = False
def CreateClone(self):
return zig_and_zag_scalpel_strategy()