Пример автоматизированной стратегии MACD
Обзор
Стратегия переносит советник MetaTrader 4 «Example of MACD Automated» на StockSharp с использованием высокоуровневого API. MACD с параметрами 12, 26, 9 рассчитывается сразу на двух таймфреймах, и сделка открывается только тогда, когда оба фильтра указывают в одну сторону. Стоп‑лосс и тейк‑профит задаются в шагах цены, а размер позиции определяется модулем AdvancedMM, который суммирует объёмы последних убыточных сделок.
Торговая логика
- Фильтр старшего таймфрейма – MACD на старшем графике (по умолчанию дневные свечи) должен иметь положительную главную линию для покупок или отрицательную для продаж.
- Подтверждение рабочего таймфрейма – MACD на рабочем таймфрейме (по умолчанию 15 минут) должен совпадать по знаку с фильтром.
- Одна позиция – одновременно удерживается только одна позиция, новые входы рассматриваются после срабатывания защитных уровней.
- Защитные уровни – дистанции стопа и цели измеряются в шагах цены инструмента и полностью повторяют параметры
StopLossиTakeProfitиз исходного советника. Значение0отключает уровень. - Расширенное управление капиталом – функция AdvancedMM увеличивает объём следующей сделки после серии убытков, суммируя объёмы убыточных сделок, и возвращает базовый объём после прибыльных сделок.
Параметры
| Имя | Описание | Значение по умолчанию |
|---|---|---|
BaseVolume |
Базовый объём сделки, с которого стартует алгоритм AdvancedMM. | 0.01 |
StopLossPoints |
Дистанция стоп‑лосса в шагах цены, 0 — без стопа. |
50 |
TakeProfitPoints |
Дистанция тейк‑профита в шагах цены, 0 — без цели. |
30 |
MacdFastLength |
Период быстрой EMA MACD для обоих таймфреймов. | 12 |
MacdSlowLength |
Период медленной EMA MACD. | 26 |
MacdSignalLength |
Период сигнальной EMA MACD. | 9 |
EntryCandleType |
Таймфрейм для входа. | 15 минут |
FilterCandleType |
Старший таймфрейм фильтра тренда. | 1 день |
Управление позицией
- После открытия позиции стоп и цель пересчитываются исходя из
PriceStepинструмента. - При касании цены защитного уровня внутри свечи предполагается исполнение по этому уровню, и результат фиксируется в истории сделок.
- AdvancedMM после каждой закрытой сделки обновляет объём:
- Истории меньше двух сделок → используется базовый объём;
- Последняя сделка убыточная → берётся её объём;
- Перед последней прибылью были последовательные убытки → их объёмы суммируются;
- В остальных случаях → возврат к базовому объёму.
Примечания
- Конверсия сохраняет оригинальное поведение: позиция удерживается до срабатывания стопа или цели, без выходов при смене знака MACD.
- Важно, чтобы у инструмента был заполнен
PriceStep, иначе расстояния защитных уровней будут рассчитаны неверно. - Стратегия работает с завершёнными свечами и требует источника данных, предоставляющего финализированные баровые сообщения.
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()