Ver en GitHub

Estrategia de Gestión de Trailing Straddle

Descripción general

La Estrategia de Gestión de Trailing Straddle replica el comportamiento del Expert Advisor original de MetaTrader 5 "Straddle&Trail". La estrategia coloca un par de órdenes stop (un straddle) alrededor del precio actual antes de eventos de noticias programados o inmediatamente a demanda. Una vez que se activa una posición, el algoritmo gestiona las transiciones de break-even, trailing stops y comandos opcionales de cierre que cancelan órdenes pendientes o cierran posiciones abiertas.

Esta implementación está construida sobre la API de alto nivel de StockSharp. La colocación de órdenes, la gestión de posiciones y los controles de riesgo se implementan sin usar procesamiento de mensajes de bajo nivel.

Lógica de trading

  1. Colocación del straddle

    • Se crean dos órdenes stop (buy stop por encima y sell stop por debajo) una vez que se alcanza la ventana del evento programado o instantáneamente si PlaceStraddleImmediately está habilitado.
    • Los precios de las órdenes se desplazan del bid/ask actual por DistanceFromPrice (expresado en pips). El desplazamiento se convierte en unidades de precio usando el paso de precio del instrumento.
    • La estrategia evita recrear el straddle múltiples veces en el mismo día a menos que las órdenes sean ajustadas o canceladas explícitamente.
  2. Gestión de órdenes pre-evento

    • Cuando AdjustPendingOrders está habilitado, las órdenes stop se cancelan y se recolocan cada minuto nuevo para que permanezcan alineadas con el precio actual.
    • Los ajustes se detienen StopAdjustMinutes antes del evento para evitar perseguir el precio cuando la volatilidad aumenta.
    • Si RemoveOppositeOrder está habilitado, la orden stop restante se cancela automáticamente una vez que un lado del straddle se activa y abre una posición.
  3. Gestión del riesgo

    • Los niveles iniciales de stop-loss y take-profit se calculan a partir de StopLossPips y TakeProfitPips y se rastrean internamente.
    • Cuando el beneficio abierto alcanza BreakevenTriggerPips, el nivel de stop se mueve al precio de entrada más BreakevenLockPips (o el valor simétrico para operaciones cortas).
    • Si TrailPips es mayor que cero, un trailing stop sigue el precio. El trailing puede comenzar inmediatamente o solo después de la condición de break-even dependiendo de TrailAfterBreakeven.
    • Las salidas por take-profit y stop se ejecutan con órdenes de mercado por fiabilidad.
  4. Cierre manual

    • Establecer ShutdownNow en true desencadena una limpieza inmediata según la opción ShutdownMode. Las acciones posibles incluyen cerrar posiciones largas/cortas y cancelar órdenes pendientes largas/cortas.

Parámetros

Parámetro Descripción
ShutdownNow Activa el procedimiento de cierre en la próxima actualización de vela. Se reinicia automáticamente a false tras la ejecución.
ShutdownMode Define qué se debe cancelar o cerrar (All, LongPositions, ShortPositions, PendingLong, PendingShort).
DistanceFromPrice Distancia entre el precio actual y cada orden stop, medida en pips.
StopLossPips Distancia inicial de stop-loss para posiciones activadas. Establecer en 0 para desactivar.
TakeProfitPips Distancia inicial de take-profit. Establecer en 0 para desactivar.
TrailPips Distancia del trailing stop. Establecer en 0 para desactivar el trailing.
TrailAfterBreakeven Cuando es true, el trailing comienza solo después de que se satisface la condición de break-even.
BreakevenLockPips Beneficio bloqueado cuando se activa el disparador de break-even.
BreakevenTriggerPips Umbral de beneficio que activa la lógica de break-even.
EventHour / EventMinute Hora del evento programado (hora del bróker/servidor). Establecer ambos en 0 para desactivar el programador de eventos.
PreEventEntryMinutes Minutos antes del evento cuando se debe colocar el straddle. Se ignora cuando el evento está desactivado o cuando la colocación inmediata está habilitada.
StopAdjustMinutes Número de minutos antes del evento cuando se detiene el ajuste automático de órdenes pendientes.
RemoveOppositeOrder Cancela la orden stop no ejecutada cuando se activa la primera parte del straddle.
AdjustPendingOrders Habilita el recentrado automático de órdenes pendientes mientras se espera el evento.
PlaceStraddleImmediately Coloca el straddle justo después de que la estrategia comienza, omitiendo el programador de eventos.
CandleType Suscripción de velas utilizada para el seguimiento del tiempo. Por defecto velas de 1 minuto.

Volumen – la propiedad Volume de StockSharp controla el tamaño de la orden. Está establecido en 1 por defecto y puede modificarse antes de iniciar la estrategia.

Suscripciones de datos

La estrategia se suscribe a:

  • La serie de velas configurada (por defecto 1 minuto) para ejecutar el programador, la lógica de trailing y las comprobaciones de cierre.
  • El libro de órdenes para mantener un seguimiento de los últimos precios bid/ask para la alineación precisa de órdenes stop.

Notas y limitaciones

  • La gestión de stop-loss y take-profit se ejecuta mediante órdenes de mercado en lugar de modificar órdenes de protección del lado del bróker. Esto refleja el comportamiento original manteniendo la implementación simple.
  • La estrategia usa el PriceStep del instrumento para aproximar el tamaño del pip. Para instrumentos exóticos, ajuste los parámetros en consecuencia.
  • El comando de cierre se evalúa solo cuando llegan nuevos datos de velas. Para una acción inmediata, reduzca el marco temporal de las velas.
  • La implementación en Python se omite intencionalmente según lo solicitado.

Notas de conversión

  • La lógica de break-even y trailing se porta línea por línea de la versión MQL. La versión StockSharp mantiene las mismas relaciones numéricas pero opera con precios decimales y usa salidas de mercado.
  • El manejo de trades manuales (magic number 0 en MQL) no se reproduce porque las estrategias de StockSharp gestionan sus propias posiciones. Toda la lógica de protección se aplica solo a los trades generados por la estrategia.
  • La función CalcMagic es innecesaria en StockSharp y por lo tanto fue eliminada. El estado de la estrategia es rastreado internamente por el framework.
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;
	}
}