Открыть на GitHub

Стратегия Escape Mean Reversion

Обзор

Escape — это порт на StockSharp советника MetaTrader 4 escape.mq4. Оригинальный робот торгует пятиминутные свечи и пытается поймать возврат к среднему: покупает, когда цена закрытия опускается ниже короткой скользящей средней, и продаёт, когда закрытие поднимается выше другой быстрой средней. Каждая позиция защищается тейк-профитом и стоп-лоссом фиксированной ширины, задаваемой в пунктах MetaTrader. Версия на C# сохраняет минималистичную идею и выносит все ключевые дистанции в параметры стратегии.

Логика работы

  1. Инициализация

    • Выполняется подписка на серию CandleType (по умолчанию пятиминутные свечи).
    • Создаются две скользящие средние SimpleMovingAverage с периодами 5 и 4, которые питаются ценами открытия свечей.
    • Вычисляется эквивалент метатрейдеровского Point из Security.PriceStep; он используется для перевода дистанций в абсолютные цены.
  2. Обработка каждой свечи

    • Через SubscribeCandles(...).WhenCandlesFinished(ProcessCandle) обрабатываются только закрытые свечи.
    • Сначала стратегия проверяет, достигла ли текущая позиция стоп-лосса или тейк-профита, сравнивая максимум/минимум свечи с сохранёнными уровнями выхода. При пробое уровень закрывается рыночным ордером, а повторная отправка заявки блокируется внутренним флагом.
    • Если позиции нет, предыдущие значения обеих SMA получены, торговля разрешена, а капитал достаточен (Portfolio.CurrentValue >= MinimumMarginPerLot * TradeVolume), выполняется оценка сигналов:
      • Лонг — текущее закрытие ниже предыдущей SMA(5) от цен открытия.
      • Шорт — текущее закрытие выше предыдущей SMA(4) от цен открытия.
    • При срабатывании сигнала уровни стопа и тейка рассчитываются от цены закрытия и переводятся из пунктов MetaTrader в абсолютные цены.
  3. Управление риском

    • TradeVolume задаёт объём для всех рыночных ордеров.
    • MinimumMarginPerLot приближённо повторяет проверку AccountFreeMargin из MetaTrader. Если доступный капитал меньше требуемого, вход пропускается и пишется лог.

Параметры

Имя Значение по умолчанию Описание
LongTakeProfitPoints 10 Дистанция тейк-профита для лонгов в пунктах MetaTrader. Значение 0 отключает цель.
ShortTakeProfitPoints 10 Дистанция тейк-профита для шортов в пунктах MetaTrader. Значение 0 отключает цель.
LongStopLossPoints 1000 Дистанция стоп-лосса для лонгов в пунктах MetaTrader. Значение 0 отключает защитный стоп.
ShortStopLossPoints 1000 Дистанция стоп-лосса для шортов в пунктах MetaTrader. Значение 0 отключает защитный стоп.
TradeVolume 0.2 Объём рыночных ордеров.
MinimumMarginPerLot 500 Минимальный капитал на один лот перед открытием сделки.
CandleType Пятиминутный таймфрейм Серия свечей, на которой считаются индикаторы и формируются сигналы.

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

  • Индикаторы обновляются вручную внутри ProcessCandle ценами открытия, чтобы сохранённые значения соответствовали предыдущей свече (аналог shift=1 в вызове iMA).
  • Уровни выхода хранятся в полях типа decimal, дополнительных коллекций не создаётся, что соответствует требованиям высокоуровневого API.
  • Стопы и тейки сверяются с экстремумами свечи; поскольку доступен только OHLC, проверка стоп-лосса выполняется раньше тейк-профита, что приближает приоритет исполнения MetaTrader.
  • При наличии области графика стратегия рисует свечи, обе скользящие средние и собственные сделки для наглядного контроля.

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

  • В MetaTrader стоп и тейк прикрепляются напрямую к ордерам. В StockSharp они реализованы мониторингом максимумов/минимумов свечи и рыночным выходом; если внутри одной свечи сработают оба уровня, порядок исполнения может отличаться.
  • Цена входа берётся из цены закрытия сигнальной свечи, а не из точного Bid/Ask MetaTrader, поэтому спред и проскальзывание должны настраиваться в коннекторе.
  • Проверка AccountFreeMargin() заменена сравнением Portfolio.CurrentValue с требуемой суммой. При необходимости логику можно расширить.
  • Убраны косметические параметры оригинала (цвета, звук, настройка проскальзывания); C#-вариант сосредоточен на торговой части.
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Escape Mean Reversion: SMA crossover with ATR stops.
/// </summary>
public class EscapeMeanReversionStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _smaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevClose;
	private decimal _entryPrice;

	public EscapeMeanReversionStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_smaLength = Param(nameof(SmaLength), 5)
			.SetDisplay("SMA Length", "SMA period.", "Indicators");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int SmaLength
	{
		get => _smaLength.Value;
		set => _smaLength.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

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

		_prevClose = 0;
		_entryPrice = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_prevClose = 0;
		_entryPrice = 0;

		var sma = new SimpleMovingAverage { Length = SmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };

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

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

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

		var close = candle.ClosePrice;

		if (_prevClose == 0 || atrVal <= 0)
		{
			_prevClose = close;
			return;
		}

		if (Position > 0)
		{
			if (close >= _entryPrice + atrVal * 2m || close <= _entryPrice - atrVal * 1.5m || close > smaVal)
			{
				SellMarket();
				_entryPrice = 0;
			}
		}
		else if (Position < 0)
		{
			if (close <= _entryPrice - atrVal * 2m || close >= _entryPrice + atrVal * 1.5m || close < smaVal)
			{
				BuyMarket();
				_entryPrice = 0;
			}
		}

		if (Position == 0)
		{
			if (close < smaVal && _prevClose >= smaVal)
			{
				_entryPrice = close;
				BuyMarket();
			}
			else if (close > smaVal && _prevClose <= smaVal)
			{
				_entryPrice = close;
				SellMarket();
			}
		}

		_prevClose = close;
	}
}