Открыть на GitHub

Стратегия EMA Cross 2

Обзор

Эта стратегия представляет собой перенос на StockSharp эксперта MetaTrader 4 «EMA_CROSS_2». Первоначальная версия следила за двумя экспоненциальными скользящими средними (EMA) и открывала сделки при смене их относительного положения. Конвертация сохраняет контртрендовый характер алгоритма: покупка выполняется, когда длинная EMA оказывается выше короткой, а продажа — когда короткая EMA закрывается выше длинной. Логика обёрнута в высокоуровневый каркас стратегий StockSharp.

Стратегия работает с завершёнными свечами выбранного типа. Сигналы анализируются на закрытии свечи, что исключает повторные срабатывания внутри одного бара. Модуль управления рисками воспроизводит поведение MetaTrader — уровни стоп-лосса, тейк-профита и трейлинг-стопа задаются в пунктах (price step).

Торговая логика

  1. Расчёт индикаторов
    • На каждой завершённой свече вычисляются короткая и длинная EMA.
    • Первая итерация пропускается в соответствии с флагом first_time исходного эксперта.
    • Далее фиксируется изменение направления, когда длинная и короткая EMA меняются местами.
  2. Интерпретация сигналов
    • Если длинная EMA поднимается выше короткой, формируется сигнал на покупку. Стратегия сохраняет эту контртрендовую трактовку, несмотря на то что она противоположна классическому пересечению средних.
    • Если короткая EMA закрывается выше длинной, отправляется рыночная заявка на продажу.
    • Новая позиция открывается только при отсутствии текущего экспозиции, что повторяет условие OrdersTotal() < 1 в MQL4.
  3. Исполнение ордеров
    • Заявки отправляются по рынку с фиксированным объёмом.
    • При входе фиксируются уровни стоп-лосса и тейк-профита, рассчитанные из параметров в пунктах.
  4. Управление рисками
    • После закрытия каждой свечи проверяется, достигла ли цена стоп-уровней. При пробое позиция немедленно закрывается рыночным ордером.
    • Трейлинг-стоп активируется, когда цена проходит в прибыльном направлении больше заданной дистанции. Для лонга защитный уровень подтягивается вверх, для шорта — опускается вниз.
    • При отсутствии позиции все защитные уровни очищаются.

Параметры

Имя Описание Значение по умолчанию
CandleType Тип свечей для расчёта индикаторов и поиска сигналов. Таймфрейм 15 минут
OrderVolume Объём рыночной заявки в лотах/контрактах. 2
TakeProfitPoints Дистанция до тейк-профита в пунктах (price step). 0 — без тейк-профита. 20
StopLossPoints Дистанция до стоп-лосса в пунктах. 0 — без стоп-лосса. 30
TrailingStopPoints Дистанция трейлинг-стопа в пунктах. 0 — трейлинг отключён. 50
ShortEmaPeriod Период короткой EMA. 5
LongEmaPeriod Период длинной EMA. 60

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

  • Для получения данных используется высокоуровневый вызов SubscribeCandles().Bind(shortEma, longEma, ProcessCandle), что избавляет от ручного управления буферами.
  • Значения индикаторов поступают в обработчик уже в виде decimal, поэтому не требуется обращаться к GetValue().
  • Все дистанции пересчитываются из пунктов MetaTrader в реальные цены умножением на PriceStep. Для инструментов с дробными пунктами (3 или 5 знаков) вспомогательная функция корректно определяет размер pip.
  • Стоп-лосс, тейк-профит и трейлинг реализованы через рыночные выходы, поскольку в StockSharp отсутствует прямой аналог OrderModify. При этом логика остаётся эквивалентной: уровни проверяются на каждой свече и при пробое позиция закрывается.
  • Первое пересечение игнорируется намеренно, чтобы повторить защиту от ложного старта в оригинальном коде.

Отличия от версии MetaTrader

  • Управление объёмом. В MQL4 торговля велась строго по параметру Lots. В перенесённой версии этот параметр представлен как OrderVolume и дополнительно присваивается свойству Volume стратегии для удобства оптимизации.
  • Регистрация заявок. MetaTrader выставлял стоп-уровни прямо в OrderSend. В StockSharp значения сохраняются внутри стратегии и приводят к рыночному выходу при достижении.
  • Точность трейлинга. В исходном эксперте уровни подтягивались по тиковой цене Bid/Ask. Здесь обновление происходит на закрытии свечи — это максимальная доступная частота в демонстрационном проекте. Правила активации остаются прежними.
  • Обработка ошибок. Вместо вывода кодов ошибок MetaTrader используется стандартный журнал StockSharp.

Рекомендации по использованию

  • Подбирайте CandleType в соответствии с тестовым или боевым таймфреймом, чтобы поведение EMA совпадало с оригиналом.
  • Для инструментов с дробными пунктами проверяйте, соответствует ли заданное количество Points желаемому числу пунктов (например, на EURUSD 10 points ≈ 1 pip).
  • Настройте OrderVolume под требования вашей торговой площадки — автоматического масштабирования нет.
  • Задействуйте встроенные флаги оптимизации у параметров, чтобы подбирать периоды EMA и дистанции риск-менеджмента аналогично оптимизации входных данных в MetaTrader.

