在 GitHub 上查看

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 输出一致。
  • 多头入场条件:
    1. 收盘价高于 SMA。
    2. Momentum 高于阈值(默认 100)。
    3. 当前收盘价高于上一根 K 线的收盘价。
  • 空头入场条件:
    1. 收盘价低于 SMA。
    2. Momentum 低于阈值。
    3. 当前收盘价低于上一根收盘价。
  • 离场规则:
    • 多头:Momentum 回落至阈值以下或达到盈利目标时平仓。
    • 空头:Momentum 升至阈值以上或达到盈利目标时平仓。

交易时段过滤

原始 MQL4 程序提供了可选的交易时间窗口(默认 14:00–16:00)。移植版本通过 UseTimeFilterStartHourEndHour 参数还原同样的行为。启用过滤后,策略会在时段之外跳过入场与出场逻辑,完全复制原脚本的早退处理方式。

风险控制

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 位小数报价的外汇品种上依然准确。
  • 若主机环境支持,可通过 DrawCandlesDrawIndicatorDrawOwnTrades 等函数快速在图表上可视化策略行为。

使用建议

  • 根据交易品种调整蜡烛类型;对于多数外汇货币对,默认的 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;
	}
}