Russian20 Time Filter Momentum Strategy
Overview
The Russian20 Time Filter Momentum Strategy is a conversion of the MetaTrader 4 expert advisor Russian20-hp1.mq4, originally distributed by Gordago Software Corp. The algorithm combines a 20-period simple moving average (SMA) with a 5-period Momentum indicator evaluated on 30-minute candles. Positions are only opened when price momentum and trend direction align, optionally restricted to a user-defined intraday trading window.
Trading Logic
- Data frequency: Uses the configurable candle type (default: 30-minute candles, matching
PERIOD_M30 from the MT4 script). All signals are evaluated only on fully closed candles to stay faithful to the bar-close execution of the original expert.
- Indicators:
- Simple Moving Average with adjustable length (default 20).
- Momentum indicator with configurable lookback (default 5) and a neutral level set to 100, just like in MetaTrader.
- Long entry: Triggered when the following conditions align on the latest closed bar:
- The close price is above the SMA.
- Momentum prints above the neutral threshold (default 100).
- The current close price is higher than the previous candle close.
- Short entry: Triggered when:
- The close price is below the SMA.
- Momentum is below the neutral threshold.
- The current close price is lower than the previous close.
- Exit rules:
- Long positions are closed when Momentum drops back to or below the threshold or when the take-profit target (if enabled) is hit.
- Short positions are closed when Momentum rises to or above the threshold or when the take-profit target is achieved.
Session Filter
The MetaTrader script offered an optional trading window (default 14:00–16:00). The StockSharp port exposes the same behaviour through the UseTimeFilter, StartHour, and EndHour parameters. When the filter is active, the strategy skips both entries and exits outside the selected hours, mirroring the original expert’s early return logic.
Risk Management
The MQL4 version attached a fixed 20-pip take profit to every order. The conversion keeps this feature and expresses the distance in “pips,” automatically adjusting for fractional pip pricing (3/5 decimals) via the instrument’s PriceStep. Setting TakeProfitPips to zero disables the profit target entirely.
Parameters
| Parameter |
Default |
Description |
CandleType |
30-minute candles |
Data type used for price/indicator calculations. |
MovingAverageLength |
20 |
Lookback for the SMA trend filter. |
MomentumPeriod |
5 |
Lookback for the Momentum indicator. |
MomentumThreshold |
100 |
Neutral Momentum level used for entries and exits. |
TakeProfitPips |
20 |
Profit target distance in pips. Zero disables the target. |
UseTimeFilter |
false |
Enables the intraday trading session filter. |
StartHour |
14 |
Inclusive start hour of the trading window (0–23). |
EndHour |
16 |
Inclusive end hour of the trading window (0–23). |
All parameters are defined through StrategyParam<T>, keeping them visible in the UI and ready for optimisation.
Implementation Notes
- Uses the high-level
SubscribeCandles().Bind(...) API so indicator values are streamed directly into the processing routine without manual series management.
- Stores only the latest close price to compare consecutive candles, avoiding heavy historical queries and complying with repository performance guidelines.
- Automatically recalculates the pip multiplier from
Security.PriceStep, ensuring correct take-profit distances across Forex symbols with 4/5-digit pricing.
- Adds optional chart rendering hooks (
DrawCandles, DrawIndicator, DrawOwnTrades) for convenient visual analysis when the host environment supports it.
Usage Tips
- Align the candle type with the timeframe you intend to trade; for Forex pairs the original 30-minute setting is a reasonable starting point.
- When
UseTimeFilter is enabled, make sure StartHour is less than or equal to EndHour. Setting the start hour later than the end hour effectively disables trading because the MT4 logic simply skipped processing outside the specified interval.
- Because the expert never used a stop-loss, consider pairing the strategy with additional risk controls (manual or via StockSharp protective features) when trading live capital.
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>
/// Russian20 Time Filter Momentum strategy converted from MetaTrader 4 (Russian20-hp1.mq4).
/// Combines a 20-period simple moving average with a 5-period momentum filter and optional trading hours restriction.
/// </summary>
public class Russian20TimeFilterMomentumStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _movingAverageLength;
private readonly StrategyParam<int> _momentumPeriod;
private readonly StrategyParam<decimal> _momentumThreshold;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<bool> _useTimeFilter;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private SimpleMovingAverage _movingAverage;
private Momentum _momentum;
private decimal? _previousClose;
private decimal? _entryPrice;
private decimal _pipSize;
private decimal _takeProfitOffset;
/// <summary>
/// Candle type for strategy calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Period of the simple moving average filter.
/// </summary>
public int MovingAverageLength
{
get => _movingAverageLength.Value;
set => _movingAverageLength.Value = value;
}
/// <summary>
/// Lookback period for the momentum indicator.
/// </summary>
public int MomentumPeriod
{
get => _momentumPeriod.Value;
set => _momentumPeriod.Value = value;
}
/// <summary>
/// Neutral momentum level used for entry and exit decisions.
/// </summary>
public decimal MomentumThreshold
{
get => _momentumThreshold.Value;
set => _momentumThreshold.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips for both long and short trades.
/// Set to zero to disable the profit target.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Enables the optional trading session filter.
/// </summary>
public bool UseTimeFilter
{
get => _useTimeFilter.Value;
set => _useTimeFilter.Value = value;
}
/// <summary>
/// Start hour (inclusive) of the allowed trading window.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// End hour (inclusive) of the allowed trading window.
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Initializes strategy parameters with defaults aligned with the original expert advisor.
/// </summary>
public Russian20TimeFilterMomentumStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle type used for analysis", "General");
_movingAverageLength = Param(nameof(MovingAverageLength), 20)
.SetGreaterThanZero()
.SetDisplay("SMA Length", "Simple moving average lookback", "Indicators")
.SetOptimize(10, 40, 5);
_momentumPeriod = Param(nameof(MomentumPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("Momentum Period", "Momentum indicator lookback", "Indicators")
.SetOptimize(3, 12, 1);
_momentumThreshold = Param(nameof(MomentumThreshold), 100m)
.SetGreaterThanZero()
.SetDisplay("Momentum Threshold", "Neutral momentum level for signals", "Indicators");
_takeProfitPips = Param(nameof(TakeProfitPips), 20m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");
_useTimeFilter = Param(nameof(UseTimeFilter), false)
.SetDisplay("Use Time Filter", "Restrict trading to a session", "Session");
_startHour = Param(nameof(StartHour), 14)
.SetDisplay("Start Hour", "Inclusive start hour of the trading session", "Session")
.SetRange(0, 23);
_endHour = Param(nameof(EndHour), 16)
.SetDisplay("End Hour", "Inclusive end hour of the trading session", "Session")
.SetRange(0, 23);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_movingAverage = null;
_momentum = null;
_previousClose = null;
_entryPrice = null;
_pipSize = 0m;
_takeProfitOffset = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
UpdatePipSettings();
_movingAverage = new SimpleMovingAverage
{
Length = MovingAverageLength,
};
_momentum = new Momentum
{
Length = MomentumPeriod,
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_movingAverage, _momentum, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _movingAverage);
DrawIndicator(area, _momentum);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal maValue, decimal momentumValue)
{
// Ignore incomplete candles to mirror the bar-close execution of the MQL script.
if (candle.State != CandleStates.Finished)
return;
// Honour trading session boundaries when the filter is enabled.
if (UseTimeFilter)
{
var hour = candle.OpenTime.Hour;
if (hour < StartHour || hour > EndHour)
{
_previousClose = candle.ClosePrice;
return;
}
}
// Ensure the infrastructure allows trading and indicators are ready.
if (!_movingAverage.IsFormed || !_momentum.IsFormed)
{
_previousClose = candle.ClosePrice;
return;
}
if (_pipSize == 0m)
UpdatePipSettings();
var closePrice = candle.ClosePrice;
if (_previousClose is null)
{
_previousClose = closePrice;
return;
}
var entryPrice = _entryPrice;
if (Position == 0 && entryPrice.HasValue)
{
// Reset entry price if an external action flattened the position.
_entryPrice = null;
entryPrice = null;
}
if (Position == 0)
{
// Evaluate entry conditions only when flat.
var bullishSignal = closePrice > maValue && momentumValue > MomentumThreshold && closePrice > _previousClose.Value;
var bearishSignal = closePrice < maValue && momentumValue < MomentumThreshold && closePrice < _previousClose.Value;
if (bullishSignal)
{
// Enter long on a bullish alignment of filters.
BuyMarket();
_entryPrice = closePrice;
}
else if (bearishSignal)
{
// Enter short on a bearish alignment of filters.
SellMarket();
_entryPrice = closePrice;
}
}
else if (Position > 0)
{
// Exit long when momentum weakens or the take profit target is achieved.
var exitByMomentum = momentumValue <= MomentumThreshold;
var exitByTake = entryPrice.HasValue && _takeProfitOffset > 0m && closePrice >= entryPrice.Value + _takeProfitOffset;
if (exitByMomentum || exitByTake)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
_entryPrice = null;
}
}
else
{
// Exit short when momentum strengthens or the profit target is touched.
var exitByMomentum = momentumValue >= MomentumThreshold;
var exitByTake = entryPrice.HasValue && _takeProfitOffset > 0m && closePrice <= entryPrice.Value - _takeProfitOffset;
if (exitByMomentum || exitByTake)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
_entryPrice = null;
}
}
_previousClose = closePrice;
}
private void UpdatePipSettings()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
{
_pipSize = 1m;
}
else
{
var decimals = GetDecimalPlaces(step);
var multiplier = decimals == 3 || decimals == 5 ? 10m : 1m;
_pipSize = step * multiplier;
}
_takeProfitOffset = TakeProfitPips > 0m ? TakeProfitPips * _pipSize : 0m;
}
private static int GetDecimalPlaces(decimal value)
{
var bits = decimal.GetBits(value);
return (bits[3] >> 16) & 0xFF;
}
}
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
from StockSharp.Algo.Indicators import Momentum, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class russian20_time_filter_momentum_strategy(Strategy):
"""SMA + Momentum filter strategy with optional trading hours restriction.
Buy when close > SMA, momentum > threshold, and close > previous close.
Sell when close < SMA, momentum < threshold, and close < previous close."""
def __init__(self):
super(russian20_time_filter_momentum_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle type used for analysis", "General")
self._moving_average_length = self.Param("MovingAverageLength", 20) \
.SetGreaterThanZero() \
.SetDisplay("SMA Length", "Simple moving average lookback", "Indicators")
self._momentum_period = self.Param("MomentumPeriod", 5) \
.SetGreaterThanZero() \
.SetDisplay("Momentum Period", "Momentum indicator lookback", "Indicators")
self._momentum_threshold = self.Param("MomentumThreshold", 100.0) \
.SetDisplay("Momentum Threshold", "Neutral momentum level for signals", "Indicators")
self._take_profit_pips = self.Param("TakeProfitPips", 20.0) \
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
self._use_time_filter = self.Param("UseTimeFilter", False) \
.SetDisplay("Use Time Filter", "Restrict trading to a session", "Session")
self._start_hour = self.Param("StartHour", 14) \
.SetDisplay("Start Hour", "Inclusive start hour of the trading session", "Session")
self._end_hour = self.Param("EndHour", 16) \
.SetDisplay("End Hour", "Inclusive end hour of the trading session", "Session")
self._previous_close = None
self._entry_price = None
self._pip_size = 0.0
self._take_profit_offset = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def MovingAverageLength(self):
return self._moving_average_length.Value
@property
def MomentumPeriod(self):
return self._momentum_period.Value
@property
def MomentumThreshold(self):
return self._momentum_threshold.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def UseTimeFilter(self):
return self._use_time_filter.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.Value
def OnReseted(self):
super(russian20_time_filter_momentum_strategy, self).OnReseted()
self._previous_close = None
self._entry_price = None
self._pip_size = 0.0
self._take_profit_offset = 0.0
def _update_pip_settings(self):
step = self.Security.PriceStep if self.Security is not None else 0.0
if step is None or float(step) <= 0:
self._pip_size = 1.0
else:
step_val = float(step)
# Detect 3/5-digit broker
digits = self._get_decimal_places(step_val)
multiplier = 10.0 if (digits == 3 or digits == 5) else 1.0
self._pip_size = step_val * multiplier
tp = float(self.TakeProfitPips)
self._take_profit_offset = tp * self._pip_size if tp > 0 else 0.0
def _get_decimal_places(self, value):
digits = 0
v = abs(value)
while v != int(v) and digits < 10:
v *= 10.0
digits += 1
return digits
def OnStarted2(self, time):
super(russian20_time_filter_momentum_strategy, self).OnStarted2(time)
self._update_pip_settings()
ma = SimpleMovingAverage()
ma.Length = self.MovingAverageLength
mom = Momentum()
mom.Length = self.MomentumPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ma, mom, self._process_candle).Start()
def _process_candle(self, candle, ma_value, momentum_value):
if candle.State != CandleStates.Finished:
return
ma_val = float(ma_value)
mom_val = float(momentum_value)
close = float(candle.ClosePrice)
# Time filter
if self.UseTimeFilter:
hour = candle.OpenTime.Hour
if hour < self.StartHour or hour > self.EndHour:
self._previous_close = close
return
if self._pip_size == 0.0:
self._update_pip_settings()
if self._previous_close is None:
self._previous_close = close
return
prev_close = self._previous_close
threshold = float(self.MomentumThreshold)
if self.Position == 0 and self._entry_price is not None:
self._entry_price = None
if self.Position == 0:
# Entry conditions
bullish = close > ma_val and mom_val > threshold and close > prev_close
bearish = close < ma_val and mom_val < threshold and close < prev_close
if bullish:
self.BuyMarket()
self._entry_price = close
elif bearish:
self.SellMarket()
self._entry_price = close
elif self.Position > 0:
# Exit long: momentum weakens or TP hit
exit_momentum = mom_val <= threshold
exit_tp = (self._entry_price is not None and self._take_profit_offset > 0
and close >= self._entry_price + self._take_profit_offset)
if exit_momentum or exit_tp:
self.SellMarket()
self._entry_price = None
else:
# Exit short: momentum strengthens or TP hit
exit_momentum = mom_val >= threshold
exit_tp = (self._entry_price is not None and self._take_profit_offset > 0
and close <= self._entry_price - self._take_profit_offset)
if exit_momentum or exit_tp:
self.BuyMarket()
self._entry_price = None
self._previous_close = close
def CreateClone(self):
return russian20_time_filter_momentum_strategy()