Открыть на GitHub

Стратегия Universal MA Cross

Обзор

Universal MA Cross Strategy — конвертация оригинального эксперта MQL5 «UniversalMACrossEA» в высокоуровневый фреймворк StockSharp. Алгоритм сравнивает быструю и медленную скользящие средние, позволяя задать метод сглаживания и тип цены для каждой линии. Дополнительные параметры управляют подтверждением сигналов, поведением «стоп и разворот», логикой риск-менеджмента и допуском к торговле по времени.

Логика торговли

Расчёт индикаторов

  • Рассчитываются две скользящие средние по выбранной серии свечей. Для каждой можно задать собственный период, метод (SMA, EMA, SMMA или LWMA) и тип цены (close, open, high, low, median, typical или weighted).
  • Параметр MinCrossDistance требует, чтобы на баре пересечения расстояние между средней и сигнальной линиями было не меньше заданного значения в ценовых единицах.
  • Если включено ConfirmedOnEntry, пересечение проверяется по двум предыдущим закрытым барам (аналог индексов 2 и 1 в исходном эксперте). При отключении сравнивается текущий завершённый бар с предыдущим, что имитирует «тик-режим» MQL.
  • Параметр ReverseCondition меняет местами условия покупок и продаж без изменения индикаторных настроек.

Правила входа

  1. Для открытия длинной позиции быстрая средняя должна пересечь медленную сверху вниз с запасом не меньше MinCrossDistance. Для короткой позиции требуется обратное пересечение.
  2. При активном параметре StopAndReverse появление противоположного сигнала приводит к закрытию текущей позиции перед рассмотрением новых ордеров.
  3. Если OneEntryPerBar установлено в true, стратегия запоминает время бара последнего входа и не открывает повторную сделку в пределах той же свечи.
  4. Объём каждой сделки задаётся параметром Volume.

Сопровождение позиции

  • Стоп-лосс и тейк-профит задаются в абсолютных ценовых величинах. В режиме PureSar они отключаются, что соответствует опции «Pure SAR» в исходном советнике.
  • Трейлинг-стоп активируется после движения цены на величину TrailingStop + TrailingStep от входа. Каждое последующее смещение не менее TrailingStep подтягивает стоп на расстояние TrailingStop. В режиме Pure SAR трейлинг не используется.
  • Защитные уровни контролируются по завершённым свечам. Если диапазон свечи пробивает стоп или тейк-профит, позиция закрывается рыночным ордером.

Временной фильтр

  • При включённом UseHourTrade сделки совершаются только в случае, если час открытия свечи лежит между StartHour и EndHour (включительно). Управление трейлинг-стопом продолжается и вне этого интервала, но новые входы и «стоп и разворот» не выполняются.

Параметры

Параметр Описание
FastMaPeriod, SlowMaPeriod Периоды быстрой и медленной скользящих средних.
FastMaType, SlowMaType Методы расчёта: простая, экспоненциальная, сглаженная (RMA) или линейно-взвешенная.
FastPriceType, SlowPriceType Типы цен, подаваемые на соответствующие средние.
StopLoss, TakeProfit Защитные расстояния в абсолютных ценовых единицах (0 отключает уровень).
TrailingStop, TrailingStep Величина трейлинг-стопа и минимальный дополнительный ход перед подтяжкой.
MinCrossDistance Минимальная дистанция между средними на баре пересечения.
ReverseCondition Переворот логики входов.
ConfirmedOnEntry Подтверждение сигналов только закрытыми барами.
OneEntryPerBar Не более одного входа на свечу.
StopAndReverse Закрыть текущую позицию и перевернуться по противоположному сигналу.
PureSar Отключить стоп-лосс, тейк-профит и трейлинг.
UseHourTrade, StartHour, EndHour Временной фильтр (часы 0–23).
Volume Объём заявки.
CandleType Тип свечных данных для расчётов.

