MACD 多周期专家策略
概述
该策略在 StockSharp 框架中复刻了原始的 MetaTrader "MACD Expert" 智能交易系统。它同时跟踪 5 分钟、15 分钟、1 小时和 4 小时四个周期上的 MACD 趋势,仅当所有周期给出同向信号时才允许开仓,从而在过滤高点差区间的同时捕捉多周期动量。
数据与指标
- K 线:使用 5 分钟(执行)、15 分钟、1 小时和 4 小时周期,全部仅处理已完成的蜡烛。
- 指标:每个周期单独实例化
MovingAverageConvergenceDivergenceSignal(默认参数 12/26/9),避免不同周期之间的状态污染。 - 一级行情:订阅最优买卖价以在下单前实时检查点差。
交易逻辑
- 等待四个 MACD 实例都生成最终数值。
- 计算各周期上 MACD 主线与信号线的相对位置。
- 按点数(PriceStep)评估实时点差并与
MaxSpreadPoints比较。 - 同一时间只允许持有一笔仓位,必须由止损或止盈结束后才会寻找下一次入场机会。
多头条件
- 所有监控周期上信号线都高于 MACD 主线。
- 实时点差不超过
MaxSpreadPoints。 - 在最新完成的 5 分钟蜡烛收盘价按
OrderVolume手数买入。
空头条件
- 所有监控周期上信号线都低于 MACD 主线。
- 实时点差不超过
MaxSpreadPoints。 - 在最新完成的 5 分钟蜡烛收盘价按
OrderVolume手数卖出。
仓位管理
- 多头使用
TakeProfitPoints点的目标以及StopLossPoints点的保护性止损。 - 空头将目标设置在入场价下方
TakeProfitPoints点,并在上方StopLossPoints点设置止损。 - 只要 5 分钟蜡烛的最高价/最低价触及相应价位,就在蜡烛收盘后通过市价单离场。
- 持仓期间忽略反向信号,完全依赖止盈或止损结束交易,保持与原 MQL 版本一致的行为。
参数
| 名称 | 默认值 | 说明 |
|---|---|---|
OrderVolume |
0.1 | 仓位手数,对应 MQL 中的 Lots 输入。 |
StopLossPoints |
200 | 止损距离(点)。 |
TakeProfitPoints |
400 | 止盈距离(点)。 |
MaxSpreadPoints |
20 | 允许的最大点差(点),超出则跳过入场。 |
FastPeriod |
12 | MACD 快速 EMA 长度。 |
SlowPeriod |
26 | MACD 慢速 EMA 长度。 |
SignalPeriod |
9 | MACD 信号线 EMA 长度。 |
FiveMinuteCandleType |
5 分钟 K 线 | 主执行周期。 |
FifteenMinuteCandleType |
15 分钟 K 线 | 第一确认周期。 |
HourCandleType |
1 小时 K 线 | 第二确认周期。 |
FourHourCandleType |
4 小时 K 线 | 第三确认周期。 |
实现细节
- 使用
BindEx直接接收强类型的 MACD 值,遵守项目禁止调用GetValue的规范。 - 将 MACD 与信号线的相对位置映射为
{-1, 0, 1}标记,便于统一判断多周期一致性。 - 点差检测以
Security.PriceStep为单位,将最优买卖价差转换为“点”以贴近 MetaTrader 行为。 - 关键交易事件通过
LogInfo输出,方便在 Designer 或 Runner 中调试。 - 按需求仅提供 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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Multi-timeframe MACD confirmation strategy that aligns primary and confirmation timeframe trends.
/// </summary>
public class MacdMultiTimeframeExpertStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _signalPeriod;
private readonly StrategyParam<DataType> _primaryType;
private readonly StrategyParam<DataType> _confirmType;
private MovingAverageConvergenceDivergenceSignal _macdPrimary;
private MovingAverageConvergenceDivergenceSignal _macdConfirm;
private int? _relationPrimary;
private int? _relationConfirm;
private int _lastTradeDirection;
private int _candlesSinceEntry;
private decimal _entryPrice;
/// <summary>
/// Order volume in lots.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Fast EMA period used by MACD.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow EMA period used by MACD.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Signal line period used by MACD.
/// </summary>
public int SignalPeriod
{
get => _signalPeriod.Value;
set => _signalPeriod.Value = value;
}
/// <summary>
/// Candle type for the primary execution timeframe.
/// </summary>
public DataType PrimaryCandleType
{
get => _primaryType.Value;
set => _primaryType.Value = value;
}
/// <summary>
/// Candle type for the confirmation timeframe.
/// </summary>
public DataType ConfirmCandleType
{
get => _confirmType.Value;
set => _confirmType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="MacdMultiTimeframeExpertStrategy"/> class.
/// </summary>
public MacdMultiTimeframeExpertStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Position size in lots", "Trading");
_stopLossPoints = Param(nameof(StopLossPoints), 1500m)
.SetNotNegative()
.SetDisplay("Stop Loss Points", "Stop-loss distance in points", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2500m)
.SetNotNegative()
.SetDisplay("Take Profit Points", "Take-profit distance in points", "Risk");
_fastPeriod = Param(nameof(FastPeriod), 12)
.SetGreaterThanZero()
.SetDisplay("MACD Fast", "Fast EMA period", "MACD");
_slowPeriod = Param(nameof(SlowPeriod), 26)
.SetGreaterThanZero()
.SetDisplay("MACD Slow", "Slow EMA period", "MACD");
_signalPeriod = Param(nameof(SignalPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("MACD Signal", "Signal EMA period", "MACD");
_primaryType = Param(nameof(PrimaryCandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Primary", "Primary execution timeframe", "Timeframes");
_confirmType = Param(nameof(ConfirmCandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Confirm", "Confirmation timeframe", "Timeframes");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, PrimaryCandleType);
yield return (Security, ConfirmCandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_macdPrimary = null;
_macdConfirm = null;
_relationPrimary = null;
_relationConfirm = null;
_lastTradeDirection = 0;
_candlesSinceEntry = 0;
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_macdPrimary = CreateMacd();
_macdConfirm = CreateMacd();
var primarySubscription = SubscribeCandles(PrimaryCandleType);
primarySubscription
.Bind(ProcessPrimaryCandle)
.Start();
SubscribeCandles(ConfirmCandleType)
.Bind(ProcessConfirmCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, primarySubscription);
DrawOwnTrades(area);
}
}
private MovingAverageConvergenceDivergenceSignal CreateMacd()
{
return new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = FastPeriod },
LongMa = { Length = SlowPeriod }
},
SignalMa = { Length = SignalPeriod }
};
}
private void ProcessPrimaryCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var macdValue = _macdPrimary.Process(candle);
if (!TryUpdateRelation(macdValue, out var relation))
return;
_relationPrimary = relation;
_candlesSinceEntry++;
// Manage protective exits whenever a position is open.
if (Position != 0)
{
ManageOpenPosition(candle);
// If position was closed by SL/TP, allow new entry below
if (Position != 0)
return;
}
if (!_relationConfirm.HasValue)
return;
if (OrderVolume <= 0)
return;
// Cooldown: require at least 6 candles between trades.
if (_candlesSinceEntry < 6)
return;
// Determine aligned direction: both timeframes must agree.
var alignedDirection = 0;
if (_relationPrimary == 1 && _relationConfirm == 1)
alignedDirection = 1;
else if (_relationPrimary == -1 && _relationConfirm == -1)
alignedDirection = -1;
if (alignedDirection == 0)
return;
// Avoid repeated entries in the same direction.
if (_lastTradeDirection == alignedDirection)
return;
_lastTradeDirection = alignedDirection;
_candlesSinceEntry = 0;
if (alignedDirection > 0)
{
BuyMarket(OrderVolume);
_entryPrice = candle.ClosePrice;
}
else
{
SellMarket(OrderVolume);
_entryPrice = candle.ClosePrice;
}
}
private void ProcessConfirmCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var macdValue = _macdConfirm.Process(candle);
if (TryUpdateRelation(macdValue, out var relation))
_relationConfirm = relation;
}
private bool TryUpdateRelation(IIndicatorValue macdValue, out int relation)
{
relation = 0;
if (!macdValue.IsFinal)
return false;
var typed = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
if (typed.Macd is not decimal macd || typed.Signal is not decimal signal)
return false;
// Standard MACD: macd > signal = bullish, macd < signal = bearish.
if (macd > signal)
relation = 1;
else if (macd < signal)
relation = -1;
else
relation = 0;
return true;
}
private void ManageOpenPosition(ICandleMessage candle)
{
// Derive the point value. Fall back to 1 if the security lacks a price step.
var point = Security?.PriceStep ?? 0m;
if (point <= 0)
point = 1m;
if (Position > 0)
{
if (TakeProfitPoints > 0 && candle.HighPrice >= _entryPrice + TakeProfitPoints * point)
{
SellMarket(Position);
_entryPrice = 0m;
_lastTradeDirection = 0;
return;
}
if (StopLossPoints > 0 && candle.LowPrice <= _entryPrice - StopLossPoints * point)
{
SellMarket(Position);
_entryPrice = 0m;
_lastTradeDirection = 0;
}
}
else if (Position < 0)
{
var volume = Position.Abs();
if (TakeProfitPoints > 0 && candle.LowPrice <= _entryPrice - TakeProfitPoints * point)
{
BuyMarket(volume);
_entryPrice = 0m;
_lastTradeDirection = 0;
return;
}
if (StopLossPoints > 0 && candle.HighPrice >= _entryPrice + StopLossPoints * point)
{
BuyMarket(volume);
_entryPrice = 0m;
_lastTradeDirection = 0;
}
}
else
{
_entryPrice = 0m;
}
}
}
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 MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
class macd_multi_timeframe_expert_strategy(Strategy):
def __init__(self):
super(macd_multi_timeframe_expert_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 0.1)
self._stop_loss_points = self.Param("StopLossPoints", 1500.0)
self._take_profit_points = self.Param("TakeProfitPoints", 2500.0)
self._fast_period = self.Param("FastPeriod", 12)
self._slow_period = self.Param("SlowPeriod", 26)
self._signal_period = self.Param("SignalPeriod", 9)
self._primary_type = self.Param("PrimaryCandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30)))
self._confirm_type = self.Param("ConfirmCandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._macd_primary = None
self._macd_confirm = None
self._relation_primary = None
self._relation_confirm = None
self._last_trade_direction = 0
self._candles_since_entry = 0
self._entry_price = 0.0
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def FastPeriod(self):
return self._fast_period.Value
@property
def SlowPeriod(self):
return self._slow_period.Value
@property
def SignalPeriod(self):
return self._signal_period.Value
@property
def PrimaryCandleType(self):
return self._primary_type.Value
@property
def ConfirmCandleType(self):
return self._confirm_type.Value
def OnStarted2(self, time):
super(macd_multi_timeframe_expert_strategy, self).OnStarted2(time)
self._macd_primary = self._create_macd()
self._macd_confirm = self._create_macd()
primary_subscription = self.SubscribeCandles(self.PrimaryCandleType)
primary_subscription.BindEx(self._macd_primary, self._process_primary_candle).Start()
self.SubscribeCandles(self.ConfirmCandleType).BindEx(self._macd_confirm, self._process_confirm_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, primary_subscription)
self.DrawOwnTrades(area)
def _create_macd(self):
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self.FastPeriod
macd.Macd.LongMa.Length = self.SlowPeriod
macd.SignalMa.Length = self.SignalPeriod
return macd
def _try_update_relation(self, macd_value):
if not macd_value.IsFinal:
return (False, 0)
try:
macd_val = float(macd_value.Macd)
signal_val = float(macd_value.Signal)
except Exception:
return (False, 0)
if macd_val > signal_val:
return (True, 1)
elif macd_val < signal_val:
return (True, -1)
else:
return (True, 0)
def _process_primary_candle(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
ok, relation = self._try_update_relation(macd_value)
if not ok:
return
self._relation_primary = relation
self._candles_since_entry += 1
# Manage protective exits whenever a position is open.
pos = float(self.Position)
if pos != 0:
self._manage_open_position(candle)
# If position was closed by SL/TP, allow new entry below
pos = float(self.Position)
if pos != 0:
return
if self._relation_confirm is None:
return
if float(self.OrderVolume) <= 0:
return
# Cooldown: require at least 6 candles between trades.
if self._candles_since_entry < 6:
return
# Determine aligned direction: both timeframes must agree.
aligned_direction = 0
if self._relation_primary == 1 and self._relation_confirm == 1:
aligned_direction = 1
elif self._relation_primary == -1 and self._relation_confirm == -1:
aligned_direction = -1
if aligned_direction == 0:
return
# Avoid repeated entries in the same direction.
if self._last_trade_direction == aligned_direction:
return
self._last_trade_direction = aligned_direction
self._candles_since_entry = 0
if aligned_direction > 0:
self.BuyMarket(float(self.OrderVolume))
self._entry_price = float(candle.ClosePrice)
else:
self.SellMarket(float(self.OrderVolume))
self._entry_price = float(candle.ClosePrice)
def _process_confirm_candle(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
ok, relation = self._try_update_relation(macd_value)
if ok:
self._relation_confirm = relation
def _manage_open_position(self, candle):
sec = self.Security
point = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
if point <= 0:
point = 1.0
pos = float(self.Position)
if pos > 0:
if float(self.TakeProfitPoints) > 0 and float(candle.HighPrice) >= self._entry_price + float(self.TakeProfitPoints) * point:
self.SellMarket(pos)
self._entry_price = 0.0
self._last_trade_direction = 0
return
if float(self.StopLossPoints) > 0 and float(candle.LowPrice) <= self._entry_price - float(self.StopLossPoints) * point:
self.SellMarket(pos)
self._entry_price = 0.0
self._last_trade_direction = 0
elif pos < 0:
volume = abs(pos)
if float(self.TakeProfitPoints) > 0 and float(candle.LowPrice) <= self._entry_price - float(self.TakeProfitPoints) * point:
self.BuyMarket(volume)
self._entry_price = 0.0
self._last_trade_direction = 0
return
if float(self.StopLossPoints) > 0 and float(candle.HighPrice) >= self._entry_price + float(self.StopLossPoints) * point:
self.BuyMarket(volume)
self._entry_price = 0.0
self._last_trade_direction = 0
else:
self._entry_price = 0.0
def OnReseted(self):
super(macd_multi_timeframe_expert_strategy, self).OnReseted()
self._macd_primary = None
self._macd_confirm = None
self._relation_primary = None
self._relation_confirm = None
self._last_trade_direction = 0
self._candles_since_entry = 0
self._entry_price = 0.0
def CreateClone(self):
return macd_multi_timeframe_expert_strategy()