// RVGI: Relative Vigor Index
// Measures market vigor by comparing closing strength (close-open) to the full
// intrabar range (high-low), smoothed via 4-tap SWMA then averaged over a period.
// John Ehlers, "Rocket Science for Traders" (2002), Chapter 12.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// RVGI: Ehlers Relative Vigor Index
///
///
/// Dual-output oscillator built in four stages:
///
/// - SWMA(close−open, 4 bars) with weights [1,2,2,1]/6 → numerator per bar
/// - SWMA(high−low, 4 bars) with same weights → denominator per bar
/// - SMA(numerator, period) / SMA(denominator, period) → RVGI line
/// - SWMA(RVGI, 4 bars) → Signal line
///
/// Both SMA stages use O(1) circular buffers with count-based warmup.
/// Defensive division: denominator SMA == 0 returns 0.
///
/// References:
/// Ehlers, J.F. (2002). Rocket Science for Traders. Wiley.
/// PineScript reference: rvgi.pine
///
[SkipLocalsInit]
public sealed class Rvgi : ITValuePublisher
{
private readonly int _period;
// Two circular buffers for O(1) SMA of numerator and denominator
private readonly double[] _numBuf;
private readonly double[] _denBuf;
// Snapshots for idempotent isNew=false rollback (circular-buffer-snapshot-rollback pattern)
private readonly double[] _numSnap;
private readonly double[] _denSnap;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double NumSum,
double DenSum,
int Idx,
int Count,
// SWMA history for 4-bar kernel on bars (3 history slots: t-1, t-2, t-3)
double Co1, double Co2, double Co3, // close-open history
double Hl1, double Hl2, double Hl3, // high-low history
// SWMA history for signal line (3 history slots of RVGI)
double Rv1, double Rv2, double Rv3,
// Last-valid substitution fields
double LastValidOpen, double LastValidHigh, double LastValidLow, double LastValidClose,
double RvgiValue, double SignalValue);
private State _s;
private State _ps;
private readonly TBarPublishedHandler _barHandler;
/// Display name for the indicator.
public string Name { get; }
/// Bars required for the first valid output.
public int WarmupPeriod { get; }
/// True once the SMA window is fully populated.
public bool IsHot => _s.Count >= _period;
/// Primary output: the RVGI line value.
public TValue Last { get; private set; }
/// RVGI line (same as Last.Value).
public double RvgiValue => _s.RvgiValue;
/// Signal line: 4-bar SWMA of RVGI.
public double Signal => _s.SignalValue;
public event TValuePublishedHandler? Pub;
///
/// Creates RVGI with the specified SMA smoothing period.
///
/// SMA period (must be > 0, default 10)
public Rvgi(int period = 10)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_numBuf = new double[period];
_denBuf = new double[period];
_numSnap = new double[period];
_denSnap = new double[period];
_s = new State(
NumSum: 0.0, DenSum: 0.0, Idx: 0, Count: 0,
Co1: 0.0, Co2: 0.0, Co3: 0.0,
Hl1: 0.0, Hl2: 0.0, Hl3: 0.0,
Rv1: 0.0, Rv2: 0.0, Rv3: 0.0,
LastValidOpen: double.NaN, LastValidHigh: double.NaN,
LastValidLow: double.NaN, LastValidClose: double.NaN,
RvgiValue: 0.0, SignalValue: 0.0);
_ps = _s;
WarmupPeriod = period;
Name = $"Rvgi({period})";
_barHandler = HandleBar;
}
///
/// Creates RVGI chained to a TBarSeries source.
///
public Rvgi(TBarSeries source, int period = 10) : this(period)
{
Prime(source);
source.Pub += _barHandler;
}
private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PubEvent(TValue value, bool isNew) =>
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
/// Resets all state to initial conditions.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_s = new State(
NumSum: 0.0, DenSum: 0.0, Idx: 0, Count: 0,
Co1: 0.0, Co2: 0.0, Co3: 0.0,
Hl1: 0.0, Hl2: 0.0, Hl3: 0.0,
Rv1: 0.0, Rv2: 0.0, Rv3: 0.0,
LastValidOpen: double.NaN, LastValidHigh: double.NaN,
LastValidLow: double.NaN, LastValidClose: double.NaN,
RvgiValue: 0.0, SignalValue: 0.0);
_ps = _s;
Last = default;
Array.Clear(_numBuf);
Array.Clear(_denBuf);
Array.Clear(_numSnap);
Array.Clear(_denSnap);
}
///
/// Updates RVGI with a new bar.
///
/// OHLCV bar data
/// True to advance state; false to rewrite the latest bar
/// Current RVGI value as TValue (primary output)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
var s = _s;
if (isNew)
{
// Snapshot all circular buffers before mutation — required for idempotent rollback
_ps = s;
Array.Copy(_numBuf, _numSnap, _period);
Array.Copy(_denBuf, _denSnap, _period);
s.Count++;
}
else
{
// Restore scalar state and buffer snapshots atomically
s = _ps;
Array.Copy(_numSnap, _numBuf, _period);
Array.Copy(_denSnap, _denBuf, _period);
}
// Sanitize OHLC inputs — last-valid substitution on NaN/Infinity
double open = input.Open;
double high = input.High;
double low = input.Low;
double close = input.Close;
if (double.IsFinite(open)) { s.LastValidOpen = open; } else { open = double.IsNaN(s.LastValidOpen) ? 0.0 : s.LastValidOpen; }
if (double.IsFinite(high)) { s.LastValidHigh = high; } else { high = double.IsNaN(s.LastValidHigh) ? open : s.LastValidHigh; }
if (double.IsFinite(low)) { s.LastValidLow = low; } else { low = double.IsNaN(s.LastValidLow) ? open : s.LastValidLow; }
if (double.IsFinite(close)) { s.LastValidClose = close; } else { close = double.IsNaN(s.LastValidClose) ? open : s.LastValidClose; }
// Step 1: Per-bar contributions to SWMA kernel
double co0 = close - open;
double hl0 = high - low;
// Step 2: SWMA(close-open, 4) = (co3 + 2*co2 + 2*co1 + co0) / 6
double swmaNum = Math.FusedMultiplyAdd(2.0, s.Co1, Math.FusedMultiplyAdd(2.0, s.Co2, s.Co3 + co0)) / 6.0;
// Step 3: SWMA(high-low, 4) = (hl3 + 2*hl2 + 2*hl1 + hl0) / 6
double swmaDen = Math.FusedMultiplyAdd(2.0, s.Hl1, Math.FusedMultiplyAdd(2.0, s.Hl2, s.Hl3 + hl0)) / 6.0;
// Shift bar SWMA history
s.Co3 = s.Co2;
s.Co2 = s.Co1;
s.Co1 = co0;
s.Hl3 = s.Hl2;
s.Hl2 = s.Hl1;
s.Hl1 = hl0;
// Step 4: O(1) circular-buffer SMA for numerator
int idx = s.Idx;
s.NumSum = s.NumSum - _numBuf[idx] + swmaNum;
s.DenSum = s.DenSum - _denBuf[idx] + swmaDen;
_numBuf[idx] = swmaNum;
_denBuf[idx] = swmaDen;
// Advance circular index on new bars only
if (isNew)
{
s.Idx = (idx + 1) % _period;
}
// Step 5: RVGI = SMA(num) / SMA(den) — defensive against zero denominator
int effective = Math.Min(s.Count, _period);
if (effective < 1) { effective = 1; }
double smaNum = s.NumSum / effective;
double smaDen = s.DenSum / effective;
double rvgiVal = smaDen != 0.0 ? smaNum / smaDen : 0.0;
// Step 6: Signal = SWMA(RVGI, 4) = (rv3 + 2*rv2 + 2*rv1 + rvgi) / 6
double sigVal = Math.FusedMultiplyAdd(2.0, s.Rv1, Math.FusedMultiplyAdd(2.0, s.Rv2, s.Rv3 + rvgiVal)) / 6.0;
// Shift RVGI history
s.Rv3 = s.Rv2;
s.Rv2 = s.Rv1;
s.Rv1 = rvgiVal;
s.RvgiValue = rvgiVal;
s.SignalValue = sigVal;
_s = s;
Last = new TValue(input.Time, rvgiVal);
PubEvent(Last, isNew);
return Last;
}
///
/// Updates RVGI from a TValue (creates a synthetic bar with all OHLC == value).
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true) =>
Update(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew);
///
/// Updates RVGI from a TBarSeries, computing RVGI and Signal series.
///
public (TSeries Rvgi, TSeries Signal) UpdateAll(TBarSeries source)
{
int len = source.Count;
if (len == 0)
{
return (new TSeries([], []), new TSeries([], []));
}
var rvgiList = new List(len);
var sigList = new List(len);
CollectionsMarshal.SetCount(rvgiList, len);
CollectionsMarshal.SetCount(sigList, len);
var rvgiSpan = CollectionsMarshal.AsSpan(rvgiList);
var sigSpan = CollectionsMarshal.AsSpan(sigList);
Batch(
source.OpenValues, source.HighValues,
source.LowValues, source.CloseValues,
rvgiSpan, sigSpan, _period);
var tList = new List(len);
CollectionsMarshal.SetCount(tList, len);
source.Open.Times.CopyTo(CollectionsMarshal.AsSpan(tList));
// Re-prime internal state for continued streaming
Reset();
for (int i = 0; i < len; i++)
{
Update(source[i], isNew: true);
}
return (new TSeries(tList, rvgiList), new TSeries(tList, sigList));
}
///
/// Batch-computes RVGI over raw OHLC spans. Zero-allocation path for large datasets.
///
public static void Batch(
ReadOnlySpan open,
ReadOnlySpan high,
ReadOnlySpan low,
ReadOnlySpan close,
Span rvgiOutput,
Span signalOutput,
int period = 10)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = open.Length;
if (high.Length != len)
{
throw new ArgumentException("High length must match open length", nameof(high));
}
if (low.Length != len)
{
throw new ArgumentException("Low length must match open length", nameof(low));
}
if (close.Length != len)
{
throw new ArgumentException("Close length must match open length", nameof(close));
}
if (rvgiOutput.Length != len)
{
throw new ArgumentException("rvgiOutput length must match input length", nameof(rvgiOutput));
}
if (signalOutput.Length != len)
{
throw new ArgumentException("signalOutput length must match input length", nameof(signalOutput));
}
if (len == 0)
{
return;
}
const int StackallocThreshold = 256;
double[]? rentedNum = null;
double[]? rentedDen = null;
scoped Span numBuf;
scoped Span denBuf;
if (period <= StackallocThreshold)
{
numBuf = stackalloc double[period];
denBuf = stackalloc double[period];
}
else
{
rentedNum = ArrayPool.Shared.Rent(period);
rentedDen = ArrayPool.Shared.Rent(period);
numBuf = rentedNum.AsSpan(0, period);
denBuf = rentedDen.AsSpan(0, period);
}
try
{
numBuf.Clear();
denBuf.Clear();
double numSum = 0.0;
double denSum = 0.0;
int idx = 0;
int count = 0;
// SWMA bar history
double co1 = 0.0, co2 = 0.0, co3 = 0.0;
double hl1 = 0.0, hl2 = 0.0, hl3 = 0.0;
// Signal SWMA history
double rv1 = 0.0, rv2 = 0.0, rv3 = 0.0;
for (int i = 0; i < len; i++)
{
double o = open[i];
double h = high[i];
double l = low[i];
double c = close[i];
double co0 = c - o;
double hl0 = h - l;
double swmaNum = Math.FusedMultiplyAdd(2.0, co1, Math.FusedMultiplyAdd(2.0, co2, co3 + co0)) / 6.0;
double swmaDen = Math.FusedMultiplyAdd(2.0, hl1, Math.FusedMultiplyAdd(2.0, hl2, hl3 + hl0)) / 6.0;
co3 = co2; co2 = co1; co1 = co0;
hl3 = hl2; hl2 = hl1; hl1 = hl0;
numSum = numSum - numBuf[idx] + swmaNum;
denSum = denSum - denBuf[idx] + swmaDen;
numBuf[idx] = swmaNum;
denBuf[idx] = swmaDen;
idx = (idx + 1) % period;
count++;
int effective = Math.Min(count, period);
double smaNum = numSum / effective;
double smaDen = denSum / effective;
double rvgiVal = smaDen != 0.0 ? smaNum / smaDen : 0.0;
double sigVal = Math.FusedMultiplyAdd(2.0, rv1, Math.FusedMultiplyAdd(2.0, rv2, rv3 + rvgiVal)) / 6.0;
rv3 = rv2; rv2 = rv1; rv1 = rvgiVal;
rvgiOutput[i] = rvgiVal;
signalOutput[i] = sigVal;
}
}
finally
{
if (rentedNum != null) { ArrayPool.Shared.Return(rentedNum); }
if (rentedDen != null) { ArrayPool.Shared.Return(rentedDen); }
}
}
/// Primes the indicator by replaying historical data without firing events.
public void Prime(TBarSeries source)
{
foreach (var bar in source)
{
Update(bar, isNew: true);
}
}
}