OBV: средневозврат по наклону
Стратегия OBV Slope Mean Reversion сосредоточена на экстремальных значениях индикатора объёма по балансу (OBV), чтобы использовать возврат к среднему. Широкие отклонения от нормального уровня редко продолжаются долго.
Сделки открываются, когда индикатор значительно отклоняется от своего среднего значения и начинает разворачиваться. Вход в длинные и короткие позиции сопровождается защитным стопом.
Подходит свинг‑трейдерам, ожидающим колебаний; стратегия закрывает позицию, когда OBV возвращается к равновесию. Начальное значение ObvSmaPeriod = 20.
Детали
- Условия входа: Индикатор разворачивается в сторону среднего значения.
- Длинные/Короткие: Оба направления.
- Условия выхода: Индикатор возвращается к среднему.
- Стопы: Да.
- Значения по умолчанию:
ObvSmaPeriod= 20LookbackPeriod= 20DeviationMultiplier= 2.0mStopLossPercent= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Фильтры:
- Категория: Средневозвратная
- Направление: Оба
- Индикаторы: OBV
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Краткосрочный
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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>
/// OBV slope mean reversion strategy.
/// Trades reversion of extreme OBV slope values with an EMA direction filter.
/// </summary>
public class ObvSlopeMeanReversionStrategy : Strategy
{
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _thresholdMultiplier;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private OnBalanceVolume _obv;
private ExponentialMovingAverage _ema;
private decimal _previousObvValue;
private decimal[] _slopeHistory;
private int _currentIndex;
private int _filledCount;
private int _cooldown;
private bool _isInitialized;
/// <summary>
/// Lookback used to estimate slope mean and standard deviation.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Standard deviation multiplier for entry threshold.
/// </summary>
public decimal ThresholdMultiplier
{
get => _thresholdMultiplier.Value;
set => _thresholdMultiplier.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// EMA period.
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// Bars to wait between orders.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="ObvSlopeMeanReversionStrategy"/>.
/// </summary>
public ObvSlopeMeanReversionStrategy()
{
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Period for OBV slope statistics", "Strategy Parameters")
.SetOptimize(10, 50, 5);
_thresholdMultiplier = Param(nameof(ThresholdMultiplier), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Threshold Multiplier", "Standard deviation multiplier for entries", "Strategy Parameters")
.SetOptimize(1m, 3m, 0.5m);
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management");
_emaPeriod = Param(nameof(EmaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "Period for EMA direction filter", "Indicator Parameters");
_cooldownBars = Param(nameof(CooldownBars), 1200)
.SetRange(1, 5000)
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk Management");
_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();
_obv = null;
_ema = null;
_previousObvValue = default;
_slopeHistory = new decimal[LookbackPeriod];
_currentIndex = default;
_filledCount = default;
_cooldown = default;
_isInitialized = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_obv = new OnBalanceVolume();
_ema = new ExponentialMovingAverage { Length = EmaPeriod };
_slopeHistory = new decimal[LookbackPeriod];
_currentIndex = 0;
_filledCount = 0;
_cooldown = 0;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_obv, _ema, ProcessObv)
.Start();
StartProtection(new(), new Unit(StopLossPercent, UnitTypes.Percent));
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawIndicator(area, _obv);
DrawOwnTrades(area);
}
}
private void ProcessObv(ICandleMessage candle, decimal obvValue, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_obv.IsFormed || !_ema.IsFormed)
return;
if (!_isInitialized)
{
_previousObvValue = obvValue;
_isInitialized = true;
return;
}
var slope = obvValue - _previousObvValue;
_previousObvValue = obvValue;
_slopeHistory[_currentIndex] = slope;
_currentIndex = (_currentIndex + 1) % LookbackPeriod;
if (_filledCount < LookbackPeriod)
_filledCount++;
if (_filledCount < LookbackPeriod)
return;
CalculateStatistics(out var averageSlope, out var slopeStdDev);
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (slopeStdDev <= 0)
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var lowerThreshold = averageSlope - ThresholdMultiplier * slopeStdDev;
var upperThreshold = averageSlope + ThresholdMultiplier * slopeStdDev;
var priceAboveEma = candle.ClosePrice >= emaValue;
var priceBelowEma = candle.ClosePrice <= emaValue;
if (Position == 0)
{
if (slope <= lowerThreshold && priceAboveEma)
{
BuyMarket();
_cooldown = CooldownBars;
}
else if (slope >= upperThreshold && priceBelowEma)
{
SellMarket();
_cooldown = CooldownBars;
}
}
else if (Position > 0)
{
if (slope >= averageSlope || priceBelowEma)
{
SellMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
}
else if (Position < 0)
{
if (slope <= averageSlope || priceAboveEma)
{
BuyMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
}
}
private void CalculateStatistics(out decimal averageSlope, out decimal slopeStdDev)
{
averageSlope = 0m;
var sumSquaredDiffs = 0m;
for (var i = 0; i < LookbackPeriod; i++)
averageSlope += _slopeHistory[i];
averageSlope /= LookbackPeriod;
for (var i = 0; i < LookbackPeriod; i++)
{
var diff = _slopeHistory[i] - averageSlope;
sumSquaredDiffs += diff * diff;
}
slopeStdDev = (decimal)Math.Sqrt((double)(sumSquaredDiffs / LookbackPeriod));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan, Math
from StockSharp.Messages import DataType, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Indicators import OnBalanceVolume, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class obv_slope_mean_reversion_strategy(Strategy):
"""
OBV slope mean reversion strategy.
Trades reversion of extreme OBV slope values with an EMA direction filter.
"""
def __init__(self):
super(obv_slope_mean_reversion_strategy, self).__init__()
self._lookback_period = self.Param("LookbackPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Lookback Period", "Period for OBV slope statistics", "Strategy Parameters")
self._threshold_multiplier = self.Param("ThresholdMultiplier", 1.5) \
.SetGreaterThanZero() \
.SetDisplay("Threshold Multiplier", "Standard deviation multiplier for entries", "Strategy Parameters")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management")
self._ema_period = self.Param("EmaPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("EMA Period", "Period for EMA direction filter", "Indicator Parameters")
self._cooldown_bars = self.Param("CooldownBars", 1200) \
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._obv = None
self._ema = None
self._previous_obv_value = 0.0
self._slope_history = None
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
self._is_initialized = False
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(obv_slope_mean_reversion_strategy, self).OnReseted()
self._obv = None
self._ema = None
self._previous_obv_value = 0.0
lb = int(self._lookback_period.Value)
self._slope_history = [0.0] * lb
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
self._is_initialized = False
def OnStarted2(self, time):
super(obv_slope_mean_reversion_strategy, self).OnStarted2(time)
lb = int(self._lookback_period.Value)
self._slope_history = [0.0] * lb
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
self._obv = OnBalanceVolume()
self._ema = ExponentialMovingAverage()
self._ema.Length = int(self._ema_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._obv, self._ema, self._process_candle).Start()
self.StartProtection(Unit(), Unit(self._stop_loss_percent.Value, UnitTypes.Percent))
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ema)
self.DrawIndicator(area, self._obv)
self.DrawOwnTrades(area)
def _process_candle(self, candle, obv_value, ema_value):
if candle.State != CandleStates.Finished:
return
if not self._obv.IsFormed or not self._ema.IsFormed:
return
ov = float(obv_value)
ev = float(ema_value)
if not self._is_initialized:
self._previous_obv_value = ov
self._is_initialized = True
return
slope = ov - self._previous_obv_value
self._previous_obv_value = ov
lb = int(self._lookback_period.Value)
self._slope_history[self._current_index] = slope
self._current_index = (self._current_index + 1) % lb
if self._filled_count < lb:
self._filled_count += 1
if self._filled_count < lb:
return
avg_slope = 0.0
for i in range(lb):
avg_slope += self._slope_history[i]
avg_slope /= float(lb)
sum_sq = 0.0
for i in range(lb):
diff = self._slope_history[i] - avg_slope
sum_sq += diff * diff
std_slope = math.sqrt(sum_sq / float(lb))
if not self.IsFormedAndOnlineAndAllowTrading():
return
if std_slope <= 0:
return
if self._cooldown > 0:
self._cooldown -= 1
return
tm = float(self._threshold_multiplier.Value)
lower_threshold = avg_slope - tm * std_slope
upper_threshold = avg_slope + tm * std_slope
close_price = float(candle.ClosePrice)
price_above_ema = close_price >= ev
price_below_ema = close_price <= ev
if self.Position == 0:
if slope <= lower_threshold and price_above_ema:
self.BuyMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif slope >= upper_threshold and price_below_ema:
self.SellMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position > 0:
if slope >= avg_slope or price_below_ema:
self.SellMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position < 0:
if slope <= avg_slope or price_above_ema:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
def CreateClone(self):
return obv_slope_mean_reversion_strategy()