MACD 自动化策略示例
概述
该策略使用 StockSharp 高级 API 复刻 MetaTrader 4 的“Example of MACD Automated”专家顾问。系统同时监控两个周期上的 MACD 主线,当趋势过滤器一致时才开仓。止损和止盈以价格最小变动单位表示,仓位大小遵循原始 EA 的 AdvancedMM 资金管理,会累加最近亏损交易的手数。
交易逻辑
- 高周期过滤 – 在高周期(默认:日线)计算的 MACD(12, 26, 9) 主线为正值时允许做多,为负值时允许做空。
- 入场周期确认 – 入场周期(默认:15 分钟)上使用相同参数的 MACD 需要与高周期方向一致。
- 单笔持仓 – 任意时刻只有一笔仓位,只有在止损或止盈平仓之后才会寻找新的入场机会。
- 保护性订单 – 止损与止盈以价格步长的倍数表示,对应 MT4 中的
StopLoss和TakeProfit输入,填0表示关闭。 - 高级资金管理 – 资金管理模块在连续亏损时将亏损单的手数累加用于下一笔交易,在盈利之后恢复到基础手数,完全模拟
AdvancedMM()函数。
参数
| 名称 | 说明 | 默认值 |
|---|---|---|
BaseVolume |
AdvancedMM 逻辑使用的基础下单手数。 | 0.01 |
StopLossPoints |
以价格步长表示的止损距离,0 表示不设置。 |
50 |
TakeProfitPoints |
以价格步长表示的止盈距离,0 表示不设置。 |
30 |
MacdFastLength |
两个周期 MACD 的快线 EMA 周期。 | 12 |
MacdSlowLength |
MACD 的慢线 EMA 周期。 | 26 |
MacdSignalLength |
MACD 信号线周期。 | 9 |
EntryCandleType |
入场所用的 K 线周期。 | 15 分钟 |
FilterCandleType |
趋势过滤所用的高周期。 | 1 天 |
仓位管理
- 每次建仓都会根据标的的
PriceStep重新计算止损和止盈价位。 - 当单根 K 线触及任一保护价位时,策略假设订单在该价格成交,并记录实际盈亏。
- 每次平仓后 AdvancedMM 会调整下一笔交易的手数:
- 历史交易少于两笔 → 使用基础手数;
- 最近一笔为亏损 → 复用该笔交易的手数;
- 在最近一次盈利之前存在连续亏损 → 将这些亏损单的手数累加;
- 其他情况 → 回到基础手数。
说明
- 转换版本保持了原策略的风格,仅依靠止损和止盈退出,不会因为 MACD 反向而强制平仓。
- 请确保标的证券提供有效的
PriceStep信息,以便正确换算点差距离。 - 策略只处理已完成的 K 线,需在提供完整 K 线数据的环境中使用。
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>
/// Conversion of the "Example of MACD Automated" MQL4 expert advisor.
/// The strategy waits for MACD agreement on two timeframes and uses AdvancedMM sizing.
/// </summary>
public class ExampleOfMacdAutomatedStrategy : Strategy
{
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<int> _macdFastLength;
private readonly StrategyParam<int> _macdSlowLength;
private readonly StrategyParam<int> _macdSignalLength;
private readonly StrategyParam<DataType> _entryCandleType;
private readonly StrategyParam<DataType> _filterCandleType;
private MovingAverageConvergenceDivergenceSignal _entryMacd = null!;
private MovingAverageConvergenceDivergenceSignal _filterMacd = null!;
private decimal? _lastEntryMacd;
private decimal? _lastFilterMacd;
private readonly List<TradeInfo> _tradeHistory = new();
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takeProfitPrice;
private decimal _entryVolume;
private int _entryDirection;
/// <summary>
/// Initializes a new instance of the <see cref="ExampleOfMacdAutomatedStrategy"/> class.
/// </summary>
public ExampleOfMacdAutomatedStrategy()
{
_baseVolume = Param(nameof(BaseVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Base Volume", "Starting order volume for AdvancedMM", "Risk")
;
_stopLossPoints = Param(nameof(StopLossPoints), 50m)
.SetNotNegative()
.SetDisplay("Stop Loss (steps)", "Stop-loss distance in price steps", "Risk")
;
_takeProfitPoints = Param(nameof(TakeProfitPoints), 30m)
.SetNotNegative()
.SetDisplay("Take Profit (steps)", "Take-profit distance in price steps", "Risk")
;
_macdFastLength = Param(nameof(MacdFastLength), 12)
.SetGreaterThanZero()
.SetDisplay("MACD Fast", "Fast EMA length", "Indicators")
;
_macdSlowLength = Param(nameof(MacdSlowLength), 26)
.SetGreaterThanZero()
.SetDisplay("MACD Slow", "Slow EMA length", "Indicators")
;
_macdSignalLength = Param(nameof(MacdSignalLength), 9)
.SetGreaterThanZero()
.SetDisplay("MACD Signal", "Signal EMA length", "Indicators")
;
_entryCandleType = Param(nameof(EntryCandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Entry Timeframe", "Working timeframe for entries", "General");
_filterCandleType = Param(nameof(FilterCandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Filter Timeframe", "Higher timeframe used as trend filter", "General");
}
/// <summary>
/// Base volume parameter.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Stop-loss distance in price steps.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in price steps.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// MACD fast EMA length.
/// </summary>
public int MacdFastLength
{
get => _macdFastLength.Value;
set => _macdFastLength.Value = value;
}
/// <summary>
/// MACD slow EMA length.
/// </summary>
public int MacdSlowLength
{
get => _macdSlowLength.Value;
set => _macdSlowLength.Value = value;
}
/// <summary>
/// MACD signal EMA length.
/// </summary>
public int MacdSignalLength
{
get => _macdSignalLength.Value;
set => _macdSignalLength.Value = value;
}
/// <summary>
/// Timeframe used for entries.
/// </summary>
public DataType EntryCandleType
{
get => _entryCandleType.Value;
set => _entryCandleType.Value = value;
}
/// <summary>
/// Higher timeframe used as a trend filter.
/// </summary>
public DataType FilterCandleType
{
get => _filterCandleType.Value;
set => _filterCandleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, EntryCandleType), (Security, FilterCandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_lastEntryMacd = null;
_lastFilterMacd = null;
_tradeHistory.Clear();
_entryPrice = null;
_stopPrice = null;
_takeProfitPrice = null;
_entryVolume = 0m;
_entryDirection = 0;
_entryMacd?.Reset();
_filterMacd?.Reset();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create MACD indicators for entry and filter timeframes.
_entryMacd = CreateMacd();
_filterMacd = CreateMacd();
var entrySubscription = SubscribeCandles(EntryCandleType);
entrySubscription
.BindEx(_entryMacd, ProcessEntryCandle)
.Start();
var filterSubscription = SubscribeCandles(FilterCandleType);
filterSubscription
.BindEx(_filterMacd, ProcessFilterCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, entrySubscription);
DrawIndicator(area, _entryMacd);
DrawIndicator(area, _filterMacd);
DrawOwnTrades(area);
}
}
private MovingAverageConvergenceDivergenceSignal CreateMacd()
{
// Instantiate MACD with shared parameters for both timeframes.
return new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = MacdFastLength },
LongMa = { Length = MacdSlowLength },
},
SignalMa = { Length = MacdSignalLength }
};
}
private void ProcessFilterCandle(ICandleMessage candle, IIndicatorValue macdValue)
{
// Process only completed candles on the filter timeframe.
if (candle.State != CandleStates.Finished)
return;
var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
_lastFilterMacd = macd.Macd;
}
private void ProcessEntryCandle(ICandleMessage candle, IIndicatorValue macdValue)
{
// Ensure that we operate on final candle values only.
if (candle.State != CandleStates.Finished)
return;
var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
var currentEntryMacd = macd.Macd;
// Manage protective exits before searching for new entries.
if (HandleProtection(candle))
{
_lastEntryMacd = currentEntryMacd;
return;
}
// Skip further processing if there is still an open position.
if (Position != 0)
{
_lastEntryMacd = currentEntryMacd;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_lastEntryMacd = currentEntryMacd;
return;
}
if (!_entryMacd.IsFormed || !_filterMacd.IsFormed)
{
_lastEntryMacd = currentEntryMacd;
return;
}
if (_lastEntryMacd is not decimal previousEntryMacd || _lastFilterMacd is not decimal filterMacdValue)
{
_lastEntryMacd = currentEntryMacd;
return;
}
// Enter only on a zero-line crossover aligned with the higher timeframe filter.
if (previousEntryMacd <= 0m && currentEntryMacd > 0m && filterMacdValue > 0m)
{
EnterPosition(candle.ClosePrice, true);
}
else if (previousEntryMacd >= 0m && currentEntryMacd < 0m && filterMacdValue < 0m)
{
EnterPosition(candle.ClosePrice, false);
}
_lastEntryMacd = currentEntryMacd;
}
private void EnterPosition(decimal price, bool isLong)
{
var volume = CalculateTradeVolume();
if (volume <= 0m)
return;
if (isLong)
{
BuyMarket(volume);
RegisterEntry(price, volume, 1);
}
else
{
SellMarket(volume);
RegisterEntry(price, volume, -1);
}
}
private void RegisterEntry(decimal price, decimal volume, int direction)
{
// Store entry information for later profit calculation.
_entryPrice = price;
_entryVolume = volume;
_entryDirection = direction;
UpdateProtectionLevels(price, direction > 0);
}
private void UpdateProtectionLevels(decimal price, bool isLong)
{
var point = GetPointValue();
if (point <= 0m)
{
_stopPrice = null;
_takeProfitPrice = null;
return;
}
if (isLong)
{
_stopPrice = StopLossPoints > 0m ? price - StopLossPoints * point : null;
_takeProfitPrice = TakeProfitPoints > 0m ? price + TakeProfitPoints * point : null;
}
else
{
_stopPrice = StopLossPoints > 0m ? price + StopLossPoints * point : null;
_takeProfitPrice = TakeProfitPoints > 0m ? price - TakeProfitPoints * point : null;
}
}
private bool HandleProtection(ICandleMessage candle)
{
if (Position == 0 || _entryDirection == 0)
return false;
if (_entryDirection > 0)
{
if (TryGetLongExitPrice(candle, out var exitPrice))
{
SellMarket(Math.Abs(Position));
RegisterClosedTrade(exitPrice);
return true;
}
}
else
{
if (TryGetShortExitPrice(candle, out var exitPrice))
{
BuyMarket(Math.Abs(Position));
RegisterClosedTrade(exitPrice);
return true;
}
}
return false;
}
private bool TryGetLongExitPrice(ICandleMessage candle, out decimal exitPrice)
{
exitPrice = 0m;
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
exitPrice = _stopPrice.Value;
return true;
}
if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
{
exitPrice = _takeProfitPrice.Value;
return true;
}
return false;
}
private bool TryGetShortExitPrice(ICandleMessage candle, out decimal exitPrice)
{
exitPrice = 0m;
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
exitPrice = _stopPrice.Value;
return true;
}
if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
{
exitPrice = _takeProfitPrice.Value;
return true;
}
return false;
}
private void RegisterClosedTrade(decimal exitPrice)
{
if (!_entryPrice.HasValue || _entryVolume <= 0m || _entryDirection == 0)
return;
var entryPrice = _entryPrice.Value;
var volume = _entryVolume;
var direction = _entryDirection;
var profit = (exitPrice - entryPrice) * direction * volume;
_tradeHistory.Add(new TradeInfo(volume, profit));
if (_tradeHistory.Count > 200)
_tradeHistory.RemoveAt(0);
_entryPrice = null;
_entryVolume = 0m;
_entryDirection = 0;
_stopPrice = null;
_takeProfitPrice = null;
}
private decimal CalculateTradeVolume()
{
var baseVolume = BaseVolume;
if (baseVolume <= 0m)
return 0m;
if (_tradeHistory.Count < 2)
return baseVolume;
var advancedLots = 0m;
var profit1 = false;
var profit2 = false;
var firstIteration = true;
for (var i = _tradeHistory.Count - 1; i >= 0; i--)
{
var trade = _tradeHistory[i];
var isProfit = trade.Profit >= 0m;
if (isProfit && profit1)
return baseVolume;
if (firstIteration)
{
if (isProfit)
{
profit1 = true;
}
else
{
return trade.Volume;
}
firstIteration = false;
}
if (isProfit && profit2)
return advancedLots > 0m ? advancedLots : baseVolume;
if (isProfit)
{
profit2 = true;
}
else
{
profit1 = false;
profit2 = false;
advancedLots += trade.Volume;
}
}
return advancedLots > 0m ? advancedLots : baseVolume;
}
private decimal GetPointValue()
{
var step = Security?.PriceStep ?? 0m;
return step > 0m ? step : 1m;
}
private readonly struct TradeInfo
{
public TradeInfo(decimal volume, decimal profit)
{
Volume = volume;
Profit = profit;
}
public decimal Volume { get; }
public decimal Profit { get; }
}
}
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 MovingAverageConvergenceDivergence
from StockSharp.Algo.Strategies import Strategy
class example_of_macd_automated_strategy(Strategy):
def __init__(self):
super(example_of_macd_automated_strategy, self).__init__()
self._base_volume = self.Param("BaseVolume", 1.0)
self._stop_loss_points = self.Param("StopLossPoints", 50.0)
self._take_profit_points = self.Param("TakeProfitPoints", 30.0)
self._macd_fast_length = self.Param("MacdFastLength", 12)
self._macd_slow_length = self.Param("MacdSlowLength", 26)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._last_entry_macd = None
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
self._entry_direction = 0
@property
def BaseVolume(self):
return self._base_volume.Value
@BaseVolume.setter
def BaseVolume(self, value):
self._base_volume.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.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 MacdFastLength(self):
return self._macd_fast_length.Value
@MacdFastLength.setter
def MacdFastLength(self, value):
self._macd_fast_length.Value = value
@property
def MacdSlowLength(self):
return self._macd_slow_length.Value
@MacdSlowLength.setter
def MacdSlowLength(self, value):
self._macd_slow_length.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(example_of_macd_automated_strategy, self).OnStarted2(time)
self._last_entry_macd = None
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
self._entry_direction = 0
macd = MovingAverageConvergenceDivergence()
macd.ShortMa.Length = self.MacdFastLength
macd.LongMa.Length = self.MacdSlowLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(macd, self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
current_macd = float(macd_value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self._handle_protection(candle):
self._last_entry_macd = current_macd
return
if self.Position != 0:
self._last_entry_macd = current_macd
return
if self._last_entry_macd is None:
self._last_entry_macd = current_macd
return
prev_macd = self._last_entry_macd
if prev_macd <= 0.0 and current_macd > 0.0:
self._enter_position(close, True)
elif prev_macd >= 0.0 and current_macd < 0.0:
self._enter_position(close, False)
self._last_entry_macd = current_macd
def _enter_position(self, price, is_long):
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
sl_pts = float(self.StopLossPoints)
tp_pts = float(self.TakeProfitPoints)
if is_long:
self.BuyMarket()
self._entry_price = price
self._entry_direction = 1
self._stop_price = price - sl_pts * step if sl_pts > 0.0 else None
self._take_profit_price = price + tp_pts * step if tp_pts > 0.0 else None
else:
self.SellMarket()
self._entry_price = price
self._entry_direction = -1
self._stop_price = price + sl_pts * step if sl_pts > 0.0 else None
self._take_profit_price = price - tp_pts * step if tp_pts > 0.0 else None
def _handle_protection(self, candle):
if self.Position == 0 or self._entry_direction == 0:
return False
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self._entry_direction > 0:
if self._stop_price is not None and low <= self._stop_price:
self.SellMarket()
self._reset_state()
return True
if self._take_profit_price is not None and high >= self._take_profit_price:
self.SellMarket()
self._reset_state()
return True
else:
if self._stop_price is not None and high >= self._stop_price:
self.BuyMarket()
self._reset_state()
return True
if self._take_profit_price is not None and low <= self._take_profit_price:
self.BuyMarket()
self._reset_state()
return True
return False
def _reset_state(self):
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
self._entry_direction = 0
def OnReseted(self):
super(example_of_macd_automated_strategy, self).OnReseted()
self._last_entry_macd = None
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
self._entry_direction = 0
def CreateClone(self):
return example_of_macd_automated_strategy()