OBV Breakout 戦略
On-Balance Volume (OBV)は出来高を累積することで買い圧力と売り圧力を追跡します。この戦略は、価格が動きを確認している間、ルックバック期間内でOBVが高値を上抜けるか安値を下抜けるブレイクアウトを探します。
テストでは年平均リターン約178%を示しています。株式市場での運用に最も適しています。
OBVのブレイクアウトは強い関心を示しています。OBVが直前の最大値を超えればロング、最小値を割ればショートに入ります。OBVがその移動平均とクロスした時点でエグジットします。
これはボリュームモメンタムと価格アクションを組み合わせています。
詳細
- エントリー条件: OBVがルックバック期間内の最高値または最安値を超える。
- ロング/ショート: 両方向。
- エグジット条件: OBVがそのMAをクロスまたはストップ。
- ストップ: あり。
- デフォルト値:
LookbackPeriod= 20OBVMAPeriod= 20CandleType= TimeSpan.FromMinutes(5)
- フィルター:
- カテゴリ: ブレイクアウト
- 方向: 両方
- インジケーター: OBV, MA
- ストップ: はい
- 複雑さ: 中級
- 時間軸: イントラデイ
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 中
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Trades strict breaks of prior rolling OBV extrema with matching price direction.
/// Fully exits on any actual OBV/OBV-SMA crossing or actual-fill percent protection.
/// </summary>
public class ObvBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<int> _obvMaPeriod;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _priorObvs = new();
private SimpleMovingAverage _obvAverage;
private decimal? _previousPrice;
private decimal? _previousReadyObv;
private decimal _previousMean;
private Order _pendingOrder;
public int LookbackPeriod { get => _lookbackPeriod.Value; set => _lookbackPeriod.Value = value; }
public int OBVMAPeriod { get => _obvMaPeriod.Value; set => _obvMaPeriod.Value = value; }
public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public ObvBreakoutStrategy()
{
_lookbackPeriod = Param(nameof(LookbackPeriod), 20).SetGreaterThanZero()
.SetDisplay("Lookback Period", "Number of PRIOR finished OBV values in the breakout range", "Entry");
_obvMaPeriod = Param(nameof(OBVMAPeriod), 20).SetGreaterThanZero()
.SetDisplay("OBV MA Period", "Current-inclusive SMA length for OBV, not price", "Indicators")
.SetOptimize(10, 50, 10);
_stopLossPercent = Param(nameof(StopLossPercent), 2m).SetNotNegative()
.SetDisplay("Stop Loss (%)", "Actual-fill percent stop; zero disables it.", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
OrderRegistering += order => _pendingOrder = order;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ClearSignalState();
_obvAverage = null;
}
private void ClearSignalState()
{
_priorObvs.Clear();
_previousPrice = null;
_previousReadyObv = null;
_previousMean = 0m;
_pendingOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearSignalState();
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var obv = new OnBalanceVolume();
_obvAverage = new SimpleMovingAverage { Length = OBVMAPeriod };
Indicators.Add(_obvAverage);
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(obv, ProcessCandle, false).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before this callback, including between finished candles.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue obvValue)
{
if (candle.State != CandleStates.Finished)
return;
// The native SMA averages OBV itself, including warmup and pending-order bars.
var meanValue = _obvAverage.Process(obvValue);
var currentObv = obvValue.GetValue<decimal>();
var priorPrice = _previousPrice;
_previousPrice = candle.ClosePrice;
// Capture the PRIOR range before inserting the breakout candle.
var rangeReady = _priorObvs.Count == LookbackPeriod;
var upper = rangeReady ? _priorObvs.Max() : 0m;
var lower = rangeReady ? _priorObvs.Min() : 0m;
_priorObvs.Enqueue(currentObv);
while (_priorObvs.Count > LookbackPeriod)
_priorObvs.Dequeue();
if (!obvValue.Indicator.IsFormed || !_obvAverage.IsFormed)
return;
var mean = meanValue.GetValue<decimal>();
var upwardCross = _previousReadyObv is decimal up && up <= _previousMean && currentObv > mean;
var downwardCross = _previousReadyObv is decimal down && down >= _previousMean && currentObv < mean;
// Zero OBV/mean are valid values, not an uninitialized sentinel.
_previousReadyObv = currentObv;
_previousMean = mean;
if (!IsFormedAndOnlineAndAllowTrading() ||
_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position != 0m && (upwardCross || downwardCross))
{
if (Position > 0m) SellMarket(Position);
else BuyMarket(Math.Abs(Position));
}
else if (Position == 0m && rangeReady && priorPrice is decimal previousClose)
{
if (currentObv > upper && candle.ClosePrice > previousClose) BuyMarket(Volume);
else if (currentObv < lower && candle.ClosePrice < previousClose) SellMarket(Volume);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, OrderStates, Level1Fields
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Indicators import OnBalanceVolume, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class obv_breakout_strategy(Strategy):
"""
Trades strict breaks of prior rolling OBV extrema with matching price direction.
Fully exits on any actual OBV/OBV-SMA crossing or actual-fill percent protection.
"""
def __init__(self):
super(obv_breakout_strategy, self).__init__()
self._lookback_period = self.Param("LookbackPeriod", 20).SetGreaterThanZero().SetDisplay("Lookback Period", "Number of PRIOR finished OBV values in the breakout range", "Entry")
self._obv_ma_period = self.Param("OBVMAPeriod", 20).SetGreaterThanZero().SetDisplay("OBV MA Period", "Current-inclusive SMA length for OBV, not price", "Indicators")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss (%)", "Actual-fill percent stop; zero disables it.", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._prior_obvs = []
self._obv_average = None
self._previous_price = None
self._previous_ready_obv = None
self._previous_mean = Decimal.Zero
self._pending_order = None
self.OrderRegistering += self._track_pending
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
def _clear_signal_state(self):
self._prior_obvs = []
self._previous_price = None
self._previous_ready_obv = None
self._previous_mean = Decimal.Zero
self._pending_order = None
def OnReseted(self):
super(obv_breakout_strategy, self).OnReseted()
self._clear_signal_state()
self._obv_average = None
def OnStarted2(self, time):
super(obv_breakout_strategy, self).OnStarted2(time)
self._clear_signal_state()
self.StartProtection(Unit(), Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
obv = OnBalanceVolume()
self._obv_average = SimpleMovingAverage()
self._obv_average.Length = self._obv_ma_period.Value
self.Indicators.Add(self._obv_average)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(obv, self._process_candle, False).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between finished candles.
pass
def _process_candle(self, candle, obv_value):
if candle.State != CandleStates.Finished:
return
# Average OBV itself, including warmup and pending-order bars.
mean_value = self._obv_average.Process(obv_value)
current_obv = obv_value.GetValue[Decimal](None)
prior_price = self._previous_price
self._previous_price = candle.ClosePrice
# Capture the PRIOR range before inserting the breakout candle.
range_ready = len(self._prior_obvs) == self._lookback_period.Value
upper = max(self._prior_obvs) if range_ready else Decimal.Zero
lower = min(self._prior_obvs) if range_ready else Decimal.Zero
self._prior_obvs.append(current_obv)
while len(self._prior_obvs) > self._lookback_period.Value:
del self._prior_obvs[0]
if not obv_value.Indicator.IsFormed or not self._obv_average.IsFormed:
return
mean = mean_value.GetValue[Decimal](None)
upward_cross = self._previous_ready_obv is not None and self._previous_ready_obv <= self._previous_mean and current_obv > mean
downward_cross = self._previous_ready_obv is not None and self._previous_ready_obv >= self._previous_mean and current_obv < mean
# Zero OBV/mean are valid values, not an uninitialized sentinel.
self._previous_ready_obv = current_obv
self._previous_mean = mean
if not self.IsFormedAndOnlineAndAllowTrading() or (self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed)):
return
if self.Position != 0 and (upward_cross or downward_cross):
if self.Position > 0:
self.SellMarket(self.Position)
else:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and range_ready and prior_price is not None:
if current_obv > upper and candle.ClosePrice > prior_price:
self.BuyMarket(self.Volume)
elif current_obv < lower and candle.ClosePrice < prior_price:
self.SellMarket(self.Volume)
def CreateClone(self):
return obv_breakout_strategy()