在 GitHub 上查看

Momentum M15 策略

本策略移植自 MetaTrader 5 顾问 Momentum-M15(原始文件 Momentum-M15.mq5)。它在 15 分钟级别蜡烛上运 行,结合带有水平偏移的移动平均线与按开盘价计算的 Momentum 指标。核心思想是在价格位于偏移平均线相对 侧时反向交易:价格偏低时做多、价格偏高时做空,同时使用跳空过滤器和可选的追踪止损控制风险。

移植要点

  • 使用 StockSharp 自带组件实现指标:可配置的移动平均线(默认 Smoothed)以及可以选择价格来源的 Momentum 指标。
  • MA 的水平偏移通过缓存指标数值并回取 MaShift 个已完成柱的值来模拟,无需重写指标算法。
  • Momentum 单调性检测保留原始 CheckMO_Up / CheckMO_Down 逻辑,仅存储所需数量的最新数值。
  • 大幅向上跳空保护 (GapLevel / GapTimeout) 与 MQL 版本一致,使用 Security.PriceStep 将点值转换为价 格步长。
  • 追踪止损通过监控价位并在触发时市价平仓来完成,对应 MQL 代码中每个新柱修改止损订单的行为。

参数

名称 说明 默认值
TradeVolume 每笔交易的数量。 0.1
CandleType 主要时间框架。 15m
MaPeriod 移动平均线周期。 26
MaShift 移动平均线向前偏移的柱数。 8
MaMethod 移动平均线类型(SimpleExponentialSmoothedWeighted)。 Smoothed
MaPrice 送入移动平均线的价格。 Low
MomentumPeriod Momentum 指标周期。 23
MomentumPrice Momentum 指标使用的价格。 Open
MomentumThreshold Momentum 判定基准值。 100
MomentumShift 在基准值基础上的偏移量。 -0.2
MomentumOpenLength 触发进场所需的 Momentum 单调序列长度。 6
MomentumCloseLength 触发离场所需的单调序列长度。 10
GapLevel 暂停交易的最小正跳空(按价格步长计)。 30
GapTimeout 跳空后保持暂停的柱数。 100
TrailingStop 追踪止损距离(价格步长,0 为关闭)。 0

交易逻辑

进场

  • 做多 条件:
    • 最新 Momentum 小于 MomentumThreshold + MomentumShift
    • 前一根收盘价与当前开盘价都在偏移均线之下。
    • Momentum 连续 MomentumOpenLength 根保持非上升趋势。
  • 做空 条件:
    • 最新 Momentum 大于 MomentumThreshold - MomentumShift
    • 前收与今开均高于偏移均线。
    • Momentum 连续 MomentumOpenLength 根保持非下降趋势。

只有在没有持仓且未被跳空过滤器锁定时才会开仓。

离场

  • 多头 平仓条件:
    • Momentum 连续 MomentumCloseLength 根不升,或
    • 前一根收盘价跌破偏移均线,或
    • 触发追踪止损(当前最低价减去 TrailingStop 距离)。
  • 空头 平仓条件:
    • Momentum 连续 MomentumCloseLength 根不降,或
    • 前一根收盘价突破偏移均线,或
    • 触发追踪止损(当前最高价加上 TrailingStop 距离)。

跳空过滤

  1. 计算当前开盘价与前一收盘价的差值(换算为价格步长)。
  2. 当差值超过 GapLevel 时,将计时器设置为 GapTimeout
  3. 每根完成的蜡烛将计时器减一,直到归零才允许再次交易。

重要说明

  • 策略只处理已完成的蜡烛 (CandleStates.Finished),因此信号会在下一根柱子开仓/平仓,与原始 EA 在新柱 第一笔成交触发的效果一致。
  • MetaTrader 中的“点”通过 Security.PriceStep 进行近似转换。如果合约没有正确配置价格步长,则跳空过滤 和追踪止损会自动停用。
  • 移动平均线和 Momentum 的价格来源可以独立设置,与原始版本保持一致。
  • 策略不会下达止损订单,而是通过市价单实现与 PositionModify 类似的止损调整。

