Russian20 Time Filter Momentum 策略
概述
Russian20 Time Filter Momentum 策略 源自 MetaTrader 4 专家顾问 Russian20-hp1.mq4,原作者为 Gordago Software Corp. 该算法在 30 分钟 K 线图上结合 20 周期简单移动平均线(SMA)与 5 周期 Momentum 指标,只在趋势与动量方向一致时入场,并可选地限制在指定交易时段内执行。
交易逻辑
- 数据周期: 默认使用 30 分钟 K 线(对应 MT4 的
PERIOD_M30),所有信号均在蜡烛收盘后评估,以保持与原脚本相同的“收盘触发”行为。 - 指标:
- 可配置周期的简单移动平均线(默认 20)。
- 可配置周期的 Momentum 指标(默认 5),中性水平设为 100,与 MetaTrader 输出一致。
- 多头入场条件:
- 收盘价高于 SMA。
- Momentum 高于阈值(默认 100)。
- 当前收盘价高于上一根 K 线的收盘价。
- 空头入场条件:
- 收盘价低于 SMA。
- Momentum 低于阈值。
- 当前收盘价低于上一根收盘价。
- 离场规则:
- 多头:Momentum 回落至阈值以下或达到盈利目标时平仓。
- 空头:Momentum 升至阈值以上或达到盈利目标时平仓。
交易时段过滤
原始 MQL4 程序提供了可选的交易时间窗口(默认 14:00–16:00)。移植版本通过 UseTimeFilter、StartHour、EndHour 参数还原同样的行为。启用过滤后,策略会在时段之外跳过入场与出场逻辑,完全复制原脚本的早退处理方式。
风险控制
MQL4 版本为每笔交易附加固定 20 点的盈利目标。本策略同样以“点”(pip)为单位设置距离,并根据合约的 PriceStep 自动调整,以兼容 3 位或 5 位小数报价。将 TakeProfitPips 设为 0 可关闭盈利目标。
参数说明
| 参数 | 默认值 | 说明 |
|---|---|---|
CandleType |
30 分钟 K 线 | 用于计算信号的数据类型。 |
MovingAverageLength |
20 | SMA 周期。 |
MomentumPeriod |
5 | Momentum 周期。 |
MomentumThreshold |
100 | Momentum 中性阈值,用于入场与离场判断。 |
TakeProfitPips |
20 | 盈利目标距离(点),0 表示禁用。 |
UseTimeFilter |
false | 是否启用时段过滤。 |
StartHour |
14 | 允许交易的起始小时(含,0–23)。 |
EndHour |
16 | 允许交易的结束小时(含,0–23)。 |
所有参数都通过 StrategyParam<T> 定义,可直接在 StockSharp 界面中查看并用于优化。
实现细节
- 使用高层 API
SubscribeCandles().Bind(...),指标值直接传入处理函数,无需手动维护历史序列。 - 仅缓存上一根收盘价,用于比较连续蜡烛,符合仓库对性能与内存的要求。
- 根据
Security.PriceStep自动推导“点”大小,确保盈利目标在 4/5 位小数报价的外汇品种上依然准确。 - 若主机环境支持,可通过
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()