Ver en GitHub

Estrategia de Ruptura del Rango de Apertura

Esta estrategia define un rango de apertura y opera las rupturas por encima o por debajo de él. Tras el cierre de la ventana del rango de apertura, si la amplitud supera un porcentaje del precio de cierre, se preparan órdenes stop en los límites del rango. Las posiciones utilizan un stop loss y un objetivo de beneficio basados en el tamaño del rango. Opcionalmente solo se realiza una operación por día, y las operaciones perdedoras pueden revertirse. Todas las posiciones se cierran al final de la sesión.

Detalles

  • Criterios de entrada:
    • Largo: el precio rompe por encima del máximo del rango de apertura.
    • Corto: el precio rompe por debajo del mínimo del rango de apertura.
  • Largo/Corto: Ambos.
  • Criterios de salida:
    • Stop loss o take profit basado en el rango.
    • Cierre al final del día.
  • Stops: Sí.
  • Valores predeterminados:
    • Rango de apertura = 09:30–10:15.
    • Fin del día = 15:45.
    • MinRangePercent = 0.35.
    • RewardRisk = 1.1.
    • Retrace = 0.5.
  • Filtros:
    • Categoría: Ruptura
    • Dirección: Ambos
    • Indicadores: Precio
    • Stops: Sí
    • Complejidad: Moderado
    • Marco temporal: Intradía
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Opening range breakout strategy.
/// The high and low of the candles between RangeStart and RangeEnd form the opening range. After it closes, and only if its width
/// exceeds MinRangePercent of the close, a break above the high goes long and a break below the low goes short. The stop sits
/// Retrace times the range from the breakout level and the target RewardRisk times that distance. Optionally only one trade per
/// day is taken and a stopped-out trade reverses. Everything is flat at DayEnd.
/// </summary>
public class OpeningRangeBreakout2Strategy : Strategy
{
	private readonly StrategyParam<TimeSpan> _rangeStart;
	private readonly StrategyParam<TimeSpan> _rangeEnd;
	private readonly StrategyParam<TimeSpan> _dayEnd;
	private readonly StrategyParam<decimal> _minRangePercent;
	private readonly StrategyParam<decimal> _rewardRisk;
	private readonly StrategyParam<decimal> _retrace;
	private readonly StrategyParam<bool> _oneTradePerDay;
	private readonly StrategyParam<bool> _reverseOnLoss;
	private readonly StrategyParam<DataType> _candleType;

	private DateTime _currentDay;
	private decimal? _rangeHigh;
	private decimal? _rangeLow;
	private bool _rangeValid;
	private bool _tradedToday;
	private bool _reversedToday;
	private decimal _stopPrice;
	private decimal _takePrice;

	/// <summary>
	/// Opening range start time (UTC).
	/// </summary>
	public TimeSpan RangeStart
	{
		get => _rangeStart.Value;
		set => _rangeStart.Value = value;
	}

	/// <summary>
	/// Opening range end time (UTC).
	/// </summary>
	public TimeSpan RangeEnd
	{
		get => _rangeEnd.Value;
		set => _rangeEnd.Value = value;
	}

	/// <summary>
	/// Time when all positions are closed (UTC).
	/// </summary>
	public TimeSpan DayEnd
	{
		get => _dayEnd.Value;
		set => _dayEnd.Value = value;
	}

	/// <summary>
	/// Minimum range width as a percent of the close.
	/// </summary>
	public decimal MinRangePercent
	{
		get => _minRangePercent.Value;
		set => _minRangePercent.Value = value;
	}

	/// <summary>
	/// Target distance in multiples of the stop distance.
	/// </summary>
	public decimal RewardRisk
	{
		get => _rewardRisk.Value;
		set => _rewardRisk.Value = value;
	}

	/// <summary>
	/// Stop distance as a fraction of the range width.
	/// </summary>
	public decimal Retrace
	{
		get => _retrace.Value;
		set => _retrace.Value = value;
	}

	/// <summary>
	/// Take at most one breakout per day.
	/// </summary>
	public bool OneTradePerDay
	{
		get => _oneTradePerDay.Value;
		set => _oneTradePerDay.Value = value;
	}

	/// <summary>
	/// Reverse the position when the stop is hit.
	/// </summary>
	public bool ReverseOnLoss
	{
		get => _reverseOnLoss.Value;
		set => _reverseOnLoss.Value = value;
	}

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

