Historical Volatility Ratio Strategy
Strategy based on the Historical Volatility Ratio (HVR). It compares short-term volatility over 6 bars to long-term volatility over 100 bars using log returns. When the ratio rises above the threshold, the system goes long expecting volatility expansion. When it falls below the threshold, the system goes short.
Details
- Entry Criteria:
- Long:
HVR > RatioThreshold - Short:
HVR < RatioThreshold
- Long:
- Long/Short: Both
- Exit Criteria: Opposite signal
- Stops: No
- Default Values:
ShortPeriod= 6LongPeriod= 100RatioThreshold= 1.0CandleType=TimeSpan.FromMinutes(15).TimeFrame()
- Filters:
- Category: Volatility
- Direction: Both
- Indicators: Historical volatility (short and long)
- Stops: No
- Complexity: Intermediate
- Timeframe: Intraday
- 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>
/// Strategy based on the Historical Volatility Ratio (HVR).
/// Compares short-term volatility against long-term volatility.
/// Buys when short-term vol exceeds long-term, sells when below.
/// </summary>
public class HvrStrategy : Strategy
{
private readonly StrategyParam<int> _shortPeriod;
private readonly StrategyParam<int> _longPeriod;
private readonly StrategyParam<decimal> _ratioThreshold;
private readonly StrategyParam<DataType> _candleType;
private StandardDeviation _shortSd;
private StandardDeviation _longSd;
private decimal? _prevClose;
public int ShortPeriod { get => _shortPeriod.Value; set => _shortPeriod.Value = value; }
public int LongPeriod { get => _longPeriod.Value; set => _longPeriod.Value = value; }
public decimal RatioThreshold { get => _ratioThreshold.Value; set => _ratioThreshold.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public HvrStrategy()
{
_shortPeriod = Param(nameof(ShortPeriod), 6)
.SetGreaterThanZero()
.SetDisplay("Short HV Period", "Bars for short-term volatility", "Parameters");
_longPeriod = Param(nameof(LongPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Long HV Period", "Bars for long-term volatility", "Parameters");
_ratioThreshold = Param(nameof(RatioThreshold), 1m)
.SetDisplay("Ratio Threshold", "HVR level for trade direction", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for calculation", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_shortSd = default;
_longSd = default;
_prevClose = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevClose = null;
_shortSd = new StandardDeviation { Length = ShortPeriod };
_longSd = new StandardDeviation { Length = LongPeriod };
Indicators.Add(_shortSd);
Indicators.Add(_longSd);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_prevClose is not decimal prevClose || prevClose <= 0)
{
_prevClose = candle.ClosePrice;
return;
}
var logReturn = (decimal)Math.Log((double)(candle.ClosePrice / prevClose));
_prevClose = candle.ClosePrice;
var shortResult = _shortSd.Process(logReturn, candle.OpenTime, true);
var longResult = _longSd.Process(logReturn, candle.OpenTime, true);
if (!shortResult.IsFormed || !longResult.IsFormed)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var shortVal = shortResult.ToDecimal();
var longVal = longResult.ToDecimal();
if (longVal == 0)
return;
var ratio = shortVal / longVal;
if (ratio > RatioThreshold && Position <= 0)
{
if (Position < 0) BuyMarket();
BuyMarket();
}
else if (ratio < RatioThreshold && Position >= 0)
{
if (Position > 0) SellMarket();
SellMarket();
}
}
}
import clr
import math
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
from StockSharp.Algo.Indicators import StandardDeviation
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class hvr_strategy(Strategy):
def __init__(self):
super(hvr_strategy, self).__init__()
self._short_period = self.Param("ShortPeriod", 6) \
.SetDisplay("Short HV Period", "Bars for short-term volatility", "Parameters")
self._long_period = self.Param("LongPeriod", 100) \
.SetDisplay("Long HV Period", "Bars for long-term volatility", "Parameters")
self._ratio_threshold = self.Param("RatioThreshold", 1.0) \
.SetDisplay("Ratio Threshold", "HVR level for trade direction", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe used for calculation", "General")
self._short_sd = None
self._long_sd = None
self._prev_close = None
@property
def ShortPeriod(self):
return self._short_period.Value
@ShortPeriod.setter
def ShortPeriod(self, value):
self._short_period.Value = value
@property
def LongPeriod(self):
return self._long_period.Value
@LongPeriod.setter
def LongPeriod(self, value):
self._long_period.Value = value
@property
def RatioThreshold(self):
return self._ratio_threshold.Value
@RatioThreshold.setter
def RatioThreshold(self, value):
self._ratio_threshold.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(hvr_strategy, self).OnStarted2(time)
self._prev_close = None
self._short_sd = StandardDeviation()
self._short_sd.Length = self.ShortPeriod
self._long_sd = StandardDeviation()
self._long_sd.Length = self.LongPeriod
self.SubscribeCandles(self.CandleType) \
.Bind(self.ProcessCandle) \
.Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
if self._prev_close is None or self._prev_close <= 0:
self._prev_close = close
return
log_return = math.log(close / self._prev_close)
self._prev_close = close
t = candle.OpenTime
short_result = process_float(self._short_sd, log_return, t, True)
long_result = process_float(self._long_sd, log_return, t, True)
if not short_result.IsFormed or not long_result.IsFormed:
return
short_val = float(short_result)
long_val = float(long_result)
if long_val == 0:
return
ratio = short_val / long_val
threshold = float(self.RatioThreshold)
if ratio > threshold and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif ratio < threshold and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
def OnReseted(self):
super(hvr_strategy, self).OnReseted()
self._short_sd = None
self._long_sd = None
self._prev_close = None
def CreateClone(self):
return hvr_strategy()