GitHub で見る

Fast Slow MA Crossover Strategy

Overview

The Fast Slow MA Crossover Strategy reproduces the behaviour of the original MetaTrader 4 expert advisor _HPCS_FastSlowMACrosssover_MT4_EA_V01_WE. The strategy watches two exponential moving averages (EMAs) calculated on the selected candle series and issues trades when the fast average crosses the slow one inside a configurable intraday trading window. Protective take-profit and stop-loss exits are expressed in pips so the behaviour matches the MQL implementation that relies on broker digits for price scaling.

Trading Logic

  1. Subscribe to the configured candle type (default: 1-minute candles).
  2. Calculate two EMAs:
    • Fast EMA period (default 14).
    • Slow EMA period (default 21).
  3. Evaluate each finished candle:
    • Check that the candle close time falls inside the allowed trading window.
    • Detect a bullish crossover when the fast EMA crosses above the slow EMA.
    • Detect a bearish crossover when the fast EMA crosses below the slow EMA.
  4. Execute orders:
    • Close the opposite exposure if an inverse position is open.
    • Enter a market order with the configured volume (Trade Volume parameter).
    • Store the candle close price as the entry anchor for risk calculations.
  5. Manage open positions using candle highs and lows:
    • Close a long position if the price moves Stop Loss (pips) below the entry.
    • Close a long position if the price rallies Take Profit (pips) above the entry.
    • Apply the symmetrical logic for short positions (stop above entry, target below entry).

Parameters

Parameter Description
Fast MA Period Length of the fast EMA used for crossover detection.
Slow MA Period Length of the slow EMA.
Take Profit (pips) Distance, in pips, used to compute the long and short profit targets.
Stop Loss (pips) Distance, in pips, used to compute the protective stop prices.
Start Time Beginning of the daily trading window (inclusive).
Stop Time End of the daily trading window (inclusive).
Candle Type Candle series used to feed the indicators.
Trade Volume Market order volume for each signal.

Notes

  • Pip size is derived from the security price step and decimal precision. When the instrument uses 5 or 3 decimal digits, the strategy multiplies the price step by 10 to match the MetaTrader pip calculation.
  • The time filter supports overnight sessions. When Start Time is later than Stop Time, trading remains active until midnight and resumes from midnight to the stop time.
  • Only one signal per candle is allowed, ensuring the behaviour matches the original EA that guarded against multiple submissions per bar.
  • Protective exit orders are executed by the strategy logic instead of resting orders. This mirrors the EA approach where the stop loss and take profit levels were defined at order submission.
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>
/// Fast and slow moving average crossover strategy with intraday time filter and pip-based risk controls.
/// </summary>
public class FastSlowMaCrossoverStrategy : Strategy
{
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<TimeSpan> _startTime;
	private readonly StrategyParam<TimeSpan> _stopTime;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;

	private decimal _pipSize;
	private decimal? _previousFast;
	private decimal? _previousSlow;
	private DateTimeOffset? _lastSignalTime;
	private bool _hasActivePosition;
	private bool _isLongPosition;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _targetPrice;

