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>
/// On-Balance Volume slope mean reversion.
/// The slope of the smoothed OBV is compared with its average: buys when the slope is far below the average
/// and starts turning up, sells when it is far above and starts turning down.
/// Exits when the slope returns to its average.
/// </summary>
public class ObvSlopeMeanReversionStrategy : Strategy
{
private readonly StrategyParam<int> _obvSmaPeriod;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _deviationMultiplier;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _obvSma;
private SimpleMovingAverage _slopeAverage;
private StandardDeviation _slopeStdDev;
private decimal? _prevObv;
private decimal? _prevSlope;
/// <summary>
/// Period of the OBV moving average.
/// </summary>
public int ObvSmaPeriod
{
get => _obvSmaPeriod.Value;
set => _obvSmaPeriod.Value = value;
}
/// <summary>
/// Lookback period for slope statistics.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Standard deviation multiplier for extreme slope.
/// </summary>
public decimal DeviationMultiplier
{
get => _deviationMultiplier.Value;
set => _deviationMultiplier.Value = value;
}
/// <summary>
/// Stop-loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize <see cref="ObvSlopeMeanReversionStrategy"/>.
/// </summary>
public ObvSlopeMeanReversionStrategy()
{
_obvSmaPeriod = Param(nameof(ObvSmaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("OBV SMA Period", "Period of the OBV moving average", "Indicators");
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Period for slope statistics", "Strategy");
_deviationMultiplier = Param(nameof(DeviationMultiplier), 2.0m)
.SetGreaterThanZero()
.SetDisplay("Deviation Multiplier", "Standard deviation multiplier for extreme slope", "Strategy");
_stopLossPercent = Param(nameof(StopLossPercent), 2.0m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop-loss percentage", "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();
_obvSma = null;
_slopeAverage = null;
_slopeStdDev = null;
_prevObv = null;
_prevSlope = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var obv = new OnBalanceVolume();
_obvSma = new SimpleMovingAverage { Length = ObvSmaPeriod };
_slopeAverage = new SimpleMovingAverage { Length = LookbackPeriod };
_slopeStdDev = new StandardDeviation { Length = LookbackPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(obv, ProcessCandle)
.Start();
StartProtection(
takeProfit: null,
stopLoss: StopLossPercent > 0 ? new Unit(StopLossPercent, UnitTypes.Percent) : null
);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
var obvArea = CreateChartArea();
if (obvArea != null)
DrawIndicator(obvArea, obv);
}
}
private void ProcessCandle(ICandleMessage candle, decimal obvValue)
{
if (candle.State != CandleStates.Finished)
return;
var smoothed = _obvSma.Process(obvValue, candle.ServerTime, true).ToDecimal();
if (!_obvSma.IsFormed)
return;
if (_prevObv is not decimal prevObv)
{
_prevObv = smoothed;
return;
}
_prevObv = smoothed;
var slope = smoothed - prevObv;
var avgSlope = _slopeAverage.Process(slope, candle.ServerTime, true).ToDecimal();
var stdSlope = _slopeStdDev.Process(slope, candle.ServerTime, true).ToDecimal();
var prevSlope = _prevSlope;
_prevSlope = slope;
if (!_slopeAverage.IsFormed || !_slopeStdDev.IsFormed || prevSlope is not decimal prev)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Extreme reading that has started to turn back toward the average.
if (slope < avgSlope - DeviationMultiplier * stdSlope && slope > prev && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
}
else if (slope > avgSlope + DeviationMultiplier * stdSlope && slope < prev && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
}
else if (Position > 0 && slope >= avgSlope)
{
SellMarket(Position);
}
else if (Position < 0 && slope <= avgSlope)
{
BuyMarket(-Position);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import OnBalanceVolume, SimpleMovingAverage, StandardDeviation
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class obv_slope_mean_reversion_strategy(Strategy):
"""
On-Balance Volume slope mean reversion.
The slope of the smoothed OBV is compared with its average: buys when the slope is far below the average
and starts turning up, sells when it is far above and starts turning down.
Exits when the slope returns to its average.
"""
def __init__(self):
super(obv_slope_mean_reversion_strategy, self).__init__()
self._obv_sma_period = self.Param("ObvSmaPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("OBV SMA Period", "Period of the OBV moving average", "Indicators")
self._lookback_period = self.Param("LookbackPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Lookback Period", "Period for slope statistics", "Strategy")
self._deviation_multiplier = self.Param("DeviationMultiplier", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Deviation Multiplier", "Standard deviation multiplier for extreme slope", "Strategy")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetNotNegative() \
.SetDisplay("Stop Loss %", "Stop-loss percentage", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._obv_sma = None
self._slope_average = None
self._slope_std_dev = None
self._prev_obv = None
self._prev_slope = None
@property
def CandleType(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnReseted(self):
super(obv_slope_mean_reversion_strategy, self).OnReseted()
self._obv_sma = None
self._slope_average = None
self._slope_std_dev = None
self._prev_obv = None
self._prev_slope = None
def OnStarted2(self, time):
super(obv_slope_mean_reversion_strategy, self).OnStarted2(time)
obv = OnBalanceVolume()
self._obv_sma = SimpleMovingAverage()
self._obv_sma.Length = self._obv_sma_period.Value
self._slope_average = SimpleMovingAverage()
self._slope_average.Length = self._lookback_period.Value
self._slope_std_dev = StandardDeviation()
self._slope_std_dev.Length = self._lookback_period.Value
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(obv, self._process_candle).Start()
stop = float(self._stop_loss_percent.Value)
self.StartProtection(None, Unit(stop, UnitTypes.Percent) if stop > 0 else None)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
obv_area = self.CreateChartArea()
if obv_area is not None:
self.DrawIndicator(obv_area, obv)
def _process_candle(self, candle, obv_value):
if candle.State != CandleStates.Finished:
return
smoothed = float(process_float(self._obv_sma, obv_value, candle.ServerTime, True))
if not self._obv_sma.IsFormed:
return
if self._prev_obv is None:
self._prev_obv = smoothed
return
slope = smoothed - self._prev_obv
self._prev_obv = smoothed
avg_slope = float(process_float(self._slope_average, slope, candle.ServerTime, True))
std_slope = float(process_float(self._slope_std_dev, slope, candle.ServerTime, True))
prev = self._prev_slope
self._prev_slope = slope
if not self._slope_average.IsFormed or not self._slope_std_dev.IsFormed or prev is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
k = float(self._deviation_multiplier.Value)
# Extreme reading that has started to turn back toward the average.
if slope < avg_slope - k * std_slope and slope > prev and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif slope > avg_slope + k * std_slope and slope < prev and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
elif self.Position > 0 and slope >= avg_slope:
self.SellMarket(self.Position)
elif self.Position < 0 and slope <= avg_slope:
self.BuyMarket(-self.Position)
def CreateClone(self):
return obv_slope_mean_reversion_strategy()