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>
/// Momentum based strategy converted from the MetaTrader 5 "Momentum-M15" expert advisor.
/// </summary>
public class MomentumM15Strategy : Strategy
{
/// <summary>
/// Moving average method options aligned with the original expert advisor inputs.
/// </summary>
public enum MovingAverageMethods
{
/// <summary>
/// Simple moving average.
/// </summary>
Simple,
/// <summary>
/// Exponential moving average.
/// </summary>
Exponential,
/// <summary>
/// Smoothed moving average.
/// </summary>
Smoothed,
/// <summary>
/// Weighted moving average.
/// </summary>
Weighted
}
public enum CandlePrices
{
Open,
High,
Low,
Close,
Median,
Typical,
Weighted
}
private readonly StrategyParam<decimal> _volumeParam;
private readonly StrategyParam<DataType> _candleTypeParam;
private readonly StrategyParam<int> _maPeriodParam;
private readonly StrategyParam<int> _maShiftParam;
private readonly StrategyParam<MovingAverageMethods> _maMethodParam;
private readonly StrategyParam<CandlePrices> _maPriceParam;
private readonly StrategyParam<int> _momentumPeriodParam;
private readonly StrategyParam<CandlePrices> _momentumPriceParam;
private readonly StrategyParam<decimal> _momentumThresholdParam;
private readonly StrategyParam<decimal> _momentumShiftParam;
private readonly StrategyParam<int> _momentumOpenLengthParam;
private readonly StrategyParam<int> _momentumCloseLengthParam;
private readonly StrategyParam<int> _gapLevelParam;
private readonly StrategyParam<int> _gapTimeoutParam;
private readonly StrategyParam<decimal> _trailingStopParam;
private IIndicator _ma = null!;
private Momentum _momentum = null!;
private readonly List<decimal> _maHistory = new();
private readonly List<decimal> _momentumHistory = new();
private decimal? _previousClose;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
private int _gapTimer;
/// <summary>
/// Initializes a new instance of <see cref="MomentumM15Strategy"/>.
/// </summary>
public MomentumM15Strategy()
{
_volumeParam = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Default order volume", "Trading")
.SetOptimize(0.05m, 0.5m, 0.05m);
_candleTypeParam = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for calculations", "Common");
_maPeriodParam = Param(nameof(MaPeriod), 26)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Moving average lookback length", "Indicators")
.SetOptimize(10, 60, 5);
_maShiftParam = Param(nameof(MaShift), 8)
.SetNotNegative()
.SetDisplay("MA Shift", "Horizontal shift applied to moving average", "Indicators");
_maMethodParam = Param(nameof(MaMethod), MovingAverageMethods.Smoothed)
.SetDisplay("MA Method", "Type of moving average", "Indicators");
_maPriceParam = Param(nameof(MaPrice), CandlePrices.Low)
.SetDisplay("MA Price", "Price source for moving average", "Indicators");
_momentumPeriodParam = Param(nameof(MomentumPeriod), 23)
.SetGreaterThanZero()
.SetDisplay("Momentum Period", "Momentum indicator lookback", "Indicators")
.SetOptimize(10, 40, 1);
_momentumPriceParam = Param(nameof(MomentumPrice), CandlePrices.Open)
.SetDisplay("Momentum Price", "Price source for momentum", "Indicators");
_momentumThresholdParam = Param(nameof(MomentumThreshold), 100m)
.SetDisplay("Momentum Threshold", "Baseline momentum threshold", "Trading Rules");
_momentumShiftParam = Param(nameof(MomentumShift), -0.2m)
.SetDisplay("Momentum Shift", "Shift applied to momentum threshold", "Trading Rules");
_momentumOpenLengthParam = Param(nameof(MomentumOpenLength), 6)
.SetNotNegative()
.SetDisplay("Momentum Open Length", "Bars required for monotonic momentum on entries", "Trading Rules");
_momentumCloseLengthParam = Param(nameof(MomentumCloseLength), 10)
.SetNotNegative()
.SetDisplay("Momentum Close Length", "Bars required for monotonic momentum on exits", "Trading Rules");
_gapLevelParam = Param(nameof(GapLevel), 30)
.SetNotNegative()
.SetDisplay("Gap Level", "Minimum gap in price steps to pause trading", "Risk Management");
_gapTimeoutParam = Param(nameof(GapTimeout), 100)
.SetNotNegative()
.SetDisplay("Gap Timeout", "Number of bars to skip after a large gap", "Risk Management");
_trailingStopParam = Param(nameof(TrailingStop), 0m)
.SetNotNegative()
.SetDisplay("Trailing Stop", "Trailing stop distance in price steps", "Risk Management");
}
/// <summary>
/// Default trade volume.
/// </summary>
public decimal TradeVolume
{
get => _volumeParam.Value;
set => _volumeParam.Value = value;
}
/// <summary>
/// Candle type for the strategy.
/// </summary>
public DataType CandleType
{
get => _candleTypeParam.Value;
set => _candleTypeParam.Value = value;
}
/// <summary>
/// Moving average period.
/// </summary>
public int MaPeriod
{
get => _maPeriodParam.Value;
set => _maPeriodParam.Value = value;
}
/// <summary>
/// Number of bars to shift the moving average.
/// </summary>
public int MaShift
{
get => _maShiftParam.Value;
set => _maShiftParam.Value = value;
}
/// <summary>
/// Moving average calculation method.
/// </summary>
public MovingAverageMethods MaMethod
{
get => _maMethodParam.Value;
set => _maMethodParam.Value = value;
}
/// <summary>
/// Price source for the moving average.
/// </summary>
public CandlePrices MaPrice
{
get => _maPriceParam.Value;
set => _maPriceParam.Value = value;
}
/// <summary>
/// Momentum indicator lookback period.
/// </summary>
public int MomentumPeriod
{
get => _momentumPeriodParam.Value;
set => _momentumPeriodParam.Value = value;
}
/// <summary>
/// Price source for the momentum indicator.
/// </summary>
public CandlePrices MomentumPrice
{
get => _momentumPriceParam.Value;
set => _momentumPriceParam.Value = value;
}
/// <summary>
/// Baseline momentum threshold.
/// </summary>
public decimal MomentumThreshold
{
get => _momentumThresholdParam.Value;
set => _momentumThresholdParam.Value = value;
}
/// <summary>
/// Shift applied to the momentum threshold.
/// </summary>
public decimal MomentumShift
{
get => _momentumShiftParam.Value;
set => _momentumShiftParam.Value = value;
}
/// <summary>
/// Sequence length for entry momentum validation.
/// </summary>
public int MomentumOpenLength
{
get => _momentumOpenLengthParam.Value;
set => _momentumOpenLengthParam.Value = value;
}
/// <summary>
/// Sequence length for exit momentum validation.
/// </summary>
public int MomentumCloseLength
{
get => _momentumCloseLengthParam.Value;
set => _momentumCloseLengthParam.Value = value;
}
/// <summary>
/// Minimum gap (in price steps) that suspends new entries.
/// </summary>
public int GapLevel
{
get => _gapLevelParam.Value;
set => _gapLevelParam.Value = value;
}
/// <summary>
/// Number of bars to wait after a gap before trading resumes.
/// </summary>
public int GapTimeout
{
get => _gapTimeoutParam.Value;
set => _gapTimeoutParam.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in price steps.
/// </summary>
public decimal TrailingStop
{
get => _trailingStopParam.Value;
set => _trailingStopParam.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ma = null!;
_momentum = null!;
_maHistory.Clear();
_momentumHistory.Clear();
_previousClose = null;
_longTrailingStop = null;
_shortTrailingStop = null;
_gapTimer = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ma = CreateMovingAverage(MaMethod, MaPeriod);
_momentum = new Momentum { Length = MomentumPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_ma is null || _momentum is null)
return;
var maValue = ProcessMovingAverage(candle);
var momentumValue = ProcessMomentum(candle);
if (maValue is null || momentumValue is null)
{
_previousClose = candle.ClosePrice;
return;
}
var previousClose = _previousClose;
_previousClose = candle.ClosePrice;
if (previousClose is null)
return;
HandleGapFilter(previousClose.Value, candle.OpenPrice);
if (_gapTimer > 0)
{
_gapTimer--;
if (_gapTimer > 0)
return;
}
if (Position == 0)
{
TryOpenPositions(previousClose.Value, candle.OpenPrice, maValue.Value, momentumValue.Value);
}
else
{
ManageExistingPosition(previousClose.Value, candle, maValue.Value, momentumValue.Value);
}
}
private decimal? ProcessMovingAverage(ICandleMessage candle)
{
var price = GetPrice(candle, MaPrice);
var value = _ma.Process(new DecimalIndicatorValue(_ma, price, candle.OpenTime) { IsFinal = true });
if (value.IsEmpty || !_ma.IsFormed)
return null;
var ma = value.ToDecimal();
_maHistory.Add(ma);
var maxCount = MaShift + 1;
while (_maHistory.Count > maxCount)
_maHistory.RemoveAt(0);
var index = _maHistory.Count - 1 - MaShift;
if (index < 0 || index >= _maHistory.Count)
return null;
return _maHistory[index];
}
private decimal? ProcessMomentum(ICandleMessage candle)
{
var price = GetPrice(candle, MomentumPrice);
var value = _momentum.Process(new DecimalIndicatorValue(_momentum, price, candle.OpenTime) { IsFinal = true });
if (value.IsEmpty || !_momentum.IsFormed)
return null;
var momentum = value.ToDecimal();
_momentumHistory.Add(momentum);
var maxLen = Math.Max(Math.Max(MomentumOpenLength, MomentumCloseLength), 1);
while (_momentumHistory.Count > maxLen)
_momentumHistory.RemoveAt(0);
return momentum;
}
private void HandleGapFilter(decimal previousClose, decimal currentOpen)
{
var priceStep = Security.PriceStep ?? 0m;
if (priceStep <= 0m)
return;
var gap = (currentOpen - previousClose) / priceStep;
if (gap > GapLevel)
_gapTimer = GapTimeout;
}
private void TryOpenPositions(decimal previousClose, decimal currentOpen, decimal maValue, decimal momentumValue)
{
var longMomentumOk = MomentumOpenLength > 0 && IsMomentumDownSequence(MomentumOpenLength);
var shortMomentumOk = MomentumOpenLength > 0 && IsMomentumUpSequence(MomentumOpenLength);
var longCondition = momentumValue < MomentumThreshold + MomentumShift
&& previousClose < maValue
&& currentOpen < maValue
&& longMomentumOk;
var shortCondition = momentumValue > MomentumThreshold - MomentumShift
&& previousClose > maValue
&& currentOpen > maValue
&& shortMomentumOk;
if (longCondition)
{
BuyMarket(TradeVolume);
_longTrailingStop = null;
_shortTrailingStop = null;
}
else if (shortCondition)
{
SellMarket(TradeVolume);
_longTrailingStop = null;
_shortTrailingStop = null;
}
}
private void ManageExistingPosition(decimal previousClose, ICandleMessage candle, decimal maValue, decimal momentumValue)
{
if (Position > 0)
{
var exitMomentum = MomentumCloseLength > 0 && IsMomentumDownSequence(MomentumCloseLength);
var shouldClose = exitMomentum || previousClose < maValue;
if (shouldClose)
{
SellMarket(Position);
_longTrailingStop = null;
return;
}
UpdateLongTrailingStop(candle);
}
else if (Position < 0)
{
var exitMomentum = MomentumCloseLength > 0 && IsMomentumUpSequence(MomentumCloseLength);
var shouldClose = exitMomentum || previousClose > maValue;
if (shouldClose)
{
BuyMarket(Math.Abs(Position));
_shortTrailingStop = null;
return;
}
UpdateShortTrailingStop(candle);
}
}
private void UpdateLongTrailingStop(ICandleMessage candle)
{
if (TrailingStop <= 0m)
return;
var priceStep = Security.PriceStep ?? 0m;
if (priceStep <= 0m)
return;
var distance = TrailingStop * priceStep;
var candidate = candle.LowPrice - distance;
if (_longTrailingStop is null || candidate > _longTrailingStop)
_longTrailingStop = candidate;
if (_longTrailingStop is decimal stop && candle.LowPrice <= stop)
{
SellMarket(Position);
_longTrailingStop = null;
}
}
private void UpdateShortTrailingStop(ICandleMessage candle)
{
if (TrailingStop <= 0m)
return;
var priceStep = Security.PriceStep ?? 0m;
if (priceStep <= 0m)
return;
var distance = TrailingStop * priceStep;
var candidate = candle.HighPrice + distance;
if (_shortTrailingStop is null || candidate < _shortTrailingStop)
_shortTrailingStop = candidate;
if (_shortTrailingStop is decimal stop && candle.HighPrice >= stop)
{
BuyMarket(Math.Abs(Position));
_shortTrailingStop = null;
}
}
private bool IsMomentumDownSequence(int length)
{
if (length <= 0 || _momentumHistory.Count < length)
return false;
var start = _momentumHistory.Count - length;
var previous = _momentumHistory[start];
for (var i = start + 1; i < _momentumHistory.Count; i++)
{
var current = _momentumHistory[i];
if (current > previous)
return false;
previous = current;
}
return true;
}
private bool IsMomentumUpSequence(int length)
{
if (length <= 0 || _momentumHistory.Count < length)
return false;
var start = _momentumHistory.Count - length;
var previous = _momentumHistory[start];
for (var i = start + 1; i < _momentumHistory.Count; i++)
{
var current = _momentumHistory[i];
if (current < previous)
return false;
previous = current;
}
return true;
}
private static decimal GetPrice(ICandleMessage candle, CandlePrices price)
{
return price switch
{
CandlePrices.Open => candle.OpenPrice,
CandlePrices.High => candle.HighPrice,
CandlePrices.Low => candle.LowPrice,
CandlePrices.Close => candle.ClosePrice,
CandlePrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
CandlePrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
CandlePrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
_ => candle.ClosePrice,
};
}
private static IIndicator CreateMovingAverage(MovingAverageMethods method, int period)
{
return method switch
{
MovingAverageMethods.Simple => new SimpleMovingAverage { Length = period },
MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = period },
MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = period },
MovingAverageMethods.Weighted => new WeightedMovingAverage { Length = period },
_ => new SimpleMovingAverage { Length = period },
};
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import SmoothedMovingAverage, Momentum
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math, Decimal
from indicator_extensions import *
class momentum_m15_strategy(Strategy):
def __init__(self):
super(momentum_m15_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 0.1)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._ma_period = self.Param("MaPeriod", 26)
self._ma_shift = self.Param("MaShift", 8)
self._momentum_period = self.Param("MomentumPeriod", 23)
self._momentum_threshold = self.Param("MomentumThreshold", 100.0)
self._momentum_shift = self.Param("MomentumShift", -0.2)
self._momentum_open_length = self.Param("MomentumOpenLength", 6)
self._momentum_close_length = self.Param("MomentumCloseLength", 10)
self._gap_level = self.Param("GapLevel", 30)
self._gap_timeout = self.Param("GapTimeout", 100)
self._trailing_stop = self.Param("TrailingStop", 0.0)
self._ma = None
self._momentum = None
self._ma_history = []
self._momentum_history = []
self._previous_close = None
self._long_trailing_stop = None
self._short_trailing_stop = None
self._gap_timer = 0
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(momentum_m15_strategy, self).OnStarted2(time)
self._ma = SmoothedMovingAverage()
self._ma.Length = self._ma_period.Value
self._momentum = Momentum()
self._momentum.Length = self._momentum_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._ma is None or self._momentum is None:
return
ma_value = self._process_ma(candle)
momentum_value = self._process_momentum(candle)
if ma_value is None or momentum_value is None:
self._previous_close = float(candle.ClosePrice)
return
previous_close = self._previous_close
self._previous_close = float(candle.ClosePrice)
if previous_close is None:
return
self._handle_gap_filter(previous_close, float(candle.OpenPrice))
if self._gap_timer > 0:
self._gap_timer -= 1
if self._gap_timer > 0:
return
if self.Position == 0:
self._try_open_positions(previous_close, float(candle.OpenPrice), ma_value, momentum_value)
else:
self._manage_existing_position(previous_close, candle, ma_value, momentum_value)
def _process_ma(self, candle):
price = float(candle.LowPrice)
value = process_float(self._ma, Decimal(float(price)), candle.OpenTime, True)
if value.IsEmpty or not self._ma.IsFormed:
return None
ma = float(value.Value)
self._ma_history.append(ma)
max_count = self._ma_shift.Value + 1
while len(self._ma_history) > max_count:
self._ma_history.pop(0)
index = len(self._ma_history) - 1 - self._ma_shift.Value
if index < 0 or index >= len(self._ma_history):
return None
return self._ma_history[index]
def _process_momentum(self, candle):
price = float(candle.OpenPrice)
value = process_float(self._momentum, Decimal(float(price)), candle.OpenTime, True)
if value.IsEmpty or not self._momentum.IsFormed:
return None
mom = float(value.Value)
self._momentum_history.append(mom)
max_len = max(max(self._momentum_open_length.Value, self._momentum_close_length.Value), 1)
while len(self._momentum_history) > max_len:
self._momentum_history.pop(0)
return mom
def _handle_gap_filter(self, previous_close, current_open):
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
if price_step <= 0:
return
gap = (current_open - previous_close) / price_step
if gap > self._gap_level.Value:
self._gap_timer = self._gap_timeout.Value
def _try_open_positions(self, previous_close, current_open, ma_value, momentum_value):
long_momentum_ok = self._momentum_open_length.Value > 0 and self._is_momentum_down_sequence(self._momentum_open_length.Value)
short_momentum_ok = self._momentum_open_length.Value > 0 and self._is_momentum_up_sequence(self._momentum_open_length.Value)
long_condition = (momentum_value < self._momentum_threshold.Value + self._momentum_shift.Value
and previous_close < ma_value
and current_open < ma_value
and long_momentum_ok)
short_condition = (momentum_value > self._momentum_threshold.Value - self._momentum_shift.Value
and previous_close > ma_value
and current_open > ma_value
and short_momentum_ok)
if long_condition:
vol = self._trade_volume.Value if self._trade_volume.Value > 0 else float(self.Volume)
self.BuyMarket(vol)
self._long_trailing_stop = None
self._short_trailing_stop = None
elif short_condition:
vol = self._trade_volume.Value if self._trade_volume.Value > 0 else float(self.Volume)
self.SellMarket(vol)
self._long_trailing_stop = None
self._short_trailing_stop = None
def _manage_existing_position(self, previous_close, candle, ma_value, momentum_value):
if self.Position > 0:
exit_momentum = self._momentum_close_length.Value > 0 and self._is_momentum_down_sequence(self._momentum_close_length.Value)
should_close = exit_momentum or previous_close < ma_value
if should_close:
self.SellMarket(self.Position)
self._long_trailing_stop = None
return
self._update_long_trailing(candle)
elif self.Position < 0:
exit_momentum = self._momentum_close_length.Value > 0 and self._is_momentum_up_sequence(self._momentum_close_length.Value)
should_close = exit_momentum or previous_close > ma_value
if should_close:
self.BuyMarket(abs(self.Position))
self._short_trailing_stop = None
return
self._update_short_trailing(candle)
def _update_long_trailing(self, candle):
if self._trailing_stop.Value <= 0:
return
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
if price_step <= 0:
return
distance = self._trailing_stop.Value * price_step
candidate = float(candle.LowPrice) - distance
if self._long_trailing_stop is None or candidate > self._long_trailing_stop:
self._long_trailing_stop = candidate
if self._long_trailing_stop is not None and float(candle.LowPrice) <= self._long_trailing_stop:
self.SellMarket(self.Position)
self._long_trailing_stop = None
def _update_short_trailing(self, candle):
if self._trailing_stop.Value <= 0:
return
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
if price_step <= 0:
return
distance = self._trailing_stop.Value * price_step
candidate = float(candle.HighPrice) + distance
if self._short_trailing_stop is None or candidate < self._short_trailing_stop:
self._short_trailing_stop = candidate
if self._short_trailing_stop is not None and float(candle.HighPrice) >= self._short_trailing_stop:
self.BuyMarket(abs(self.Position))
self._short_trailing_stop = None
def _is_momentum_down_sequence(self, length):
if length <= 0 or len(self._momentum_history) < length:
return False
start = len(self._momentum_history) - length
previous = self._momentum_history[start]
for i in range(start + 1, len(self._momentum_history)):
current = self._momentum_history[i]
if current > previous:
return False
previous = current
return True
def _is_momentum_up_sequence(self, length):
if length <= 0 or len(self._momentum_history) < length:
return False
start = len(self._momentum_history) - length
previous = self._momentum_history[start]
for i in range(start + 1, len(self._momentum_history)):
current = self._momentum_history[i]
if current < previous:
return False
previous = current
return True
def OnReseted(self):
super(momentum_m15_strategy, self).OnReseted()
self._ma = None
self._momentum = None
self._ma_history = []
self._momentum_history = []
self._previous_close = None
self._long_trailing_stop = None
self._short_trailing_stop = None
self._gap_timer = 0
def CreateClone(self):
return momentum_m15_strategy()