	/// <summary>
	/// Initializes a new instance of the <see cref="FastSlowMaCrossoverStrategy"/> class.
	/// </summary>
	public FastSlowMaCrossoverStrategy()
	{
		_fastMaPeriod = Param(nameof(FastMaPeriod), 30)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Length of the fast moving average", "Parameters")
			;

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 80)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Length of the slow moving average", "Parameters")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 80)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Distance in pips for profit taking", "Risk Management")
			;

		_stopLossPips = Param(nameof(StopLossPips), 80)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Distance in pips for protective stop", "Risk Management")
			;

		_startTime = Param(nameof(StartTime), new TimeSpan(8, 0, 0))
			.SetDisplay("Start Time", "Start of the allowed trading window", "Schedule");

		_stopTime = Param(nameof(StopTime), new TimeSpan(18, 0, 0))
			.SetDisplay("Stop Time", "End of the allowed trading window", "Schedule");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(120).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Volume of each market order", "Trading")
			;
	}

	/// <summary>
	/// Period of the fast moving average.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Period of the slow moving average.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Profit target distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Start time of the allowed trading window.
	/// </summary>
	public TimeSpan StartTime
	{
		get => _startTime.Value;
		set => _startTime.Value = value;
	}

	/// <summary>
	/// Stop time of the allowed trading window.
	/// </summary>
	public TimeSpan StopTime
	{
		get => _stopTime.Value;
		set => _stopTime.Value = value;
	}

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

	/// <summary>
	/// Volume used for each market order.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set
		{
			_tradeVolume.Value = value;
			Volume = value;
		}
	}

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

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

		_pipSize = 0m;
		_previousFast = null;
		_previousSlow = null;
		_lastSignalTime = null;
		ResetPositionState();
	}

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

		Volume = TradeVolume;
		_pipSize = CalculatePipSize();

		var fastMa = new EMA { Length = FastMaPeriod };
		var slowMa = new EMA { Length = SlowMaPeriod };

		SubscribeCandles(CandleType)
			.Bind(fastMa, slowMa, (candle, fastValue, slowValue) => ProcessCandle(candle, fastValue, slowValue, fastMa.IsFormed && slowMa.IsFormed))
			.Start();
	}

	private decimal CalculatePipSize()
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			return 0.0001m;

		var decimals = Security?.Decimals ?? 0;
		var factor = (decimals == 3 || decimals == 5) ? 10m : 1m;
		return priceStep * factor;
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue, bool indicatorsFormed)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var timeOfDay = candle.CloseTime.TimeOfDay;
		if (!IsWithinTradingWindow(timeOfDay))
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		ManageExistingPosition(candle);

		if (!indicatorsFormed)
		{
			_previousFast = fastValue;
			_previousSlow = slowValue;
			return;
		}

		if (_previousFast is null || _previousSlow is null)
		{
			_previousFast = fastValue;
			_previousSlow = slowValue;
			return;
		}

		var crossUp = _previousFast <= _previousSlow && fastValue > slowValue;
		var crossDown = _previousFast >= _previousSlow && fastValue < slowValue;

		if (_pipSize <= 0m)
		{
			_previousFast = fastValue;
			_previousSlow = slowValue;
			return;
		}

		var currentCandleTime = candle.CloseTime;

		if (crossUp && Position <= 0m && _lastSignalTime != currentCandleTime)
		{
			var volume = TradeVolume;

			if (Position < 0m)
				volume += -Position;

			if (volume > 0m)
			{
				BuyMarket(volume);
				RecordEntryState(candle.ClosePrice, true);
				_lastSignalTime = currentCandleTime;
			}
		}
		else if (crossDown && Position >= 0m && _lastSignalTime != currentCandleTime)
		{
			var volume = TradeVolume;

			if (Position > 0m)
				volume += Position;

			if (volume > 0m)
			{
				SellMarket(volume);
				RecordEntryState(candle.ClosePrice, false);
				_lastSignalTime = currentCandleTime;
			}
		}

		_previousFast = fastValue;
		_previousSlow = slowValue;
	}

	private void ManageExistingPosition(ICandleMessage candle)
	{
		if (!_hasActivePosition || _pipSize <= 0m)
			return;

		var high = candle.HighPrice;
		var low = candle.LowPrice;

		if (_isLongPosition)
		{
			if (StopLossPips > 0 && low <= _stopPrice)
			{
				SellMarket(Position);
				ResetPositionState();
				return;
			}

			if (TakeProfitPips > 0 && high >= _targetPrice)
			{
				SellMarket(Position);
				ResetPositionState();
			}
		}
		else
		{
			if (StopLossPips > 0 && high >= _stopPrice)
			{
				BuyMarket(-Position);
				ResetPositionState();
				return;
			}

			if (TakeProfitPips > 0 && low <= _targetPrice)
			{
				BuyMarket(-Position);
				ResetPositionState();
			}
		}
	}

	private void RecordEntryState(decimal closePrice, bool isLong)
	{
		_hasActivePosition = true;
		_isLongPosition = isLong;
		_entryPrice = closePrice;

		var takeOffset = TakeProfitPips > 0 ? TakeProfitPips * _pipSize : 0m;
		var stopOffset = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;

		if (isLong)
		{
			_targetPrice = takeOffset > 0m ? (_entryPrice + takeOffset) : 0m;
			_stopPrice = stopOffset > 0m ? (_entryPrice - stopOffset) : 0m;
		}
		else
		{
			_targetPrice = takeOffset > 0m ? (_entryPrice - takeOffset) : 0m;
			_stopPrice = stopOffset > 0m ? (_entryPrice + stopOffset) : 0m;
		}
	}

	private bool IsWithinTradingWindow(TimeSpan current)
	{
		var start = StartTime;
		var stop = StopTime;

		if (start == stop)
			return true;

		if (start < stop)
			return current >= start && current <= stop;

		return current >= start || current <= stop;
	}

	private void ResetPositionState()
	{
		_hasActivePosition = false;
		_isLongPosition = false;
		_entryPrice = 0m;
		_stopPrice = 0m;
		_targetPrice = 0m;
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0m)
		{
			ResetPositionState();
		}
		else
		{
			_hasActivePosition = true;
			_isLongPosition = Position > 0m;
		}
	}
}