Ver no GitHub

MA on Momentum Min Profit Strategy

Overview

This strategy replicates the MetaTrader 5 expert advisor MA on Momentum Min Profit.mq5 by trading the crossover between a Momentum indicator and a moving average that is calculated on top of the momentum series. A bullish signal appears when momentum crosses above its average while the previous bar kept momentum below the neutral 100 level. A bearish signal is generated when momentum crosses below the average with the previous bar above 100. The implementation keeps the original money based equity stop and the fixed take-profit distance measured in points.

Trading logic

  1. Request candles defined by CandleType and feed them into the Momentum indicator.
  2. Smooth the momentum stream with a moving average defined by MomentumMovingAverageType and MomentumMovingAveragePeriod.
  3. Detect crossovers using the previous bar values to avoid double signals.
  4. Optional features from the MQL version:
    • Reverse the direction of the generated signals.
    • Close the opposite exposure before entering a new trade or skip the entry entirely.
    • Enforce a single net position at any time.
    • Allow triggering on the current (forming) candle instead of the fully closed bar.
  5. Apply risk management:
    • Equity stop in money: PnL + Position * (close - PositionPrice) must remain above StopLossMoney.
    • Take-profit distance in points converted through Security.PriceStep.

Parameters

Parameter Type Default Description
CandleType DataType TimeSpan.FromMinutes(5).TimeFrame() Candles used to compute momentum.
MomentumPeriod int 14 Lookback period of the Momentum indicator.
MomentumMovingAveragePeriod int 6 Length of the moving average applied to momentum.
MomentumMovingAverageType MomentumMovingAverageType Smoothed Moving average algorithm (Simple, Exponential, Smoothed, Weighted).
ReverseSignals bool false Mirror MetaTrader buy/sell signals.
CloseOpposite bool true Close the opposite exposure before opening a new position.
OnlyOnePosition bool true Keep a single net position.
UseCurrentCandle bool false Evaluate signals on the current forming candle instead of the closed bar.
StopLossMoney decimal 15 Equity drawdown allowed before closing all trades.
TakeProfitPoints decimal 460 Profit target in instrument points (multiplied by PriceStep).
MomentumReference decimal 100 Neutral momentum level copied from the MQL strategy.

Implementation notes

  • The moving average is implemented with LengthIndicator<decimal> instances to reuse StockSharp built-in SMA/EMA/SMMA/WMA classes.
  • The original order queue and magic-number filters map to StockSharp net positions, therefore the strategy sends a single market order sized to both flatten the opposite side and open the new exposure when CloseOpposite is enabled.
  • Equity protection closes all positions via CloseAll() once the floating loss breaches the threshold, exactly matching the MetaTrader behaviour of monitoring the combined commission, swap and profit.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Momentum crossing its own moving average strategy.
/// Converted from MetaTrader 5 (MA on Momentum Min Profit.mq5).
/// </summary>
public class MaOnMomentumMinProfitStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _momentumPeriod;
	private readonly StrategyParam<int> _maPeriod;

	private Momentum _momentum;
	private readonly Queue<decimal> _momentumHistory = new();
	private decimal? _prevMomentum;
	private decimal? _prevSignal;

	/// <summary>
	/// Candle type used for signal calculation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

	/// <summary>
	/// Moving average period applied to momentum values.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="MaOnMomentumMinProfitStrategy"/>.
	/// </summary>
	public MaOnMomentumMinProfitStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for the momentum calculation", "General");

		_momentumPeriod = Param(nameof(MomentumPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Momentum Period", "Lookback for the momentum indicator", "Momentum");

		_maPeriod = Param(nameof(MaPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Period of the moving average applied to momentum", "Momentum");
	}

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

		_prevMomentum = null;
		_prevSignal = null;
		_momentumHistory.Clear();

		_momentum = new Momentum { Length = MomentumPeriod };

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

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

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

		_momentumHistory.Enqueue(momentumValue);
		while (_momentumHistory.Count > MaPeriod)
			_momentumHistory.Dequeue();

		if (!_momentum.IsFormed)
			return;

		if (_momentumHistory.Count < MaPeriod)
		{
			_prevMomentum = momentumValue;
			return;
		}

		// Calculate SMA of momentum
		var sum = 0m;
		var history = _momentumHistory.ToArray();
		foreach (var v in history)
			sum += v;
		var signalValue = sum / history.Length;

		if (_prevMomentum is null || _prevSignal is null)
		{
			_prevMomentum = momentumValue;
			_prevSignal = signalValue;
			return;
		}

		var crossUp = _prevMomentum < _prevSignal && momentumValue > signalValue;
		var crossDown = _prevMomentum > _prevSignal && momentumValue < signalValue;

		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		var minSpread = 0.5m;

		if (crossUp && Math.Abs(momentumValue - signalValue) >= minSpread)
		{
			if (Position <= 0)
				BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
		}
		else if (crossDown && Math.Abs(momentumValue - signalValue) >= minSpread)
		{
			if (Position >= 0)
				SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
		}

		_prevMomentum = momentumValue;
		_prevSignal = signalValue;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_prevMomentum = null;
		_prevSignal = null;
		_momentum = null;
		_momentumHistory.Clear();

		base.OnReseted();
	}
}