Открыть на GitHub

Стратегия Straddle Trail v2.40

Straddle Trail v2.40 — это перенос на StockSharp советника MetaTrader 4 "Straddle&Trail" версии 2.40. Алгоритм выставляет симметричный стреддл из стоп-заявок перед важным событием, сопровождает активированную позицию переводом в безубыток и трейлинг-стопом, а также контролирует вручную открытые сделки.

Основной цикл работы

  1. Подготовка
    • Стратегия подписывается на стакан заявок, чтобы получать актуальные цены Bid/Ask, и на минутные свечи (тип задаётся параметром) для расчёта расписания.
    • На основании точности инструмента определяется размер пункта, поэтому все расстояния, заданные в пунктах, корректно преобразуются в цены.
  2. Выставление стреддла
    • За PreEventEntryMinutes минут до события (или сразу при включении параметра PlaceStraddleImmediately) выставляются Buy Stop и Sell Stop на расстоянии DistanceFromPrice пунктов от текущей цены.
    • Пока до события ещё есть время, при активном AdjustPendingOrders заявки каждую минуту переставляются ближе к рынку. За StopAdjustMinutes минут до события перестановки прекращаются.
  3. Управление ордерами
    • После срабатывания одной стороны при включённом RemoveOppositeOrder противоположный отложенный ордер удаляется, чтобы исключить двойное направление.
    • Параметры ShutdownNow и ShutdownOption позволяют в любой момент закрыть позиции и/или снять отложенные заявки.
  4. Защита позиции
    • Первоначальные уровни стоп-лосса и тейк-профита рассчитываются из параметров в пунктах.
    • Достижение триггера безубытка переносит стоп на BreakevenLockPips пунктов в прибыль.
    • Трейлинг-стоп может стартовать сразу либо только после безубытка (контролируется TrailAfterBreakeven).
    • Если ManageManualTrades включён, стратегия применяет эту же логику к вручную открытым позициям.

Параметры

Параметр Описание
ShutdownNow Запускает процедуру экстренного закрытия на следующей свече.
ShutdownOption Что закрывать: всё, только активированные сделки, только лонги, только шорты, все отложенные, только buy stop или только sell stop.
DistanceFromPrice Расстояние в пунктах между ценой и стоп-заявками стреддла.
StopLossPips Начальный стоп-лосс в пунктах.
TakeProfitPips Начальный тейк-профит в пунктах. Ноль отключает цель.
TrailPips Дистанция трейлинг-стопа в пунктах. Ноль отключает трейлинг.
TrailAfterBreakeven Включает трейлинг только после перевода в безубыток.
BreakevenLockPips Прибыль, фиксируемая после срабатывания безубытка.
BreakevenTriggerPips Порог прибыли в пунктах для перевода стопа в безубыток.
EventHour / EventMinute Время новости по времени брокера. Для отключения расписания установите оба параметра в 0.
PreEventEntryMinutes За сколько минут до события ставится стреддл.
StopAdjustMinutes За сколько минут до события переставлять заявки больше не нужно (минимум 1 минута).
RemoveOppositeOrder Удаляет противоположный отложенный ордер после срабатывания стреддла.
AdjustPendingOrders Переставляет стоп-заявки каждую минуту до наступления запретного окна.
PlaceStraddleImmediately Выставляет стреддл сразу после старта стратегии, игнорируя расписание.
ManageManualTrades Применяет перевод в безубыток и трейлинг к вручную открытым позициям.
CandleType Тип свечей, используемый для расчёта логики (по умолчанию минутки).

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

  • Убедитесь, что параметры инструмента корректно задают размер пункта, иначе значения в пунктах не совпадут с ценами.
  • Закрытие позиций выполняется рыночными ордерами при достижении условий стопа или цели, что повторяет подход оригинального советника.
  • Если расписание активно и PlaceStraddleImmediately выключен, стреддл выставляется один раз в день. Для повторной работы в тот же день перезапустите стратегию.
  • Функция аварийного выключения позволяет мгновенно снять риск и убрать отложенные заявки.

Детали конверсии

  • Все комментарии переведены на английский язык и дополнены пояснениями к логике.
  • Используются высокоуровневые методы StockSharp (BuyStop, SellStop, ClosePosition), что соответствует принятым практикам проекта.
  • Алгоритм работает на подписках свечей и стакана, не обращаясь к индикаторам напрямую, как требуют правила из AGENTS.md.
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Straddle breakout strategy converted from the MetaTrader 4 Straddle and Trail EA (v2.40).
/// Uses price channel breakouts with trailing stop management.
/// </summary>
public class StraddleTrailV240Strategy : Strategy
{
	private readonly StrategyParam<int> _channelPeriod;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _highs = new();
	private readonly List<decimal> _lows = new();

	/// <summary>
	/// Channel lookback period for breakout detection.
	/// </summary>
	public int ChannelPeriod
	{
		get => _channelPeriod.Value;
		set => _channelPeriod.Value = value;
	}

	/// <summary>
	/// Stop loss distance in absolute price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Take profit distance in absolute price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance.
	/// </summary>
	public StraddleTrailV240Strategy()
	{
		_channelPeriod = Param(nameof(ChannelPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Channel Period", "Lookback period for breakout levels", "Parameters");

		_stopLoss = Param(nameof(StopLoss), 500m)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop loss distance", "Risk");

		_takeProfit = Param(nameof(TakeProfit), 500m)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take profit distance", "Risk");

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

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_highs.Clear();
		_lows.Clear();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		var ema = new EMA { Length = 10 };

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

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

		var tp = TakeProfit > 0 ? new Unit(TakeProfit, UnitTypes.Absolute) : null;
		var sl = StopLoss > 0 ? new Unit(StopLoss, UnitTypes.Absolute) : null;
		StartProtection(tp, sl);

		base.OnStarted2(time);
	}

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

		_highs.Add(candle.HighPrice);
		_lows.Add(candle.LowPrice);

		while (_highs.Count > ChannelPeriod)
			_highs.RemoveAt(0);
		while (_lows.Count > ChannelPeriod)
			_lows.RemoveAt(0);

		if (_highs.Count < ChannelPeriod)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var upper = _highs.Take(_highs.Count - 1).Max();
		var lower = _lows.Take(_lows.Count - 1).Min();

		// Breakout above channel -> buy
		if (candle.ClosePrice > upper && Position <= 0)
		{
			if (Position < 0)
				BuyMarket(Math.Abs(Position));
			BuyMarket(Volume);
		}
		// Breakout below channel -> sell
		else if (candle.ClosePrice < lower && Position >= 0)
		{
			if (Position > 0)
				SellMarket(Position);
			SellMarket(Volume);
		}
	}

	/// <summary>
	/// Shutdown modes enum (preserved from original).
	/// </summary>
	public enum ShutdownModes
	{
		All = 0,
		TriggeredPositions = 1,
		TriggeredLong = 2,
		TriggeredShort = 3,
		PendingOrders = 4,
		PendingLong = 5,
		PendingShort = 6
	}
}