Volume Slope Mean Reversion
The Volume Slope Mean Reversion strategy focuses on extreme readings of the Volume to exploit reversion. Wide departures from the typical level rarely last.
Trades trigger when the indicator swings far from its mean and then begins to reverse. Both long and short setups include a protective stop.
Suited for swing traders expecting oscillations, the strategy closes out once the Volume returns toward balance. Starting parameter VolumeMaPeriod = 20.
Details
- Entry Criteria: Indicator crosses back toward mean.
- Long/Short: Both directions.
- Exit Criteria: Indicator reverts to average.
- Stops: Yes.
- Default Values:
VolumeMaPeriod= 20LookbackPeriod= 20DeviationMultiplier= 2.0mStopLossPercent= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Mean Reversion
- Direction: Both
- Indicators: Volume
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Short-term
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
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>
/// Volume slope mean reversion strategy.
/// Trades reversion of extreme volume-ratio slope values.
/// </summary>
public class VolumeSlopeMeanReversionStrategy : Strategy
{
private readonly StrategyParam<int> _volumeMaPeriod;
private readonly StrategyParam<int> _slopeLookback;
private readonly StrategyParam<decimal> _thresholdMultiplier;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _volumeAverage;
private decimal _previousVolumeRatio;
private decimal[] _slopeHistory;
private int _currentIndex;
private int _filledCount;
private int _cooldown;
private bool _isInitialized;
/// <summary>
/// Volume moving average period.
/// </summary>
public int VolumeMaPeriod
{
get => _volumeMaPeriod.Value;
set => _volumeMaPeriod.Value = value;
}
/// <summary>
/// Lookback used to estimate slope mean and standard deviation.
/// </summary>
public int SlopeLookback
{
get => _slopeLookback.Value;
set => _slopeLookback.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>
/// 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="VolumeSlopeMeanReversionStrategy"/>.
/// </summary>
public VolumeSlopeMeanReversionStrategy()
{
_volumeMaPeriod = Param(nameof(VolumeMaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Volume MA Period", "Period for the volume moving average", "Indicator Parameters");
_slopeLookback = Param(nameof(SlopeLookback), 20)
.SetGreaterThanZero()
.SetDisplay("Slope Lookback", "Period for slope statistics", "Strategy Parameters");
_thresholdMultiplier = Param(nameof(ThresholdMultiplier), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Threshold Multiplier", "Standard deviation multiplier for entries", "Strategy Parameters");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management");
_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();
_volumeAverage = null;
_previousVolumeRatio = default;
_slopeHistory = new decimal[SlopeLookback];
_currentIndex = default;
_filledCount = default;
_cooldown = default;
_isInitialized = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_volumeAverage = new SimpleMovingAverage
{
Length = VolumeMaPeriod,
Source = Level1Fields.Volume,
};
_slopeHistory = new decimal[SlopeLookback];
_currentIndex = 0;
_filledCount = 0;
_cooldown = 0;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_volumeAverage, ProcessCandle)
.Start();
StartProtection(new(), new Unit(StopLossPercent, UnitTypes.Percent));
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _volumeAverage);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal averageVolume)
{
if (candle.State != CandleStates.Finished)
return;
if (!_volumeAverage.IsFormed || averageVolume <= 0)
return;
var volumeRatio = candle.TotalVolume / averageVolume;
if (!_isInitialized)
{
_previousVolumeRatio = volumeRatio;
_isInitialized = true;
return;
}
var slope = volumeRatio - _previousVolumeRatio;
_previousVolumeRatio = volumeRatio;
_slopeHistory[_currentIndex] = slope;
_currentIndex = (_currentIndex + 1) % SlopeLookback;
if (_filledCount < SlopeLookback)
_filledCount++;
if (_filledCount < SlopeLookback)
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 isBullishCandle = candle.ClosePrice >= candle.OpenPrice;
var isBearishCandle = candle.ClosePrice <= candle.OpenPrice;
if (Position == 0)
{
if (slope <= lowerThreshold && isBullishCandle)
{
BuyMarket();
_cooldown = CooldownBars;
}
else if (slope >= upperThreshold && isBearishCandle)
{
SellMarket();
_cooldown = CooldownBars;
}
}
else if (Position > 0)
{
if (slope >= averageSlope || isBearishCandle)
{
SellMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
}
else if (Position < 0)
{
if (slope <= averageSlope || isBullishCandle)
{
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 < SlopeLookback; i++)
averageSlope += _slopeHistory[i];
averageSlope /= SlopeLookback;
for (var i = 0; i < SlopeLookback; i++)
{
var diff = _slopeHistory[i] - averageSlope;
sumSquaredDiffs += diff * diff;
}
slopeStdDev = (decimal)Math.Sqrt((double)(sumSquaredDiffs / SlopeLookback));
}
}
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, Level1Fields
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class volume_slope_mean_reversion_strategy(Strategy):
"""
Volume slope mean reversion strategy.
Trades reversion of extreme volume-ratio slope values.
"""
def __init__(self):
super(volume_slope_mean_reversion_strategy, self).__init__()
self._volume_ma_period = self.Param("VolumeMaPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Volume MA Period", "Period for the volume moving average", "Indicator Parameters")
self._slope_lookback = self.Param("SlopeLookback", 20) \
.SetGreaterThanZero() \
.SetDisplay("Slope Lookback", "Period for 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._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._volume_average = None
self._previous_volume_ratio = 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(volume_slope_mean_reversion_strategy, self).OnReseted()
self._volume_average = None
self._previous_volume_ratio = 0.0
lb = int(self._slope_lookback.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(volume_slope_mean_reversion_strategy, self).OnStarted2(time)
lb = int(self._slope_lookback.Value)
self._slope_history = [0.0] * lb
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
self._volume_average = SimpleMovingAverage()
self._volume_average.Length = int(self._volume_ma_period.Value)
self._volume_average.Source = Level1Fields.Volume
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._volume_average, 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._volume_average)
self.DrawOwnTrades(area)
def _process_candle(self, candle, average_volume):
if candle.State != CandleStates.Finished:
return
if not self._volume_average.IsFormed:
return
av = float(average_volume)
if av <= 0:
return
volume_ratio = float(candle.TotalVolume) / av
if not self._is_initialized:
self._previous_volume_ratio = volume_ratio
self._is_initialized = True
return
slope = volume_ratio - self._previous_volume_ratio
self._previous_volume_ratio = volume_ratio
lb = int(self._slope_lookback.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
is_bullish_candle = float(candle.ClosePrice) >= float(candle.OpenPrice)
is_bearish_candle = float(candle.ClosePrice) <= float(candle.OpenPrice)
if self.Position == 0:
if slope <= lower_threshold and is_bullish_candle:
self.BuyMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif slope >= upper_threshold and is_bearish_candle:
self.SellMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position > 0:
if slope >= avg_slope or is_bearish_candle:
self.SellMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position < 0:
if slope <= avg_slope or is_bullish_candle:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
def CreateClone(self):
return volume_slope_mean_reversion_strategy()