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>
/// MAMACD trend-following strategy converted from the original MetaTrader 5 expert advisor.
/// Combines two low-price LWMA filters, a fast EMA trigger, and a MACD confirmation filter.
/// </summary>
public class MamAcdStrategy : Strategy
{
private readonly StrategyParam<int> _firstLowMaLength;
private readonly StrategyParam<int> _secondLowMaLength;
private readonly StrategyParam<int> _triggerEmaLength;
private readonly StrategyParam<int> _macdFastLength;
private readonly StrategyParam<int> _macdSlowLength;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<DataType> _candleType;
private WeightedMovingAverage _firstLowMa = null!;
private WeightedMovingAverage _secondLowMa = null!;
private ExponentialMovingAverage _triggerEma = null!;
private MovingAverageConvergenceDivergence _macd = null!;
private decimal? _previousMacd;
private bool _readyForLong;
private bool _readyForShort;
private decimal _pipSize;
/// <summary>
/// Period of the first LWMA calculated on low prices.
/// </summary>
public int FirstLowMaLength
{
get => _firstLowMaLength.Value;
set => _firstLowMaLength.Value = value;
}
/// <summary>
/// Period of the second LWMA calculated on low prices.
/// </summary>
public int SecondLowMaLength
{
get => _secondLowMaLength.Value;
set => _secondLowMaLength.Value = value;
}
/// <summary>
/// Period of the fast EMA calculated on close prices.
/// </summary>
public int TriggerEmaLength
{
get => _triggerEmaLength.Value;
set => _triggerEmaLength.Value = value;
}
/// <summary>
/// Fast EMA period of the MACD filter.
/// </summary>
public int MacdFastLength
{
get => _macdFastLength.Value;
set => _macdFastLength.Value = value;
}
/// <summary>
/// Slow EMA period of the MACD filter.
/// </summary>
public int MacdSlowLength
{
get => _macdSlowLength.Value;
set => _macdSlowLength.Value = value;
}
/// <summary>
/// Stop-loss distance in pips. Set to zero to disable protective stop.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance in pips. Set to zero to disable take-profit.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes <see cref="MamAcdStrategy"/> with default parameters.
/// </summary>
public MamAcdStrategy()
{
_firstLowMaLength = Param(nameof(FirstLowMaLength), 85)
.SetGreaterThanZero()
.SetDisplay("LWMA #1", "Length of the first LWMA on lows", "Indicators")
;
_secondLowMaLength = Param(nameof(SecondLowMaLength), 75)
.SetGreaterThanZero()
.SetDisplay("LWMA #2", "Length of the second LWMA on lows", "Indicators")
;
_triggerEmaLength = Param(nameof(TriggerEmaLength), 5)
.SetGreaterThanZero()
.SetDisplay("Trigger EMA", "Length of the EMA on closes", "Indicators")
;
_macdFastLength = Param(nameof(MacdFastLength), 15)
.SetGreaterThanZero()
.SetDisplay("MACD Fast", "Fast EMA length of MACD", "Indicators")
;
_macdSlowLength = Param(nameof(MacdSlowLength), 26)
.SetGreaterThanZero()
.SetDisplay("MACD Slow", "Slow EMA length of MACD", "Indicators")
;
_stopLossPips = Param(nameof(StopLossPips), 500)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk management");
_takeProfitPips = Param(nameof(TakeProfitPips), 500)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousMacd = null;
_readyForLong = false;
_readyForShort = false;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_firstLowMa = new WeightedMovingAverage { Length = FirstLowMaLength };
_secondLowMa = new WeightedMovingAverage { Length = SecondLowMaLength };
_triggerEma = new EMA { Length = TriggerEmaLength };
_macd = new MovingAverageConvergenceDivergence
{
ShortMa = { Length = MacdFastLength },
LongMa = { Length = MacdSlowLength }
};
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
_pipSize = CalculatePipSize();
var takeProfit = TakeProfitPips > 0 ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : new Unit();
var stopLoss = StopLossPips > 0 ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : new Unit();
StartProtection(takeProfit, stopLoss);
var priceArea = CreateChartArea();
if (priceArea != null)
{
DrawCandles(priceArea, subscription);
DrawIndicator(priceArea, _firstLowMa);
DrawIndicator(priceArea, _secondLowMa);
DrawIndicator(priceArea, _triggerEma);
DrawOwnTrades(priceArea);
var macdArea = CreateChartArea();
if (macdArea != null)
{
macdArea.Title = "MACD";
DrawIndicator(macdArea, _macd);
}
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// Feed indicator chain: LWMAs work on low prices, EMA and MACD on closes.
var firstLowValue = _firstLowMa.Process(new DecimalIndicatorValue(_firstLowMa, candle.LowPrice, candle.OpenTime) { IsFinal = true });
var secondLowValue = _secondLowMa.Process(new DecimalIndicatorValue(_secondLowMa, candle.LowPrice, candle.OpenTime) { IsFinal = true });
var triggerValue = _triggerEma.Process(new DecimalIndicatorValue(_triggerEma, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
var macdValue = _macd.Process(new DecimalIndicatorValue(_macd, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
// Wait for all indicators to collect enough history.
if (!_firstLowMa.IsFormed || !_secondLowMa.IsFormed || !_triggerEma.IsFormed || !_macd.IsFormed)
{
if (_macd.IsFormed)
_previousMacd = macdValue.ToDecimal();
return;
}
// indicators already checked above
var ma1 = firstLowValue.ToDecimal();
var ma2 = secondLowValue.ToDecimal();
var ma3 = triggerValue.ToDecimal();
var macd = macdValue.ToDecimal();
// Store the first complete MACD observation before evaluating signals.
if (_previousMacd is null)
{
_previousMacd = macd;
return;
}
// Skip calculations when MACD lacks momentum confirmation just like the original EA.
if (macd == 0m || _previousMacd.Value == 0m)
{
_previousMacd = macd;
return;
}
// Track reset flags: EMA must dip below both LWMAs to prepare for a new long, and rise above them for shorts.
if (ma3 < ma1 && ma3 < ma2)
_readyForLong = true;
if (ma3 > ma1 && ma3 > ma2)
_readyForShort = true;
var macdImproving = macd > _previousMacd.Value;
var longSignal = ma3 > ma1 && ma3 > ma2 && _readyForLong && (macd > 0m || macdImproving);
var shortSignal = ma3 < ma1 && ma3 < ma2 && _readyForShort && (macd < 0m || !macdImproving);
if (longSignal && Position <= 0)
{
var volume = Volume + (Position < 0 ? -Position : 0m);
if (volume > 0)
{
BuyMarket();
_readyForLong = false;
}
}
else if (shortSignal && Position >= 0)
{
var volume = Volume + (Position > 0 ? Position : 0m);
if (volume > 0)
{
SellMarket();
_readyForShort = false;
}
}
_previousMacd = macd;
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 1m;
if (step <= 0m)
return 1m;
var decimals = CountDecimalPlaces(step);
return decimals == 3 || decimals == 5 ? step * 10m : step;
}
private static int CountDecimalPlaces(decimal value)
{
value = Math.Abs(value);
var count = 0;
while (value != Math.Truncate(value) && count < 10)
{
value *= 10m;
count++;
}
return count;
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import (
WeightedMovingAverage,
ExponentialMovingAverage,
MovingAverageConvergenceDivergence,
)
from indicator_extensions import *
class mam_acd_strategy(Strategy):
"""MAMACD: two LWMA filters on lows, EMA trigger on close, MACD confirmation."""
def __init__(self):
super(mam_acd_strategy, self).__init__()
self._first_low_ma_length = self.Param("FirstLowMaLength", 85) \
.SetGreaterThanZero() \
.SetDisplay("LWMA #1", "Length of the first LWMA on lows", "Indicators")
self._second_low_ma_length = self.Param("SecondLowMaLength", 75) \
.SetGreaterThanZero() \
.SetDisplay("LWMA #2", "Length of the second LWMA on lows", "Indicators")
self._trigger_ema_length = self.Param("TriggerEmaLength", 5) \
.SetGreaterThanZero() \
.SetDisplay("Trigger EMA", "Length of the EMA on closes", "Indicators")
self._macd_fast_length = self.Param("MacdFastLength", 15) \
.SetGreaterThanZero() \
.SetDisplay("MACD Fast", "Fast EMA length of MACD", "Indicators")
self._macd_slow_length = self.Param("MacdSlowLength", 26) \
.SetGreaterThanZero() \
.SetDisplay("MACD Slow", "Slow EMA length of MACD", "Indicators")
self._stop_loss_pips = self.Param("StopLossPips", 500) \
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk management")
self._take_profit_pips = self.Param("TakeProfitPips", 500) \
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles used for calculations", "General")
self._previous_macd = None
self._ready_for_long = False
self._ready_for_short = False
self._pip_size = 0.0
@property
def FirstLowMaLength(self):
return int(self._first_low_ma_length.Value)
@property
def SecondLowMaLength(self):
return int(self._second_low_ma_length.Value)
@property
def TriggerEmaLength(self):
return int(self._trigger_ema_length.Value)
@property
def MacdFastLength(self):
return int(self._macd_fast_length.Value)
@property
def MacdSlowLength(self):
return int(self._macd_slow_length.Value)
@property
def StopLossPips(self):
return int(self._stop_loss_pips.Value)
@property
def TakeProfitPips(self):
return int(self._take_profit_pips.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _calc_pip_size(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
if step <= 0:
return 1.0
# Count decimal places
v = abs(step)
count = 0
while v != int(v) and count < 10:
v *= 10
count += 1
return step * 10.0 if (count == 3 or count == 5) else step
def OnStarted2(self, time):
super(mam_acd_strategy, self).OnStarted2(time)
self._previous_macd = None
self._ready_for_long = False
self._ready_for_short = False
self._first_low_ma = WeightedMovingAverage()
self._first_low_ma.Length = self.FirstLowMaLength
self._second_low_ma = WeightedMovingAverage()
self._second_low_ma.Length = self.SecondLowMaLength
self._trigger_ema = ExponentialMovingAverage()
self._trigger_ema.Length = self.TriggerEmaLength
self._macd_ind = MovingAverageConvergenceDivergence()
self._macd_ind.ShortMa.Length = self.MacdFastLength
self._macd_ind.LongMa.Length = self.MacdSlowLength
self._pip_size = self._calc_pip_size()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
tp = Unit(self.TakeProfitPips * self._pip_size, UnitTypes.Absolute) if self.TakeProfitPips > 0 else None
sl = Unit(self.StopLossPips * self._pip_size, UnitTypes.Absolute) if self.StopLossPips > 0 else None
if tp is not None and sl is not None:
self.StartProtection(takeProfit=tp, stopLoss=sl)
elif tp is not None:
self.StartProtection(takeProfit=tp)
elif sl is not None:
self.StartProtection(stopLoss=sl)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._first_low_ma)
self.DrawIndicator(area, self._second_low_ma)
self.DrawIndicator(area, self._trigger_ema)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
t = candle.ServerTime
lo = candle.LowPrice
close = candle.ClosePrice
first_low_val = process_float(self._first_low_ma, lo, t, True)
second_low_val = process_float(self._second_low_ma, lo, t, True)
trigger_val = process_float(self._trigger_ema, close, t, True)
macd_val = process_float(self._macd_ind, close, t, True)
if (not self._first_low_ma.IsFormed or not self._second_low_ma.IsFormed
or not self._trigger_ema.IsFormed or not self._macd_ind.IsFormed):
if self._macd_ind.IsFormed:
self._previous_macd = float(macd_val.Value)
return
ma1 = float(first_low_val.Value)
ma2 = float(second_low_val.Value)
ma3 = float(trigger_val.Value)
macd = float(macd_val.Value)
if self._previous_macd is None:
self._previous_macd = macd
return
if macd == 0 or self._previous_macd == 0:
self._previous_macd = macd
return
# Track readiness flags
if ma3 < ma1 and ma3 < ma2:
self._ready_for_long = True
if ma3 > ma1 and ma3 > ma2:
self._ready_for_short = True
macd_improving = macd > self._previous_macd
long_signal = ma3 > ma1 and ma3 > ma2 and self._ready_for_long and (macd > 0 or macd_improving)
short_signal = ma3 < ma1 and ma3 < ma2 and self._ready_for_short and (macd < 0 or not macd_improving)
if long_signal and self.Position <= 0:
self.BuyMarket()
self._ready_for_long = False
elif short_signal and self.Position >= 0:
self.SellMarket()
self._ready_for_short = False
self._previous_macd = macd
def OnReseted(self):
super(mam_acd_strategy, self).OnReseted()
self._previous_macd = None
self._ready_for_long = False
self._ready_for_short = False
self._pip_size = 0.0
def CreateClone(self):
return mam_acd_strategy()