使用建议

  1. 选择目标证券,并确保 CandleType 与回测时的时间框架一致(原始脚本为 15 分钟)。
  2. 根据账户规模设置 TradeVolume
  3. 通过调整 MomentumOpenLength / MomentumCloseLength 控制 Momentum 序列的严格程度。
  4. 若希望完全匹配原始“点”距离,可根据交易所的价格步长换算出合适的 TrailingStopGapLevel
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>
/// Momentum based strategy converted from the MetaTrader 5 "Momentum-M15" expert advisor.
/// </summary>
public class MomentumM15Strategy : Strategy
{
	/// <summary>
	/// Moving average method options aligned with the original expert advisor inputs.
	/// </summary>
	public enum MovingAverageMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Simple,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Exponential,

		/// <summary>
		/// Smoothed moving average.
		/// </summary>
		Smoothed,

		/// <summary>
		/// Weighted moving average.
		/// </summary>
		Weighted
	}

	public enum CandlePrices
	{
		Open,
		High,
		Low,
		Close,
		Median,
		Typical,
		Weighted
	}

	private readonly StrategyParam<decimal> _volumeParam;
	private readonly StrategyParam<DataType> _candleTypeParam;
	private readonly StrategyParam<int> _maPeriodParam;
	private readonly StrategyParam<int> _maShiftParam;
	private readonly StrategyParam<MovingAverageMethods> _maMethodParam;
	private readonly StrategyParam<CandlePrices> _maPriceParam;
	private readonly StrategyParam<int> _momentumPeriodParam;
	private readonly StrategyParam<CandlePrices> _momentumPriceParam;
	private readonly StrategyParam<decimal> _momentumThresholdParam;
	private readonly StrategyParam<decimal> _momentumShiftParam;
	private readonly StrategyParam<int> _momentumOpenLengthParam;
	private readonly StrategyParam<int> _momentumCloseLengthParam;
	private readonly StrategyParam<int> _gapLevelParam;
	private readonly StrategyParam<int> _gapTimeoutParam;
	private readonly StrategyParam<decimal> _trailingStopParam;

	private IIndicator _ma = null!;
	private Momentum _momentum = null!;
	private readonly List<decimal> _maHistory = new();
	private readonly List<decimal> _momentumHistory = new();
	private decimal? _previousClose;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private int _gapTimer;

	/// <summary>
	/// Initializes a new instance of <see cref="MomentumM15Strategy"/>.
	/// </summary>
	public MomentumM15Strategy()
	{
		_volumeParam = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Default order volume", "Trading")
			
			.SetOptimize(0.05m, 0.5m, 0.05m);

		_candleTypeParam = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for calculations", "Common");

		_maPeriodParam = Param(nameof(MaPeriod), 26)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Moving average lookback length", "Indicators")
			
			.SetOptimize(10, 60, 5);

		_maShiftParam = Param(nameof(MaShift), 8)
			.SetNotNegative()
			.SetDisplay("MA Shift", "Horizontal shift applied to moving average", "Indicators");

		_maMethodParam = Param(nameof(MaMethod), MovingAverageMethods.Smoothed)
			.SetDisplay("MA Method", "Type of moving average", "Indicators");

		_maPriceParam = Param(nameof(MaPrice), CandlePrices.Low)
			.SetDisplay("MA Price", "Price source for moving average", "Indicators");

		_momentumPeriodParam = Param(nameof(MomentumPeriod), 23)
			.SetGreaterThanZero()
			.SetDisplay("Momentum Period", "Momentum indicator lookback", "Indicators")
			
			.SetOptimize(10, 40, 1);

		_momentumPriceParam = Param(nameof(MomentumPrice), CandlePrices.Open)
			.SetDisplay("Momentum Price", "Price source for momentum", "Indicators");

		_momentumThresholdParam = Param(nameof(MomentumThreshold), 100m)
			.SetDisplay("Momentum Threshold", "Baseline momentum threshold", "Trading Rules");

		_momentumShiftParam = Param(nameof(MomentumShift), -0.2m)
			.SetDisplay("Momentum Shift", "Shift applied to momentum threshold", "Trading Rules");

		_momentumOpenLengthParam = Param(nameof(MomentumOpenLength), 6)
			.SetNotNegative()
			.SetDisplay("Momentum Open Length", "Bars required for monotonic momentum on entries", "Trading Rules");

		_momentumCloseLengthParam = Param(nameof(MomentumCloseLength), 10)
			.SetNotNegative()
			.SetDisplay("Momentum Close Length", "Bars required for monotonic momentum on exits", "Trading Rules");

		_gapLevelParam = Param(nameof(GapLevel), 30)
			.SetNotNegative()
			.SetDisplay("Gap Level", "Minimum gap in price steps to pause trading", "Risk Management");

		_gapTimeoutParam = Param(nameof(GapTimeout), 100)
			.SetNotNegative()
			.SetDisplay("Gap Timeout", "Number of bars to skip after a large gap", "Risk Management");

		_trailingStopParam = Param(nameof(TrailingStop), 0m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop", "Trailing stop distance in price steps", "Risk Management");
	}

	/// <summary>
	/// Default trade volume.
	/// </summary>
	public decimal TradeVolume
	{
		get => _volumeParam.Value;
		set => _volumeParam.Value = value;
	}

	/// <summary>
	/// Candle type for the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleTypeParam.Value;
		set => _candleTypeParam.Value = value;
	}

	/// <summary>
	/// Moving average period.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriodParam.Value;
		set => _maPeriodParam.Value = value;
	}

	/// <summary>
	/// Number of bars to shift the moving average.
	/// </summary>
	public int MaShift
	{
		get => _maShiftParam.Value;
		set => _maShiftParam.Value = value;
	}

	/// <summary>
	/// Moving average calculation method.
	/// </summary>
	public MovingAverageMethods MaMethod
	{
		get => _maMethodParam.Value;
		set => _maMethodParam.Value = value;
	}

	/// <summary>
	/// Price source for the moving average.
	/// </summary>
	public CandlePrices MaPrice
	{
		get => _maPriceParam.Value;
		set => _maPriceParam.Value = value;
	}

	/// <summary>
	/// Momentum indicator lookback period.
	/// </summary>
	public int MomentumPeriod
	{
		get => _momentumPeriodParam.Value;
		set => _momentumPeriodParam.Value = value;
	}

	/// <summary>
	/// Price source for the momentum indicator.
	/// </summary>
	public CandlePrices MomentumPrice
	{
		get => _momentumPriceParam.Value;
		set => _momentumPriceParam.Value = value;
	}

	/// <summary>
	/// Baseline momentum threshold.
	/// </summary>
	public decimal MomentumThreshold
	{
		get => _momentumThresholdParam.Value;
		set => _momentumThresholdParam.Value = value;
	}

	/// <summary>
	/// Shift applied to the momentum threshold.
	/// </summary>
	public decimal MomentumShift
	{
		get => _momentumShiftParam.Value;
		set => _momentumShiftParam.Value = value;
	}

	/// <summary>
	/// Sequence length for entry momentum validation.
	/// </summary>
	public int MomentumOpenLength
	{
		get => _momentumOpenLengthParam.Value;
		set => _momentumOpenLengthParam.Value = value;
	}

	/// <summary>
	/// Sequence length for exit momentum validation.
	/// </summary>
	public int MomentumCloseLength
	{
		get => _momentumCloseLengthParam.Value;
		set => _momentumCloseLengthParam.Value = value;
	}

	/// <summary>
	/// Minimum gap (in price steps) that suspends new entries.
	/// </summary>
	public int GapLevel
	{
		get => _gapLevelParam.Value;
		set => _gapLevelParam.Value = value;
	}

	/// <summary>
	/// Number of bars to wait after a gap before trading resumes.
	/// </summary>
	public int GapTimeout
	{
		get => _gapTimeoutParam.Value;
		set => _gapTimeoutParam.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in price steps.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStopParam.Value;
		set => _trailingStopParam.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_ma = null!;
		_momentum = null!;
		_maHistory.Clear();
		_momentumHistory.Clear();
		_previousClose = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_gapTimer = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_ma = CreateMovingAverage(MaMethod, MaPeriod);
		_momentum = new Momentum { Length = MomentumPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		return;

		if (_ma is null || _momentum is null)
		return;

		var maValue = ProcessMovingAverage(candle);
		var momentumValue = ProcessMomentum(candle);

		if (maValue is null || momentumValue is null)
		{
			_previousClose = candle.ClosePrice;
			return;
		}

		var previousClose = _previousClose;
		_previousClose = candle.ClosePrice;

		if (previousClose is null)
		return;

		HandleGapFilter(previousClose.Value, candle.OpenPrice);

		if (_gapTimer > 0)
		{
			_gapTimer--;
			if (_gapTimer > 0)
			return;
		}

		if (Position == 0)
		{
			TryOpenPositions(previousClose.Value, candle.OpenPrice, maValue.Value, momentumValue.Value);
		}
		else
		{
			ManageExistingPosition(previousClose.Value, candle, maValue.Value, momentumValue.Value);
		}
	}

	private decimal? ProcessMovingAverage(ICandleMessage candle)
	{
		var price = GetPrice(candle, MaPrice);
		var value = _ma.Process(new DecimalIndicatorValue(_ma, price, candle.OpenTime) { IsFinal = true });

		if (value.IsEmpty || !_ma.IsFormed)
		return null;

		var ma = value.ToDecimal();
		_maHistory.Add(ma);

		var maxCount = MaShift + 1;
		while (_maHistory.Count > maxCount)
		_maHistory.RemoveAt(0);

		var index = _maHistory.Count - 1 - MaShift;
		if (index < 0 || index >= _maHistory.Count)
		return null;

		return _maHistory[index];
	}

	private decimal? ProcessMomentum(ICandleMessage candle)
	{
		var price = GetPrice(candle, MomentumPrice);
		var value = _momentum.Process(new DecimalIndicatorValue(_momentum, price, candle.OpenTime) { IsFinal = true });

		if (value.IsEmpty || !_momentum.IsFormed)
		return null;

		var momentum = value.ToDecimal();
		_momentumHistory.Add(momentum);

		var maxLen = Math.Max(Math.Max(MomentumOpenLength, MomentumCloseLength), 1);
		while (_momentumHistory.Count > maxLen)
		_momentumHistory.RemoveAt(0);

		return momentum;
	}

	private void HandleGapFilter(decimal previousClose, decimal currentOpen)
	{
		var priceStep = Security.PriceStep ?? 0m;
		if (priceStep <= 0m)
		return;

		var gap = (currentOpen - previousClose) / priceStep;
		if (gap > GapLevel)
		_gapTimer = GapTimeout;
	}

	private void TryOpenPositions(decimal previousClose, decimal currentOpen, decimal maValue, decimal momentumValue)
	{
		var longMomentumOk = MomentumOpenLength > 0 && IsMomentumDownSequence(MomentumOpenLength);
		var shortMomentumOk = MomentumOpenLength > 0 && IsMomentumUpSequence(MomentumOpenLength);

		var longCondition = momentumValue < MomentumThreshold + MomentumShift
		&& previousClose < maValue
		&& currentOpen < maValue
		&& longMomentumOk;

		var shortCondition = momentumValue > MomentumThreshold - MomentumShift
		&& previousClose > maValue
		&& currentOpen > maValue
		&& shortMomentumOk;

		if (longCondition)
		{
			BuyMarket(TradeVolume);
			_longTrailingStop = null;
			_shortTrailingStop = null;
		}
		else if (shortCondition)
		{
			SellMarket(TradeVolume);
			_longTrailingStop = null;
			_shortTrailingStop = null;
		}
	}

	private void ManageExistingPosition(decimal previousClose, ICandleMessage candle, decimal maValue, decimal momentumValue)
	{
		if (Position > 0)
		{
			var exitMomentum = MomentumCloseLength > 0 && IsMomentumDownSequence(MomentumCloseLength);
			var shouldClose = exitMomentum || previousClose < maValue;

			if (shouldClose)
			{
				SellMarket(Position);
				_longTrailingStop = null;
				return;
			}

			UpdateLongTrailingStop(candle);
		}
		else if (Position < 0)
		{
			var exitMomentum = MomentumCloseLength > 0 && IsMomentumUpSequence(MomentumCloseLength);
			var shouldClose = exitMomentum || previousClose > maValue;

			if (shouldClose)
			{
				BuyMarket(Math.Abs(Position));
				_shortTrailingStop = null;
				return;
			}

			UpdateShortTrailingStop(candle);
		}
	}

	private void UpdateLongTrailingStop(ICandleMessage candle)
	{
		if (TrailingStop <= 0m)
		return;

		var priceStep = Security.PriceStep ?? 0m;
		if (priceStep <= 0m)
		return;

		var distance = TrailingStop * priceStep;
		var candidate = candle.LowPrice - distance;

		if (_longTrailingStop is null || candidate > _longTrailingStop)
		_longTrailingStop = candidate;

		if (_longTrailingStop is decimal stop && candle.LowPrice <= stop)
		{
			SellMarket(Position);
			_longTrailingStop = null;
		}
	}

	private void UpdateShortTrailingStop(ICandleMessage candle)
	{
		if (TrailingStop <= 0m)
		return;

		var priceStep = Security.PriceStep ?? 0m;
		if (priceStep <= 0m)
		return;

		var distance = TrailingStop * priceStep;
		var candidate = candle.HighPrice + distance;

		if (_shortTrailingStop is null || candidate < _shortTrailingStop)
		_shortTrailingStop = candidate;

		if (_shortTrailingStop is decimal stop && candle.HighPrice >= stop)
		{
			BuyMarket(Math.Abs(Position));
			_shortTrailingStop = null;
		}
	}

	private bool IsMomentumDownSequence(int length)
	{
		if (length <= 0 || _momentumHistory.Count < length)
		return false;

		var start = _momentumHistory.Count - length;
		var previous = _momentumHistory[start];

		for (var i = start + 1; i < _momentumHistory.Count; i++)
		{
			var current = _momentumHistory[i];
			if (current > previous)
			return false;

			previous = current;
		}

		return true;
	}

	private bool IsMomentumUpSequence(int length)
	{
		if (length <= 0 || _momentumHistory.Count < length)
		return false;

		var start = _momentumHistory.Count - length;
		var previous = _momentumHistory[start];

		for (var i = start + 1; i < _momentumHistory.Count; i++)
		{
			var current = _momentumHistory[i];
			if (current < previous)
			return false;

			previous = current;
		}

		return true;
	}

	private static decimal GetPrice(ICandleMessage candle, CandlePrices price)
	{
		return price switch
		{
			CandlePrices.Open => candle.OpenPrice,
			CandlePrices.High => candle.HighPrice,
			CandlePrices.Low => candle.LowPrice,
			CandlePrices.Close => candle.ClosePrice,
			CandlePrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			CandlePrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			CandlePrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice,
		};
	}

	private static IIndicator CreateMovingAverage(MovingAverageMethods method, int period)
	{
		return method switch
		{
			MovingAverageMethods.Simple => new SimpleMovingAverage { Length = period },
			MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = period },
			MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = period },
			MovingAverageMethods.Weighted => new WeightedMovingAverage { Length = period },
			_ => new SimpleMovingAverage { Length = period },
		};
	}
}