Ver no GitHub

Estratégia de impulso do filtro de tempo Russian20

Visão geral

A Estratégia Russian20 Time Filter Momentum é uma conversão do MetaTrader 4 consultor especialista Russian20-hp1.mq4, originalmente distribuído pela Gordago Software Corp. O algoritmo combina uma média móvel simples de 20 períodos (SMA) com um indicador Momentum de 5 períodos avaliado em velas de 30 minutos. As posições só são abertas quando a dinâmica do preço e a direção da tendência se alinham, opcionalmente restritas a uma janela de negociação intradiária definida pelo usuário.

Lógica de negociação

  • Frequência de dados: usa o tipo de vela configurável (padrão: velas de 30 minutos, correspondendo a PERIOD_M30 do script MT4). Todos os sinais são avaliados apenas em velas totalmente fechadas para permanecerem fiéis à execução de fechamento da barra do especialista original.
  • Indicadores:
    • Média Móvel Simples com comprimento ajustável (padrão 20).
    • Indicador de impulso com lookback configurável (padrão 5) e um nível neutro definido como 100, assim como em MetaTrader.
  • Entrada longa: Acionada quando as seguintes condições se alinham na última barra fechada:
    1. O preço de fechamento está acima de SMA.
    2. O impulso é impresso acima do limite neutro (padrão 100).
    3. O preço de fechamento atual é superior ao fechamento da vela anterior.
  • Entrada curta: Acionada quando:
    1. O preço de fechamento está abaixo de SMA.
    2. O momentum está abaixo do limite neutro.
    3. O preço de fechamento atual é inferior ao fechamento anterior.
  • Regras de saída:
    • As posições longas são fechadas quando o Momentum cai para ou abaixo do limite ou quando a meta de lucro (se habilitada) é atingida.
    • As posições curtas são fechadas quando o Momentum sobe para ou acima do limite ou quando a meta de lucro é alcançada.

Filtro de sessão

O script MetaTrader ofereceu uma janela de negociação opcional (padrão 14h00–16h00). A porta StockSharp expõe o mesmo comportamento por meio dos parâmetros UseTimeFilter, StartHour e EndHour. Quando o filtro está ativo, a estratégia pula entradas e saídas fora do horário selecionado, espelhando a lógica de retorno antecipado do especialista original.

Gestão de risco

A versão MQL4 anexou um lucro fixo de 20 pip a cada pedido. A conversão mantém esse recurso e expressa a distância em “pips”, ajustando automaticamente o preço do pip fracionário (3/5 decimais) por meio do PriceStep do instrumento. Definir TakeProfitPips como zero desativa totalmente a meta de lucro.

Parâmetros

Parâmetro Padrão Descrição
CandleType Velas de 30 minutos Tipo de dados usado para cálculos de preços/indicadores.
MovingAverageLength 20 Lookback para o filtro de tendência SMA.
MomentumPeriod 5 Lookback para o indicador Momentum.
MomentumThreshold 100 Nível de Momentum Neutro usado para entradas e saídas.
TakeProfitPips 20 Distância alvo de lucro em pips. Zero desativa o alvo.
UseTimeFilter falso Habilita o filtro de sessão de negociação intradiária.
StartHour 14 Hora de início inclusiva da janela de negociação (0–23).
EndHour 16 Hora final inclusiva da janela de negociação (0–23).

Todos os parâmetros são definidos por meio de StrategyParam<T>, mantendo-os visíveis na UI e prontos para otimização.

Notas de implementação

  • Usa o SubscribeCandles().Bind(...) API de alto nível para que os valores dos indicadores sejam transmitidos diretamente para a rotina de processamento sem gerenciamento manual de séries.
  • Armazena apenas o último preço de fechamento para comparar velas consecutivas, evitando consultas históricas pesadas e cumprindo as diretrizes de desempenho do repositório.
  • Recalcula automaticamente o multiplicador pip de Security.PriceStep, garantindo distâncias corretas de lucro entre símbolos Forex com preços de 4/5 dígitos.
  • Adiciona ganchos opcionais de renderização de gráficos (DrawCandles, DrawIndicator, DrawOwnTrades) para análise visual conveniente quando o ambiente host oferece suporte.

Dicas de uso

  • Alinhe o tipo de vela com o prazo que você pretende negociar; para pares Forex, a configuração original de 30 minutos é um ponto de partida razoável.
  • Quando UseTimeFilter estiver ativado, certifique-se de que StartHour seja menor ou igual a EndHour. Definir a hora de início depois da hora de término desativa efetivamente a negociação porque a lógica MT4 simplesmente ignorou o processamento fora do intervalo especificado.
  • Como o especialista nunca usou um stop-loss, considere combinar a estratégia com controles de risco adicionais (manuais ou por meio de StockSharp recursos de proteção) ao negociar capital real.
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;
	}
}