ダイバージェンス検出付き改良OBV戦略
この戦略は選択可能な移動平均でOn-Balance Volume(OBV)を平滑化し、シグナルラインを生成します。平滑化されたOBVがシグナルと交差したときに取引が発生します。さらに、フラクタル検出を使用して価格とOBV間の通常ダイバージェンスおよび隠れダイバージェンスを記録します。
詳細
- エントリー条件: OBV-Mがシグナルラインを上方/下方にクロスしたとき。
- ロング/ショート: 両方向。
- エグジット条件: 逆方向のクロスオーバー。
- ストップ: なし。
- デフォルト値:
MaType= ExponentialObvMaLength= 7SignalLength= 10CandleType= TimeSpan.FromMinutes(5)
- フィルター:
- カテゴリ: ダイバージェンス
- 方向: 両方
- インジケーター: OBV、MA
- ストップ: いいえ
- 複雑さ: 中級
- 時間軸: イントラデイ
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: はい
- リスクレベル: 中
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Modified OBV with divergence detection.
/// On-Balance Volume is smoothed by a selectable moving average (OBV-M) and a signal line is an EMA of OBV-M.
/// A cross of OBV-M above the signal goes long and a cross below goes short, reversing the opposite position.
/// Regular and hidden divergences between price and OBV-M are found with five-bar fractals and only logged.
/// </summary>
public class ModifiedObvWithDivergenceDetectionStrategy : Strategy
{
/// <summary>
/// Moving average types for OBV smoothing.
/// </summary>
public enum MaTypes
{
/// <summary>Simple moving average.</summary>
Simple,
/// <summary>Exponential moving average.</summary>
Exponential,
/// <summary>Weighted moving average.</summary>
Weighted,
/// <summary>Smoothed moving average.</summary>
Smoothed,
}
private readonly StrategyParam<MaTypes> _maType;
private readonly StrategyParam<int> _obvMaLength;
private readonly StrategyParam<int> _signalLength;
private readonly StrategyParam<DataType> _candleType;
private IIndicator _obvMa;
private ExponentialMovingAverage _signalMa;
private decimal? _prevObvm;
private decimal? _prevSignal;
private readonly decimal[] _oscWindow = new decimal[5];
private readonly decimal[] _highWindow = new decimal[5];
private readonly decimal[] _lowWindow = new decimal[5];
private int _windowCount;
private decimal? _lastTopOsc;
private decimal? _lastTopPrice;
private decimal? _lastBottomOsc;
private decimal? _lastBottomPrice;
/// <summary>
/// Moving average type used to smooth OBV.
/// </summary>
public MaTypes MaType
{
get => _maType.Value;
set => _maType.Value = value;
}
/// <summary>
/// Length of the OBV smoothing average.
/// </summary>
public int ObvMaLength
{
get => _obvMaLength.Value;
set => _obvMaLength.Value = value;
}
/// <summary>
/// Length of the signal line EMA.
/// </summary>
public int SignalLength
{
get => _signalLength.Value;
set => _signalLength.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public ModifiedObvWithDivergenceDetectionStrategy()
{
_maType = Param(nameof(MaType), MaTypes.Exponential)
.SetDisplay("MA Type", "Moving average type used to smooth OBV", "Indicators");
_obvMaLength = Param(nameof(ObvMaLength), 7)
.SetGreaterThanZero()
.SetDisplay("OBV MA Length", "Length of the OBV smoothing average", "Indicators");
_signalLength = Param(nameof(SignalLength), 10)
.SetGreaterThanZero()
.SetDisplay("Signal Length", "Length of the signal line EMA", "Indicators");
_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()
{
_prevObvm = null;
_prevSignal = null;
_windowCount = 0;
Array.Clear(_oscWindow);
Array.Clear(_highWindow);
Array.Clear(_lowWindow);
_lastTopOsc = null;
_lastTopPrice = null;
_lastBottomOsc = null;
_lastBottomPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var obv = new OnBalanceVolume();
_obvMa = CreateMa(MaType, ObvMaLength);
_signalMa = new ExponentialMovingAverage { Length = SignalLength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(obv, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
var oscillators = CreateChartArea();
if (oscillators != null)
{
DrawIndicator(oscillators, _obvMa);
DrawIndicator(oscillators, _signalMa);
}
}
}
private static IIndicator CreateMa(MaTypes type, int length)
{
return type switch
{
MaTypes.Simple => new SimpleMovingAverage { Length = length },
MaTypes.Weighted => new WeightedMovingAverage { Length = length },
MaTypes.Smoothed => new SmoothedMovingAverage { Length = length },
_ => new ExponentialMovingAverage { Length = length },
};
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue obvValue)
{
if (candle.State != CandleStates.Finished)
return;
var obvmValue = _obvMa.Process(obvValue);
if (!_obvMa.IsFormed)
return;
var obvm = obvmValue.ToDecimal();
var signalValue = _signalMa.Process(obvmValue);
DetectDivergence(candle, obvm);
if (!_signalMa.IsFormed)
return;
var signal = signalValue.ToDecimal();
var prevObvm = _prevObvm;
var prevSignal = _prevSignal;
_prevObvm = obvm;
_prevSignal = signal;
if (prevObvm is not decimal po || prevSignal is not decimal ps)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var crossUp = po <= ps && obvm > signal;
var crossDown = po >= ps && obvm < signal;
if (crossUp && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (crossDown && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
}
private void DetectDivergence(ICandleMessage candle, decimal obvm)
{
for (var i = 0; i < 4; i++)
{
_oscWindow[i] = _oscWindow[i + 1];
_highWindow[i] = _highWindow[i + 1];
_lowWindow[i] = _lowWindow[i + 1];
}
_oscWindow[4] = obvm;
_highWindow[4] = candle.HighPrice;
_lowWindow[4] = candle.LowPrice;
if (_windowCount < 5)
{
_windowCount++;
if (_windowCount < 5)
return;
}
// The fractal is confirmed two bars after its middle bar.
var mid = _oscWindow[2];
if (mid > _oscWindow[0] && mid > _oscWindow[1] && mid > _oscWindow[3] && mid > _oscWindow[4])
{
var price = _highWindow[2];
if (_lastTopOsc is decimal prevOsc && _lastTopPrice is decimal prevPrice)
{
if (price > prevPrice && mid < prevOsc)
LogInfo($"Regular bearish divergence at {candle.OpenTime}.");
else if (price < prevPrice && mid > prevOsc)
LogInfo($"Hidden bearish divergence at {candle.OpenTime}.");
}
_lastTopOsc = mid;
_lastTopPrice = price;
}
if (mid < _oscWindow[0] && mid < _oscWindow[1] && mid < _oscWindow[3] && mid < _oscWindow[4])
{
var price = _lowWindow[2];
if (_lastBottomOsc is decimal prevOsc && _lastBottomPrice is decimal prevPrice)
{
if (price < prevPrice && mid > prevOsc)
LogInfo($"Regular bullish divergence at {candle.OpenTime}.");
else if (price > prevPrice && mid < prevOsc)
LogInfo($"Hidden bullish divergence at {candle.OpenTime}.");
}
_lastBottomOsc = mid;
_lastBottomPrice = price;
}
}
}
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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import (OnBalanceVolume, SimpleMovingAverage, ExponentialMovingAverage,
WeightedMovingAverage, SmoothedMovingAverage)
from StockSharp.Algo.Strategies import Strategy
MA_SIMPLE = 0
MA_EXPONENTIAL = 1
MA_WEIGHTED = 2
MA_SMOOTHED = 3
class modified_obv_with_divergence_detection_strategy(Strategy):
"""
Modified OBV with divergence detection.
On-Balance Volume is smoothed by a selectable moving average (OBV-M) and a signal line is an EMA of OBV-M.
A cross of OBV-M above the signal goes long and a cross below goes short, reversing the opposite position.
Regular and hidden divergences between price and OBV-M are found with five-bar fractals and only logged.
"""
def __init__(self):
super(modified_obv_with_divergence_detection_strategy, self).__init__()
self._ma_type = self.Param("MaType", MA_EXPONENTIAL).SetDisplay("MA Type", "Moving average type used to smooth OBV (0 Simple, 1 Exponential, 2 Weighted, 3 Smoothed)", "Indicators")
self._obv_ma_length = self.Param("ObvMaLength", 7).SetGreaterThanZero().SetDisplay("OBV MA Length", "Length of the OBV smoothing average", "Indicators")
self._signal_length = self.Param("SignalLength", 10).SetGreaterThanZero().SetDisplay("Signal Length", "Length of the signal line EMA", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._obv_ma = None
self._signal_ma = None
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._prev_obvm = None
self._prev_signal = None
self._osc_window = []
self._high_window = []
self._low_window = []
self._last_top_osc = None
self._last_top_price = None
self._last_bottom_osc = None
self._last_bottom_price = None
def OnReseted(self):
super(modified_obv_with_divergence_detection_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(modified_obv_with_divergence_detection_strategy, self).OnStarted2(time)
self._reset_state()
obv = OnBalanceVolume()
self._obv_ma = self._create_ma(int(self._ma_type.Value), self._obv_ma_length.Value)
self._signal_ma = ExponentialMovingAverage()
self._signal_ma.Length = self._signal_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(obv, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, self._obv_ma)
self.DrawIndicator(oscillators, self._signal_ma)
def _create_ma(self, ma_type, length):
if ma_type == MA_SIMPLE:
ma = SimpleMovingAverage()
elif ma_type == MA_WEIGHTED:
ma = WeightedMovingAverage()
elif ma_type == MA_SMOOTHED:
ma = SmoothedMovingAverage()
else:
ma = ExponentialMovingAverage()
ma.Length = length
return ma
def _process_candle(self, candle, obv_value):
if candle.State != CandleStates.Finished:
return
obvm_value = self._obv_ma.Process(obv_value)
if not self._obv_ma.IsFormed:
return
obvm = float(obvm_value)
signal_value = self._signal_ma.Process(obvm_value)
self._detect_divergence(candle, obvm)
if not self._signal_ma.IsFormed:
return
signal = float(signal_value)
prev_obvm = self._prev_obvm
prev_signal = self._prev_signal
self._prev_obvm = obvm
self._prev_signal = signal
if prev_obvm is None or prev_signal is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
cross_up = prev_obvm <= prev_signal and obvm > signal
cross_down = prev_obvm >= prev_signal and obvm < signal
if cross_up and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif cross_down and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
def _detect_divergence(self, candle, obvm):
self._osc_window.append(obvm)
self._high_window.append(float(candle.HighPrice))
self._low_window.append(float(candle.LowPrice))
if len(self._osc_window) > 5:
self._osc_window.pop(0)
self._high_window.pop(0)
self._low_window.pop(0)
if len(self._osc_window) < 5:
return
# The fractal is confirmed two bars after its middle bar.
w = self._osc_window
mid = w[2]
if mid > w[0] and mid > w[1] and mid > w[3] and mid > w[4]:
price = self._high_window[2]
if self._last_top_osc is not None and self._last_top_price is not None:
if price > self._last_top_price and mid < self._last_top_osc:
self.LogInfo("Regular bearish divergence at {0}.".format(candle.OpenTime))
elif price < self._last_top_price and mid > self._last_top_osc:
self.LogInfo("Hidden bearish divergence at {0}.".format(candle.OpenTime))
self._last_top_osc = mid
self._last_top_price = price
if mid < w[0] and mid < w[1] and mid < w[3] and mid < w[4]:
price = self._low_window[2]
if self._last_bottom_osc is not None and self._last_bottom_price is not None:
if price < self._last_bottom_price and mid > self._last_bottom_osc:
self.LogInfo("Regular bullish divergence at {0}.".format(candle.OpenTime))
elif price > self._last_bottom_price and mid < self._last_bottom_osc:
self.LogInfo("Hidden bullish divergence at {0}.".format(candle.OpenTime))
self._last_bottom_osc = mid
self._last_bottom_price = price
def CreateClone(self):
return modified_obv_with_divergence_detection_strategy()