Открыть на GitHub

Стратегия Master Exit Plan

Обзор

MasterExitPlanStrategy переносит в StockSharp логику метатрейдеровского советника «Master Exit Plan». Стратегия не инициирует сделки – она управляет уже открытыми позициями: отслеживает несколько типов стопов, подтягивает отложенные заявки и закрывает все позиции при достижении заданной цели по капиталу.

Для воспроизведения вызовов iOpen(symbol, PERIOD_M1, 1) используется подписка на минутные свечи. Проверки выполняются раз в секунду через таймер стратегии, что соответствует EventSetTimer(1) в MQL4.

Возможности

  • Целевая прибыль по капиталу – закрывает все позиции при достижении заданного процента роста капитала.
  • Статические и динамические стопы – контролирует расстояние от цены входа и уровни, привязанные к открытию последней минутной свечи.
  • Скрытые стопы – закрывает позицию рыночными ордерами без выставления биржевых заявок.
  • Блок трейлинг-стопа – активируется после фиксации минимальной прибыли и учитывает актуальный спред.
  • Трейлинг отложенных заявок – переставляет buy stop и sell stop ближе к рынку.

Параметры

Имя Описание Значение по умолчанию
EnableTargetEquity Включить закрытие по целевой доходности. false
TargetEquityPercent Процент роста капитала для срабатывания. 1
EnableStopLoss Активировать «жёсткий» стоп-лосс. false
StopLossPoints Расстояние жёсткого стопа в пунктах. 2000
EnableDynamicStopLoss Привязать стоп к последней минутной свече. false
DynamicStopLossPoints Дистанция динамического стопа (пункты). 2000
EnableHiddenStopLoss Включить скрытый статический стоп. false
HiddenStopLossPoints Дистанция скрытого статического стопа. 800
EnableHiddenDynamicStopLoss Включить скрытый динамический стоп. false
HiddenDynamicStopLossPoints Дистанция скрытого динамического стопа. 800
EnableTrailingStop Активировать блок трейлинг-стопа. false
TrailingStopPoints Дистанция трейлинга (пункты). 5
TrailingTargetPercent Минимальный процент прибыли для активации. 0.2
SureProfitPoints Дополнительный запас пунктов перед включением трейлинга. 30
EnableTrailPendingOrders Переставлять активные stop-заявки. false
TrailPendingOrderPoints Отступ для трейлинга отложенных ордеров. 10

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

  1. Подключите стратегию к инструменту, где позиции открываются внешними модулями или вручную. Поле Volume должно соответствовать объёму, который следует закрывать при срабатывании защитных правил.
  2. Портфель должен предоставлять Portfolio.CurrentValue. Это значение заменяет AccountBalance и AccountEquity из MetaTrader. Если оценка капитала недоступна, блок закрытия по цели не работает.
  3. Для расчёта спреда стратегия использует лучшие котировки, поэтому необходимы данные уровня Level1.
  4. Все защитные действия выполняются рыночными заявками. Биржевые стоп-приказы не выставляются, что повторяет «скрытый» характер оригинального советника.

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

  • Вместо OrderModify стопы реализованы через постоянный контроль и принудительное закрытие позиции по рынку.
  • Динамические уровни рассчитываются по последней завершённой минутной свече из подписки SubscribeCandles.
  • Трейлинг отложенных ордеров не затрагивает внешние защитные заявки – стратегия их не создаёт.
  • Для оценки капитала используется Portfolio.CurrentValue (резерв – Portfolio.BeginValue).

Тестирование

Автоматические тесты не поставляются. Перед реальной торговлей прогоните стратегию в тестере StockSharp на исторических данных выбранного инструмента.

namespace StockSharp.Samples.Strategies;

using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// Master Exit Plan strategy: EMA trend following with ATR-based trailing stop exit.
/// Enters on EMA crossover, exits when price retraces by ATR multiple.
/// </summary>
public class MasterExitPlanStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _atrMultiplier;

	private decimal _entryPrice;
	private decimal _trailStop;
	private bool _wasBullish;
	private bool _hasTrendState;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
	public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }

	public MasterExitPlanStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_fastPeriod = Param(nameof(FastPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Fast EMA period", "Indicators");
		_slowPeriod = Param(nameof(SlowPeriod), 60)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Slow EMA period", "Indicators");
		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR period for trailing stop", "Risk");
		_atrMultiplier = Param(nameof(AtrMultiplier), 3m)
			.SetDisplay("ATR Multiplier", "ATR multiplier for trailing distance", "Risk");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_entryPrice = 0;
		_trailStop = 0;
		_wasBullish = false;
		_hasTrendState = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_entryPrice = 0;
		_trailStop = 0;
		_wasBullish = false;
		_hasTrendState = false;
		var fastEma = new ExponentialMovingAverage { Length = FastPeriod };
		var slowEma = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(fastEma, slowEma, ProcessCandle).Start();
	}

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

		var close = candle.ClosePrice;
		var range = candle.HighPrice - candle.LowPrice;
		if (range <= 0) return;

		var trailDist = range * AtrMultiplier;
		var isBullish = fastValue > slowValue;
		var crossedUp = _hasTrendState && !_wasBullish && isBullish;
		var crossedDown = _hasTrendState && _wasBullish && !isBullish;

		if (Position > 0)
		{
			var newStop = close - trailDist;
			if (newStop > _trailStop)
				_trailStop = newStop;
			if (close < _trailStop)
			{
				SellMarket();
				_trailStop = 0;
				_entryPrice = 0;
				_wasBullish = isBullish;
				_hasTrendState = true;
				return;
			}
			else if (crossedDown)
			{
				SellMarket();
				_trailStop = 0;
				_entryPrice = 0;
				_wasBullish = isBullish;
				_hasTrendState = true;
				return;
			}
		}
		else if (Position < 0)
		{
			var newStop = close + trailDist;
			if (_trailStop == 0 || newStop < _trailStop)
				_trailStop = newStop;
			if (close > _trailStop)
			{
				BuyMarket();
				_trailStop = 0;
				_entryPrice = 0;
				_wasBullish = isBullish;
				_hasTrendState = true;
				return;
			}
			else if (crossedUp)
			{
				BuyMarket();
				_trailStop = 0;
				_entryPrice = 0;
				_wasBullish = isBullish;
				_hasTrendState = true;
				return;
			}
		}

		if (Position == 0)
		{
			if (crossedUp)
			{
				BuyMarket();
				_entryPrice = close;
				_trailStop = close - trailDist;
			}
			else if (crossedDown)
			{
				SellMarket();
				_entryPrice = close;
				_trailStop = close + trailDist;
			}
		}

		_wasBullish = isBullish;
		_hasTrendState = true;
	}
}