开盘区间突破策略
该策略构建开盘区间,并在价格突破区间高低点时进行交易。开盘区间结束后,如果区间宽度超过收盘价的一定百分比,则在区间边界准备止损单。头寸的止损和止盈基于区间大小。可选择每天只进行一次初始交易,亏损交易可以反向。所有头寸在会话结束时平仓。
细节
- 入场条件:
- 做多:价格突破区间高点。
- 做空:价格跌破区间低点。
- 多空方向:双向。
- 出场条件:
- 基于区间的止损或止盈。
- 会话结束平仓。
- 止损:有。
- 默认值:
开盘区间= 09:30–10:15。收盘时间= 15:45。MinRangePercent= 0.35。RewardRisk= 1.1。Retrace= 0.5。
- 过滤器:
- 类别: 突破
- 方向: 双向
- 指标: 价格
- 止损: 有
- 复杂度: 中等
- 时间框架: 日内
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()