Открыть на GitHub

Стратегия Straddle Trail

Описание

Стратегия Straddle Trail повторяет логику советника MetaTrader 5 "Straddle&Trail". Алгоритм выставляет пару стоп-заявок (стреддл) перед новостным событием либо сразу после запуска, а затем сопровождает открытую позицию переводом в безубыток, трейлинг-стопом и по команде может закрыть позиции или отменить отложенные заявки.

Реализация построена на высокоуровневом API StockSharp. Вся работа ведётся через стандартные методы Strategy без низкоуровневой обработки сообщений.

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

  1. Постановка стреддла

    • Buy Stop и Sell Stop размещаются при наступлении временного окна события или мгновенно, если включён параметр PlaceStraddleImmediately.
    • Цены заявок отстоят от текущих Bid/Ask на DistanceFromPrice (в пипсах). Значение пересчитывается в абсолютную цену через шаг цены инструмента.
    • В течение одного торгового дня новая пара заявок не создаётся повторно, пока текущие ордера не будут отменены или скорректированы.
  2. Настройка перед событием

    • При активном AdjustPendingOrders обе заявки раз в минуту удаляются и выставляются заново, чтобы оставаться симметричными относительно рынка.
    • Корректировка прекращается за StopAdjustMinutes минут до события, чтобы избежать гонки за ценой в момент роста волатильности.
    • Параметр RemoveOppositeOrder удаляет противоположную стоп-заявку после срабатывания первой половины стреддла.
  3. Сопровождение позиции

    • Первичные стоп-лосс и тейк-профит рассчитываются по StopLossPips и TakeProfitPips и отслеживаются внутри стратегии.
    • При достижении прибыли BreakevenTriggerPips стоп переносится в безубыток с фиксацией BreakevenLockPips.
    • Трейлинг-стоп (TrailPips) может стартовать сразу или только после безубытка (TrailAfterBreakeven). Выходы выполняются рыночными заявками.
  4. Режим остановки

    • Флаг ShutdownNow инициирует процедуру остановки, тип которой задаётся параметром ShutdownMode (закрыть лонги, шорты, отменить отложенные заявки или всё сразу).

Параметры

Параметр Назначение
ShutdownNow Флаг запуска процедуры остановки на ближайшей свече. После выполнения автоматически сбрасывается.
ShutdownMode Что следует закрыть/отменить: All, LongPositions, ShortPositions, PendingLong, PendingShort.
DistanceFromPrice Расстояние от рынка до каждой стоп-заявки в пипсах.
StopLossPips Начальный стоп-лосс. 0 отключает стоп.
TakeProfitPips Начальный тейк-профит. 0 отключает тейк.
TrailPips Ширина трейлинг-стопа. 0 отключает трейлинг.
TrailAfterBreakeven Запуск трейлинга только после переноса в безубыток.
BreakevenLockPips Количество пипсов, фиксируемое после безубытка.
BreakevenTriggerPips Порог прибыли для активации безубытка.
EventHour / EventMinute Время новостного события по серверу. Обнуление обоих параметров отключает расписание.
PreEventEntryMinutes За сколько минут до события ставить стреддл. Игнорируется при мгновенной постановке.
StopAdjustMinutes За сколько минут до события прекратить корректировку заявок.
RemoveOppositeOrder Удалять оставшийся стоп после открытия позиции.
AdjustPendingOrders Поддерживать симметрию отложенных заявок.
PlaceStraddleImmediately Ставить стреддл сразу после старта стратегии.
CandleType Тип свечей, используемых для расчётов (по умолчанию 1 минута).

Объём. Размер позиции задаётся свойством Volume стратегии (по умолчанию 1 контракт/лот).

Подписки на данные

  • Свечи выбранного таймфрейма для оценки времени, трейлинга и обработки команд.
  • Книга заявок для получения актуальных Bid/Ask и корректной расстановки стоп-заявок.

Особенности

  • Стопы и тейки исполняются рыночными ордерами, что упрощает портирование и повторяет оригинальный советник.
  • Для расчёта пипса используется PriceStep. Для инструментов с нестандартным шагом подберите параметры вручную.
  • Команда остановки проверяется только при приходе новой свечи. Для мгновенной реакции сократите таймфрейм.
  • Поддержка ручных сделок (магик 0 в MQL) не реализована — стратегия управляет только собственными позициями.
  • Версия на Python по требованию не создавалась.

