OBV 均值回归策略
OBV 指标跟踪累计成交量的流向,以判断买卖力量。本策略在 OBV 与其均值出现显著背离时寻找回归机会。
测试表明年均收益约为 79%,该策略在股票市场表现最佳。
当 OBV 低于均值减 Multiplier 倍标准差且价格在均线下方时买入;当 OBV 高于均值加同样倍数并且价格在均线上方时卖出。OBV 回到均值线附近即平仓。
该策略适合将成交量因素纳入考虑的交易者,百分比止损用于应对成交量持续扩大的情况。
详细信息
- 入场条件:
- 做多: OBV < Avg - Multiplier * StdDev && Close < MA
- 做空: OBV > Avg + Multiplier * StdDev && Close > MA
- 多空方向: 双向
- 退出条件:
- 做多: Exit when OBV > Avg
- 做空: Exit when OBV < Avg
- 止损: 是
- 默认值:
AveragePeriod= 20Multiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- 筛选条件:
- 类别: 均值回归
- 方向: 双向
- 指标: OBV
- 止损: 是
- 复杂度: 中等
- 时间框架: 日内
- 季节性: 否
- 神经网络: 否
- 背离: 否
- 风险等级: 中等
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// OBV Mean Reversion Strategy (244).
/// Enter when OBV deviates from its average by a certain multiple of standard deviation.
/// Exit when OBV returns to its average.
/// </summary>
public class ObvMeanReversionStrategy : Strategy
{
private readonly StrategyParam<int> _averagePeriod;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<DataType> _candleType;
private OnBalanceVolume _obv;
private SimpleMovingAverage _obvAverage;
private StandardDeviation _obvStdDev;
private decimal? _currentObv;
private decimal? _obvAvgValue;
private decimal? _obvStdDevValue;
/// <summary>
/// Period for OBV average calculation.
/// </summary>
public int AveragePeriod
{
get => _averagePeriod.Value;
set => _averagePeriod.Value = value;
}
/// <summary>
/// Standard deviation multiplier for entry.
/// </summary>
public decimal Multiplier
{
get => _multiplier.Value;
set => _multiplier.Value = value;
}
/// <summary>
/// Type of candles to use.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ObvMeanReversionStrategy"/>.
/// </summary>
public ObvMeanReversionStrategy()
{
_averagePeriod = Param(nameof(AveragePeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Average Period", "Period for OBV average calculation", "Strategy Parameters")
.SetOptimize(10, 30, 5);
_multiplier = Param(nameof(Multiplier), 2.0m)
.SetGreaterThanZero()
.SetDisplay("StdDev Multiplier", "Standard deviation multiplier for entry", "Strategy Parameters")
.SetOptimize(1.0m, 3.0m, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "Strategy Parameters");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_currentObv = default;
_obvAvgValue = default;
_obvStdDevValue = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators
_obv = new OnBalanceVolume();
_obvAverage = new SMA { Length = AveragePeriod };
_obvStdDev = new StandardDeviation { Length = AveragePeriod };
// Create candle subscription
var subscription = SubscribeCandles(CandleType);
// Create processing chain
subscription
.BindEx(_obv, ProcessObv)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _obv);
DrawOwnTrades(area);
}
// Enable position protection
StartProtection(
takeProfit: new Unit(5, UnitTypes.Percent),
stopLoss: new Unit(2, UnitTypes.Percent)
);
}
private void ProcessObv(ICandleMessage candle, IIndicatorValue obvValue)
{
if (candle.State != CandleStates.Finished)
return;
// Extract OBV value
_currentObv = obvValue.ToDecimal();
// Process OBV through average and standard deviation indicators
var avgIndicatorValue = _obvAverage.Process(obvValue);
var stdDevIndicatorValue = _obvStdDev.Process(obvValue);
_obvAvgValue = avgIndicatorValue.ToDecimal();
_obvStdDevValue = stdDevIndicatorValue.ToDecimal();
// Check if strategy is ready for trading
if (!IsFormedAndOnlineAndAllowTrading() || !_obvAverage.IsFormed || !_obvStdDev.IsFormed)
return;
// Ensure we have all needed values
if (!_currentObv.HasValue || !_obvAvgValue.HasValue || !_obvStdDevValue.HasValue)
return;
// Calculate bands
var upperBand = _obvAvgValue.Value + Multiplier * _obvStdDevValue.Value;
var lowerBand = _obvAvgValue.Value - Multiplier * _obvStdDevValue.Value;
LogInfo($"OBV: {_currentObv}, OBV Avg: {_obvAvgValue}, Upper: {upperBand}, Lower: {lowerBand}");
// Entry logic
if (Position == 0)
{
// Long Entry: OBV is below lower band (OBV oversold)
if (_currentObv.Value < lowerBand)
{
LogInfo($"Buy Signal - OBV ({_currentObv}) < Lower Band ({lowerBand})");
BuyMarket(Volume);
}
// Short Entry: OBV is above upper band (OBV overbought)
else if (_currentObv.Value > upperBand)
{
LogInfo($"Sell Signal - OBV ({_currentObv}) > Upper Band ({upperBand})");
SellMarket(Volume);
}
}
// Exit logic
else if (Position > 0 && _currentObv.Value > _obvAvgValue.Value)
{
// Exit Long: OBV returned to average
LogInfo($"Exit Long - OBV ({_currentObv}) > OBV Avg ({_obvAvgValue})");
SellMarket(Math.Abs(Position));
}
else if (Position < 0 && _currentObv.Value < _obvAvgValue.Value)
{
// Exit Short: OBV returned to average
LogInfo($"Exit Short - OBV ({_currentObv}) < OBV Avg ({_obvAvgValue})");
BuyMarket(Math.Abs(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, Math
from StockSharp.Messages import DataType, CandleStates, UnitTypes, Unit
from StockSharp.Algo.Indicators import OnBalanceVolume, SimpleMovingAverage, StandardDeviation
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class obv_mean_reversion_strategy(Strategy):
"""
OBV Mean Reversion Strategy (244).
Enter when OBV deviates from its average by a certain multiple of standard deviation.
Exit when OBV returns to its average.
"""
def __init__(self):
super(obv_mean_reversion_strategy, self).__init__()
# Initialize strategy parameters
self._average_period = self.Param("AveragePeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Average Period", "Period for OBV average calculation", "Strategy Parameters") \
.SetCanOptimize(True) \
.SetOptimize(10, 30, 5)
self._multiplier = self.Param("Multiplier", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("StdDev Multiplier", "Standard deviation multiplier for entry", "Strategy Parameters") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 3.0, 0.5)
self._candle_type = self.Param("CandleType", tf(5)) \
.SetDisplay("Candle Type", "Type of candles to use", "Strategy Parameters")
# Internal fields
self._obv = None
self._obv_average = None
self._obv_std_dev = None
self._current_obv = None
self._obv_avg_value = None
self._obv_std_dev_value = None
@property
def AveragePeriod(self):
"""Period for OBV average calculation."""
return self._average_period.Value
@AveragePeriod.setter
def AveragePeriod(self, value):
self._average_period.Value = value
@property
def Multiplier(self):
"""Standard deviation multiplier for entry."""
return self._multiplier.Value
@Multiplier.setter
def Multiplier(self, value):
self._multiplier.Value = value
@property
def CandleType(self):
"""Type of candles to use."""
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(obv_mean_reversion_strategy, self).OnReseted()
self._current_obv = None
self._obv_avg_value = None
self._obv_std_dev_value = None
def OnStarted2(self, time):
super(obv_mean_reversion_strategy, self).OnStarted2(time)
# Create indicators
self._obv = OnBalanceVolume()
self._obv_average = SimpleMovingAverage()
self._obv_average.Length = self.AveragePeriod
self._obv_std_dev = StandardDeviation()
self._obv_std_dev.Length = self.AveragePeriod
# Create candle subscription
subscription = self.SubscribeCandles(self.CandleType)
# Create processing chain
subscription.BindEx(self._obv, self.ProcessObv).Start()
# Setup chart visualization if available
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._obv)
self.DrawOwnTrades(area)
# Enable position protection
self.StartProtection(
takeProfit=Unit(5, UnitTypes.Percent),
stopLoss=Unit(2, UnitTypes.Percent)
)
def ProcessObv(self, candle, obv_value):
if candle.State != CandleStates.Finished:
return
# Extract OBV value
self._current_obv = float(obv_value)
# Process OBV through average and standard deviation indicators
avg_indicator_value = self._obv_average.Process(obv_value)
std_dev_indicator_value = self._obv_std_dev.Process(obv_value)
self._obv_avg_value = float(avg_indicator_value)
self._obv_std_dev_value = float(std_dev_indicator_value)
# Check if strategy is ready for trading
if not self.IsFormedAndOnlineAndAllowTrading() or not self._obv_average.IsFormed or not self._obv_std_dev.IsFormed:
return
# Ensure we have all needed values
if self._current_obv is None or self._obv_avg_value is None or self._obv_std_dev_value is None:
return
# Calculate bands
upper_band = self._obv_avg_value + self.Multiplier * self._obv_std_dev_value
lower_band = self._obv_avg_value - self.Multiplier * self._obv_std_dev_value
self.LogInfo("OBV: {0}, OBV Avg: {1}, Upper: {2}, Lower: {3}".format(
self._current_obv, self._obv_avg_value, upper_band, lower_band))
# Entry logic
if self.Position == 0:
# Long Entry: OBV is below lower band (OBV oversold)
if self._current_obv < lower_band:
self.LogInfo("Buy Signal - OBV ({0}) < Lower Band ({1})".format(self._current_obv, lower_band))
self.BuyMarket(self.Volume)
# Short Entry: OBV is above upper band (OBV overbought)
elif self._current_obv > upper_band:
self.LogInfo("Sell Signal - OBV ({0}) > Upper Band ({1})".format(self._current_obv, upper_band))
self.SellMarket(self.Volume)
# Exit logic
elif self.Position > 0 and self._current_obv > self._obv_avg_value:
# Exit Long: OBV returned to average
self.LogInfo("Exit Long - OBV ({0}) > OBV Avg ({1})".format(self._current_obv, self._obv_avg_value))
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0 and self._current_obv < self._obv_avg_value:
# Exit Short: OBV returned to average
self.LogInfo("Exit Short - OBV ({0}) < OBV Avg ({1})".format(self._current_obv, self._obv_avg_value))
self.BuyMarket(Math.Abs(self.Position))
def CreateClone(self):
"""
!! REQUIRED!! Creates a new instance of the strategy.
"""
return obv_mean_reversion_strategy()