Особенности конверсии

  • Защитные уровни контролируются внутри стратегии по максимумам и минимумам свечи, поскольку высокоуровневый StockSharp работает с завершёнными барами, а не с тиками. Такой подход сохраняет логику оригинального эксперта.
  • Алгоритм трейлинг-стопа повторяет MQL-реализацию: стоп подтягивается только после прохождения цены на TrailingStop + TrailingStep.
  • По требованию заказчика Python-версия стратегии не создавалась.
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>
/// Universal moving average crossover strategy converted from the original MQL version.
/// The strategy trades based on a fast and a slow moving average with optional signal confirmation,
/// stop-and-reverse behaviour, trailing stop management and time filtering.
/// </summary>
public class UniversalMaCrossStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation methods supported by the strategy.
	/// </summary>
	public enum MovingAverageMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	/// <summary>
	/// Price sources that can feed the moving averages.
	/// </summary>
	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<MovingAverageMethods> _fastMaType;
	private readonly StrategyParam<MovingAverageMethods> _slowMaType;
	private readonly StrategyParam<AppliedPrices> _fastPriceType;
	private readonly StrategyParam<AppliedPrices> _slowPriceType;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<decimal> _trailingStep;
	private readonly StrategyParam<decimal> _minCrossDistance;
	private readonly StrategyParam<bool> _reverseCondition;
	private readonly StrategyParam<bool> _confirmedOnEntry;
	private readonly StrategyParam<bool> _oneEntryPerBar;
	private readonly StrategyParam<bool> _stopAndReverse;
	private readonly StrategyParam<bool> _pureSar;
	private readonly StrategyParam<bool> _useHourTrade;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<DataType> _candleType;

	private IIndicator _fastMa;
	private IIndicator _slowMa;

	private decimal? _fastPrev;
	private decimal? _fastPrevPrev;
	private decimal? _slowPrev;
	private decimal? _slowPrevPrev;

	private DateTimeOffset? _lastEntryBar;
	private TradeDirections _lastTrade = TradeDirections.None;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Fast moving average period.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average period.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Fast moving average method.
	/// </summary>
	public MovingAverageMethods FastMaType
	{
		get => _fastMaType.Value;
		set => _fastMaType.Value = value;
	}

	/// <summary>
	/// Slow moving average method.
	/// </summary>
	public MovingAverageMethods SlowMaType
	{
		get => _slowMaType.Value;
		set => _slowMaType.Value = value;
	}

	/// <summary>
	/// Price type used for the fast moving average.
	/// </summary>
	public AppliedPrices FastPriceType
	{
		get => _fastPriceType.Value;
		set => _fastPriceType.Value = value;
	}

	/// <summary>
	/// Price type used for the slow moving average.
	/// </summary>
	public AppliedPrices SlowPriceType
	{
		get => _slowPriceType.Value;
		set => _slowPriceType.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

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

	/// <summary>
	/// Additional move required before shifting the trailing stop.
	/// </summary>
	public decimal TrailingStep
	{
		get => _trailingStep.Value;
		set => _trailingStep.Value = value;
	}

	/// <summary>
	/// Minimum distance between the averages to validate a crossover.
	/// </summary>
	public decimal MinCrossDistance
	{
		get => _minCrossDistance.Value;
		set => _minCrossDistance.Value = value;
	}

	/// <summary>
	/// Reverse buy and sell conditions.
	/// </summary>
	public bool ReverseCondition
	{
		get => _reverseCondition.Value;
		set => _reverseCondition.Value = value;
	}

	/// <summary>
	/// Confirm signals on closed candles only.
	/// </summary>
	public bool ConfirmedOnEntry
	{
		get => _confirmedOnEntry.Value;
		set => _confirmedOnEntry.Value = value;
	}

	/// <summary>
	/// Limit the strategy to a single entry per bar.
	/// </summary>
	public bool OneEntryPerBar
	{
		get => _oneEntryPerBar.Value;
		set => _oneEntryPerBar.Value = value;
	}

	/// <summary>
	/// Close the current trade and reverse when an opposite signal appears.
	/// </summary>
	public bool StopAndReverse
	{
		get => _stopAndReverse.Value;
		set => _stopAndReverse.Value = value;
	}

	/// <summary>
	/// Disable protective orders and rely purely on signal reversals.
	/// </summary>
	public bool PureSar
	{
		get => _pureSar.Value;
		set => _pureSar.Value = value;
	}

	/// <summary>
	/// Enable trading only within the selected hours.
	/// </summary>
	public bool UseHourTrade
	{
		get => _useHourTrade.Value;
		set => _useHourTrade.Value = value;
	}

	/// <summary>
	/// Hour when trading can start (0-23).
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour when trading must end (0-23).
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}


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

	/// <summary>
	/// Initializes <see cref="UniversalMaCrossStrategy"/>.
	/// </summary>
	public UniversalMaCrossStrategy()
	{
		_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Fast moving average length", "Indicators")
			
			.SetOptimize(5, 30, 1);

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 80)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Slow moving average length", "Indicators")
			
			.SetOptimize(30, 200, 5);

		_fastMaType = Param(nameof(FastMaType), MovingAverageMethods.Exponential)
			.SetDisplay("Fast MA Type", "Method for fast average", "Indicators");

		_slowMaType = Param(nameof(SlowMaType), MovingAverageMethods.Exponential)
			.SetDisplay("Slow MA Type", "Method for slow average", "Indicators");

		_fastPriceType = Param(nameof(FastPriceType), AppliedPrices.Close)
			.SetDisplay("Fast Price Type", "Price source for fast MA", "Indicators");

		_slowPriceType = Param(nameof(SlowPriceType), AppliedPrices.Close)
			.SetDisplay("Slow Price Type", "Price source for slow MA", "Indicators");

		_stopLoss = Param(nameof(StopLoss), 0m)
			.SetDisplay("Stop Loss", "Stop-loss distance in price", "Risk");

		_takeProfit = Param(nameof(TakeProfit), 0m)
			.SetDisplay("Take Profit", "Take-profit distance in price", "Risk");

		_trailingStop = Param(nameof(TrailingStop), 0m)
			.SetDisplay("Trailing Stop", "Trailing stop distance", "Risk");

		_trailingStep = Param(nameof(TrailingStep), 0m)
			.SetDisplay("Trailing Step", "Additional move before trailing", "Risk");

		_minCrossDistance = Param(nameof(MinCrossDistance), 0m)
			.SetDisplay("Min Cross Distance", "Minimum distance between averages", "Filters");

		_reverseCondition = Param(nameof(ReverseCondition), false)
			.SetDisplay("Reverse Signals", "Swap long and short conditions", "General");

		_confirmedOnEntry = Param(nameof(ConfirmedOnEntry), true)
			.SetDisplay("Confirmed On Entry", "Use closed candles for signals", "General");

		_oneEntryPerBar = Param(nameof(OneEntryPerBar), true)
			.SetDisplay("One Entry Per Bar", "Allow only one entry per candle", "General");

		_stopAndReverse = Param(nameof(StopAndReverse), true)
			.SetDisplay("Stop And Reverse", "Close and reverse on opposite signal", "Risk");

		_pureSar = Param(nameof(PureSar), false)
			.SetDisplay("Pure SAR", "Disable stop-loss, take-profit and trailing", "Risk");

		_useHourTrade = Param(nameof(UseHourTrade), false)
			.SetDisplay("Use Hour Filter", "Limit trading by session hours", "Session");

		_startHour = Param(nameof(StartHour), 0)
			.SetDisplay("Start Hour", "Trading window start hour", "Session");

		_endHour = Param(nameof(EndHour), 23)
			.SetDisplay("End Hour", "Trading window end hour", "Session");


		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle subscription", "General");
	}

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

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

		_fastMa = null;
		_slowMa = null;
		_fastPrev = null;
		_fastPrevPrev = null;
		_slowPrev = null;
		_slowPrevPrev = null;
		_lastEntryBar = null;
		_lastTrade = TradeDirections.None;
		ResetProtection();
	}

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

		_fastMa = CreateMovingAverage(FastMaType, FastMaPeriod);
		_slowMa = CreateMovingAverage(SlowMaType, SlowMaPeriod);

		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;

		ManageExistingPosition(candle);

		if (_fastMa is null || _slowMa is null)
			return;

		var fastPrice = GetPrice(candle, FastPriceType);
		var slowPrice = GetPrice(candle, SlowPriceType);

		var fastResult = _fastMa.Process(new DecimalIndicatorValue(_fastMa, fastPrice, candle.OpenTime) { IsFinal = true });
		var slowResult = _slowMa.Process(new DecimalIndicatorValue(_slowMa, slowPrice, candle.OpenTime) { IsFinal = true });

		if (fastResult.IsEmpty || slowResult.IsEmpty)
			return;

		var fastValue = fastResult.ToDecimal();
		var slowValue = slowResult.ToDecimal();

		var prevFast = _fastPrev;
		var prevSlow = _slowPrev;
		var prevFastPrev = _fastPrevPrev;
		var prevSlowPrev = _slowPrevPrev;

		_fastPrevPrev = prevFast;
		_slowPrevPrev = prevSlow;
		_fastPrev = fastValue;
		_slowPrev = slowValue;


		bool crossUp = false;
		bool crossDown = false;

		if (ConfirmedOnEntry)
		{
			if (prevFast.HasValue && prevSlow.HasValue && prevFastPrev.HasValue && prevSlowPrev.HasValue)
			{
				var fastPrevPrevValue = prevFastPrev.Value;
				var slowPrevPrevValue = prevSlowPrev.Value;
				var fastPrevValue = prevFast.Value;
				var slowPrevValue = prevSlow.Value;
				var diff = fastPrevValue - slowPrevValue;

				crossUp = fastPrevPrevValue < slowPrevPrevValue && fastPrevValue > slowPrevValue && diff >= MinCrossDistance;
				crossDown = fastPrevPrevValue > slowPrevPrevValue && fastPrevValue < slowPrevValue && -diff >= MinCrossDistance;
			}
		}
		else
		{
			if (prevFast.HasValue && prevSlow.HasValue)
			{
				var fastPrevValue = prevFast.Value;
				var slowPrevValue = prevSlow.Value;
				var diff = fastValue - slowValue;

				crossUp = fastPrevValue < slowPrevValue && fastValue > slowValue && diff >= MinCrossDistance;
				crossDown = fastPrevValue > slowPrevValue && fastValue < slowValue && -diff >= MinCrossDistance;
			}
		}

		bool buySignal;
		bool sellSignal;

		if (!ReverseCondition)
		{
			buySignal = crossUp;
			sellSignal = crossDown;
		}
		else
		{
			buySignal = crossDown;
			sellSignal = crossUp;
		}

		var canTrade = IsWithinTradingHours(candle);

		if (!canTrade)
			return;

		if (StopAndReverse && Position != 0)
		{
			if ((_lastTrade == TradeDirections.Long && sellSignal) || (_lastTrade == TradeDirections.Short && buySignal))
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}

		if (Position != 0)
			return;

		var entryAllowed = !OneEntryPerBar || _lastEntryBar != candle.OpenTime;

		if (!entryAllowed)
			return;

		if (buySignal)
		{
			BuyMarket(Volume);
			SetProtectionLevels(candle.ClosePrice, true);
			_lastTrade = TradeDirections.Long;
			_lastEntryBar = candle.OpenTime;
		}
		else if (sellSignal)
		{
			SellMarket(Volume);
			SetProtectionLevels(candle.ClosePrice, false);
			_lastTrade = TradeDirections.Short;
			_lastEntryBar = candle.OpenTime;
		}
	}

	private void ManageExistingPosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetProtection();
			return;
		}

		UpdateTrailingStop(candle);

		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (PureSar || TrailingStop <= 0m || !_entryPrice.HasValue)
			return;

		var activationDistance = TrailingStop + TrailingStep;

		if (Position > 0)
		{
			if (candle.ClosePrice - _entryPrice.Value > activationDistance)
			{
				var activationLevel = candle.ClosePrice - activationDistance;
				if (!_stopPrice.HasValue || _stopPrice.Value < activationLevel)
				{
					var newStop = candle.ClosePrice - TrailingStop;
					_stopPrice = _stopPrice.HasValue ? Math.Max(_stopPrice.Value, newStop) : newStop;
				}
			}
		}
		else if (Position < 0)
		{
			if (_entryPrice.Value - candle.ClosePrice > activationDistance)
			{
				var activationLevel = candle.ClosePrice + activationDistance;
				if (!_stopPrice.HasValue || _stopPrice.Value > activationLevel)
				{
					var newStop = candle.ClosePrice + TrailingStop;
					_stopPrice = _stopPrice.HasValue ? Math.Min(_stopPrice.Value, newStop) : newStop;
				}
			}
		}
	}

	private bool IsWithinTradingHours(ICandleMessage candle)
	{
		if (!UseHourTrade)
			return true;

		var hour = candle.OpenTime.Hour;
		var start = StartHour;
		var end = EndHour;

		if (start <= end)
			return hour >= start && hour <= end;

		return hour >= start || hour <= end;
	}

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

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

	private void SetProtectionLevels(decimal entryPrice, bool isLong)
	{
		_entryPrice = entryPrice;

		if (PureSar)
		{
			_stopPrice = null;
			_takeProfitPrice = null;
			return;
		}

		var stop = StopLoss;
		var take = TakeProfit;

		_stopPrice = stop > 0m ? (isLong ? entryPrice - stop : entryPrice + stop) : null;
		_takeProfitPrice = take > 0m ? (isLong ? entryPrice + take : entryPrice - take) : null;
	}

	private void ResetProtection()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	private enum TradeDirections
	{
		None,
		Long,
		Short
	}
}