Открыть на GitHub

Стратегия EMA (редакция barabashkakvn)

Конверсия экспертного советника MetaTrader 5 "EMA (barabashkakvn's edition)". Система торгует пересечение двух экспоненциальных средних, рассчитанных по медианной цене, и использует виртуальные уровни take-profit/stop-loss в пунктах. Сделка открывается только после подтверждённого пересечения и небольшого отката к экстремуму предыдущей свечи.

Суть подхода

  1. Отслеживаются EMA с периодами 5 и 10 (по медианной цене) на выбранном таймфрейме.
  2. При пересечении быстрой и медленной EMA формируется флаг сигнала вместо немедленного входа.
  3. Цена должна откатить на MoveBackPips пунктов от экстремума предыдущей свечи, при этом разница между EMA превышает 2 * pipSize.
  4. После выполнения условий открывается позиция в сторону пересечения.
  5. Управление позицией осуществляется виртуальными целями и стопами, измеряемыми в пунктах от цены входа.

Такое поведение полностью повторяет оригинал на MQL: советник ждал установки флага check, а затем требовал достаточного спреда EMA и отката цены относительно предыдущей свечи. Закрытие также идёт по "виртуальным" уровням, проверяя, достигли ли Bid/Ask заданных расстояний.

Индикаторы и данные

  • EMA(5) по медианной цене (High + Low) / 2.
  • EMA(10) по медианной цене.
  • Высота/минимум предыдущей завершённой свечи для расчёта отката.
  • Обработка ведётся по завершённым свечам источника CandleType.

Параметры

Параметр Значение по умолчанию Описание
OrderVolume 0.1 Объём сделки в лотах/контрактах.
VirtualProfitPips 5 Расстояние (в пунктах) от входа до виртуального take-profit.
MoveBackPips 3 Откат после пересечения, измеряется от экстремума предыдущей свечи.
StopLossPips 20 Расстояние (в пунктах) от входа до виртуального stop-loss.
PipSize 0.0001 Размер пункта в ценовых единицах. Нужно менять для инструментов с другой котировкой.
FastLength 5 Период быстрой EMA.
SlowLength 10 Период медленной EMA.
CandleType TimeFrame(1m) Тип свечей, используемых в расчётах.

Пипсовые параметры переводятся в ценовые расстояния через pipValue = PipSize. Если указать ноль или отрицательное значение, стратегия использует Security.PriceStep, если биржа предоставляет шаг цены.

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

Вход

  • Формирование сигнала: при любом пересечении EMA сохраняется флаг, но сделка ещё не открывается.
  • Шорт возможен при выполнении условий:
    • Флаг сигнала установлен.
    • SlowEMA - FastEMA > 2 * pipSize.
    • Максимум текущей свечи ≥ минимум предыдущей свечи + MoveBackPips * pipSize (цена откатила вверх от прежнего минимума).
  • Лонг возможен при выполнении условий:
    • Флаг сигнала установлен.
    • FastEMA - SlowEMA > 2 * pipSize.
    • Минимум текущей свечи ≤ максимум предыдущей свечи - MoveBackPips * pipSize (цена откатила вниз от прежнего максимума).

После открытия позиции флаг сбрасывается, чтобы исключить повторные входы.

Выход

Виртуальные уровни повторяют механику MQL и сравнивают экстремумы свечи с нужными дистанциями:

  • Длинная позиция:
    • Закрыть, если максимум свечи ≥ цена входа + VirtualProfitPips * pipSize.
    • Закрыть, если минимум свечи ≤ цена входа - StopLossPips * pipSize.
  • Короткая позиция:
    • Закрыть, если минимум свечи ≤ цена входа - VirtualProfitPips * pipSize.
    • Закрыть, если максимум свечи ≥ цена входа + StopLossPips * pipSize.

После выхода виртуальные уровни сбрасываются, стратегия ждёт нового пересечения.

