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>
/// MACD strategy that replicates the original MetaTrader "MACD Sample" expert advisor behaviour.
/// </summary>
public class MacdSampleClassicStrategy : Strategy
{
private readonly StrategyParam<int> _fastEmaPeriod;
private readonly StrategyParam<int> _slowEmaPeriod;
private readonly StrategyParam<int> _signalPeriod;
private readonly StrategyParam<int> _trendMaPeriod;
private readonly StrategyParam<decimal> _macdOpenLevel;
private readonly StrategyParam<decimal> _macdCloseLevel;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _minimumHistoryCandles;
private decimal _pointSize;
private decimal? _prevMacd;
private decimal? _prevSignal;
private decimal? _trendMaCurrent;
private decimal? _trendMaPrevious;
private int _finishedCandles;
private DateTimeOffset? _lastProcessedTime;
/// <summary>
/// Fast EMA period for the MACD indicator.
/// </summary>
public int FastEmaPeriod
{
get => _fastEmaPeriod.Value;
set => _fastEmaPeriod.Value = value;
}
/// <summary>
/// Slow EMA period for the MACD indicator.
/// </summary>
public int SlowEmaPeriod
{
get => _slowEmaPeriod.Value;
set => _slowEmaPeriod.Value = value;
}
/// <summary>
/// Signal line period for the MACD indicator.
/// </summary>
public int SignalPeriod
{
get => _signalPeriod.Value;
set => _signalPeriod.Value = value;
}
/// <summary>
/// Period of the trend EMA used as direction filter.
/// </summary>
public int TrendMaPeriod
{
get => _trendMaPeriod.Value;
set => _trendMaPeriod.Value = value;
}
/// <summary>
/// Threshold for MACD entries expressed in points (price steps).
/// </summary>
public decimal MacdOpenLevel
{
get => _macdOpenLevel.Value;
set => _macdOpenLevel.Value = value;
}
/// <summary>
/// Threshold for MACD exits expressed in points (price steps).
/// </summary>
public decimal MacdCloseLevel
{
get => _macdCloseLevel.Value;
set => _macdCloseLevel.Value = value;
}
/// <summary>
/// Take profit distance measured in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance measured in price points.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Number of finished candles required before the strategy begins trading.
/// </summary>
public int MinimumHistoryCandles
{
get => _minimumHistoryCandles.Value;
set => _minimumHistoryCandles.Value = value;
}
/// <summary>
/// Initialize default parameters for the MACD Sample strategy.
/// </summary>
public MacdSampleClassicStrategy()
{
Volume = 1;
_fastEmaPeriod = Param(nameof(FastEmaPeriod), 12)
.SetDisplay("Fast EMA", "Fast EMA period for MACD", "Indicators")
.SetOptimize(6, 18, 2);
_slowEmaPeriod = Param(nameof(SlowEmaPeriod), 26)
.SetDisplay("Slow EMA", "Slow EMA period for MACD", "Indicators")
.SetOptimize(20, 32, 2);
_signalPeriod = Param(nameof(SignalPeriod), 9)
.SetDisplay("Signal EMA", "Signal EMA period for MACD", "Indicators")
.SetOptimize(5, 13, 2);
_trendMaPeriod = Param(nameof(TrendMaPeriod), 26)
.SetDisplay("Trend EMA", "EMA period used for directional filter", "Indicators")
.SetOptimize(20, 40, 2);
_macdOpenLevel = Param(nameof(MacdOpenLevel), 0m)
.SetDisplay("MACD Open", "Entry threshold in MACD points", "Signals")
.SetOptimize(1m, 5m, 1m);
_macdCloseLevel = Param(nameof(MacdCloseLevel), 0m)
.SetDisplay("MACD Close", "Exit threshold in MACD points", "Signals")
.SetOptimize(1m, 4m, 1m);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 50m)
.SetDisplay("Take Profit", "Take profit distance in price points", "Risk")
.SetOptimize(20m, 100m, 10m);
_trailingStopPoints = Param(nameof(TrailingStopPoints), 30m)
.SetDisplay("Trailing Stop", "Trailing stop distance in price points", "Risk")
.SetOptimize(10m, 60m, 10m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle type used for analysis", "General");
_minimumHistoryCandles = Param(nameof(MinimumHistoryCandles), 30)
.SetDisplay("Warm-up candles", "Number of finished candles required before trading starts", "General")
.SetGreaterThanZero();
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pointSize = 0m;
_prevMacd = null;
_prevSignal = null;
_trendMaCurrent = null;
_trendMaPrevious = null;
_finishedCandles = 0;
_lastProcessedTime = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointSize = Security?.PriceStep ?? 1m;
// Configure indicators exactly as in the original expert advisor.
var macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = FastEmaPeriod },
LongMa = { Length = SlowEmaPeriod },
},
SignalMa = { Length = SignalPeriod }
};
var trendMa = new ExponentialMovingAverage { Length = TrendMaPeriod };
// Subscribe to candles and bind indicators for automatic updates.
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(macd, ProcessMacdValues);
subscription.Bind(trendMa, ProcessTrendMaValue);
subscription.Start();
// Visualize price, indicators and trades when a chart is available.
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, macd);
DrawIndicator(area, trendMa);
DrawOwnTrades(area);
}
var takeProfitDistance = TakeProfitPoints * _pointSize;
var trailingDistance = TrailingStopPoints * _pointSize;
if (takeProfitDistance > 0m || trailingDistance > 0m)
{
StartProtection(
takeProfitDistance > 0m ? new Unit(takeProfitDistance, UnitTypes.Absolute) : null,
trailingDistance > 0m ? new Unit(trailingDistance, UnitTypes.Absolute) : null,
isStopTrailing: trailingDistance > 0m);
}
}
private void ProcessTrendMaValue(ICandleMessage candle, decimal maValue)
{
if (candle.State != CandleStates.Finished)
return;
// Keep current and previous EMA values for the directional filter.
_trendMaPrevious = _trendMaCurrent;
_trendMaCurrent = maValue;
}
private void ProcessMacdValues(ICandleMessage candle, IIndicatorValue macdValue)
{
if (candle.State != CandleStates.Finished)
return;
// Avoid duplicate processing for the same candle.
if (_lastProcessedTime != candle.OpenTime)
{
_lastProcessedTime = candle.OpenTime;
_finishedCandles++;
}
// indicators checked below
if (_finishedCandles < MinimumHistoryCandles)
return;
var macdSignal = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
if (macdSignal.Macd is not decimal macdCurrent ||
macdSignal.Signal is not decimal signalCurrent)
{
return;
}
if (_prevMacd is not decimal macdPrevious ||
_prevSignal is not decimal signalPrevious ||
_trendMaCurrent is not decimal trendMaCurrent ||
_trendMaPrevious is not decimal trendMaPrevious)
{
_prevMacd = macdCurrent;
_prevSignal = signalCurrent;
return;
}
var macdOpenThreshold = MacdOpenLevel * _pointSize;
var macdCloseThreshold = MacdCloseLevel * _pointSize;
// Determine trend direction using EMA slope.
var isTrendUp = trendMaCurrent > trendMaPrevious;
var isTrendDown = trendMaCurrent < trendMaPrevious;
var buySignal = macdCurrent < 0m &&
macdCurrent > signalCurrent &&
macdPrevious < signalPrevious &&
Math.Abs(macdCurrent) > macdOpenThreshold &&
isTrendUp;
var sellSignal = macdCurrent > 0m &&
macdCurrent < signalCurrent &&
macdPrevious > signalPrevious &&
macdCurrent > macdOpenThreshold &&
isTrendDown;
var exitLongSignal = macdCurrent > 0m &&
macdCurrent < signalCurrent &&
macdPrevious > signalPrevious &&
macdCurrent > macdCloseThreshold;
var exitShortSignal = macdCurrent < 0m &&
macdCurrent > signalCurrent &&
macdPrevious < signalPrevious &&
Math.Abs(macdCurrent) > macdCloseThreshold;
if (buySignal && Position == 0m)
{
// MACD crossed up in negative territory and EMA confirms uptrend.
BuyMarket();
LogInfo($"Open long: MACD {macdCurrent:F5} above signal {signalCurrent:F5}.");
}
else if (sellSignal && Position == 0m)
{
// MACD crossed down in positive territory and EMA confirms downtrend.
SellMarket();
LogInfo($"Open short: MACD {macdCurrent:F5} below signal {signalCurrent:F5}.");
}
else if (exitLongSignal && Position > 0m)
{
// MACD crossed back below the signal line in positive zone - close long.
SellMarket();
LogInfo($"Close long: MACD {macdCurrent:F5} dropped under signal {signalCurrent:F5}.");
}
else if (exitShortSignal && Position < 0m)
{
// MACD crossed back above the signal line in negative zone - close short.
BuyMarket();
LogInfo($"Close short: MACD {macdCurrent:F5} rose above signal {signalCurrent:F5}.");
}
_prevMacd = macdCurrent;
_prevSignal = signalCurrent;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
class macd_sample_classic_strategy(Strategy):
"""Classic MACD strategy replicating the original MetaTrader MACD Sample expert advisor.
Uses MACD crossover with EMA trend filter, StartProtection for TP/trailing."""
def __init__(self):
super(macd_sample_classic_strategy, self).__init__()
self._fast_ema_period = self.Param("FastEmaPeriod", 12) \
.SetDisplay("Fast EMA", "Fast EMA period for MACD", "Indicators")
self._slow_ema_period = self.Param("SlowEmaPeriod", 26) \
.SetDisplay("Slow EMA", "Slow EMA period for MACD", "Indicators")
self._signal_period = self.Param("SignalPeriod", 9) \
.SetDisplay("Signal EMA", "Signal EMA period for MACD", "Indicators")
self._trend_ma_period = self.Param("TrendMaPeriod", 26) \
.SetDisplay("Trend EMA", "EMA period used for directional filter", "Indicators")
self._macd_open_level = self.Param("MacdOpenLevel", 0.0) \
.SetDisplay("MACD Open", "Entry threshold in MACD points", "Signals")
self._macd_close_level = self.Param("MacdCloseLevel", 0.0) \
.SetDisplay("MACD Close", "Exit threshold in MACD points", "Signals")
self._take_profit_points = self.Param("TakeProfitPoints", 50.0) \
.SetDisplay("Take Profit", "Take profit distance in price points", "Risk")
self._trailing_stop_points = self.Param("TrailingStopPoints", 30.0) \
.SetDisplay("Trailing Stop", "Trailing stop distance in price points", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Candle type used for analysis", "General")
self._minimum_history_candles = self.Param("MinimumHistoryCandles", 30) \
.SetGreaterThanZero() \
.SetDisplay("Warm-up candles", "Number of finished candles required before trading starts", "General")
self._point_size = 0.0
self._prev_macd = None
self._prev_signal = None
self._trend_ma_current = None
self._trend_ma_previous = None
self._finished_candles = 0
self._last_processed_time = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def FastEmaPeriod(self):
return self._fast_ema_period.Value
@property
def SlowEmaPeriod(self):
return self._slow_ema_period.Value
@property
def SignalPeriod(self):
return self._signal_period.Value
@property
def TrendMaPeriod(self):
return self._trend_ma_period.Value
@property
def MacdOpenLevel(self):
return self._macd_open_level.Value
@property
def MacdCloseLevel(self):
return self._macd_close_level.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def TrailingStopPoints(self):
return self._trailing_stop_points.Value
@property
def MinimumHistoryCandles(self):
return self._minimum_history_candles.Value
def OnReseted(self):
super(macd_sample_classic_strategy, self).OnReseted()
self._point_size = 0.0
self._prev_macd = None
self._prev_signal = None
self._trend_ma_current = None
self._trend_ma_previous = None
self._finished_candles = 0
self._last_processed_time = None
def OnStarted2(self, time):
super(macd_sample_classic_strategy, self).OnStarted2(time)
self._point_size = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps > 0:
self._point_size = ps
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self.FastEmaPeriod
macd.Macd.LongMa.Length = self.SlowEmaPeriod
macd.SignalMa.Length = self.SignalPeriod
trend_ma = ExponentialMovingAverage()
trend_ma.Length = self.TrendMaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(macd, self._process_macd_values)
subscription.Bind(trend_ma, self._process_trend_ma_value)
subscription.Start()
tp_distance = float(self.TakeProfitPoints) * self._point_size
trailing_distance = float(self.TrailingStopPoints) * self._point_size
if tp_distance > 0 or trailing_distance > 0:
tp_unit = Unit(tp_distance, UnitTypes.Absolute) if tp_distance > 0 else None
sl_unit = Unit(trailing_distance, UnitTypes.Absolute) if trailing_distance > 0 else None
self.StartProtection(tp_unit, sl_unit, trailing_distance > 0)
def _process_trend_ma_value(self, candle, ma_value):
if candle.State != CandleStates.Finished:
return
self._trend_ma_previous = self._trend_ma_current
self._trend_ma_current = float(ma_value)
def _process_macd_values(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
if self._last_processed_time != candle.OpenTime:
self._last_processed_time = candle.OpenTime
self._finished_candles += 1
if self._finished_candles < self.MinimumHistoryCandles:
return
macd_main_raw = macd_value.Macd
signal_raw = macd_value.Signal
if macd_main_raw is None or signal_raw is None:
return
macd_current = float(macd_main_raw)
signal_current = float(signal_raw)
if self._prev_macd is None or self._prev_signal is None or \
self._trend_ma_current is None or self._trend_ma_previous is None:
self._prev_macd = macd_current
self._prev_signal = signal_current
return
macd_previous = self._prev_macd
signal_previous = self._prev_signal
trend_ma_current = self._trend_ma_current
trend_ma_previous = self._trend_ma_previous
macd_open_threshold = float(self.MacdOpenLevel) * self._point_size
macd_close_threshold = float(self.MacdCloseLevel) * self._point_size
is_trend_up = trend_ma_current > trend_ma_previous
is_trend_down = trend_ma_current < trend_ma_previous
buy_signal = macd_current < 0 and \
macd_current > signal_current and \
macd_previous < signal_previous and \
abs(macd_current) > macd_open_threshold and \
is_trend_up
sell_signal = macd_current > 0 and \
macd_current < signal_current and \
macd_previous > signal_previous and \
macd_current > macd_open_threshold and \
is_trend_down
exit_long_signal = macd_current > 0 and \
macd_current < signal_current and \
macd_previous > signal_previous and \
macd_current > macd_close_threshold
exit_short_signal = macd_current < 0 and \
macd_current > signal_current and \
macd_previous < signal_previous and \
abs(macd_current) > macd_close_threshold
if buy_signal and self.Position == 0:
self.BuyMarket()
elif sell_signal and self.Position == 0:
self.SellMarket()
elif exit_long_signal and self.Position > 0:
self.SellMarket()
elif exit_short_signal and self.Position < 0:
self.BuyMarket()
self._prev_macd = macd_current
self._prev_signal = signal_current
def CreateClone(self):
return macd_sample_classic_strategy()