using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Momentum-based strategy converted from the "Momo Trades V3" MetaTrader expert.
/// The system combines MACD momentum patterns with a displaced EMA filter and optional breakeven management.
/// </summary>
public class MomoTradesV3Strategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _emaShift;
private readonly StrategyParam<int> _macdFast;
private readonly StrategyParam<int> _macdSlow;
private readonly StrategyParam<int> _macdSignal;
private readonly StrategyParam<int> _macdShift;
private readonly StrategyParam<decimal> _macdZeroTolerance;
private readonly StrategyParam<decimal> _priceShiftPoints;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<decimal> _riskFraction;
private readonly StrategyParam<bool> _useAutoVolume;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<bool> _closeEndDay;
private readonly StrategyParam<bool> _useBreakeven;
private readonly StrategyParam<decimal> _breakevenOffsetPoints;
private readonly List<decimal> _macdHistory = new();
private readonly List<decimal> _emaHistory = new();
private readonly List<decimal> _closeHistory = new();
private decimal _pointValue;
private decimal? _breakevenPrice;
private Sides? _breakevenSide;
private int _candlesSinceLastOrder;
private const int CooldownCandles = 200;
/// <summary>
/// Primary candle type for the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// EMA period used for the directional filter.
/// </summary>
public int MaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// Number of finished bars used when sampling the EMA and close prices.
/// </summary>
public int MaShift
{
get => _emaShift.Value;
set => _emaShift.Value = value;
}
/// <summary>
/// Fast EMA length for the MACD indicator.
/// </summary>
public int FastPeriod
{
get => _macdFast.Value;
set => _macdFast.Value = value;
}
/// <summary>
/// Slow EMA length for the MACD indicator.
/// </summary>
public int SlowPeriod
{
get => _macdSlow.Value;
set => _macdSlow.Value = value;
}
/// <summary>
/// Signal EMA length for the MACD indicator.
/// </summary>
public int SignalPeriod
{
get => _macdSignal.Value;
set => _macdSignal.Value = value;
}
/// <summary>
/// Additional bar displacement applied when reading MACD values.
/// </summary>
public int MacdShift
{
get => _macdShift.Value;
set => _macdShift.Value = value;
}
/// <summary>
/// Absolute tolerance used to treat MACD values as neutral.
/// </summary>
public decimal MacdZeroTolerance
{
get => _macdZeroTolerance.Value;
set => _macdZeroTolerance.Value = value;
}
/// <summary>
/// Minimal distance between price and EMA expressed in MetaTrader points.
/// </summary>
public decimal PriceShiftPoints
{
get => _priceShiftPoints.Value;
set => _priceShiftPoints.Value = value;
}
/// <summary>
/// Base trading volume.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Fraction of portfolio equity allocated when auto volume is enabled.
/// </summary>
public decimal RiskFraction
{
get => _riskFraction.Value;
set => _riskFraction.Value = value;
}
/// <summary>
/// Enable automatic position sizing.
/// </summary>
public bool UseAutoVolume
{
get => _useAutoVolume.Value;
set => _useAutoVolume.Value = value;
}
/// <summary>
/// Initial protective stop distance in MetaTrader points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Initial take-profit distance in MetaTrader points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Close any open position at the end of the trading day.
/// </summary>
public bool CloseEndDay
{
get => _closeEndDay.Value;
set => _closeEndDay.Value = value;
}
/// <summary>
/// Enable breakeven stop handling.
/// </summary>
public bool UseBreakeven
{
get => _useBreakeven.Value;
set => _useBreakeven.Value = value;
}
/// <summary>
/// Offset applied to the breakeven trigger in MetaTrader points.
/// </summary>
public decimal BreakevenOffsetPoints
{
get => _breakevenOffsetPoints.Value;
set => _breakevenOffsetPoints.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="MomoTradesV3Strategy"/>.
/// </summary>
public MomoTradesV3Strategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe", "General");
_emaPeriod = Param(nameof(MaPeriod), 22)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "Length of the EMA filter", "Indicators");
_emaShift = Param(nameof(MaShift), 1)
.SetGreaterThanZero()
.SetDisplay("EMA Shift", "Number of closed bars used for EMA comparison", "Indicators");
_macdFast = Param(nameof(FastPeriod), 12)
.SetGreaterThanZero()
.SetDisplay("MACD Fast", "Fast EMA period", "Indicators");
_macdSlow = Param(nameof(SlowPeriod), 26)
.SetGreaterThanZero()
.SetDisplay("MACD Slow", "Slow EMA period", "Indicators");
_macdSignal = Param(nameof(SignalPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("MACD Signal", "Signal EMA period", "Indicators");
_macdShift = Param(nameof(MacdShift), 1)
.SetGreaterThanZero()
.SetDisplay("MACD Shift", "Extra displacement for MACD history", "Indicators");
_macdZeroTolerance = Param(nameof(MacdZeroTolerance), 1e-8m)
.SetGreaterThanZero()
.SetDisplay("MACD Zero Tolerance", "Absolute tolerance for flat MACD detection", "Indicators");
_priceShiftPoints = Param(nameof(PriceShiftPoints), 10m)
.SetDisplay("Price Shift", "Required price offset from EMA in MetaTrader points", "Signals");
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Base Volume", "Default trade volume", "Trading");
_riskFraction = Param(nameof(RiskFraction), 0.1m)
.SetDisplay("Risk Fraction", "Equity fraction for auto volume", "Risk Management");
_useAutoVolume = Param(nameof(UseAutoVolume), false)
.SetDisplay("Auto Volume", "Enable risk-based volume sizing", "Risk Management");
_stopLossPoints = Param(nameof(StopLossPoints), 100m)
.SetDisplay("Stop-Loss Points", "Distance to the initial stop in MetaTrader points", "Risk Management");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 0m)
.SetDisplay("Take-Profit Points", "Distance to the initial take-profit in MetaTrader points", "Risk Management");
_closeEndDay = Param(nameof(CloseEndDay), true)
.SetDisplay("Close End of Day", "Exit positions near the session close", "Risk Management");
_useBreakeven = Param(nameof(UseBreakeven), false)
.SetDisplay("Use Breakeven", "Move the stop to breakeven after profit", "Risk Management");
_breakevenOffsetPoints = Param(nameof(BreakevenOffsetPoints), 0m)
.SetDisplay("Breakeven Offset", "Additional points added to the breakeven level", "Risk Management");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_macdHistory.Clear();
_emaHistory.Clear();
_closeHistory.Clear();
_breakevenPrice = null;
_breakevenSide = null;
_pointValue = 0;
_candlesSinceLastOrder = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointValue = Security?.PriceStep ?? 0m;
if (_pointValue <= 0m)
{
var decimals = Security?.Decimals;
if (decimals != null)
{
_pointValue = (decimal)Math.Pow(10, -decimals.Value);
}
if (_pointValue <= 0m)
_pointValue = 0.0001m;
}
Volume = TradeVolume;
var macd = new MovingAverageConvergenceDivergence();
macd.ShortMa.Length = FastPeriod;
macd.LongMa.Length = SlowPeriod;
var ema = new ExponentialMovingAverage { Length = MaPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(macd, ema, ProcessCandle)
.Start();
var stopUnit = StopLossPoints > 0m && _pointValue > 0m
? new Unit(StopLossPoints * _pointValue, UnitTypes.Absolute)
: null;
var takeUnit = TakeProfitPoints > 0m && _pointValue > 0m
? new Unit(TakeProfitPoints * _pointValue, UnitTypes.Absolute)
: null;
StartProtection(takeProfit: takeUnit, stopLoss: stopUnit);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawIndicator(area, macd);
}
}
private void ProcessCandle(ICandleMessage candle, decimal macdLine, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
_macdHistory.Insert(0, macdLine);
TrimHistory(_macdHistory);
_emaHistory.Insert(0, emaValue);
TrimHistory(_emaHistory);
_closeHistory.Insert(0, candle.ClosePrice);
TrimHistory(_closeHistory);
_candlesSinceLastOrder++;
if (!IsFormedAndOnlineAndAllowTrading())
return;
HandleEndOfDayExit(candle);
HandleBreakeven(candle);
if (Position != 0)
return;
if (_candlesSinceLastOrder < CooldownCandles)
return;
var priceShift = PriceShiftPoints * _pointValue;
var canBuy = EvaluateMacdBuy() && EvaluateEmaBuy(priceShift);
var canSell = EvaluateMacdSell() && EvaluateEmaSell(priceShift);
if (canBuy)
{
var volume = CalculateOrderVolume(candle.ClosePrice);
volume = NormalizeVolume(volume);
if (volume > 0m)
{
BuyMarket(volume);
_candlesSinceLastOrder = 0;
}
}
else if (canSell)
{
var volume = CalculateOrderVolume(candle.ClosePrice);
volume = NormalizeVolume(volume);
if (volume > 0m)
{
SellMarket(volume);
_candlesSinceLastOrder = 0;
}
}
}
private void HandleEndOfDayExit(ICandleMessage candle)
{
if (!CloseEndDay || Position == 0)
return;
var endHour = candle.OpenTime.DayOfWeek == DayOfWeek.Friday ? 21 : 23;
if (candle.OpenTime.Hour != endHour)
return;
if (Position > 0)
{
SellMarket(Math.Abs(Position));
}
else if (Position < 0)
{
BuyMarket(Math.Abs(Position));
}
_breakevenPrice = null;
_breakevenSide = null;
}
private void HandleBreakeven(ICandleMessage candle)
{
if (!UseBreakeven || Position == 0)
{
_breakevenPrice = null;
_breakevenSide = null;
return;
}
var entryPrice = candle.ClosePrice;
var offset = BreakevenOffsetPoints * _pointValue;
if (Position > 0)
{
if (_breakevenSide != Sides.Buy)
{
_breakevenSide = Sides.Buy;
_breakevenPrice = null;
}
var desired = entryPrice + offset;
if (_breakevenPrice == null)
{
if (candle.LowPrice > desired)
{
_breakevenPrice = desired;
}
}
else if (candle.LowPrice <= _breakevenPrice.Value)
{
SellMarket(Math.Abs(Position));
_breakevenPrice = null;
_breakevenSide = null;
}
}
else if (Position < 0)
{
if (_breakevenSide != Sides.Sell)
{
_breakevenSide = Sides.Sell;
_breakevenPrice = null;
}
var desired = entryPrice - offset;
if (_breakevenPrice == null)
{
if (candle.HighPrice < desired)
{
_breakevenPrice = desired;
}
}
else if (candle.HighPrice >= _breakevenPrice.Value)
{
BuyMarket(Math.Abs(Position));
_breakevenPrice = null;
_breakevenSide = null;
}
}
}
private bool EvaluateMacdBuy()
{
if (!TryGetMacd(MacdShift + 3, out var macd3) ||
!TryGetMacd(MacdShift + 4, out var macd4) ||
!TryGetMacd(MacdShift + 5, out var macd5) ||
!TryGetMacd(MacdShift + 6, out var macd6) ||
!TryGetMacd(MacdShift + 7, out var macd7) ||
!TryGetMacd(MacdShift + 8, out var macd8))
{
return false;
}
var macd5IsZero = Math.Abs(macd5) <= MacdZeroTolerance;
var pattern1 = macd3 > macd4 &&
macd4 > macd5 &&
macd5IsZero &&
macd5 > macd6 &&
macd6 > macd7;
var pattern2 = macd3 > macd4 &&
macd4 > macd5 &&
macd5 >= 0m &&
macd6 <= 0m &&
macd6 > macd7 &&
macd7 > macd8;
return pattern1 || pattern2;
}
private bool EvaluateMacdSell()
{
if (!TryGetMacd(MacdShift + 3, out var macd3) ||
!TryGetMacd(MacdShift + 4, out var macd4) ||
!TryGetMacd(MacdShift + 5, out var macd5) ||
!TryGetMacd(MacdShift + 6, out var macd6) ||
!TryGetMacd(MacdShift + 7, out var macd7) ||
!TryGetMacd(MacdShift + 8, out var macd8))
{
return false;
}
var macd5IsZero = Math.Abs(macd5) <= MacdZeroTolerance;
var pattern1 = macd3 < macd4 &&
macd4 < macd5 &&
macd5IsZero &&
macd5 < macd6 &&
macd6 < macd7;
var pattern2 = macd3 < macd4 &&
macd4 < macd5 &&
macd5 <= 0m &&
macd6 >= 0m &&
macd6 < macd7 &&
macd7 < macd8;
return pattern1 || pattern2;
}
private bool EvaluateEmaBuy(decimal priceShift)
{
if (!TryGetShiftedValue(_closeHistory, MaShift, out var close) ||
!TryGetShiftedValue(_emaHistory, MaShift, out var ema))
{
return false;
}
return close - ema > priceShift;
}
private bool EvaluateEmaSell(decimal priceShift)
{
if (!TryGetShiftedValue(_closeHistory, MaShift, out var close) ||
!TryGetShiftedValue(_emaHistory, MaShift, out var ema))
{
return false;
}
return ema - close > priceShift;
}
private bool TryGetMacd(int index, out decimal value)
{
if (index < 0 || index >= _macdHistory.Count)
{
value = 0m;
return false;
}
value = _macdHistory[index];
return true;
}
private static bool TryGetShiftedValue(List<decimal> list, int shift, out decimal value)
{
if (shift < 0 || shift >= list.Count)
{
value = 0m;
return false;
}
value = list[shift];
return true;
}
private static void TrimHistory(List<decimal> list)
{
const int maxItems = 64;
if (list.Count > maxItems)
list.RemoveRange(maxItems, list.Count - maxItems);
}
private decimal CalculateOrderVolume(decimal price)
{
if (!UseAutoVolume)
return TradeVolume;
var portfolio = Portfolio;
var security = Security;
if (portfolio?.CurrentValue == null || security == null || price <= 0m || RiskFraction <= 0m)
return TradeVolume;
var equity = portfolio.CurrentValue.Value;
if (equity <= 0m)
return TradeVolume;
var lotSize = security.Multiplier ?? 1m;
if (lotSize <= 0m)
lotSize = 1m;
var contractValue = price * lotSize;
if (contractValue <= 0m)
return TradeVolume;
var desired = equity * RiskFraction / contractValue;
var normalized = NormalizeVolume(desired);
return normalized > 0m ? normalized : TradeVolume;
}
private decimal NormalizeVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var sec = Security;
if (sec == null)
return volume;
var step = sec.VolumeStep ?? 1m;
if (step <= 0m)
step = 1m;
var steps = Math.Floor(volume / step);
var normalized = (decimal)steps * step;
var min = sec.MinVolume ?? step;
if (normalized < min)
normalized = min;
var max = sec.MaxVolume;
if (max != null && normalized > max.Value)
normalized = max.Value;
return normalized;
}
}
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
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergence, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class momo_trades_v3_strategy(Strategy):
"""
Momo Trades V3: MACD momentum patterns with displaced EMA filter.
Simplified from C# (no breakeven, no auto volume).
"""
def __init__(self):
super(momo_trades_v3_strategy, self).__init__()
self._ema_period = self.Param("MaPeriod", 22).SetDisplay("EMA Period", "EMA filter length", "Indicators")
self._ema_shift = self.Param("MaShift", 1).SetDisplay("EMA Shift", "Bars offset for EMA", "Indicators")
self._macd_fast = self.Param("FastPeriod", 12).SetDisplay("MACD Fast", "Fast EMA", "Indicators")
self._macd_slow = self.Param("SlowPeriod", 26).SetDisplay("MACD Slow", "Slow EMA", "Indicators")
self._macd_shift = self.Param("MacdShift", 1).SetDisplay("MACD Shift", "MACD history offset", "Indicators")
self._cooldown_bars = self.Param("CooldownBars", 20).SetDisplay("Cooldown", "Min bars between entries", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candles", "General")
self._macd_history = []
self._ema_history = []
self._close_history = []
self._bars_from_signal = 20
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(momo_trades_v3_strategy, self).OnReseted()
self._macd_history = []
self._ema_history = []
self._close_history = []
self._bars_from_signal = self._cooldown_bars.Value
def OnStarted2(self, time):
super(momo_trades_v3_strategy, self).OnStarted2(time)
macd = MovingAverageConvergenceDivergence()
macd.ShortMa.Length = self._macd_fast.Value
macd.LongMa.Length = self._macd_slow.Value
ema = ExponentialMovingAverage()
ema.Length = self._ema_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(macd, ema, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def _process_candle(self, candle, macd_val, ema_val):
if candle.State != CandleStates.Finished:
return
macd_f = float(macd_val)
ema_f = float(ema_val)
close = float(candle.ClosePrice)
self._macd_history.insert(0, macd_f)
self._ema_history.insert(0, ema_f)
self._close_history.insert(0, close)
if len(self._macd_history) > 64:
self._macd_history = self._macd_history[:64]
if len(self._ema_history) > 64:
self._ema_history = self._ema_history[:64]
if len(self._close_history) > 64:
self._close_history = self._close_history[:64]
self._bars_from_signal += 1
if self.Position != 0:
return
shift = self._macd_shift.Value
ema_shift = self._ema_shift.Value
can_buy = self._eval_macd_buy(shift) and self._eval_ema_buy(ema_shift)
can_sell = self._eval_macd_sell(shift) and self._eval_ema_sell(ema_shift)
if self._bars_from_signal >= self._cooldown_bars.Value and can_buy:
self.BuyMarket()
self._bars_from_signal = 0
elif self._bars_from_signal >= self._cooldown_bars.Value and can_sell:
self.SellMarket()
self._bars_from_signal = 0
def _get_macd(self, idx):
if idx < 0 or idx >= len(self._macd_history):
return None
return self._macd_history[idx]
def _eval_macd_buy(self, shift):
vals = [self._get_macd(shift + i) for i in range(3, 9)]
if any(v is None for v in vals):
return False
m3, m4, m5, m6, m7, m8 = vals
p1 = m3 > m4 and m4 > m5 and abs(m5) < 1e-8 and m5 > m6 and m6 > m7
p2 = m3 > m4 and m4 > m5 and m5 >= 0 and m6 <= 0 and m6 > m7 and m7 > m8
return p1 or p2
def _eval_macd_sell(self, shift):
vals = [self._get_macd(shift + i) for i in range(3, 9)]
if any(v is None for v in vals):
return False
m3, m4, m5, m6, m7, m8 = vals
p1 = m3 < m4 and m4 < m5 and abs(m5) < 1e-8 and m5 < m6 and m6 < m7
p2 = m3 < m4 and m4 < m5 and m5 <= 0 and m6 >= 0 and m6 < m7 and m7 < m8
return p1 or p2
def _eval_ema_buy(self, shift):
if shift >= len(self._close_history) or shift >= len(self._ema_history):
return False
return self._close_history[shift] - self._ema_history[shift] > 0
def _eval_ema_sell(self, shift):
if shift >= len(self._close_history) or shift >= len(self._ema_history):
return False
return self._ema_history[shift] - self._close_history[shift] > 0
def CreateClone(self):
return momo_trades_v3_strategy()