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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class opening_range_breakout2_strategy(Strategy):
"""
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.
"""
def __init__(self):
super(opening_range_breakout2_strategy, self).__init__()
self._range_start = self.Param("RangeStart", TimeSpan(9, 30, 0)).SetDisplay("Range Start", "Opening range start time (UTC)", "Session")
self._range_end = self.Param("RangeEnd", TimeSpan(10, 15, 0)).SetDisplay("Range End", "Opening range end time (UTC)", "Session")
self._day_end = self.Param("DayEnd", TimeSpan(15, 45, 0)).SetDisplay("Day End", "Time when all positions are closed (UTC)", "Session")
self._min_range_percent = self.Param("MinRangePercent", 0.35).SetNotNegative().SetDisplay("Min Range %", "Minimum range width as percent of close", "Trading")
self._reward_risk = self.Param("RewardRisk", 1.1).SetGreaterThanZero().SetDisplay("Reward/Risk", "Target distance in multiples of the stop distance", "Risk")
self._retrace = self.Param("Retrace", 0.5).SetGreaterThanZero().SetDisplay("Retrace", "Stop distance as a fraction of the range width", "Risk")
self._one_trade_per_day = self.Param("OneTradePerDay", True).SetDisplay("One Trade Per Day", "Take at most one breakout per day", "Trading")
self._reverse_on_loss = self.Param("ReverseOnLoss", False).SetDisplay("Reverse On Loss", "Reverse the position when the stop is hit", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._current_day = None
self._reset_day()
def _reset_day(self):
self._range_high = None
self._range_low = None
self._range_valid = False
self._traded_today = False
self._reversed_today = False
self._stop_price = Decimal(0)
self._take_price = Decimal(0)
def OnReseted(self):
super(opening_range_breakout2_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(opening_range_breakout2_strategy, self).OnStarted2(time)
self._reset_state()
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
open_time = candle.OpenTime.ToUniversalTime()
day = open_time.Date
tod = open_time.TimeOfDay.Ticks
range_start = self._range_start.Value.Ticks
range_end = self._range_end.Value.Ticks
day_end = self._day_end.Value.Ticks
if self._current_day is None or day != self._current_day:
self._current_day = day
self._reset_day()
if tod >= range_start and tod < range_end:
self._range_high = candle.HighPrice if self._range_high is None else max(self._range_high, candle.HighPrice)
self._range_low = candle.LowPrice if self._range_low is None else min(self._range_low, candle.LowPrice)
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if tod >= day_end:
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(-self.Position)
return
if tod < range_end or self._range_high is None or self._range_low is None:
return
high = self._range_high
low = self._range_low
rng = high - low
if not self._range_valid:
# The width filter is checked once, on the first candle after the window closes.
if rng <= 0 or rng < candle.ClosePrice * Decimal(self._min_range_percent.Value) / Decimal(100):
self._range_high = None
self._range_low = None
return
self._range_valid = True
stop_distance = rng * Decimal(self._retrace.Value)
reverse_on_loss = self._reverse_on_loss.Value
if self.Position > 0:
if candle.LowPrice <= self._stop_price:
if reverse_on_loss and not self._reversed_today:
self.SellMarket(self.Position + self.Volume)
self._set_levels(self._stop_price, stop_distance, False)
self._reversed_today = True
else:
self.SellMarket(self.Position)
elif candle.HighPrice >= self._take_price:
self.SellMarket(self.Position)
return
if self.Position < 0:
if candle.HighPrice >= self._stop_price:
if reverse_on_loss and not self._reversed_today:
self.BuyMarket(-self.Position + self.Volume)
self._set_levels(self._stop_price, stop_distance, True)
self._reversed_today = True
else:
self.BuyMarket(-self.Position)
elif candle.LowPrice <= self._take_price:
self.BuyMarket(-self.Position)
return
if self._one_trade_per_day.Value and self._traded_today:
return
break_up = candle.HighPrice > high
break_down = candle.LowPrice < low
# A candle through both boundaries gives no clear direction.
if break_up and not break_down:
self.BuyMarket(self.Volume)
self._set_levels(high, stop_distance, True)
self._traded_today = True
elif break_down and not break_up:
self.SellMarket(self.Volume)
self._set_levels(low, stop_distance, False)
self._traded_today = True
def _set_levels(self, entry, stop_distance, is_long):
take_distance = stop_distance * Decimal(self._reward_risk.Value)
self._stop_price = entry - stop_distance if is_long else entry + stop_distance
self._take_price = entry + take_distance if is_long else entry - take_distance
def CreateClone(self):
return opening_range_breakout2_strategy()