Стратегия Russian20 Time Filter Momentum
Обзор
Russian20 Time Filter Momentum — конвертация советника MetaTrader 4 Russian20-hp1.mq4, опубликованного компанией Gordago Software Corp. Алгоритм использует простую скользящую среднюю (SMA) с периодом 20 и индикатор Momentum с периодом 5 на 30-минутных свечах. Сделки открываются только при согласовании тренда и импульса и, при необходимости, ограничиваются заданным торговым окном внутри дня.
Логика торговли
- Таймфрейм: Пользователь задаёт тип свечей (по умолчанию 30 минут — эквивалент MQL4
PERIOD_M30). Сигналы рассчитываются только после закрытия свечи, что повторяет поведение оригинального советника.
- Индикаторы:
- Простая скользящая средняя с настраиваемой длиной (по умолчанию 20).
- Индикатор Momentum с настраиваемым окном (по умолчанию 5) и нейтральным уровнем 100, как в MetaTrader.
- Вход в покупку:
- Цена закрытия выше SMA.
- Значение Momentum превышает порог (по умолчанию 100).
- Текущая цена закрытия выше предыдущей.
- Вход в продажу:
- Цена закрытия ниже SMA.
- Momentum ниже порога.
- Текущая цена закрытия ниже предыдущей.
- Выход:
- Длинные позиции закрываются при снижении Momentum до порога или ниже, либо при срабатывании тейк-профита.
- Короткие позиции закрываются при росте Momentum до порога или выше, либо при достижении тейк-профита.
Торговая сессия
В MQL4-версии предусмотрен фильтр по времени (по умолчанию 14:00–16:00). В портированной версии его включают параметры UseTimeFilter, StartHour и EndHour. При активном фильтре стратегия пропускает и входы, и выходы вне допустимого интервала, точно так же как исходный советник завершал обработку раньше времени.
Управление рисками
Оригинальный код выставлял фиксированный тейк-профит в 20 пунктов. В C#-версии расстояние задаётся в «пунктах» и автоматически масштабируется по PriceStep, что корректно обрабатывает трёх- и пятизначные котировки. Значение 0 отключает цель.
Параметры
| Параметр |
Значение по умолчанию |
Описание |
CandleType |
Свечи 30 минут |
Тип данных для расчётов. |
MovingAverageLength |
20 |
Период SMA. |
MomentumPeriod |
5 |
Период Momentum. |
MomentumThreshold |
100 |
Нейтральный уровень Momentum для входов/выходов. |
TakeProfitPips |
20 |
Дистанция тейк-профита в пунктах (0 — отключить). |
UseTimeFilter |
false |
Включить фильтр торговых часов. |
StartHour |
14 |
Час начала торгового окна (включительно, 0–23). |
EndHour |
16 |
Час окончания окна (включительно, 0–23). |
Все параметры оформлены через StrategyParam<T>, что обеспечивает отображение в интерфейсе и возможность оптимизации.
Особенности реализации
- Используется высокоуровневый метод
SubscribeCandles().Bind(...), поэтому значения индикаторов поступают напрямую в обработчик без ручного хранения истории.
- Для сравнения свечей хранится только последнее закрытие, что соответствует требованиям репозитория по производительности.
- Размер «пункта» пересчитывается из
Security.PriceStep, благодаря чему тейк-профит корректен для инструментов с дробными котировками.
- Добавлены вызовы
DrawCandles, DrawIndicator, DrawOwnTrades для удобного отображения на графике (если поддерживается окружением).
Рекомендации по применению
- Подберите таймфрейм под инструмент; для валютных пар 30-минутные свечи соответствуют настройкам исходного советника.
- При активном фильтре времени убедитесь, что
StartHour ≤ EndHour. Обратное соотношение фактически блокирует торговлю, потому что исходный код просто завершал работу вне диапазона.
- В оригинале отсутствовал стоп-лосс, поэтому для реальной торговли стоит дополнительно настроить защиту позиции через StockSharp или вручную.
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()