Файлы

  • CS/EmaCross2Strategy.cs — исходный код стратегии.
  • README.md — документация на английском языке.
  • README_zh.md — документация на китайском языке.
  • README_ru.md — документация на русском языке (данный файл).
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>
/// Counter-trend EMA crossover strategy converted from the MetaTrader 4 expert "EMA_CROSS_2".
/// Buys when the long EMA rises above the short EMA, and sells when the short EMA climbs above the long EMA.
/// Incorporates MetaTrader-style risk management with point-based stop-loss, take-profit, and trailing stop levels.
/// </summary>
public class EmaCross2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<int> _shortEmaPeriod;
	private readonly StrategyParam<int> _longEmaPeriod;

	private ExponentialMovingAverage _shortEma;
	private ExponentialMovingAverage _longEma;

	private bool _skipFirstSignal = true;
	private int _lastDirection;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _pointSize;
	private decimal _entryPrice;

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

	/// <summary>
	/// Order volume applied to new market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in broker points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in broker points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in broker points.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Period of the short EMA.
	/// </summary>
	public int ShortEmaPeriod
	{
		get => _shortEmaPeriod.Value;
		set => _shortEmaPeriod.Value = value;
	}

	/// <summary>
	/// Period of the long EMA.
	/// </summary>
	public int LongEmaPeriod
	{
		get => _longEmaPeriod.Value;
		set => _longEmaPeriod.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public EmaCross2Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for EMA calculations", "General");

		_orderVolume = Param(nameof(OrderVolume), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume of each market order", "Trading")
		
		.SetOptimize(0.1m, 5m, 0.1m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Take Profit (points)", "Distance from entry to take-profit in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (points)", "Distance from entry to stop-loss in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (points)", "Trailing distance maintained after entry", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_shortEmaPeriod = Param(nameof(ShortEmaPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("Short EMA", "Length of the fast EMA", "Indicators")
		
		.SetOptimize(2, 40, 1);

		_longEmaPeriod = Param(nameof(LongEmaPeriod), 60)
		.SetGreaterThanZero()
		.SetDisplay("Long EMA", "Length of the slow EMA", "Indicators")
		
		.SetOptimize(10, 200, 5);
	}

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

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

		Volume = OrderVolume;
		_shortEma = null;
		_longEma = null;
		_skipFirstSignal = true;
		_lastDirection = 0;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_pointSize = 0m;
		_entryPrice = 0m;
	}

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

		Volume = OrderVolume;
		_pointSize = CalculatePointSize();

		_shortEma = new EMA { Length = ShortEmaPeriod };
		_longEma = new EMA { Length = LongEmaPeriod };

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

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

	private void ProcessCandle(ICandleMessage candle, decimal shortEmaValue, decimal longEmaValue)
	{
		// Work only with finished candles to avoid repeated signals inside the same bar.
		if (candle.State != CandleStates.Finished)
		return;

		if (_pointSize <= 0m)
		_pointSize = CalculatePointSize();

		if (CheckRisk(candle))
		return;

		if (Position != 0)
		UpdateTrailingStop(candle);
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		ResetRiskLevels();

		var signal = EvaluateCross(longEmaValue, shortEmaValue);

		if (signal == 0)
		return;

		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		if (Position != 0)
		return;

		var volume = OrderVolume;
		if (volume <= 0m)
		volume = 1m;

		if (signal == 1)
		{
			BuyMarket(volume);
			SetRiskLevels(candle.ClosePrice, true);
		}
		else if (signal == 2)
		{
			SellMarket(volume);
			SetRiskLevels(candle.ClosePrice, false);
		}
	}

	private int EvaluateCross(decimal longValue, decimal shortValue)
	{
		var currentDirection = 0;

		if (longValue > shortValue)
		currentDirection = 1;
		else if (longValue < shortValue)
		currentDirection = 2;

		if (_skipFirstSignal)
		{
			_skipFirstSignal = false;
			return 0;
		}

		if (currentDirection != 0 && currentDirection != _lastDirection)
		{
			_lastDirection = currentDirection;
			return _lastDirection;
		}

		return 0;
	}

	private bool CheckRisk(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (Position < 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		{
			ResetRiskLevels();
		}

		return false;
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m || _pointSize <= 0m)
		return;

		var distance = TrailingStopPoints * _pointSize;
		if (distance <= 0m)
		return;

		var entryPrice = _entryPrice > 0 ? _entryPrice : candle.ClosePrice;

		if (Position > 0)
		{
			var profit = candle.ClosePrice - entryPrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice - distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value < candidate)
				_stopLossPrice = candidate;
			}
		}
		else if (Position < 0)
		{
			var profit = entryPrice - candle.ClosePrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice + distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value > candidate)
				_stopLossPrice = candidate;
			}
		}
	}

	private void SetRiskLevels(decimal executionPrice, bool isLong)
	{
		if (_pointSize <= 0m)
		{
			ResetRiskLevels();
			return;
		}

		_stopLossPrice = StopLossPoints > 0m
		? executionPrice + (isLong ? -1m : 1m) * StopLossPoints * _pointSize
		: null;

		_takeProfitPrice = TakeProfitPoints > 0m
		? executionPrice + (isLong ? 1m : -1m) * TakeProfitPoints * _pointSize
		: null;
	}

	private void ResetRiskLevels()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

	private decimal CalculatePointSize()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}
}