Example of MACD Automated Strategy
Overview
The strategy replicates the "Example of MACD Automated" MetaTrader 4 expert advisor using the StockSharp high-level API. It monitors the MACD main line on two timeframes and opens a single position when both trend filters agree. Protective stop-loss and take-profit distances are applied in price steps, and the position size follows the original AdvancedMM logic that accumulates the volume of recent losing trades.
Trading Logic
- Higher timeframe filter – a MACD (12, 26, 9) computed on the higher timeframe (default: daily candles) must have a positive main line for long signals or a negative main line for short signals.
- Entry timeframe confirmation – the same MACD settings on the entry timeframe (default: 15-minute candles) must point in the same direction as the higher timeframe filter.
- Single position – the strategy trades one position at a time. New entries are skipped until the existing position is closed by protective levels.
- Protective orders – stop-loss and take-profit levels are measured in multiples of the instrument price step, mirroring the original MT4
StopLossandTakeProfitinputs. A value of0disables the corresponding protection. - Advanced money management – the trade volume increases after consecutive losing trades by summing the lot size of the losses, and reverts to the base volume after profitable trades, emulating the
AdvancedMM()function from the source EA.
Parameters
| Name | Description | Default |
|---|---|---|
BaseVolume |
Base order volume used by the AdvancedMM logic. | 0.01 |
StopLossPoints |
Stop-loss distance expressed in price steps. 0 disables the stop. |
50 |
TakeProfitPoints |
Take-profit distance expressed in price steps. 0 disables the target. |
30 |
MacdFastLength |
Fast EMA period of the MACD on both timeframes. | 12 |
MacdSlowLength |
Slow EMA period of the MACD on both timeframes. | 26 |
MacdSignalLength |
Signal line EMA period. | 9 |
EntryCandleType |
Timeframe for trade execution. | 15m candles |
FilterCandleType |
Higher timeframe used as trend filter. | 1d candles |
Position Management
- Stop-loss and take-profit prices are recalculated on every new position based on the instrument price step.
- When either protective level is touched inside a bar, the strategy assumes the order is filled at that level and records the realized profit or loss.
- After each closed trade the AdvancedMM logic updates the next order size:
- Less than two historical trades → use the base volume.
- The most recent trade was a loss → repeat its volume.
- Consecutive losses before the last win → sum their volumes to recover.
- Otherwise → revert to the base volume.
Notes
- The conversion keeps the original behaviour of holding a position until a protective level is hit; there is no exit on MACD crossovers.
- Ensure the instrument has valid
PriceStepinformation so that point-based stop and target distances are calculated correctly. - The strategy relies on completed candles and should be used with historical data or live feeds that provide finished candle updates.
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()