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:
- O preço de fechamento está acima de SMA.
- O impulso é impresso acima do limite neutro (padrão 100).
- O preço de fechamento atual é superior ao fechamento da vela anterior.
- Entrada curta: Acionada quando:
- O preço de fechamento está abaixo de SMA.
- O momentum está abaixo do limite neutro.
- 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;
}
}
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()