Примечания по конверсии

  • Логика перевода в безубыток и трейлинг-стоп полностью повторяет исходный код MQL, но реализована через внутренние переменные и рыночные выходы.
  • Система магических номеров заменена внутренним учётом заявок StockSharp, что избавляет от необходимости генерировать уникальные идентификаторы.
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>
/// Strategy that simulates a straddle approach: defines upper/lower breakout levels
/// from a consolidation range (ATR-based) and enters on breakouts with trailing stop.
/// </summary>
public class StraddleTrailStrategy : Strategy
{
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _atrMultiplier;
	private readonly StrategyParam<decimal> _stopLossMult;
	private readonly StrategyParam<decimal> _takeProfitMult;
	private readonly StrategyParam<decimal> _trailMult;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _entryPrice;
	private decimal? _stopLevel;
	private decimal? _takeLevel;
	private int _barsSinceEntry;
	private int _cooldownCounter;

	public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
	public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
	public decimal StopLossMult { get => _stopLossMult.Value; set => _stopLossMult.Value = value; }
	public decimal TakeProfitMult { get => _takeProfitMult.Value; set => _takeProfitMult.Value = value; }
	public decimal TrailMult { get => _trailMult.Value; set => _trailMult.Value = value; }
	public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public StraddleTrailStrategy()
	{
		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR calculation length", "ATR");

		_atrMultiplier = Param(nameof(AtrMultiplier), 2.5m)
			.SetGreaterThanZero()
			.SetDisplay("ATR Multiplier", "Breakout distance multiplier", "ATR");

		_stopLossMult = Param(nameof(StopLossMult), 1.0m)
			.SetGreaterThanZero()
			.SetDisplay("SL Multiplier", "Stop loss as ATR multiple", "Risk");

		_takeProfitMult = Param(nameof(TakeProfitMult), 3.0m)
			.SetGreaterThanZero()
			.SetDisplay("TP Multiplier", "Take profit as ATR multiple", "Risk");

		_trailMult = Param(nameof(TrailMult), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("Trail Multiplier", "Trailing distance as ATR multiple", "Risk");

		_cooldownBars = Param(nameof(CooldownBars), 6)
			.SetGreaterThanZero()
			.SetDisplay("Cooldown", "Bars to wait after exit", "General");

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

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_entryPrice = 0;
		_stopLevel = null;
		_takeLevel = null;
		_barsSinceEntry = 0;
		_cooldownCounter = 0;
	}

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

		var atr = new AverageTrueRange { Length = AtrPeriod };
		var sma = new SimpleMovingAverage { Length = 20 };

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

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

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var close = candle.ClosePrice;

		// Manage existing position
		if (Position != 0)
		{
			_barsSinceEntry++;

			if (Position > 0)
			{
				// Trail stop up
				var newTrail = close - TrailMult * atr;
				if (_stopLevel == null || newTrail > _stopLevel)
					_stopLevel = newTrail;

				// Check stop or take
				if (close <= _stopLevel || (_takeLevel != null && close >= _takeLevel))
				{
					SellMarket(Math.Abs(Position));
					ResetPosition();
					return;
				}
			}
			else
			{
				// Trail stop down
				var newTrail = close + TrailMult * atr;
				if (_stopLevel == null || newTrail < _stopLevel)
					_stopLevel = newTrail;

				// Check stop or take
				if (close >= _stopLevel || (_takeLevel != null && close <= _takeLevel))
				{
					BuyMarket(Math.Abs(Position));
					ResetPosition();
					return;
				}
			}

			return;
		}

		// Cooldown after exit
		if (_cooldownCounter > 0)
		{
			_cooldownCounter--;
			return;
		}

		// Entry: breakout above/below SMA + ATR distance
		var upperLevel = sma + AtrMultiplier * atr;
		var lowerLevel = sma - AtrMultiplier * atr;

		if (close > upperLevel)
		{
			BuyMarket();
			_entryPrice = close;
			_stopLevel = close - StopLossMult * atr;
			_takeLevel = close + TakeProfitMult * atr;
			_barsSinceEntry = 0;
		}
		else if (close < lowerLevel)
		{
			SellMarket();
			_entryPrice = close;
			_stopLevel = close + StopLossMult * atr;
			_takeLevel = close - TakeProfitMult * atr;
			_barsSinceEntry = 0;
		}
	}

	private void ResetPosition()
	{
		_entryPrice = 0;
		_stopLevel = null;
		_takeLevel = null;
		_barsSinceEntry = 0;
		_cooldownCounter = CooldownBars;
	}
}