Открыть на GitHub

Стратегия 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.
  • Вход в покупку:
    1. Цена закрытия выше SMA.
    2. Значение Momentum превышает порог (по умолчанию 100).
    3. Текущая цена закрытия выше предыдущей.
  • Вход в продажу:
    1. Цена закрытия ниже SMA.
    2. Momentum ниже порога.
    3. Текущая цена закрытия ниже предыдущей.
  • Выход:
    • Длинные позиции закрываются при снижении 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-минутные свечи соответствуют настройкам исходного советника.
  • При активном фильтре времени убедитесь, что StartHourEndHour. Обратное соотношение фактически блокирует торговлю, потому что исходный код просто завершал работу вне диапазона.
  • В оригинале отсутствовал стоп-лосс, поэтому для реальной торговли стоит дополнительно настроить защиту позиции через 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;
	}
}