Особенности реализации

  • Используется высокоуровневое подписывание на свечи (SubscribeCandles) и отрисовка EMA и сделок на необязательном графике.
  • Медианная цена вычисляется из High/Low, что полностью соответствует PRICE_MEDIAN в MetaTrader.
  • Флаг _hasCrossSignal воспроизводит переменную check, гарантируя торговлю только после пересечения и отката.
  • StartProtection() вызывается в OnStarted, что активирует встроенный мониторинг рисков даже при ручном закрытии сделок.
  • Все комментарии в коде написаны на английском, исходные буферы индикаторов напрямую не используются.

Советы по применению

  • Обязательно адаптируйте PipSize для инструментов с иной точкой котировки (JPY-пары, индексы, криптовалюты и т.д.).
  • Поскольку выходы ориентируются на экстремумы свечи, короткие таймфреймы (1–5 минут) лучше воспроизводят поведение оригинального тикового советника.
  • Параметры легко оптимизируются: периоды EMA, величины виртуальных уровней и отката.
  • Стратегия ведёт только одну позицию; внешние сделки по тому же инструменту могут нарушить расчёт виртуальных уровней.

Риски

  • Работа по закрытию свечей может не зафиксировать внутридневные касания виртуальных уровней; для точности используйте более мелкие интервалы.
  • Виртуальные стопы не выставляют реальные защитные заявки, поэтому при сбоях связи или проскальзывании убыток может превысить ожидаемый.
  • Как и любая стратегия на пересечениях EMA, система чувствительна к боковым участкам рынка; при необходимости добавляйте фильтры.
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>
/// EMA crossover strategy with virtual take profit and stop loss distances.
/// Converted from the MQL5 expert "EMA (barabashkakvn's edition)".
/// </summary>
public class EmaBarabashkakvnEditionStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _virtualProfitPips;
	private readonly StrategyParam<int> _moveBackPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<decimal> _pipSize;
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _slowEma;
	private bool _hasCrossSignal;
	private decimal? _prevFast;
	private decimal? _prevSlow;
	private decimal? _prevHigh;
	private decimal? _prevLow;
	private decimal? _entryPrice;
	private decimal? _virtualTarget;
	private decimal? _virtualStop;

	/// <summary>
	/// Order volume in lots.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Virtual take profit distance in pips.
	/// </summary>
	public int VirtualProfitPips
	{
		get => _virtualProfitPips.Value;
		set => _virtualProfitPips.Value = value;
	}

	/// <summary>
	/// Retracement distance after a crossover in pips.
	/// </summary>
	public int MoveBackPips
	{
		get => _moveBackPips.Value;
		set => _moveBackPips.Value = value;
	}

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

	/// <summary>
	/// Pip size in price units.
	/// </summary>
	public decimal PipSize
	{
		get => _pipSize.Value;
		set => _pipSize.Value = value;
	}

	/// <summary>
	/// Fast EMA length applied to median price.
	/// </summary>
	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length applied to median price.
	/// </summary>
	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of <see cref="EmaBarabashkakvnEditionStrategy"/>.
	/// </summary>
	public EmaBarabashkakvnEditionStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume in lots", "Trading")
			
			.SetOptimize(0.05m, 1m, 0.05m);

		_virtualProfitPips = Param(nameof(VirtualProfitPips), 5)
			.SetGreaterThanZero()
			.SetDisplay("Virtual Profit", "Take profit distance in pips", "Risk")
			
			.SetOptimize(2, 20, 1);

		_moveBackPips = Param(nameof(MoveBackPips), 3)
			.SetGreaterThanZero()
			.SetDisplay("Move Back", "Retracement after crossover in pips", "Entries")
			
			.SetOptimize(1, 10, 1);

		_stopLossPips = Param(nameof(StopLossPips), 20)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss", "Virtual stop loss distance in pips", "Risk")
			
			.SetOptimize(10, 60, 2);

		_pipSize = Param(nameof(PipSize), 0.0001m)
			.SetGreaterThanZero()
			.SetDisplay("Pip Size", "Instrument pip size in price units", "General");

		_fastLength = Param(nameof(FastLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Fast EMA length on median price", "Indicators")
			
			.SetOptimize(3, 15, 1);

		_slowLength = Param(nameof(SlowLength), 10)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Slow EMA length on median price", "Indicators")
			
			.SetOptimize(8, 40, 1);

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

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

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

		_fastEma = default;
		_slowEma = default;
		_hasCrossSignal = false;
		_prevFast = default;
		_prevSlow = default;
		_prevHigh = default;
		_prevLow = default;
		_entryPrice = default;
		_virtualTarget = default;
		_virtualStop = default;
	}

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

		_fastEma = new EMA { Length = FastLength };
		_slowEma = new EMA { Length = SlowLength };

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

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

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

		// Calculate median price as in the original expert (PRICE_MEDIAN).
		var medianPrice = (candle.HighPrice + candle.LowPrice) / 2m;

		// Update EMA values using the median price.
		var fastValue = _fastEma.Process(new DecimalIndicatorValue(_fastEma, medianPrice, candle.OpenTime) { IsFinal = true });
		var slowValue = _slowEma.Process(new DecimalIndicatorValue(_slowEma, medianPrice, candle.OpenTime) { IsFinal = true });

		if (!_fastEma.IsFormed || !_slowEma.IsFormed)
		{
			_prevHigh = candle.HighPrice;
			_prevLow = candle.LowPrice;
			_prevFast = fastValue.ToDecimal();
			_prevSlow = slowValue.ToDecimal();
			return;
		}

		var fast = fastValue.ToDecimal();
		var slow = slowValue.ToDecimal();

		if (_prevFast is decimal prevFast && _prevSlow is decimal prevSlow)
		{
			var bullishCross = prevFast <= prevSlow && fast > slow;
			var bearishCross = prevFast >= prevSlow && fast < slow;

			if (bullishCross || bearishCross)
				_hasCrossSignal = true;
		}

		_prevFast = fast;
		_prevSlow = slow;

		var pipValue = PipSize;
		if (pipValue <= 0m)
			pipValue = Security?.PriceStep ?? 0.0001m;

		var moveBackPrice = MoveBackPips * pipValue;
		var profitDistance = VirtualProfitPips * pipValue;
		var stopDistance = StopLossPips * pipValue;

		if (Position == 0 && _hasCrossSignal && _prevHigh is decimal prevHigh && _prevLow is decimal prevLow)
		{
			var bearishSpread = slow - fast;
			var bullishSpread = fast - slow;

			var bearishReady = bearishSpread > 2m * pipValue && candle.HighPrice >= prevLow + moveBackPrice;
			var bullishReady = bullishSpread > 2m * pipValue && candle.LowPrice <= prevHigh - moveBackPrice;

			if (bearishReady)
			{
				// Enter short after bearish cross and retracement above the previous low.
				_entryPrice = candle.ClosePrice;
				_virtualTarget = _entryPrice - profitDistance;
				_virtualStop = _entryPrice + stopDistance;
				SellMarket();
				_hasCrossSignal = false;
			}
			else if (bullishReady)
			{
				// Enter long after bullish cross and retracement below the previous high.
				_entryPrice = candle.ClosePrice;
				_virtualTarget = _entryPrice + profitDistance;
				_virtualStop = _entryPrice - stopDistance;
				BuyMarket();
				_hasCrossSignal = false;
			}
		}
		else if (Position != 0 && _entryPrice is decimal && _virtualTarget is decimal target && _virtualStop is decimal stop)
		{
			if (Position > 0)
			{
				// Long position: use high for profit target and low for stop.
				var hitTarget = candle.HighPrice >= target;
				var hitStop = candle.LowPrice <= stop;

				if (hitTarget || hitStop)
				{
					SellMarket();
					_hasCrossSignal = false;
					_entryPrice = null;
					_virtualTarget = null;
					_virtualStop = null;
				}
			}
			else if (Position < 0)
			{
				// Short position: use low for profit target and high for stop.
				var hitTarget = candle.LowPrice <= target;
				var hitStop = candle.HighPrice >= stop;

				if (hitTarget || hitStop)
				{
					BuyMarket();
					_hasCrossSignal = false;
					_entryPrice = null;
					_virtualTarget = null;
					_virtualStop = null;
				}
			}
		}

		if (Position == 0)
		{
			_entryPrice = null;
			_virtualTarget = null;
			_virtualStop = null;
		}

		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
	}
}