	/// <summary>
	/// Constructor.
	/// </summary>
	public OpeningRangeBreakout2Strategy()
	{
		_rangeStart = Param(nameof(RangeStart), new TimeSpan(9, 30, 0))
			.SetDisplay("Range Start", "Opening range start time (UTC)", "Session");

		_rangeEnd = Param(nameof(RangeEnd), new TimeSpan(10, 15, 0))
			.SetDisplay("Range End", "Opening range end time (UTC)", "Session");

		_dayEnd = Param(nameof(DayEnd), new TimeSpan(15, 45, 0))
			.SetDisplay("Day End", "Time when all positions are closed (UTC)", "Session");

		_minRangePercent = Param(nameof(MinRangePercent), 0.35m)
			.SetNotNegative()
			.SetDisplay("Min Range %", "Minimum range width as percent of close", "Trading");

		_rewardRisk = Param(nameof(RewardRisk), 1.1m)
			.SetGreaterThanZero()
			.SetDisplay("Reward/Risk", "Target distance in multiples of the stop distance", "Risk");

		_retrace = Param(nameof(Retrace), 0.5m)
			.SetGreaterThanZero()
			.SetDisplay("Retrace", "Stop distance as a fraction of the range width", "Risk");

		_oneTradePerDay = Param(nameof(OneTradePerDay), true)
			.SetDisplay("One Trade Per Day", "Take at most one breakout per day", "Trading");

		_reverseOnLoss = Param(nameof(ReverseOnLoss), false)
			.SetDisplay("Reverse On Loss", "Reverse the position when the stop is hit", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");
	}

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

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

	private void ResetState()
	{
		_currentDay = default;
		ResetDay();
	}

	private void ResetDay()
	{
		_rangeHigh = null;
		_rangeLow = null;
		_rangeValid = false;
		_tradedToday = false;
		_reversedToday = false;
		_stopPrice = 0m;
		_takePrice = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		ResetState();

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

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

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

		var openTime = candle.OpenTime.ToUniversalTime();
		var day = openTime.Date;
		var tod = openTime.TimeOfDay;

		if (day != _currentDay)
		{
			_currentDay = day;
			ResetDay();
		}

		if (tod >= RangeStart && tod < RangeEnd)
		{
			_rangeHigh = _rangeHigh is decimal h ? Math.Max(h, candle.HighPrice) : candle.HighPrice;
			_rangeLow = _rangeLow is decimal l ? Math.Min(l, candle.LowPrice) : candle.LowPrice;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (tod >= DayEnd)
		{
			if (Position > 0)
				SellMarket(Position);
			else if (Position < 0)
				BuyMarket(-Position);

			return;
		}

		if (tod < RangeEnd || _rangeHigh is not decimal high || _rangeLow is not decimal low)
			return;

		var range = high - low;

		if (!_rangeValid)
		{
			// The width filter is checked once, on the first candle after the window closes.
			if (range <= 0 || range < candle.ClosePrice * MinRangePercent / 100m)
			{
				_rangeHigh = null;
				_rangeLow = null;
				return;
			}

			_rangeValid = true;
		}

		var stopDistance = range * Retrace;

		if (Position > 0)
		{
			if (candle.LowPrice <= _stopPrice)
			{
				if (ReverseOnLoss && !_reversedToday)
				{
					SellMarket(Position + Volume);
					SetLevels(_stopPrice, stopDistance, false);
					_reversedToday = true;
				}
				else
				{
					SellMarket(Position);
				}
			}
			else if (candle.HighPrice >= _takePrice)
			{
				SellMarket(Position);
			}

			return;
		}

		if (Position < 0)
		{
			if (candle.HighPrice >= _stopPrice)
			{
				if (ReverseOnLoss && !_reversedToday)
				{
					BuyMarket(-Position + Volume);
					SetLevels(_stopPrice, stopDistance, true);
					_reversedToday = true;
				}
				else
				{
					BuyMarket(-Position);
				}
			}
			else if (candle.LowPrice <= _takePrice)
			{
				BuyMarket(-Position);
			}

			return;
		}

		if (OneTradePerDay && _tradedToday)
			return;

		var breakUp = candle.HighPrice > high;
		var breakDown = candle.LowPrice < low;

		// A candle through both boundaries gives no clear direction.
		if (breakUp && !breakDown)
		{
			BuyMarket(Volume);
			SetLevels(high, stopDistance, true);
			_tradedToday = true;
		}
		else if (breakDown && !breakUp)
		{
			SellMarket(Volume);
			SetLevels(low, stopDistance, false);
			_tradedToday = true;
		}
	}

	private void SetLevels(decimal entry, decimal stopDistance, bool isLong)
	{
		var takeDistance = stopDistance * RewardRisk;

		_stopPrice = isLong ? entry - stopDistance : entry + stopDistance;
		_takePrice = isLong ? entry + takeDistance : entry - takeDistance;
	}
}