feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings

This commit is contained in:
Miha Kralj
2026-03-09 13:45:46 -07:00
parent 8e43d62cbb
commit 031f1b5fe6
491 changed files with 6156 additions and 5590 deletions
+25 -2
View File
@@ -108,12 +108,35 @@ public class RviTests
}
[Fact]
public void Update_WithTBar_UsesClosePrice()
public void Update_WithTBar_UsesHighAndLow()
{
var rvi = new Rvi();
var bar = new TBar(DateTime.UtcNow, 98, 102, 97, 100, 1000);
var result = rvi.Update(bar);
Assert.Equal(50.0, result.Value, Tolerance); // First value
Assert.Equal(50.0, result.Value, Tolerance); // First value is always neutral
}
[Fact]
public void Update_WithTBar_RevisedDiffersFromOriginal()
{
// The revised RVI (high+low avg) should differ from original (close-only)
// Use oscillating close with asymmetric high/low
var rviBar = new Rvi(stdevLength: 5, rmaLength: 5);
var rviClose = new Rvi(stdevLength: 5, rmaLength: 5);
for (int i = 0; i < 50; i++)
{
var time = DateTime.UtcNow.AddSeconds(i);
double close = 100.0 + Math.Sin(i * 0.5) * 3.0; // oscillating
double high = close + 2.0 + Math.Sin(i * 0.3) * 1.5; // asymmetric highs
double low = close - 1.0 - Math.Cos(i * 0.7) * 0.8; // asymmetric lows
rviBar.Update(new TBar(time, close - 0.5, high, low, close, 1000));
rviClose.Update(new TValue(time, close));
}
// With asymmetric high/low, revised RVI should differ from close-only
Assert.NotEqual(rviBar.Last.Value, rviClose.Last.Value, 0.01);
}
[Fact]
+174 -251
View File
@@ -1,5 +1,5 @@
// Relative Volatility Index (RVI) Indicator
// Measures the direction of volatility using standard deviation and RMA smoothing
// Relative Volatility Index (RVI) Indicator — Revised (1995) version
// Averages original RVI computed on High and Low series separately
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -7,29 +7,23 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// RVI: Relative Volatility Index
/// Measures the direction of volatility by comparing upward and downward price movements
/// weighted by their standard deviations, smoothed with Wilder's RMA.
/// RVI: Relative Volatility Index (Revised)
/// Computes original RVI on the High series and on the Low series, then averages.
/// Each channel classifies stddev direction based on its own price change.
/// </summary>
/// <remarks>
/// <b>Calculation steps:</b>
/// <b>Calculation steps (per channel — High and Low independently):</b>
/// <list type="number">
/// <item>Calculate population standard deviation of prices over stdevLength</item>
/// <item>Calculate population standard deviation over stdevLength</item>
/// <item>Classify by price change: if up, upStd = stddev; if down, downStd = stddev</item>
/// <item>Smooth upStd and downStd with RMA (Wilder's smoothing with bias correction)</item>
/// <item>RVI = 100 × avgUpStd / (avgUpStd + avgDownStd)</item>
/// </list>
///
/// <b>Key characteristics:</b>
/// <list type="bullet">
/// <item>Oscillator ranging from 0 to 100</item>
/// <item>Values above 50 indicate upward volatility momentum</item>
/// <item>Values below 50 indicate downward volatility momentum</item>
/// <item>Often used to confirm RSI signals or as a standalone indicator</item>
/// <item>channelRVI = 100 × avgUpStd / (avgUpStd + avgDownStd)</item>
/// </list>
/// <b>Final:</b> RVI = (RVI_high + RVI_low) / 2
///
/// <b>Sources:</b>
/// Donald Dorsey (1993). "The Relative Volatility Index". Technical Analysis of Stocks &amp; Commodities.
/// Donald Dorsey (1993, original; 1995, revised). Technical Analysis of Stocks &amp; Commodities.
/// FM Labs: https://www.fmlabs.com/reference/RVI.htm
/// </remarks>
[SkipLocalsInit]
public sealed class Rvi : AbstractBase
@@ -39,10 +33,11 @@ public sealed class Rvi : AbstractBase
private readonly int _stdevLength;
private readonly int _rmaLength;
private readonly double _alpha;
private readonly RingBuffer _priceBuffer;
private readonly RingBuffer _hiBuf;
private readonly RingBuffer _loBuf;
[StructLayout(LayoutKind.Auto)]
private record struct State(
private record struct ChState(
double PrevPrice,
double Sum,
double SumSq,
@@ -50,46 +45,34 @@ public sealed class Rvi : AbstractBase
double EUp,
double RawRmaDown,
double EDown,
double LastValue,
int FillCount
);
private State _s;
private State _ps;
/// <summary>
/// Initializes a new instance of the Rvi class.
/// </summary>
/// <param name="stdevLength">The lookback period for standard deviation calculation (default 10).</param>
/// <param name="rmaLength">The lookback period for RMA smoothing (default 14).</param>
/// <exception cref="ArgumentException">
/// Thrown when stdevLength is less than 2, or rmaLength is less than 1.
/// </exception>
private ChState _hi, _phi;
private ChState _lo, _plo;
private double _lastValue, _pLastValue;
public Rvi(int stdevLength = 10, int rmaLength = 14)
{
if (stdevLength < 2)
{
throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
}
if (rmaLength < 1)
{
throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
}
_stdevLength = stdevLength;
_rmaLength = rmaLength;
_alpha = 1.0 / rmaLength;
_priceBuffer = new RingBuffer(stdevLength);
_hiBuf = new RingBuffer(stdevLength);
_loBuf = new RingBuffer(stdevLength);
WarmupPeriod = stdevLength + rmaLength;
Name = $"Rvi({stdevLength},{rmaLength})";
_s = new State(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 50.0, 0);
_ps = _s;
var init = new ChState(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 0);
_hi = _phi = init;
_lo = _plo = init;
_lastValue = _pLastValue = 50.0;
}
/// <summary>
/// Initializes a new instance of the Rvi class with a source.
/// </summary>
/// <param name="source">The data source for chaining.</param>
/// <param name="stdevLength">The lookback period for standard deviation calculation (default 10).</param>
/// <param name="rmaLength">The lookback period for RMA smoothing (default 14).</param>
public Rvi(ITValuePublisher source, int stdevLength = 10, int rmaLength = 14)
: this(stdevLength, rmaLength)
{
@@ -98,56 +81,27 @@ public sealed class Rvi : AbstractBase
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
/// <summary>
/// True if the indicator has enough data for valid results.
/// </summary>
public override bool IsHot => _s.FillCount >= _stdevLength;
public override bool IsHot => _hi.FillCount >= _stdevLength;
/// <summary>
/// The lookback period for standard deviation calculation.
/// </summary>
public int StdevLength => _stdevLength;
/// <summary>
/// The lookback period for RMA smoothing.
/// </summary>
public int RmaLength => _rmaLength;
/// <summary>
/// Updates the indicator with a new price value.
/// </summary>
/// <param name="input">The input price value.</param>
/// <param name="isNew">Whether this is a new bar or an update.</param>
/// <returns>The calculated RVI value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
return UpdateCore(input.Time, input.Value, isNew);
return UpdateCore(input.Time, input.Value, input.Value, isNew);
}
/// <summary>
/// Updates the indicator with a new bar (uses Close price).
/// </summary>
/// <param name="bar">The input bar.</param>
/// <param name="isNew">Whether this is a new bar or an update.</param>
/// <returns>The calculated RVI value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
return UpdateCore(bar.Time, bar.Close, isNew);
return UpdateCore(bar.Time, bar.High, bar.Low, isNew);
}
/// <summary>
/// Updates the indicator with a bar series.
/// </summary>
/// <param name="source">The source bar series.</param>
/// <returns>A TSeries containing the RVI values.</returns>
public TSeries Update(TBarSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -158,31 +112,29 @@ public sealed class Rvi : AbstractBase
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
// Extract close prices
Span<double> closes = len <= 128 ? stackalloc double[len] : new double[len];
Span<double> highs = len <= 128 ? stackalloc double[len] : new double[len];
Span<double> lows = len <= 128 ? stackalloc double[len] : new double[len];
for (int i = 0; i < len; i++)
{
closes[i] = source[i].Close;
highs[i] = source[i].High;
lows[i] = source[i].Low;
tSpan[i] = source[i].Time;
}
Batch(closes, vSpan, _stdevLength, _rmaLength);
BatchDual(highs, lows, vSpan, _stdevLength, _rmaLength);
// Update internal state
// Sync internal state
for (int i = 0; i < len; i++)
{
Update(new TValue(source[i].Time, source[i].Close), isNew: true);
}
Update(source[i], isNew: true);
return new TSeries(t, v);
}
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -193,49 +145,64 @@ public sealed class Rvi : AbstractBase
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
// Single-price series: same value to both channels
Batch(source.Values, vSpan, _stdevLength, _rmaLength);
source.Times.CopyTo(tSpan);
// Update internal state
for (int i = 0; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
}
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TValue UpdateCore(long timeTicks, double price, bool isNew)
private TValue UpdateCore(long timeTicks, double hiPrice, double loPrice, bool isNew)
{
if (isNew)
{
_ps = _s;
_priceBuffer.Snapshot();
_phi = _hi;
_plo = _lo;
_pLastValue = _lastValue;
_hiBuf.Snapshot();
_loBuf.Snapshot();
}
else
{
_s = _ps;
_priceBuffer.Restore();
_hi = _phi;
_lo = _plo;
_lastValue = _pLastValue;
_hiBuf.Restore();
_loBuf.Restore();
}
var s = _s;
// Handle non-finite price
if (!double.IsFinite(price))
// Handle non-finite
if (!double.IsFinite(hiPrice) || !double.IsFinite(loPrice))
{
Last = new TValue(timeTicks, s.LastValue);
Last = new TValue(timeTicks, _lastValue);
PubEvent(Last, isNew);
return Last;
}
double rviValue;
double rviHi = UpdateChannel(ref _hi, _hiBuf, hiPrice);
double rviLo = UpdateChannel(ref _lo, _loBuf, loPrice);
double rviValue = (rviHi + rviLo) * 0.5;
// Need previous price for direction
if (!double.IsFinite(rviValue))
rviValue = _lastValue;
else
_lastValue = rviValue;
Last = new TValue(timeTicks, rviValue);
PubEvent(Last, isNew);
return Last;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double UpdateChannel(ref ChState s, RingBuffer buf, double price)
{
if (double.IsNaN(s.PrevPrice))
{
// First price - add to buffer but no RVI yet
_priceBuffer.Add(price);
buf.Add(price);
s = s with
{
PrevPrice = price,
@@ -243,137 +210,87 @@ public sealed class Rvi : AbstractBase
SumSq = price * price,
FillCount = 1
};
rviValue = 50.0; // Neutral
return 50.0;
}
else
double priceChange = price - s.PrevPrice;
double oldSum = s.Sum;
double oldSumSq = s.SumSq;
int oldCount = s.FillCount;
if (buf.Count == _stdevLength)
{
// Calculate price change direction
double priceChange = price - s.PrevPrice;
// Update price buffer for stddev calculation
double oldSum = s.Sum;
double oldSumSq = s.SumSq;
int oldCount = s.FillCount;
// Remove oldest if buffer full
if (_priceBuffer.Count == _stdevLength)
{
double oldest = _priceBuffer[0];
oldSum -= oldest;
oldSumSq -= oldest * oldest;
oldCount--;
}
// Add new price
_priceBuffer.Add(price);
double newSum = oldSum + price;
double newSumSq = oldSumSq + (price * price);
int newCount = oldCount + 1;
// Calculate population stddev
double currentStdDev = 0.0;
if (newCount > 1)
{
double mean = newSum / newCount;
double variance = (newSumSq / newCount) - (mean * mean);
variance = Math.Max(0.0, variance);
currentStdDev = Math.Sqrt(variance);
}
// Classify stddev by direction
double upStdVal = 0.0;
double downStdVal = 0.0;
if (priceChange > 0)
{
upStdVal = currentStdDev;
}
else if (priceChange < 0)
{
downStdVal = currentStdDev;
}
// If priceChange == 0, both stay 0
// RMA with bias correction for upward stddev
double rawRmaUp = s.RawRmaUp;
double eUp = s.EUp;
rawRmaUp = Math.FusedMultiplyAdd(rawRmaUp, _rmaLength - 1, upStdVal) / _rmaLength;
eUp = (1 - _alpha) * eUp;
double avgUpStd = eUp > Epsilon ? rawRmaUp / (1.0 - eUp) : rawRmaUp;
// RMA with bias correction for downward stddev
double rawRmaDown = s.RawRmaDown;
double eDown = s.EDown;
rawRmaDown = Math.FusedMultiplyAdd(rawRmaDown, _rmaLength - 1, downStdVal) / _rmaLength;
eDown = (1 - _alpha) * eDown;
double avgDownStd = eDown > Epsilon ? rawRmaDown / (1.0 - eDown) : rawRmaDown;
// Calculate RVI
double sumAvgStd = avgUpStd + avgDownStd;
rviValue = sumAvgStd > Epsilon ? (100.0 * avgUpStd / sumAvgStd) : 50.0;
s = s with
{
PrevPrice = price,
Sum = newSum,
SumSq = newSumSq,
RawRmaUp = rawRmaUp,
EUp = eUp,
RawRmaDown = rawRmaDown,
EDown = eDown,
FillCount = newCount
};
double oldest = buf[0];
oldSum -= oldest;
oldSumSq -= oldest * oldest;
oldCount--;
}
if (!double.IsFinite(rviValue))
buf.Add(price);
double newSum = oldSum + price;
double newSumSq = oldSumSq + (price * price);
int newCount = oldCount + 1;
double currentStdDev = 0.0;
if (newCount > 1)
{
rviValue = s.LastValue;
}
else
{
s = s with { LastValue = rviValue };
double mean = newSum / newCount;
double variance = (newSumSq / newCount) - (mean * mean);
variance = Math.Max(0.0, variance);
currentStdDev = Math.Sqrt(variance);
}
_s = s;
double upStdVal = 0.0;
double downStdVal = 0.0;
if (priceChange > 0)
upStdVal = currentStdDev;
else if (priceChange < 0)
downStdVal = currentStdDev;
Last = new TValue(timeTicks, rviValue);
PubEvent(Last, isNew);
return Last;
double rawRmaUp = Math.FusedMultiplyAdd(s.RawRmaUp, _rmaLength - 1, upStdVal) / _rmaLength;
double eUp = (1 - _alpha) * s.EUp;
double avgUpStd = eUp > Epsilon ? rawRmaUp / (1.0 - eUp) : rawRmaUp;
double rawRmaDown = Math.FusedMultiplyAdd(s.RawRmaDown, _rmaLength - 1, downStdVal) / _rmaLength;
double eDown = (1 - _alpha) * s.EDown;
double avgDownStd = eDown > Epsilon ? rawRmaDown / (1.0 - eDown) : rawRmaDown;
double sumAvgStd = avgUpStd + avgDownStd;
double rvi = sumAvgStd > Epsilon ? (100.0 * avgUpStd / sumAvgStd) : 50.0;
s = new ChState(price, newSum, newSumSq, rawRmaUp, eUp, rawRmaDown, eDown, newCount);
return rvi;
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
for (int i = 0; i < source.Length; i++)
{
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
}
}
public override void Reset()
{
_s = new State(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 50.0, 0);
_ps = _s;
_priceBuffer.Clear();
var init = new ChState(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 0);
_hi = _phi = init;
_lo = _plo = init;
_lastValue = _pLastValue = 50.0;
_hiBuf.Clear();
_loBuf.Clear();
Last = default;
}
// --- Static Batch methods ---
/// <summary>
/// Calculates Relative Volatility Index for a price series (static).
/// Batch RVI for a single-price series (same value to both channels → original behavior).
/// </summary>
/// <param name="source">The source price series.</param>
/// <param name="stdevLength">The lookback period for standard deviation.</param>
/// <param name="rmaLength">The lookback period for RMA smoothing.</param>
/// <returns>A TSeries containing the RVI values.</returns>
public static TSeries Batch(TSeries source, int stdevLength = 10, int rmaLength = 14)
{
if (stdevLength < 2)
{
throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
}
if (rmaLength < 1)
{
throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
}
int len = source.Count;
var t = new List<long>(len);
@@ -381,17 +298,14 @@ public sealed class Rvi : AbstractBase
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Batch(source.Values, vSpan, stdevLength, rmaLength);
source.Times.CopyTo(tSpan);
Batch(source.Values, CollectionsMarshal.AsSpan(v), stdevLength, rmaLength);
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
return new TSeries(t, v);
}
/// <summary>
/// Calculates RVI for a bar series (static).
/// Batch RVI for a bar series (revised: high+low average).
/// </summary>
public static TSeries Batch(TBarSeries source, int stdevLength = 10, int rmaLength = 14)
{
@@ -400,12 +314,8 @@ public sealed class Rvi : AbstractBase
}
/// <summary>
/// Batch calculation using spans.
/// Span-based batch for single-price series. Same price to both channels → original behavior.
/// </summary>
/// <param name="prices">Price values.</param>
/// <param name="output">Output RVI values.</param>
/// <param name="stdevLength">The lookback period for standard deviation.</param>
/// <param name="rmaLength">The lookback period for RMA smoothing.</param>
public static void Batch(
ReadOnlySpan<double> prices,
Span<double> output,
@@ -413,27 +323,64 @@ public sealed class Rvi : AbstractBase
int rmaLength = 14)
{
if (stdevLength < 2)
{
throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
}
if (rmaLength < 1)
{
throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
}
if (output.Length < prices.Length)
{
throw new ArgumentException("Output span must be at least as long as prices span", nameof(output));
}
// Single-price: feed same data to both channels, average = original
BatchDual(prices, prices, output, stdevLength, rmaLength);
}
/// <summary>
/// Span-based batch for dual-channel (high + low) revised RVI.
/// </summary>
public static void BatchDual(
ReadOnlySpan<double> highs,
ReadOnlySpan<double> lows,
Span<double> output,
int stdevLength = 10,
int rmaLength = 14)
{
if (stdevLength < 2)
throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
if (rmaLength < 1)
throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
int len = highs.Length;
if (len == 0)
return;
if (output.Length < len)
throw new ArgumentException("Output span must be at least as long as input span", nameof(output));
// Allocate temp buffers for each channel's RVI output
Span<double> rviHi = len <= 256 ? stackalloc double[len] : new double[len];
Span<double> rviLo = len <= 256 ? stackalloc double[len] : new double[len];
BatchSingleChannel(highs, rviHi, stdevLength, rmaLength);
BatchSingleChannel(lows, rviLo, stdevLength, rmaLength);
// Average
for (int i = 0; i < len; i++)
output[i] = (rviHi[i] + rviLo[i]) * 0.5;
}
/// <summary>
/// Computes original (single-channel) RVI for one price series.
/// </summary>
private static void BatchSingleChannel(
ReadOnlySpan<double> prices,
Span<double> output,
int stdevLength,
int rmaLength)
{
int len = prices.Length;
if (len == 0)
{
return;
}
double alpha = 1.0 / rmaLength;
// Price buffer for stddev
Span<double> priceBuffer = stdevLength <= 256 ? stackalloc double[stdevLength] : new double[stdevLength];
int head = 0;
int count = 0;
@@ -442,7 +389,6 @@ public sealed class Rvi : AbstractBase
double prevPrice = double.NaN;
double lastValue = 50.0;
// RMA state
double rawRmaUp = 0;
double eUp = 1.0;
double rawRmaDown = 0;
@@ -452,21 +398,16 @@ public sealed class Rvi : AbstractBase
{
double price = prices[i];
// First price
if (double.IsNaN(prevPrice))
{
// Handle invalid first price - output neutral and continue
if (!double.IsFinite(price))
{
output[i] = lastValue;
continue;
}
// Add to buffer
if (count < stdevLength)
{
count++;
}
else
{
double oldest = priceBuffer[head];
@@ -483,22 +424,17 @@ public sealed class Rvi : AbstractBase
continue;
}
// Handle invalid price
if (!double.IsFinite(price))
{
output[i] = lastValue;
continue;
}
// Price change direction
double priceChange = price - prevPrice;
prevPrice = price;
// Update buffer
if (count < stdevLength)
{
count++;
}
else
{
double oldest = priceBuffer[head];
@@ -510,7 +446,6 @@ public sealed class Rvi : AbstractBase
sum += price;
sumSq += price * price;
// Population stddev
double currentStdDev = 0.0;
if (count > 1)
{
@@ -520,19 +455,13 @@ public sealed class Rvi : AbstractBase
currentStdDev = Math.Sqrt(variance);
}
// Classify by direction
double upStdVal = 0.0;
double downStdVal = 0.0;
if (priceChange > 0)
{
upStdVal = currentStdDev;
}
else if (priceChange < 0)
{
downStdVal = currentStdDev;
}
// RMA with bias correction
rawRmaUp = Math.FusedMultiplyAdd(rawRmaUp, rmaLength - 1, upStdVal) / rmaLength;
eUp = (1 - alpha) * eUp;
double avgUpStd = eUp > Epsilon ? rawRmaUp / (1.0 - eUp) : rawRmaUp;
@@ -541,18 +470,13 @@ public sealed class Rvi : AbstractBase
eDown = (1 - alpha) * eDown;
double avgDownStd = eDown > Epsilon ? rawRmaDown / (1.0 - eDown) : rawRmaDown;
// RVI
double sumAvgStd = avgUpStd + avgDownStd;
double rviValue = sumAvgStd > Epsilon ? (100.0 * avgUpStd / sumAvgStd) : 50.0;
if (!double.IsFinite(rviValue))
{
rviValue = lastValue;
}
else
{
lastValue = rviValue;
}
output[i] = rviValue;
}
@@ -564,5 +488,4 @@ public sealed class Rvi : AbstractBase
TSeries results = indicator.Update(source);
return (results, indicator);
}
}
}
+40 -15
View File
@@ -3,7 +3,7 @@
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Volatility |
| **Inputs** | OHLCV bar (TBar) |
| **Inputs** | OHLCV bar (TBar) or single price (TValue) |
| **Parameters** | `stdevLength` (default 10), `rmaLength` (default 14) |
| **Outputs** | Single series (Rvi) |
| **Output range** | $0$ to $100$ |
@@ -11,15 +11,16 @@
### TL;DR
- The Relative Volatility Index (RVI) is a directional volatility oscillator that distinguishes between upward and downward price volatility.
- Parameterized by `stdevlength` (default 10), `rmalength` (default 14).
- The Relative Volatility Index (RVI) implements Dorsey's **revised (1995)** version: computes original RVI separately on High and Low series, then averages.
- When fed single-price data (TValue), both channels receive the same value, reducing to the original (1993) formula.
- Parameterized by `stdevLength` (default 10), `rmaLength` (default 14).
- Output range: $0$ to $100$.
- Requires 1 bar of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
- Requires `stdevLength` bars of warmup before first valid output (IsHot = true).
- Validated against FM Labs revised RVI specification.
> "Not all volatility is created equal—upward volatility feels like profit, downward volatility feels like loss. RVI separates these psychological experiences into a quantifiable measure."
The Relative Volatility Index (RVI) is a directional volatility oscillator that distinguishes between upward and downward price volatility. Originally developed by Donald Dorsey in 1993, RVI measures the standard deviation of closing prices and categorizes this volatility based on whether prices are rising or falling. The result is an oscillator bounded between 0 and 100, where values above 50 indicate upward volatility dominance and values below 50 indicate downward volatility dominance.
The Relative Volatility Index (RVI) is a directional volatility oscillator that distinguishes between upward and downward price volatility. Originally developed by Donald Dorsey in 1993 using close prices only, RVI was **revised in 1995** to compute separate RVI values on the High and Low price series and average them. This implementation follows the revised version: when fed OHLCV bars (TBar), it runs independent RVI channels on High and Low; when fed single prices (TValue), both channels receive the same value, reducing to the original formula.
## Historical Context
@@ -27,13 +28,29 @@ Donald Dorsey introduced the Relative Volatility Index in the June 1993 issue of
The key innovation was separating volatility into directional components. Traditional volatility measures (standard deviation, ATR) treat upward and downward price movements identically. Dorsey recognized that traders experience these movements differently: upward volatility in a long position feels like opportunity, while downward volatility feels like risk.
The original 1993 formula used a 10-period standard deviation and 14-period Wilder's smoothing (RMA). This implementation follows the PineScript reference which uses bias-corrected RMA to ensure proper warmup behavior during the initial periods.
The original 1993 formula used a 10-period standard deviation of closing prices with 14-period Wilder's smoothing (RMA). In 1995, Dorsey revised the formula to average RVI computed independently on the High and Low series, capturing volatility structure across the full price range rather than just closes.
FM Labs documents both versions: the original (close-only) and the revised (high+low average). This implementation follows the **revised** version with bias-corrected RMA for proper warmup behavior.
## Architecture & Physics
### 1. Rolling Population Standard Deviation
### 1. Dual-Channel Architecture (Revised 1995)
First, compute the population standard deviation of closing prices over `stdevLength` periods:
The revised RVI computes the original RVI algorithm independently on two channels:
- **High channel:** uses bar High prices
- **Low channel:** uses bar Low prices
The final RVI is their average:
$$
\text{RVI}_{\text{revised}} = \frac{\text{RVI}_{\text{high}} + \text{RVI}_{\text{low}}}{2}
$$
When fed single prices (TValue), both channels receive the same value: $\text{RVI} = \frac{\text{RVI}_p + \text{RVI}_p}{2} = \text{RVI}_p$ (original behavior).
### 2. Per-Channel: Rolling Population Standard Deviation
For each channel, compute the population standard deviation over `stdevLength` periods:
$$
\sigma_t = \sqrt{\frac{\sum_{i=0}^{n-1}(P_{t-i} - \bar{P})^2}{n}}
@@ -51,7 +68,7 @@ $$
\sigma_t = \sqrt{\frac{\sum P_i^2}{n} - \left(\frac{\sum P_i}{n}\right)^2}
$$
### 2. Directional Classification
### 3. Directional Classification
Based on price change direction, assign the volatility to either upward or downward:
@@ -71,7 +88,7 @@ $$
Note: When $P_t = P_{t-1}$ (unchanged), both upStd and downStd are zero. The volatility is "orphaned" rather than assigned to either direction.
### 3. Bias-Corrected RMA Smoothing
### 4. Bias-Corrected RMA Smoothing
Both directional volatilities are smoothed using Wilder's RMA (Exponential Moving Average with $\alpha = 1/n$) with bias correction for proper warmup:
@@ -100,15 +117,21 @@ where $\alpha = 1/\text{rmaLength}$ and $\epsilon = 10^{-10}$.
This bias correction compensates for the zero initialization of raw RMA, preventing artificially low values during warmup.
### 4. Final RVI Calculation
### 5. Per-Channel RVI
$$
\text{RVI}_t = \begin{cases}
\text{RVI}_{\text{channel}} = \begin{cases}
100 \times \frac{\text{avgUpStd}_t}{\text{avgUpStd}_t + \text{avgDownStd}_t} & \text{if sum} > 0 \\
50 & \text{otherwise}
\end{cases}
$$
### 6. Final Revised RVI
$$
\text{RVI}_t = \frac{\text{RVI}_{\text{high}} + \text{RVI}_{\text{low}}}{2}
$$
## Mathematical Foundation
### Relationship to RSI
@@ -192,13 +215,14 @@ Dominant cost: five divisions (63%) for variance calculation and RMA updates.
| Library | Status | Notes |
| :--- | :---: | :--- |
| **FM Labs** | ✅ | Matches revised (1995) dual-channel specification |
| **TA-Lib** | N/A | Not implemented |
| **Skender** | N/A | Not implemented |
| **Tulip** | N/A | Not implemented |
| **OoplesFinance** | ❔ | Different algorithm (RSI-based) |
| **PineScript** | ✅ | Matches rvi.pine reference |
| **PineScript** | ✅ | Matches rvi.pine reference (original algorithm per channel) |
Note: Some libraries implement "RVI" as a different indicator (often RSI applied to volatility). This implementation follows Dorsey's original design using directional standard deviation.
Note: Some libraries implement "RVI" as a different indicator (often RSI applied to volatility). FM Labs distinguishes between original (1993, close-only) and revised (1995, high+low average). This implementation follows the **revised** version.
## Common Pitfalls
@@ -261,4 +285,5 @@ Price making lower lows + RVI making higher lows: Bullish divergence
- Dorsey, D. (1993). "The Relative Volatility Index." *Technical Analysis of Stocks & Commodities*, 11(6), 253-256.
- Dorsey, D. (1995). "Refining the Relative Volatility Index." *Technical Analysis of Stocks & Commodities*, 13(9).
- FM Labs. "Relative Volatility Index." https://www.fmlabs.com/reference/RVI.htm (Original vs Revised versions).
- TradingView. (2024). "PineScript Reference Implementation." rvi.pine source file.
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Relative Volatility Index (RVI)", shorttitle="RVI", overlay=false)