始値レンジ・ブレイクアウト戦略
この戦略は始値レンジを定義し、その上下のブレイクアウトを取引します。始値レンジ窓が閉じた後、レンジ幅が終値の一定割合を超えた場合、レンジの境界にストップ注文を準備します。ポジションにはレンジサイズに基づいたストップロスと利益目標を使用します。オプションで1日1取引のみに制限でき、損失取引は反転させることができます。すべてのポジションはセッション終了時にクローズされます。
詳細
- エントリー条件:
- ロング: 価格が始値レンジの高値を上抜け。
- ショート: 価格が始値レンジの安値を下抜け。
- ロング/ショート: 両方。
- エグジット条件:
- レンジに基づくストップロスまたは利益目標。
- 日中フラット(デイエンド)。
- ストップ: はい。
- デフォルト値:
始値レンジ= 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()