Modifizierter OBV mit Divergenz-Erkennung
Diese Strategie glättet den On-Balance Volume (OBV) mit einem wählbaren gleitenden Durchschnitt und erzeugt eine Signallinie. Trades entstehen, wenn der geglättete OBV die Signallinie kreuzt. Zusätzlich protokolliert die Strategie reguläre und versteckte Divergenzen zwischen Preis und OBV mithilfe von Fraktal-Erkennung.
Details
- Einstiegskriterien: OBV-M kreuzt die Signallinie von unten/oben.
- Long/Short: Beide Richtungen.
- Ausstiegskriterien: Entgegengesetzter Kreuzungspunkt.
- Stops: Nein.
- Standardwerte:
MaType= ExponentialObvMaLength= 7SignalLength= 10CandleType= TimeSpan.FromMinutes(5)
- Filter:
- Kategorie: Divergenz
- Richtung: Beide
- Indikatoren: OBV, MA
- Stops: Nein
- Komplexität: Mittel
- Zeitrahmen: Intraday
- Saisonalität: Nein
- Neuronale Netze: Nein
- Divergenz: Ja
- Risikolevel: Mittel
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()