MACD零轴过滤止盈策略
概述
本策略复刻 MetaTrader 5 专家顾问“Robot_MACD”的核心思想:利用 MACD 与信号线的交叉,并结合零轴过滤条件进行交易。策略仅针对单一标的,在确认 MACD 位置位于零轴的特定一侧后入场,并为每笔交易附加固定点数的止盈目标,与原始 EA 的点差止盈设定保持一致。
数据与指标
- 核心数据:单一 K 线订阅(默认 5 分钟周期),可通过参数
CandleType自由调整以适应不同市场。 - 指标设置:
MovingAverageConvergenceDivergenceSignal(MACD + 信号线 + 柱状图)。默认参数为 12/26 EMA 与 9 周期信号线,与 MQL 输入保持一致。
交易逻辑
- 等待 MACD 指标给出当前值与上一周期数值。
- 根据 MACD 与信号线的相对位置识别交叉:
- 看多交叉:上一周期 MACD ≤ 上一周期信号线,且当前周期 MACD > 当前周期信号线。
- 看空交叉:上一周期 MACD ≥ 上一周期信号线,且当前周期 MACD < 当前周期信号线。
- 持仓管理:
- 持有多单时出现看空交叉立即平仓。
- 持有空单时出现看多交叉立即平仓。
- 入场条件(仅在无持仓且资金充足时):
- 看多交叉且当前 MACD、信号线均位于零轴下方时买入做多。
- 看空交叉且当前 MACD、信号线均位于零轴上方时卖出做空。
- 调用
StartProtection以绝对价格单位设置止盈距离,距离 =TakeProfitPoints× 品种最小价格步长,完全对应 EA 中的点值止盈。
风险控制
- 每笔订单都会附带固定止盈(
TakeProfitPoints),策略不设置止损,以保持与原版 EA 一致。 - 在下单前检查组合市值是否至少为
MinimumCapitalPerVolume * VolumePerTrade,以模拟 MQL 中FreeMargin() < 1000 * Lots的保证金过滤。
参数说明
| 参数 | 说明 | 默认值 |
|---|---|---|
MacdFast |
MACD 快速 EMA 周期 | 12 |
MacdSlow |
MACD 慢速 EMA 周期 | 26 |
MacdSignal |
信号线平滑周期 | 9 |
TakeProfitPoints |
止盈点数(按价格点计算) | 300 |
VolumePerTrade |
每次入场的交易量(手数) | 1 |
MinimumCapitalPerVolume |
每单位交易量所需的最小组合价值 | 1000 |
CandleType |
驱动 MACD 的 K 线类型/周期 | 5 分钟 K 线 |
实施细节
- 使用
BuyMarket/SellMarket下达市价单,与 MQL 代码中的CTrade行为一致。 - 零轴过滤保证只有在 MACD 柱状图位于同一侧时才会开仓,避免逆势信号。
- 资金校验依赖
Portfolio.CurrentValue;若运行环境未提供该值,校验会默认通过,从而保持策略在模拟或历史回测中的可用性。 - 若宿主平台支持图表,策略会绘制 K 线、MACD 指标以及成交标记,便于可视化分析。
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// MACD cross strategy with zero line filter and fixed take profit.
/// </summary>
public class MacdZeroFilterTakeProfitStrategy : Strategy
{
private readonly StrategyParam<int> _macdFast;
private readonly StrategyParam<int> _macdSlow;
private readonly StrategyParam<int> _macdSignal;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<decimal> _volumePerTrade;
private readonly StrategyParam<decimal> _minimumCapitalPerVolume;
private readonly StrategyParam<DataType> _candleType;
private MovingAverageConvergenceDivergenceSignal _macd = null!;
private decimal? _previousMacd;
private decimal? _previousSignal;
/// <summary>
/// Initializes a new instance of the <see cref="MacdZeroFilterTakeProfitStrategy"/> class.
/// </summary>
public MacdZeroFilterTakeProfitStrategy()
{
_macdFast = Param(nameof(MacdFast), 12)
.SetGreaterThanZero()
.SetDisplay("MACD Fast Period", "Fast EMA period used by MACD", "Indicators")
.SetOptimize(8, 20, 2);
_macdSlow = Param(nameof(MacdSlow), 26)
.SetGreaterThanZero()
.SetDisplay("MACD Slow Period", "Slow EMA period used by MACD", "Indicators")
.SetOptimize(20, 40, 2);
_macdSignal = Param(nameof(MacdSignal), 9)
.SetGreaterThanZero()
.SetDisplay("MACD Signal Period", "Signal smoothing period for MACD", "Indicators")
.SetOptimize(6, 15, 1);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 300)
.SetGreaterThanZero()
.SetDisplay("Take Profit (points)", "Take profit distance expressed in price points", "Risk Management")
.SetOptimize(100, 600, 50);
_volumePerTrade = Param(nameof(VolumePerTrade), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Number of lots to trade on each entry", "General")
.SetOptimize(0.5m, 5m, 0.5m);
_minimumCapitalPerVolume = Param(nameof(MinimumCapitalPerVolume), 1000m)
.SetGreaterThanZero()
.SetDisplay("Capital per Volume", "Minimum portfolio value required per traded lot", "Risk Management")
.SetOptimize(500m, 5000m, 500m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for MACD calculations", "General");
}
/// <summary>
/// Fast EMA period used by MACD.
/// </summary>
public int MacdFast
{
get => _macdFast.Value;
set => _macdFast.Value = value;
}
/// <summary>
/// Slow EMA period used by MACD.
/// </summary>
public int MacdSlow
{
get => _macdSlow.Value;
set => _macdSlow.Value = value;
}
/// <summary>
/// Signal smoothing period for MACD.
/// </summary>
public int MacdSignal
{
get => _macdSignal.Value;
set => _macdSignal.Value = value;
}
/// <summary>
/// Take profit distance expressed in price points.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Number of lots to trade on each entry.
/// </summary>
public decimal VolumePerTrade
{
get => _volumePerTrade.Value;
set => _volumePerTrade.Value = value;
}
/// <summary>
/// Minimum portfolio value required per traded lot.
/// </summary>
public decimal MinimumCapitalPerVolume
{
get => _minimumCapitalPerVolume.Value;
set => _minimumCapitalPerVolume.Value = value;
}
/// <summary>
/// Timeframe for MACD calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousMacd = null;
_previousSignal = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = VolumePerTrade;
_macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = MacdFast },
LongMa = { Length = MacdSlow },
},
SignalMa = { Length = MacdSignal }
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_macd, ProcessCandle)
.Start();
var step = Security?.PriceStep ?? 1m;
StartProtection(
takeProfit: new Unit(TakeProfitPoints * step, UnitTypes.Absolute),
stopLoss: new Unit(0m),
useMarketOrders: true);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _macd);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue)
{
// Use only completed candles to avoid double counting signals.
if (candle.State != CandleStates.Finished)
return;
// Ensure the strategy is ready to trade and has a working connector.
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (macdValue is not MovingAverageConvergenceDivergenceSignalValue macdTyped)
return;
if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
return;
if (_previousMacd is null || _previousSignal is null)
{
_previousMacd = macd;
_previousSignal = signal;
return;
}
var previousMacd = _previousMacd.Value;
var previousSignal = _previousSignal.Value;
var crossedUp = previousMacd <= previousSignal && macd > signal;
var crossedDown = previousMacd >= previousSignal && macd < signal;
if (Position > 0 && crossedDown)
{
// Close long position on bearish crossover.
SellMarket();
}
else if (Position < 0 && crossedUp)
{
// Close short position on bullish crossover.
BuyMarket();
}
if (Position == 0)
{
var requiredCapital = MinimumCapitalPerVolume * VolumePerTrade;
if (HasEnoughCapital(requiredCapital))
{
if (crossedUp && macd < 0m && signal < 0m)
{
// Enter long when MACD crosses above signal under the zero line.
BuyMarket();
}
else if (crossedDown && macd > 0m && signal > 0m)
{
// Enter short when MACD crosses below signal above the zero line.
SellMarket();
}
}
}
_previousMacd = macd;
_previousSignal = signal;
}
private bool HasEnoughCapital(decimal requiredCapital)
{
var currentValue = Portfolio?.CurrentValue;
if (currentValue is null)
return true;
if (currentValue.Value >= requiredCapital)
return true;
this.LogInfo($"Insufficient capital: available {currentValue.Value:F2}, required {requiredCapital:F2}.");
return false;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
class macd_zero_filter_take_profit_strategy(Strategy):
def __init__(self):
super(macd_zero_filter_take_profit_strategy, self).__init__()
self._macd_fast = self.Param("MacdFast", 12)
self._macd_slow = self.Param("MacdSlow", 26)
self._macd_signal = self.Param("MacdSignal", 9)
self._take_profit_points = self.Param("TakeProfitPoints", 300)
self._volume_per_trade = self.Param("VolumePerTrade", 1.0)
self._minimum_capital_per_volume = self.Param("MinimumCapitalPerVolume", 1000.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._previous_macd = None
self._previous_signal = None
@property
def MacdFast(self):
return self._macd_fast.Value
@MacdFast.setter
def MacdFast(self, value):
self._macd_fast.Value = value
@property
def MacdSlow(self):
return self._macd_slow.Value
@MacdSlow.setter
def MacdSlow(self, value):
self._macd_slow.Value = value
@property
def MacdSignal(self):
return self._macd_signal.Value
@MacdSignal.setter
def MacdSignal(self, value):
self._macd_signal.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def VolumePerTrade(self):
return self._volume_per_trade.Value
@VolumePerTrade.setter
def VolumePerTrade(self, value):
self._volume_per_trade.Value = value
@property
def MinimumCapitalPerVolume(self):
return self._minimum_capital_per_volume.Value
@MinimumCapitalPerVolume.setter
def MinimumCapitalPerVolume(self, value):
self._minimum_capital_per_volume.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(macd_zero_filter_take_profit_strategy, self).OnStarted2(time)
self._previous_macd = None
self._previous_signal = None
self.Volume = self._volume_per_trade.Value
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self.MacdFast
macd.Macd.LongMa.Length = self.MacdSlow
macd.SignalMa.Length = self.MacdSignal
self._macd_ind = macd
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(macd, self.ProcessCandle).Start()
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
tp_distance = int(self.TakeProfitPoints) * step
self.StartProtection(
takeProfit=Unit(tp_distance, UnitTypes.Absolute),
stopLoss=Unit(0),
useMarketOrders=True)
def ProcessCandle(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
macd_val = macd_value.Macd
signal_val = macd_value.Signal
if macd_val is None or signal_val is None:
return
macd_f = float(macd_val)
signal_f = float(signal_val)
if self._previous_macd is None or self._previous_signal is None:
self._previous_macd = macd_f
self._previous_signal = signal_f
return
prev_macd = self._previous_macd
prev_signal = self._previous_signal
crossed_up = prev_macd <= prev_signal and macd_f > signal_f
crossed_down = prev_macd >= prev_signal and macd_f < signal_f
if self.Position > 0 and crossed_down:
self.SellMarket()
elif self.Position < 0 and crossed_up:
self.BuyMarket()
if self.Position == 0:
required_capital = float(self.MinimumCapitalPerVolume) * float(self.VolumePerTrade)
portfolio = self.Portfolio
current_value = float(portfolio.CurrentValue) if portfolio is not None and portfolio.CurrentValue is not None else 0.0
has_capital = current_value >= required_capital or portfolio is None or portfolio.CurrentValue is None
if has_capital:
if crossed_up and macd_f < 0.0 and signal_f < 0.0:
self.BuyMarket()
elif crossed_down and macd_f > 0.0 and signal_f > 0.0:
self.SellMarket()
self._previous_macd = macd_f
self._previous_signal = signal_f
def OnReseted(self):
super(macd_zero_filter_take_profit_strategy, self).OnReseted()
self._previous_macd = None
self._previous_signal = None
def CreateClone(self):
return macd_zero_filter_take_profit_strategy()