MACD Sample Classic 策略
该策略使用 StockSharp 高级 API 复刻 MetaTrader 4 中的 "MACD Sample" 智能交易系统。它仅在单一标的上双向交易:当 MACD 线在合适的零轴一侧穿越信号线且 26 周期 EMA 走势配合时入场。固定止盈与跟踪止损通过 StartProtection 模块实现,与原版 EA 的风险控制保持一致。
交易逻辑
- 等待至少 100 根收盘完成的 K 线,保证 MACD 与 EMA 拥有足够历史数据。
- 计算标准 MACD(12, 26, 9) 以及其信号线,并额外计算周期为
TrendMaPeriod(默认 26)的指数移动平均作为趋势过滤器。 - 做多入场 —— 仅在当前无持仓时触发。MACD 必须位于零轴下方并向上穿越信号线,上一根 K 线中 MACD 低于信号线,当前绝对值超过
MacdOpenLevel(以价格点表示),同时趋势 EMA 向上。 - 做空入场 —— 条件对称:MACD 位于零轴上方并向下穿越信号线,上一根 K 线 MACD 高于信号线,当前值超过
MacdOpenLevel,EMA 向下。 - 平多 —— 当 MACD 在零轴上方重新跌破信号线且数值高于
MacdCloseLevel时平仓,也可能提前被StartProtection的止盈或跟踪止损触发。 - 平空 —— 当 MACD 在零轴下方重新上穿信号线且绝对值高于
MacdCloseLevel时平仓,同样受保护模块控制。
策略始终最多持有一笔仓位,全部使用按 Volume 属性指定手数的市价单。所有“点数”参数会自动乘以标的的最小价位变动(PriceStep),从而模拟 MetaTrader 的 Point 定义。
参数说明
| 参数 | 描述 | 默认值 | 备注 |
|---|---|---|---|
FastEmaPeriod |
MACD 的快 EMA 周期 | 12 | 可优化范围 6…18。 |
SlowEmaPeriod |
MACD 的慢 EMA 周期 | 26 | 可优化范围 20…32。 |
SignalPeriod |
MACD 信号线的 EMA 周期 | 9 | 可优化范围 5…13。 |
TrendMaPeriod |
趋势过滤用 EMA 周期 | 26 | 可优化范围 20…40。 |
MacdOpenLevel |
入场阈值(MACD 点数) | 3 | 对应 MT4 中的 MACDOpenLevel。 |
MacdCloseLevel |
离场阈值(MACD 点数) | 2 | 对应 MACDCloseLevel。 |
TakeProfitPoints |
止盈距离(价格点) | 50 | 设为 0 可关闭止盈。 |
TrailingStopPoints |
跟踪止损距离(价格点) | 30 | 设为 0 可关闭跟踪止损。 |
CandleType |
指标使用的 K 线类型 | 5 分钟时间框架 | 支持任意 StockSharp 蜡烛类型。 |
实现细节
- 通过
BindEx/Bind将 MACD 与 EMA 绑定到蜡烛订阅,StockSharp 会自动提供已完成的指标值,无需手动缓存。 - 只有在
IsFormedAndOnlineAndAllowTrading()返回真时才评估信号,避免在加载历史或离线状态下误下单。 - 所有以“点”为单位的阈值都会按
Security.PriceStep缩放,精准复现 MetaTrader 的点值算法。 StartProtection把固定止盈和跟踪止损交给交易所侧保护单,可通过参数启用或关闭任一模块。- 使用
LogInfo记录每一次决策,方便与原始 EA 的回测报表进行对比验证。
使用建议
- 原策略主要针对外汇主流货币对的日内交易,建议从相同市场与时间框架入手,再按标的特性微调参数。
- 如果标的的最小跳动值较特殊,请确认
Security.PriceStep已正确配置;否则系统会使用默认值 1.0。 - 需要组合层面风控时,可结合 StockSharp 的投资组合保护功能或外部资金管理模块。
标签
- 趋势跟随
- 动量
- MACD 交叉
- 日内交易(默认 5 分钟)
- 止盈与跟踪止损
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 strategy that replicates the original MetaTrader "MACD Sample" expert advisor behaviour.
/// </summary>
public class MacdSampleClassicStrategy : Strategy
{
private readonly StrategyParam<int> _fastEmaPeriod;
private readonly StrategyParam<int> _slowEmaPeriod;
private readonly StrategyParam<int> _signalPeriod;
private readonly StrategyParam<int> _trendMaPeriod;
private readonly StrategyParam<decimal> _macdOpenLevel;
private readonly StrategyParam<decimal> _macdCloseLevel;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _minimumHistoryCandles;
private decimal _pointSize;
private decimal? _prevMacd;
private decimal? _prevSignal;
private decimal? _trendMaCurrent;
private decimal? _trendMaPrevious;
private int _finishedCandles;
private DateTimeOffset? _lastProcessedTime;
/// <summary>
/// Fast EMA period for the MACD indicator.
/// </summary>
public int FastEmaPeriod
{
get => _fastEmaPeriod.Value;
set => _fastEmaPeriod.Value = value;
}
/// <summary>
/// Slow EMA period for the MACD indicator.
/// </summary>
public int SlowEmaPeriod
{
get => _slowEmaPeriod.Value;
set => _slowEmaPeriod.Value = value;
}
/// <summary>
/// Signal line period for the MACD indicator.
/// </summary>
public int SignalPeriod
{
get => _signalPeriod.Value;
set => _signalPeriod.Value = value;
}
/// <summary>
/// Period of the trend EMA used as direction filter.
/// </summary>
public int TrendMaPeriod
{
get => _trendMaPeriod.Value;
set => _trendMaPeriod.Value = value;
}
/// <summary>
/// Threshold for MACD entries expressed in points (price steps).
/// </summary>
public decimal MacdOpenLevel
{
get => _macdOpenLevel.Value;
set => _macdOpenLevel.Value = value;
}
/// <summary>
/// Threshold for MACD exits expressed in points (price steps).
/// </summary>
public decimal MacdCloseLevel
{
get => _macdCloseLevel.Value;
set => _macdCloseLevel.Value = value;
}
/// <summary>
/// Take profit distance measured in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance measured in price points.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Number of finished candles required before the strategy begins trading.
/// </summary>
public int MinimumHistoryCandles
{
get => _minimumHistoryCandles.Value;
set => _minimumHistoryCandles.Value = value;
}
/// <summary>
/// Initialize default parameters for the MACD Sample strategy.
/// </summary>
public MacdSampleClassicStrategy()
{
Volume = 1;
_fastEmaPeriod = Param(nameof(FastEmaPeriod), 12)
.SetDisplay("Fast EMA", "Fast EMA period for MACD", "Indicators")
.SetOptimize(6, 18, 2);
_slowEmaPeriod = Param(nameof(SlowEmaPeriod), 26)
.SetDisplay("Slow EMA", "Slow EMA period for MACD", "Indicators")
.SetOptimize(20, 32, 2);
_signalPeriod = Param(nameof(SignalPeriod), 9)
.SetDisplay("Signal EMA", "Signal EMA period for MACD", "Indicators")
.SetOptimize(5, 13, 2);
_trendMaPeriod = Param(nameof(TrendMaPeriod), 26)
.SetDisplay("Trend EMA", "EMA period used for directional filter", "Indicators")
.SetOptimize(20, 40, 2);
_macdOpenLevel = Param(nameof(MacdOpenLevel), 0m)
.SetDisplay("MACD Open", "Entry threshold in MACD points", "Signals")
.SetOptimize(1m, 5m, 1m);
_macdCloseLevel = Param(nameof(MacdCloseLevel), 0m)
.SetDisplay("MACD Close", "Exit threshold in MACD points", "Signals")
.SetOptimize(1m, 4m, 1m);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 50m)
.SetDisplay("Take Profit", "Take profit distance in price points", "Risk")
.SetOptimize(20m, 100m, 10m);
_trailingStopPoints = Param(nameof(TrailingStopPoints), 30m)
.SetDisplay("Trailing Stop", "Trailing stop distance in price points", "Risk")
.SetOptimize(10m, 60m, 10m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle type used for analysis", "General");
_minimumHistoryCandles = Param(nameof(MinimumHistoryCandles), 30)
.SetDisplay("Warm-up candles", "Number of finished candles required before trading starts", "General")
.SetGreaterThanZero();
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pointSize = 0m;
_prevMacd = null;
_prevSignal = null;
_trendMaCurrent = null;
_trendMaPrevious = null;
_finishedCandles = 0;
_lastProcessedTime = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointSize = Security?.PriceStep ?? 1m;
// Configure indicators exactly as in the original expert advisor.
var macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = FastEmaPeriod },
LongMa = { Length = SlowEmaPeriod },
},
SignalMa = { Length = SignalPeriod }
};
var trendMa = new ExponentialMovingAverage { Length = TrendMaPeriod };
// Subscribe to candles and bind indicators for automatic updates.
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(macd, ProcessMacdValues);
subscription.Bind(trendMa, ProcessTrendMaValue);
subscription.Start();
// Visualize price, indicators and trades when a chart is available.
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, macd);
DrawIndicator(area, trendMa);
DrawOwnTrades(area);
}
var takeProfitDistance = TakeProfitPoints * _pointSize;
var trailingDistance = TrailingStopPoints * _pointSize;
if (takeProfitDistance > 0m || trailingDistance > 0m)
{
StartProtection(
takeProfitDistance > 0m ? new Unit(takeProfitDistance, UnitTypes.Absolute) : null,
trailingDistance > 0m ? new Unit(trailingDistance, UnitTypes.Absolute) : null,
isStopTrailing: trailingDistance > 0m);
}
}
private void ProcessTrendMaValue(ICandleMessage candle, decimal maValue)
{
if (candle.State != CandleStates.Finished)
return;
// Keep current and previous EMA values for the directional filter.
_trendMaPrevious = _trendMaCurrent;
_trendMaCurrent = maValue;
}
private void ProcessMacdValues(ICandleMessage candle, IIndicatorValue macdValue)
{
if (candle.State != CandleStates.Finished)
return;
// Avoid duplicate processing for the same candle.
if (_lastProcessedTime != candle.OpenTime)
{
_lastProcessedTime = candle.OpenTime;
_finishedCandles++;
}
// indicators checked below
if (_finishedCandles < MinimumHistoryCandles)
return;
var macdSignal = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
if (macdSignal.Macd is not decimal macdCurrent ||
macdSignal.Signal is not decimal signalCurrent)
{
return;
}
if (_prevMacd is not decimal macdPrevious ||
_prevSignal is not decimal signalPrevious ||
_trendMaCurrent is not decimal trendMaCurrent ||
_trendMaPrevious is not decimal trendMaPrevious)
{
_prevMacd = macdCurrent;
_prevSignal = signalCurrent;
return;
}
var macdOpenThreshold = MacdOpenLevel * _pointSize;
var macdCloseThreshold = MacdCloseLevel * _pointSize;
// Determine trend direction using EMA slope.
var isTrendUp = trendMaCurrent > trendMaPrevious;
var isTrendDown = trendMaCurrent < trendMaPrevious;
var buySignal = macdCurrent < 0m &&
macdCurrent > signalCurrent &&
macdPrevious < signalPrevious &&
Math.Abs(macdCurrent) > macdOpenThreshold &&
isTrendUp;
var sellSignal = macdCurrent > 0m &&
macdCurrent < signalCurrent &&
macdPrevious > signalPrevious &&
macdCurrent > macdOpenThreshold &&
isTrendDown;
var exitLongSignal = macdCurrent > 0m &&
macdCurrent < signalCurrent &&
macdPrevious > signalPrevious &&
macdCurrent > macdCloseThreshold;
var exitShortSignal = macdCurrent < 0m &&
macdCurrent > signalCurrent &&
macdPrevious < signalPrevious &&
Math.Abs(macdCurrent) > macdCloseThreshold;
if (buySignal && Position == 0m)
{
// MACD crossed up in negative territory and EMA confirms uptrend.
BuyMarket();
LogInfo($"Open long: MACD {macdCurrent:F5} above signal {signalCurrent:F5}.");
}
else if (sellSignal && Position == 0m)
{
// MACD crossed down in positive territory and EMA confirms downtrend.
SellMarket();
LogInfo($"Open short: MACD {macdCurrent:F5} below signal {signalCurrent:F5}.");
}
else if (exitLongSignal && Position > 0m)
{
// MACD crossed back below the signal line in positive zone - close long.
SellMarket();
LogInfo($"Close long: MACD {macdCurrent:F5} dropped under signal {signalCurrent:F5}.");
}
else if (exitShortSignal && Position < 0m)
{
// MACD crossed back above the signal line in negative zone - close short.
BuyMarket();
LogInfo($"Close short: MACD {macdCurrent:F5} rose above signal {signalCurrent:F5}.");
}
_prevMacd = macdCurrent;
_prevSignal = signalCurrent;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
class macd_sample_classic_strategy(Strategy):
"""Classic MACD strategy replicating the original MetaTrader MACD Sample expert advisor.
Uses MACD crossover with EMA trend filter, StartProtection for TP/trailing."""
def __init__(self):
super(macd_sample_classic_strategy, self).__init__()
self._fast_ema_period = self.Param("FastEmaPeriod", 12) \
.SetDisplay("Fast EMA", "Fast EMA period for MACD", "Indicators")
self._slow_ema_period = self.Param("SlowEmaPeriod", 26) \
.SetDisplay("Slow EMA", "Slow EMA period for MACD", "Indicators")
self._signal_period = self.Param("SignalPeriod", 9) \
.SetDisplay("Signal EMA", "Signal EMA period for MACD", "Indicators")
self._trend_ma_period = self.Param("TrendMaPeriod", 26) \
.SetDisplay("Trend EMA", "EMA period used for directional filter", "Indicators")
self._macd_open_level = self.Param("MacdOpenLevel", 0.0) \
.SetDisplay("MACD Open", "Entry threshold in MACD points", "Signals")
self._macd_close_level = self.Param("MacdCloseLevel", 0.0) \
.SetDisplay("MACD Close", "Exit threshold in MACD points", "Signals")
self._take_profit_points = self.Param("TakeProfitPoints", 50.0) \
.SetDisplay("Take Profit", "Take profit distance in price points", "Risk")
self._trailing_stop_points = self.Param("TrailingStopPoints", 30.0) \
.SetDisplay("Trailing Stop", "Trailing stop distance in price points", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Candle type used for analysis", "General")
self._minimum_history_candles = self.Param("MinimumHistoryCandles", 30) \
.SetGreaterThanZero() \
.SetDisplay("Warm-up candles", "Number of finished candles required before trading starts", "General")
self._point_size = 0.0
self._prev_macd = None
self._prev_signal = None
self._trend_ma_current = None
self._trend_ma_previous = None
self._finished_candles = 0
self._last_processed_time = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def FastEmaPeriod(self):
return self._fast_ema_period.Value
@property
def SlowEmaPeriod(self):
return self._slow_ema_period.Value
@property
def SignalPeriod(self):
return self._signal_period.Value
@property
def TrendMaPeriod(self):
return self._trend_ma_period.Value
@property
def MacdOpenLevel(self):
return self._macd_open_level.Value
@property
def MacdCloseLevel(self):
return self._macd_close_level.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def TrailingStopPoints(self):
return self._trailing_stop_points.Value
@property
def MinimumHistoryCandles(self):
return self._minimum_history_candles.Value
def OnReseted(self):
super(macd_sample_classic_strategy, self).OnReseted()
self._point_size = 0.0
self._prev_macd = None
self._prev_signal = None
self._trend_ma_current = None
self._trend_ma_previous = None
self._finished_candles = 0
self._last_processed_time = None
def OnStarted2(self, time):
super(macd_sample_classic_strategy, self).OnStarted2(time)
self._point_size = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps > 0:
self._point_size = ps
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self.FastEmaPeriod
macd.Macd.LongMa.Length = self.SlowEmaPeriod
macd.SignalMa.Length = self.SignalPeriod
trend_ma = ExponentialMovingAverage()
trend_ma.Length = self.TrendMaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(macd, self._process_macd_values)
subscription.Bind(trend_ma, self._process_trend_ma_value)
subscription.Start()
tp_distance = float(self.TakeProfitPoints) * self._point_size
trailing_distance = float(self.TrailingStopPoints) * self._point_size
if tp_distance > 0 or trailing_distance > 0:
tp_unit = Unit(tp_distance, UnitTypes.Absolute) if tp_distance > 0 else None
sl_unit = Unit(trailing_distance, UnitTypes.Absolute) if trailing_distance > 0 else None
self.StartProtection(tp_unit, sl_unit, trailing_distance > 0)
def _process_trend_ma_value(self, candle, ma_value):
if candle.State != CandleStates.Finished:
return
self._trend_ma_previous = self._trend_ma_current
self._trend_ma_current = float(ma_value)
def _process_macd_values(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
if self._last_processed_time != candle.OpenTime:
self._last_processed_time = candle.OpenTime
self._finished_candles += 1
if self._finished_candles < self.MinimumHistoryCandles:
return
macd_main_raw = macd_value.Macd
signal_raw = macd_value.Signal
if macd_main_raw is None or signal_raw is None:
return
macd_current = float(macd_main_raw)
signal_current = float(signal_raw)
if self._prev_macd is None or self._prev_signal is None or \
self._trend_ma_current is None or self._trend_ma_previous is None:
self._prev_macd = macd_current
self._prev_signal = signal_current
return
macd_previous = self._prev_macd
signal_previous = self._prev_signal
trend_ma_current = self._trend_ma_current
trend_ma_previous = self._trend_ma_previous
macd_open_threshold = float(self.MacdOpenLevel) * self._point_size
macd_close_threshold = float(self.MacdCloseLevel) * self._point_size
is_trend_up = trend_ma_current > trend_ma_previous
is_trend_down = trend_ma_current < trend_ma_previous
buy_signal = macd_current < 0 and \
macd_current > signal_current and \
macd_previous < signal_previous and \
abs(macd_current) > macd_open_threshold and \
is_trend_up
sell_signal = macd_current > 0 and \
macd_current < signal_current and \
macd_previous > signal_previous and \
macd_current > macd_open_threshold and \
is_trend_down
exit_long_signal = macd_current > 0 and \
macd_current < signal_current and \
macd_previous > signal_previous and \
macd_current > macd_close_threshold
exit_short_signal = macd_current < 0 and \
macd_current > signal_current and \
macd_previous < signal_previous and \
abs(macd_current) > macd_close_threshold
if buy_signal and self.Position == 0:
self.BuyMarket()
elif sell_signal and self.Position == 0:
self.SellMarket()
elif exit_long_signal and self.Position > 0:
self.SellMarket()
elif exit_short_signal and self.Position < 0:
self.BuyMarket()
self._prev_macd = macd_current
self._prev_signal = signal_current
def CreateClone(self):
return macd_sample_classic_strategy()