Ver en GitHub

Crossover MA Strategy

Overview

This strategy is a StockSharp port of the MetaTrader 5 expert advisor CrossoverMA.mq5. The original robot waits for a candle to cross a moving average and only opens a position when the average is sloping in the same direction as the breakout. The StockSharp version keeps the same behaviour while taking advantage of the high-level API for candle subscriptions, indicator management, and automatic chart rendering.

Trading Logic

  1. Subscribe to the configured candle series and calculate a simple moving average (SMA) over the candle close price.
  2. When a finished candle is received, measure:
    • The candle open and close distances from the SMA.
    • The slope of the SMA by comparing the current value with the previous one.
  3. Generate signals:
    • Bullish breakout – the candle opens below the SMA, closes above it, and the SMA is rising. The strategy closes any short exposure and opens/extends a long position.
    • Bearish breakout – the candle opens above the SMA, closes below it, and the SMA is falling. The strategy closes any long exposure and opens/extends a short position.
  4. Ignore duplicate signals that do not change the current position side.

The port keeps the MetaTrader rule that only finished candles are processed and that one extra candle is required before the first trade (to measure the SMA slope).

Parameters

Name Description Default Notes
Candle Type Time frame used to build candles. 1 minute time frame Any StockSharp-supported candle data type can be selected.
MA Length Number of completed candles included in the SMA. 12 Matches the default period of the MetaTrader expert.
Trade Volume Market order volume for entries. 1 The strategy closes the opposite exposure before opening a new position.

All parameters are available for optimisation in StockSharp Designer or Runner.

Implementation Notes

  • The strategy relies on SubscribeCandles and Bind so indicator values are streamed directly into the processing method without manual history management.
  • The SMA is stored in a private field to draw it on the chart area when one is available.
  • Signals are processed only when IsFormedAndOnlineAndAllowTrading() returns true, ensuring the strategy respects the global trading state.
  • Position reversals follow the MetaTrader template: close the current exposure first, then open the new side with the configured trade volume.

Files

  • CS/CrossoverMaStrategy.cs – C# implementation of the converted strategy.
  • README.md – English documentation.
  • README_zh.md – Chinese documentation.
  • README_ru.md – Russian documentation.

Porting Differences

  • Money-management, trailing-stop, and other MetaTrader framework classes are omitted because StockSharp manages position sizing and risk externally. The Trade Volume parameter replaces the fixed lot settings from the original expert.
  • MetaTrader used separate data series for candle open and close prices. StockSharp candles already include both prices, so no extra indicators are required.
  • Indicator initialisation, validation, and lifecycle management are handled automatically by StockSharp, removing the lengthy boilerplate from the MQL version.
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;

public class CrossoverMaStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public CrossoverMaStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}