MAMACD 策略
概述
本策略是在 StockSharp 高级 API 中对 MetaTrader 5 专家顾问 MAMACD(barabashkakvn 版本)(位于 MQL/19334 目录)的逐句移植。策略通过两条基于最低价的线性加权均线(LWMA)识别趋势通道,配合一条基于收盘价的快速 EMA 触发线,并使用 MACD 主线过滤信号。所有计算都在蜡烛完成之后执行,同时保留原始 EA 的“准备”标志机制——只有当快速 EMA 重新穿越 LWMA 通道后才允许新的交易。
指标
- LWMA #1(最低价,默认 85):慢速趋势基准。
- LWMA #2(最低价,默认 75):略快的确认均线,用于构建通道。
- EMA 触发线(收盘价,默认 5):必须重新穿越两条 LWMA 才会激活买入或卖出信号。
- MACD 主线(快速 15,慢速 26):趋势确认,做多需要 MACD 为正或向上,做空需要 MACD 为负或向下。
入场逻辑
- 仅在蜡烛状态为
CandleStates.Finished时处理,忽略未完成数据。 - 当 EMA 触发线跌破两条 LWMA 时,设置“多头准备”标志;只有在 EMA 返回到两条 LWMA 之上且 MACD > 0 或者高于上一根值时,才会开多,并且同一时间只允许持有一张多单。
- 当 EMA 触发线上穿两条 LWMA 时,设置“空头准备”标志;只有在 EMA 回落到 LWMA 之下且 MACD < 0 或者低于上一根值时,才会开空,并且同一时间只允许持有一张空单。
- 成交量来自策略的
Volume属性;当信号反向时,先平掉原有仓位,再建立新的方向。
出场逻辑
- 原版 EA 不包含主动平仓条件。这里通过
StartProtection配置的止损与止盈(单位为“点”)来保护仓位,只要任一保护触发即可自动平仓。
参数
| 名称 | 说明 |
|---|---|
FirstLowMaLength |
第一条 LWMA 的周期(最低价,默认 85)。 |
SecondLowMaLength |
第二条 LWMA 的周期(最低价,默认 75)。 |
TriggerEmaLength |
快速 EMA 触发线的周期(收盘价,默认 5)。 |
MacdFastLength |
MACD 快速 EMA 周期(默认 15)。 |
MacdSlowLength |
MACD 慢速 EMA 周期(默认 26)。 |
StopLossPips |
止损距离(点);0 表示禁用(默认 15)。 |
TakeProfitPips |
止盈距离(点);0 表示禁用(默认 15)。 |
CandleType |
处理的蜡烛类型/时间框架(默认 1 小时)。 |
实现细节
- “点值”由
Security.PriceStep推导而来;若价格精度为 3 位或 5 位,则自动乘以 10,以匹配 MT5 中对“点”的定义。 - MACD 缓冲区与 EA 一致:先记录第一条有效数据,再用作下一根蜡烛的比较基准。
_readyForLong与_readyForShort标志完全复刻原始startb/starts状态机,确保 EMA 必须离开 LWMA 通道后才允许再次入场。- 代码会绘制价格与三条均线,并为 MACD 创建单独图层,方便验证移植结果。
映射关系
| MT5 元素 | StockSharp 对应实现 |
|---|---|
iMA(最低价/收盘价) |
WeightedMovingAverage(最低价输入)与 ExponentialMovingAverage(收盘价输入) |
iMACD 主线 |
MovingAverageConvergenceDivergence 主输出 |
buy / sell 计数 |
通过 Position 的正负与 BuyMarket / SellMarket 控制仓位 |
| Magic number 与滑点 | 在高级 API 中无需显式处理 |
| 点数止损/止盈 | StartProtection + 由点值换算得到的绝对价格偏移 |
该实现忠实还原 MT5 版本的交易逻辑,并利用 StockSharp 的订阅、指标绑定以及风控工具实现结构化管理。
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>
/// MAMACD trend-following strategy converted from the original MetaTrader 5 expert advisor.
/// Combines two low-price LWMA filters, a fast EMA trigger, and a MACD confirmation filter.
/// </summary>
public class MamAcdStrategy : Strategy
{
private readonly StrategyParam<int> _firstLowMaLength;
private readonly StrategyParam<int> _secondLowMaLength;
private readonly StrategyParam<int> _triggerEmaLength;
private readonly StrategyParam<int> _macdFastLength;
private readonly StrategyParam<int> _macdSlowLength;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<DataType> _candleType;
private WeightedMovingAverage _firstLowMa = null!;
private WeightedMovingAverage _secondLowMa = null!;
private ExponentialMovingAverage _triggerEma = null!;
private MovingAverageConvergenceDivergence _macd = null!;
private decimal? _previousMacd;
private bool _readyForLong;
private bool _readyForShort;
private decimal _pipSize;
/// <summary>
/// Period of the first LWMA calculated on low prices.
/// </summary>
public int FirstLowMaLength
{
get => _firstLowMaLength.Value;
set => _firstLowMaLength.Value = value;
}
/// <summary>
/// Period of the second LWMA calculated on low prices.
/// </summary>
public int SecondLowMaLength
{
get => _secondLowMaLength.Value;
set => _secondLowMaLength.Value = value;
}
/// <summary>
/// Period of the fast EMA calculated on close prices.
/// </summary>
public int TriggerEmaLength
{
get => _triggerEmaLength.Value;
set => _triggerEmaLength.Value = value;
}
/// <summary>
/// Fast EMA period of the MACD filter.
/// </summary>
public int MacdFastLength
{
get => _macdFastLength.Value;
set => _macdFastLength.Value = value;
}
/// <summary>
/// Slow EMA period of the MACD filter.
/// </summary>
public int MacdSlowLength
{
get => _macdSlowLength.Value;
set => _macdSlowLength.Value = value;
}
/// <summary>
/// Stop-loss distance in pips. Set to zero to disable protective stop.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance in pips. Set to zero to disable take-profit.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes <see cref="MamAcdStrategy"/> with default parameters.
/// </summary>
public MamAcdStrategy()
{
_firstLowMaLength = Param(nameof(FirstLowMaLength), 85)
.SetGreaterThanZero()
.SetDisplay("LWMA #1", "Length of the first LWMA on lows", "Indicators")
;
_secondLowMaLength = Param(nameof(SecondLowMaLength), 75)
.SetGreaterThanZero()
.SetDisplay("LWMA #2", "Length of the second LWMA on lows", "Indicators")
;
_triggerEmaLength = Param(nameof(TriggerEmaLength), 5)
.SetGreaterThanZero()
.SetDisplay("Trigger EMA", "Length of the EMA on closes", "Indicators")
;
_macdFastLength = Param(nameof(MacdFastLength), 15)
.SetGreaterThanZero()
.SetDisplay("MACD Fast", "Fast EMA length of MACD", "Indicators")
;
_macdSlowLength = Param(nameof(MacdSlowLength), 26)
.SetGreaterThanZero()
.SetDisplay("MACD Slow", "Slow EMA length of MACD", "Indicators")
;
_stopLossPips = Param(nameof(StopLossPips), 500)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk management");
_takeProfitPips = Param(nameof(TakeProfitPips), 500)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousMacd = null;
_readyForLong = false;
_readyForShort = false;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_firstLowMa = new WeightedMovingAverage { Length = FirstLowMaLength };
_secondLowMa = new WeightedMovingAverage { Length = SecondLowMaLength };
_triggerEma = new EMA { Length = TriggerEmaLength };
_macd = new MovingAverageConvergenceDivergence
{
ShortMa = { Length = MacdFastLength },
LongMa = { Length = MacdSlowLength }
};
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
_pipSize = CalculatePipSize();
var takeProfit = TakeProfitPips > 0 ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : new Unit();
var stopLoss = StopLossPips > 0 ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : new Unit();
StartProtection(takeProfit, stopLoss);
var priceArea = CreateChartArea();
if (priceArea != null)
{
DrawCandles(priceArea, subscription);
DrawIndicator(priceArea, _firstLowMa);
DrawIndicator(priceArea, _secondLowMa);
DrawIndicator(priceArea, _triggerEma);
DrawOwnTrades(priceArea);
var macdArea = CreateChartArea();
if (macdArea != null)
{
macdArea.Title = "MACD";
DrawIndicator(macdArea, _macd);
}
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// Feed indicator chain: LWMAs work on low prices, EMA and MACD on closes.
var firstLowValue = _firstLowMa.Process(new DecimalIndicatorValue(_firstLowMa, candle.LowPrice, candle.OpenTime) { IsFinal = true });
var secondLowValue = _secondLowMa.Process(new DecimalIndicatorValue(_secondLowMa, candle.LowPrice, candle.OpenTime) { IsFinal = true });
var triggerValue = _triggerEma.Process(new DecimalIndicatorValue(_triggerEma, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
var macdValue = _macd.Process(new DecimalIndicatorValue(_macd, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
// Wait for all indicators to collect enough history.
if (!_firstLowMa.IsFormed || !_secondLowMa.IsFormed || !_triggerEma.IsFormed || !_macd.IsFormed)
{
if (_macd.IsFormed)
_previousMacd = macdValue.ToDecimal();
return;
}
// indicators already checked above
var ma1 = firstLowValue.ToDecimal();
var ma2 = secondLowValue.ToDecimal();
var ma3 = triggerValue.ToDecimal();
var macd = macdValue.ToDecimal();
// Store the first complete MACD observation before evaluating signals.
if (_previousMacd is null)
{
_previousMacd = macd;
return;
}
// Skip calculations when MACD lacks momentum confirmation just like the original EA.
if (macd == 0m || _previousMacd.Value == 0m)
{
_previousMacd = macd;
return;
}
// Track reset flags: EMA must dip below both LWMAs to prepare for a new long, and rise above them for shorts.
if (ma3 < ma1 && ma3 < ma2)
_readyForLong = true;
if (ma3 > ma1 && ma3 > ma2)
_readyForShort = true;
var macdImproving = macd > _previousMacd.Value;
var longSignal = ma3 > ma1 && ma3 > ma2 && _readyForLong && (macd > 0m || macdImproving);
var shortSignal = ma3 < ma1 && ma3 < ma2 && _readyForShort && (macd < 0m || !macdImproving);
if (longSignal && Position <= 0)
{
var volume = Volume + (Position < 0 ? -Position : 0m);
if (volume > 0)
{
BuyMarket();
_readyForLong = false;
}
}
else if (shortSignal && Position >= 0)
{
var volume = Volume + (Position > 0 ? Position : 0m);
if (volume > 0)
{
SellMarket();
_readyForShort = false;
}
}
_previousMacd = macd;
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 1m;
if (step <= 0m)
return 1m;
var decimals = CountDecimalPlaces(step);
return decimals == 3 || decimals == 5 ? step * 10m : step;
}
private static int CountDecimalPlaces(decimal value)
{
value = Math.Abs(value);
var count = 0;
while (value != Math.Truncate(value) && count < 10)
{
value *= 10m;
count++;
}
return count;
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import (
WeightedMovingAverage,
ExponentialMovingAverage,
MovingAverageConvergenceDivergence,
)
from indicator_extensions import *
class mam_acd_strategy(Strategy):
"""MAMACD: two LWMA filters on lows, EMA trigger on close, MACD confirmation."""
def __init__(self):
super(mam_acd_strategy, self).__init__()
self._first_low_ma_length = self.Param("FirstLowMaLength", 85) \
.SetGreaterThanZero() \
.SetDisplay("LWMA #1", "Length of the first LWMA on lows", "Indicators")
self._second_low_ma_length = self.Param("SecondLowMaLength", 75) \
.SetGreaterThanZero() \
.SetDisplay("LWMA #2", "Length of the second LWMA on lows", "Indicators")
self._trigger_ema_length = self.Param("TriggerEmaLength", 5) \
.SetGreaterThanZero() \
.SetDisplay("Trigger EMA", "Length of the EMA on closes", "Indicators")
self._macd_fast_length = self.Param("MacdFastLength", 15) \
.SetGreaterThanZero() \
.SetDisplay("MACD Fast", "Fast EMA length of MACD", "Indicators")
self._macd_slow_length = self.Param("MacdSlowLength", 26) \
.SetGreaterThanZero() \
.SetDisplay("MACD Slow", "Slow EMA length of MACD", "Indicators")
self._stop_loss_pips = self.Param("StopLossPips", 500) \
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk management")
self._take_profit_pips = self.Param("TakeProfitPips", 500) \
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles used for calculations", "General")
self._previous_macd = None
self._ready_for_long = False
self._ready_for_short = False
self._pip_size = 0.0
@property
def FirstLowMaLength(self):
return int(self._first_low_ma_length.Value)
@property
def SecondLowMaLength(self):
return int(self._second_low_ma_length.Value)
@property
def TriggerEmaLength(self):
return int(self._trigger_ema_length.Value)
@property
def MacdFastLength(self):
return int(self._macd_fast_length.Value)
@property
def MacdSlowLength(self):
return int(self._macd_slow_length.Value)
@property
def StopLossPips(self):
return int(self._stop_loss_pips.Value)
@property
def TakeProfitPips(self):
return int(self._take_profit_pips.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _calc_pip_size(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
if step <= 0:
return 1.0
# Count decimal places
v = abs(step)
count = 0
while v != int(v) and count < 10:
v *= 10
count += 1
return step * 10.0 if (count == 3 or count == 5) else step
def OnStarted2(self, time):
super(mam_acd_strategy, self).OnStarted2(time)
self._previous_macd = None
self._ready_for_long = False
self._ready_for_short = False
self._first_low_ma = WeightedMovingAverage()
self._first_low_ma.Length = self.FirstLowMaLength
self._second_low_ma = WeightedMovingAverage()
self._second_low_ma.Length = self.SecondLowMaLength
self._trigger_ema = ExponentialMovingAverage()
self._trigger_ema.Length = self.TriggerEmaLength
self._macd_ind = MovingAverageConvergenceDivergence()
self._macd_ind.ShortMa.Length = self.MacdFastLength
self._macd_ind.LongMa.Length = self.MacdSlowLength
self._pip_size = self._calc_pip_size()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
tp = Unit(self.TakeProfitPips * self._pip_size, UnitTypes.Absolute) if self.TakeProfitPips > 0 else None
sl = Unit(self.StopLossPips * self._pip_size, UnitTypes.Absolute) if self.StopLossPips > 0 else None
if tp is not None and sl is not None:
self.StartProtection(takeProfit=tp, stopLoss=sl)
elif tp is not None:
self.StartProtection(takeProfit=tp)
elif sl is not None:
self.StartProtection(stopLoss=sl)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._first_low_ma)
self.DrawIndicator(area, self._second_low_ma)
self.DrawIndicator(area, self._trigger_ema)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
t = candle.ServerTime
lo = candle.LowPrice
close = candle.ClosePrice
first_low_val = process_float(self._first_low_ma, lo, t, True)
second_low_val = process_float(self._second_low_ma, lo, t, True)
trigger_val = process_float(self._trigger_ema, close, t, True)
macd_val = process_float(self._macd_ind, close, t, True)
if (not self._first_low_ma.IsFormed or not self._second_low_ma.IsFormed
or not self._trigger_ema.IsFormed or not self._macd_ind.IsFormed):
if self._macd_ind.IsFormed:
self._previous_macd = float(macd_val.Value)
return
ma1 = float(first_low_val.Value)
ma2 = float(second_low_val.Value)
ma3 = float(trigger_val.Value)
macd = float(macd_val.Value)
if self._previous_macd is None:
self._previous_macd = macd
return
if macd == 0 or self._previous_macd == 0:
self._previous_macd = macd
return
# Track readiness flags
if ma3 < ma1 and ma3 < ma2:
self._ready_for_long = True
if ma3 > ma1 and ma3 > ma2:
self._ready_for_short = True
macd_improving = macd > self._previous_macd
long_signal = ma3 > ma1 and ma3 > ma2 and self._ready_for_long and (macd > 0 or macd_improving)
short_signal = ma3 < ma1 and ma3 < ma2 and self._ready_for_short and (macd < 0 or not macd_improving)
if long_signal and self.Position <= 0:
self.BuyMarket()
self._ready_for_long = False
elif short_signal and self.Position >= 0:
self.SellMarket()
self._ready_for_short = False
self._previous_macd = macd
def OnReseted(self):
super(mam_acd_strategy, self).OnReseted()
self._previous_macd = None
self._ready_for_long = False
self._ready_for_short = False
self._pip_size = 0.0
def CreateClone(self):
return mam_acd_strategy()