MACD с фильтром по нулевой линии
Обзор
Стратегия «MACD с фильтром по нулевой линии» представляет собой порт MetaTrader 4-советника Robot_MACD_12.26.9. Исходный робот
отслеживает пересечения линии MACD и сигнальной линии, но разрешает открывать длинные позиции только тогда, когда обе линии
остаются ниже нуля, а короткие позиции — только при нахождении обеих линий выше нуля. Реализация на StockSharp сохраняет эти
фильтры, добавляет встроенную систему управления риском (контроль баланса портфеля и фиксированный тейк-профит в пунктах) и
вынесена в параметры, пригодные для оптимизации.
Стратегия работает с завершёнными свечами выбранного таймфрейма. Индикатор MovingAverageConvergenceDivergenceSignal подключается
через BindEx, что позволяет получить значения MACD и сигнальной линии в одном колбэке без ручного обращения к GetValue.
Логика стратегии
Расчёт индикаторов
- Линия MACD — разница между быстрой и медленной EMA (по умолчанию 12 и 26 периодов).
- Сигнальная линия — EMA от MACD (по умолчанию 9 периодов).
- Фильтр по нулю — проверяет знак обоих значений и решает, допускается ли вход по текущему пересечению.
Правила входа
- Покупка
- MACD пересекает сигнал снизу вверх (
MACD[t-1] < Signal[t-1], MACD[t] > Signal[t]).
- После пересечения обе линии остаются ниже нуля.
- Текущая позиция должна быть пустой или короткой; при наличии шорта он закрывается и только затем допускается новый вход.
- Дополнительно проверяется баланс портфеля:
CurrentValue должен быть не меньше MinimumBalancePerVolume * LotVolume.
- Продажа
- MACD пересекает сигнал сверху вниз (
MACD[t-1] > Signal[t-1], MACD[t] < Signal[t]).
- Обе линии выше нулевой отметки.
- Позиция должна быть пустой или длинной; лонги закрываются до отправки новой продажи.
- Балансовый фильтр действует симметрично.
Правила выхода
- Пересечение в противоположную сторону — закрывает текущую позицию рыночной заявкой, полностью повторяя поведение оригинала.
- Фиксированный тейк-профит — задаётся в пунктах и запускается через
StartProtection, аналог параметра TakeProfit в МТ4.
Управление риском
- Объём сделки —
LotVolume соответствует параметру Lots в МТ4 и используется для каждой заявки.
- Фильтр баланса —
MinimumBalancePerVolume задаёт минимальное значение портфеля на единицу объёма. При недостатке средств
стратегия выводит диагностическое сообщение и пропускает сигнал.
- Контроль данных — обработка ведётся только по закрытым свечам и после того, как
IsFormedAndOnlineAndAllowTrading()
подтверждает готовность соединения и индикаторов.
Параметры
| Параметр |
Описание |
FastPeriod |
Период быстрой EMA в расчёте MACD. |
SlowPeriod |
Период медленной EMA в расчёте MACD. |
SignalPeriod |
Период сглаживания сигнальной линии. |
TakeProfitPoints |
Расстояние до тейк-профита в пунктах (0 — отключить). |
LotVolume |
Объём заявки, эквивалентный лотам в MT4. |
MinimumBalancePerVolume |
Требуемый баланс портфеля на единицу объёма (0 — фильтр отключён). |
CandleType |
Таймфрейм свечей, используемых для расчётов. |
Дополнительно
- Все комментарии в исходном коде написаны на английском языке согласно общим требованиям.
- Python-версии стратегии не существует, реализована только C#-модификация.
- Для максимального сходства с MT4 рекомендуется выбрать тот же таймфрейм и объём, что использовались в исходном советнике.
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>
/// Port of the MetaTrader 4 expert advisor Robot_MACD_12.26.9.
/// Trades MACD signal-line crossovers, but only enters longs while both lines stay below zero and shorts while they stay above zero.
/// Includes an optional balance filter and a fixed take-profit expressed in instrument points.
/// </summary>
public class MacdZeroFilteredCrossStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _signalPeriod;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _lotVolume;
private readonly StrategyParam<decimal> _minimumBalancePerVolume;
private readonly StrategyParam<DataType> _candleType;
private MovingAverageConvergenceDivergenceSignal _macd = null!;
private decimal? _previousMacd;
private decimal? _previousSignal;
/// <summary>
/// Fast EMA length used by MACD.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow EMA length used by MACD.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Signal line smoothing length for MACD.
/// </summary>
public int SignalPeriod
{
get => _signalPeriod.Value;
set => _signalPeriod.Value = value;
}
/// <summary>
/// Take-profit distance expressed in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Base trading volume that mirrors the "Lots" setting in the original robot.
/// </summary>
public decimal LotVolume
{
get => _lotVolume.Value;
set => _lotVolume.Value = value;
}
/// <summary>
/// Minimum account value required per traded volume unit before opening new positions.
/// </summary>
public decimal MinimumBalancePerVolume
{
get => _minimumBalancePerVolume.Value;
set => _minimumBalancePerVolume.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the strategy.
/// </summary>
public MacdZeroFilteredCrossStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 12)
.SetGreaterThanZero()
.SetDisplay("Fast Period", "Short EMA period for MACD", "MACD")
.SetOptimize(6, 18, 1);
_slowPeriod = Param(nameof(SlowPeriod), 26)
.SetGreaterThanZero()
.SetDisplay("Slow Period", "Long EMA period for MACD", "MACD")
.SetOptimize(20, 40, 2);
_signalPeriod = Param(nameof(SignalPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("Signal Period", "Signal line length for MACD", "MACD")
.SetOptimize(6, 12, 1);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 300m)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Fixed take-profit distance in price points", "Risk Management");
_lotVolume = Param(nameof(LotVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Lot Volume", "Trading volume per order", "Trading")
.SetOptimize(1m, 5m, 1m);
_minimumBalancePerVolume = Param(nameof(MinimumBalancePerVolume), 1000m)
.SetNotNegative()
.SetDisplay("Balance per Volume", "Required balance per volume unit before opening trades", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe that drives MACD calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousMacd = null;
_previousSignal = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
_macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = FastPeriod },
LongMa = { Length = SlowPeriod },
},
SignalMa = { Length = SignalPeriod }
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_macd, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
if (TakeProfitPoints > 0m)
{
StartProtection(new Unit(TakeProfitPoints, UnitTypes.Absolute), null);
}
base.OnStarted2(time);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue)
{
// Work only with completed candles to avoid premature signals.
if (candle.State != CandleStates.Finished)
return;
// Skip processing when the strategy is not ready or trading is disabled.
if (!IsFormedAndOnlineAndAllowTrading())
return;
var typed = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
// Ensure both MACD and signal components are available before calculating.
if (typed.Macd is not decimal macdLine || typed.Signal is not decimal signalLine)
return;
if (_previousMacd is decimal prevMacd && _previousSignal is decimal prevSignal)
{
var crossUp = prevMacd < prevSignal && macdLine > signalLine;
var crossDown = prevMacd > prevSignal && macdLine < signalLine;
// Close existing long position when MACD crosses below the signal line.
if (crossDown && Position > 0m)
{
if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
_previousMacd = macdLine;
_previousSignal = signalLine;
return;
}
// Close existing short position when MACD crosses above the signal line.
if (crossUp && Position < 0m)
{
if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
_previousMacd = macdLine;
_previousSignal = signalLine;
return;
}
// Enter long only when the crossover happens below zero (momentum still negative).
if (crossUp && macdLine < 0m && signalLine < 0m && Position <= 0m && HasRequiredBalance())
{
var volume = LotVolume;
BuyMarket(volume);
}
// Enter short only when the crossover happens above zero (momentum still positive).
else if (crossDown && macdLine > 0m && signalLine > 0m && Position >= 0m && HasRequiredBalance())
{
var volume = LotVolume;
SellMarket(volume);
}
}
_previousMacd = macdLine;
_previousSignal = signalLine;
}
private bool HasRequiredBalance()
{
// If portfolio information is not available, assume requirements are met.
var balance = Portfolio?.CurrentValue;
if (balance is null)
return true;
var required = MinimumBalancePerVolume * LotVolume;
if (required <= 0m)
return true;
if (balance.Value >= required)
return true;
return false;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, UnitTypes, Unit
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal
class macd_zero_filtered_cross_strategy(Strategy):
def __init__(self):
super(macd_zero_filtered_cross_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 12) \
.SetDisplay("Fast Period", "Short EMA period for MACD", "MACD")
self._slow_period = self.Param("SlowPeriod", 26) \
.SetDisplay("Slow Period", "Long EMA period for MACD", "MACD")
self._signal_period = self.Param("SignalPeriod", 9) \
.SetDisplay("Signal Period", "Signal line length for MACD", "MACD")
self._take_profit_points = self.Param("TakeProfitPoints", 300.0) \
.SetDisplay("Take Profit (points)", "Fixed take-profit distance in price points", "Risk Management")
self._lot_volume = self.Param("LotVolume", 1.0) \
.SetDisplay("Lot Volume", "Trading volume per order", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe that drives MACD calculations", "General")
self._previous_macd = None
self._previous_signal = None
@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 TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def LotVolume(self):
return self._lot_volume.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(macd_zero_filtered_cross_strategy, self).OnStarted2(time)
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self.FastPeriod
macd.Macd.LongMa.Length = self.SlowPeriod
macd.SignalMa.Length = self.SignalPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(macd, self.ProcessCandle).Start()
tp = float(self.TakeProfitPoints)
if tp > 0:
self.StartProtection(Unit(tp, UnitTypes.Absolute), None)
def ProcessCandle(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
macd_line = macd_value.Macd
signal_line = macd_value.Signal
if macd_line is None or signal_line is None:
return
macd_line = float(macd_line)
signal_line = float(signal_line)
if self._previous_macd is not None and self._previous_signal is not None:
cross_up = self._previous_macd < self._previous_signal and macd_line > signal_line
cross_down = self._previous_macd > self._previous_signal and macd_line < signal_line
if cross_down and self.Position > 0:
self.SellMarket(self.Position)
self._previous_macd = macd_line
self._previous_signal = signal_line
return
if cross_up and self.Position < 0:
self.BuyMarket(abs(self.Position))
self._previous_macd = macd_line
self._previous_signal = signal_line
return
if cross_up and macd_line < 0 and signal_line < 0 and self.Position <= 0:
self.BuyMarket(float(self.LotVolume))
elif cross_down and macd_line > 0 and signal_line > 0 and self.Position >= 0:
self.SellMarket(float(self.LotVolume))
self._previous_macd = macd_line
self._previous_signal = signal_line
def OnReseted(self):
super(macd_zero_filtered_cross_strategy, self).OnReseted()
self._previous_macd = None
self._previous_signal = None
def CreateClone(self):
return macd_zero_filtered_cross_strategy()