Estrategia Momentum de filtro de tiempo de Russian20
Descripción general
La Estrategia Momentum de filtro de tiempo Russian20 es una conversión del MetaTrader 4 asesor experto Russian20-hp1.mq4, distribuido originalmente por Gordago Software Corp. El algoritmo combina una media móvil simple de 20 períodos (SMA) con un indicador Momentum de 5 períodos evaluado en velas de 30 minutos. Las posiciones solo se abren cuando el impulso del precio y la dirección de la tendencia se alinean, opcionalmente restringidas a una ventana de negociación intradiaria definida por el usuario.
Lógica de trading
- Frecuencia de datos: Utiliza el tipo de vela configurable (predeterminado: velas de 30 minutos, que coinciden con
PERIOD_M30 del script MT4). Todas las señales se evalúan únicamente en velas completamente cerradas para permanecer fieles a la ejecución de cierre de barra del experto original.
- Indicadores:
- Media móvil simple con longitud ajustable (predeterminado 20).
- Indicador de impulso con retrospectiva configurable (predeterminado 5) y un nivel neutral establecido en 100, como en MetaTrader.
- Entrada larga: Se activa cuando las siguientes condiciones se alinean en la última barra cerrada:
- El precio de cierre está por encima del SMA.
- El impulso se imprime por encima del umbral neutral (predeterminado 100).
- El precio de cierre actual es más alto que el cierre de la vela anterior.
- Entrada breve: Se activa cuando:
- El precio de cierre está por debajo del SMA.
- El impulso está por debajo del umbral neutral.
- El precio de cierre actual es más bajo que el cierre anterior.
- Reglas de salida:
- Las posiciones largas se cierran cuando Momentum vuelve a caer al umbral o por debajo de él o cuando se alcanza el objetivo de obtención de beneficios (si está habilitado).
- Las posiciones cortas se cierran cuando el Momentum alcanza o supera el umbral o cuando se alcanza el objetivo de obtención de beneficios.
Filtro de sesión
El script MetaTrader ofrecía una ventana de negociación opcional (predeterminada de 14:00 a 16:00). El puerto StockSharp expone el mismo comportamiento a través de los parámetros UseTimeFilter, StartHour y EndHour. Cuando el filtro está activo, la estrategia omite tanto las entradas como las salidas fuera de las horas seleccionadas, reflejando la lógica de retorno anticipado del experto original.
Gestión del riesgo
La versión MQL4 adjuntó una ganancia fija de 20 pips a cada pedido. La conversión mantiene esta característica y expresa la distancia en "pips", ajustándose automáticamente al precio de pips fraccionarios (3/5 decimales) a través del PriceStep del instrumento. Establecer TakeProfitPips en cero desactiva por completo el objetivo de ganancias.
Parámetros
| Parámetro |
Predeterminado |
Descripción |
CandleType |
velas de 30 minutos |
Tipo de datos utilizado para los cálculos de precio/indicador. |
MovingAverageLength |
20 |
Búsqueda retrospectiva del filtro de tendencias SMA. |
MomentumPeriod |
5 |
Búsqueda retrospectiva del indicador Momentum. |
MomentumThreshold |
100 |
Nivel de impulso neutro utilizado para entradas y salidas. |
TakeProfitPips |
20 |
Distancia objetivo de ganancias en pips. Cero desactiva el objetivo. |
UseTimeFilter |
falso |
Habilita el filtro de sesión de negociación intradiaria. |
StartHour |
14 |
Hora de inicio incluida de la ventana de negociación (0–23). |
EndHour |
16 |
Hora de finalización incluida de la ventana de negociación (0–23). |
Todos los parámetros se definen a través de StrategyParam<T>, manteniéndolos visibles en la interfaz de usuario y listos para la optimización.
Notas de implementación
- Utiliza el
SubscribeCandles().Bind(...) API de alto nivel para que los valores del indicador se transmitan directamente a la rutina de procesamiento sin administración manual de series.
- Almacena solo el último precio de cierre para comparar velas consecutivas, evitando consultas históricas pesadas y cumpliendo con las pautas de rendimiento del repositorio.
- Vuelve a calcular automáticamente el multiplicador de pips desde
Security.PriceStep, lo que garantiza distancias correctas de obtención de beneficios entre los símbolos Forex con precios de 4/5 dígitos.
- Agrega ganchos de representación de gráficos opcionales (
DrawCandles, DrawIndicator, DrawOwnTrades) para un análisis visual conveniente cuando el entorno del host lo admite.
Consejos de uso
- Alinee el tipo de vela con el período de tiempo en el que desea operar; Para los pares de Forex, la configuración original de 30 minutos es un punto de partida razonable.
- Cuando
UseTimeFilter esté habilitado, asegúrese de que StartHour sea menor o igual a EndHour. Establecer la hora de inicio más tarde que la hora de finalización desactiva efectivamente el comercio porque la lógica MT4 simplemente omitió el procesamiento fuera del intervalo especificado.
- Dado que el experto nunca utilizó un stop-loss, considere combinar la estrategia con controles de riesgo adicionales (manuales o mediante StockSharp funciones de protección) cuando opere con 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()