mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 19:48:05 +00:00
adding missing validations
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class RvgiIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RvgiIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new RvgiIndicator();
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("RVGI - Relative Vigor Index", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvgiIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new RvgiIndicator { Period = 10 };
|
||||
|
||||
Assert.Equal(0, RvgiIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvgiIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new RvgiIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("RVGI", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvgiIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new RvgiIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Rvgi.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvgiIndicator_Initialize_CreatesTwoLineSeries()
|
||||
{
|
||||
var indicator = new RvgiIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
// RVGI line + Signal line
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvgiIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RvgiIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double rvgiValue = indicator.LinesSeries[0].GetValue(0);
|
||||
double signalValue = indicator.LinesSeries[1].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(rvgiValue));
|
||||
Assert.True(double.IsFinite(signalValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvgiIndicator_ProcessUpdate_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new RvgiIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
// After new bar, series should have grown
|
||||
Assert.True(indicator.LinesSeries[0].Count >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvgiIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new RvgiIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvgiIndicator_DifferentOhlcSource_ComputesValues()
|
||||
{
|
||||
var indicator = new RvgiIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100.0 + i * 0.5;
|
||||
indicator.HistoricalData.AddBar(
|
||||
now.AddMinutes(i),
|
||||
open: basePrice,
|
||||
high: basePrice + 3.0,
|
||||
low: basePrice - 2.0,
|
||||
close: basePrice + 1.0);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Both lines should have finite values
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[1].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RvgiIndicator_BullishBars_ParallelOutput_Positive()
|
||||
{
|
||||
// Persistent up bars → RVGI line should be positive
|
||||
var indicator = new RvgiIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100.0 + i;
|
||||
indicator.HistoricalData.AddBar(
|
||||
now.AddMinutes(i),
|
||||
open: basePrice,
|
||||
high: basePrice + 4.0,
|
||||
low: basePrice - 1.0,
|
||||
close: basePrice + 3.0);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double rvgiValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(rvgiValue > 0.0, $"Expected RVGI line > 0 for bullish bars, got {rvgiValue}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class RvgiIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 5000, 1, 0)]
|
||||
public int Period { get; set; } = 10;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Rvgi _rvgi = null!;
|
||||
private readonly LineSeries _rvgiLine;
|
||||
private readonly LineSeries _signalLine;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"RVGI ({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/rvgi/Rvgi.Quantower.cs";
|
||||
|
||||
public RvgiIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "RVGI - Relative Vigor Index";
|
||||
Description = "Dual-output oscillator comparing closing strength to the full bar range, smoothed via 4-tap SWMA and averaged over a period. RVGI > 0 in uptrends, < 0 in downtrends.";
|
||||
|
||||
_rvgiLine = new LineSeries("RVGI", Color.Yellow, 2, LineStyle.Solid);
|
||||
_signalLine = new LineSeries("Signal", Color.Cyan, 1, LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_rvgiLine);
|
||||
AddLineSeries(_signalLine);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_rvgi = new Rvgi(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
_ = _rvgi.Update(this.GetInputBar(args), args.IsNewBar());
|
||||
|
||||
_rvgiLine.SetValue(_rvgi.RvgiValue, _rvgi.IsHot, ShowColdValues);
|
||||
_signalLine.SetValue(_rvgi.Signal, _rvgi.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class RvgiTests
|
||||
{
|
||||
private readonly GBM _gbm = new(100.0, 0.05, 0.2, seed: 42);
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
// ───── A) Constructor validation ─────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriod_IsValid()
|
||||
{
|
||||
var rvgi = new Rvgi();
|
||||
Assert.Equal("Rvgi(10)", rvgi.Name);
|
||||
Assert.Equal(10, rvgi.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rvgi(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rvgi(period: -1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomPeriod_SetsCorrectly()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 14);
|
||||
Assert.Equal("Rvgi(14)", rvgi.Name);
|
||||
Assert.Equal(14, rvgi.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ───── B) Basic calculation ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
var result = rvgi.Update(bar);
|
||||
Assert.IsType<TValue>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_IsAccessible()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
rvgi.Update(bar);
|
||||
Assert.True(double.IsFinite(rvgi.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_RvgiAndSignal_Accessible()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rvgi.Update(_gbm.Next(isNew: true));
|
||||
}
|
||||
Assert.True(double.IsFinite(rvgi.RvgiValue));
|
||||
Assert.True(double.IsFinite(rvgi.Signal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsHot_FalseBeforeWarmup()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
Assert.False(rvgi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Name_MatchesPeriod()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 7);
|
||||
Assert.Equal("Rvgi(7)", rvgi.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BullishBars_PositiveRvgi()
|
||||
{
|
||||
// Bars where close > open (up bars) should yield positive RVGI
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double open = 100.0;
|
||||
double close = 105.0; // consistently close > open
|
||||
double high = 107.0;
|
||||
double low = 98.0;
|
||||
rvgi.Update(new TBar(DateTime.UtcNow.AddMinutes(i), open, high, low, close, 1000), isNew: true);
|
||||
}
|
||||
Assert.True(rvgi.RvgiValue > 0.0, $"Expected RVGI > 0, got {rvgi.RvgiValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BearishBars_NegativeRvgi()
|
||||
{
|
||||
// Bars where close < open (down bars) should yield negative RVGI
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double open = 105.0;
|
||||
double close = 100.0; // consistently close < open
|
||||
double high = 107.0;
|
||||
double low = 98.0;
|
||||
rvgi.Update(new TBar(DateTime.UtcNow.AddMinutes(i), open, high, low, close, 1000), isNew: true);
|
||||
}
|
||||
Assert.True(rvgi.RvgiValue < 0.0, $"Expected RVGI < 0, got {rvgi.RvgiValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_DojiBars_ZeroDenominator_ReturnsZero()
|
||||
{
|
||||
// Doji bars: high == low (zero range) → denominator = 0 → RVGI = 0
|
||||
var rvgi = new Rvgi(period: 3);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
// high == low == open == close → zero range
|
||||
rvgi.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100.0, 100.0, 100.0, 100.0, 0), isNew: true);
|
||||
}
|
||||
Assert.Equal(0.0, rvgi.RvgiValue, Tolerance);
|
||||
}
|
||||
|
||||
// ───── C) State + bar correction ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_True_AdvancesState()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
rvgi.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
_ = rvgi.RvgiValue;
|
||||
rvgi.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
// state should change (different bar advances output)
|
||||
Assert.True(double.IsFinite(rvgi.RvgiValue));
|
||||
Assert.True(double.IsFinite(rvgi.Signal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_False_RollsBack()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
rvgi.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
// Two corrections with same bar must yield identical result (idempotent)
|
||||
var bar = new TBar(DateTime.UtcNow, 105, 110, 100, 107, 1000);
|
||||
rvgi.Update(bar, isNew: false);
|
||||
double rv1 = rvgi.RvgiValue;
|
||||
double sg1 = rvgi.Signal;
|
||||
|
||||
rvgi.Update(bar, isNew: false);
|
||||
double rv2 = rvgi.RvgiValue;
|
||||
double sg2 = rvgi.Signal;
|
||||
|
||||
Assert.Equal(rv1, rv2, Tolerance);
|
||||
Assert.Equal(sg1, sg2, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_Restore()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
var bars = new TBar[15];
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
{
|
||||
bars[i] = _gbm.Next(isNew: true);
|
||||
}
|
||||
|
||||
foreach (var b in bars)
|
||||
{
|
||||
rvgi.Update(b, isNew: true);
|
||||
}
|
||||
|
||||
double baselineRvgi = rvgi.RvgiValue;
|
||||
double baselineSig = rvgi.Signal;
|
||||
|
||||
// Corrupt then restore to last bar
|
||||
rvgi.Update(new TBar(DateTime.UtcNow, 200, 250, 150, 220, 5000), isNew: false);
|
||||
rvgi.Update(new TBar(DateTime.UtcNow, 999, 1050, 900, 1000, 9999), isNew: false);
|
||||
rvgi.Update(bars[^1], isNew: false);
|
||||
|
||||
Assert.Equal(baselineRvgi, rvgi.RvgiValue, Tolerance);
|
||||
Assert.Equal(baselineSig, rvgi.Signal, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rvgi.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
rvgi.Reset();
|
||||
|
||||
Assert.False(rvgi.IsHot);
|
||||
// After reset, output should be 0 (doji bar with no bars prior)
|
||||
Assert.Equal(default, rvgi.Last);
|
||||
}
|
||||
|
||||
// ───── D) Warmup / IsHot ─────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAfterPeriodBars()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
Assert.False(rvgi.IsHot);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
rvgi.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
Assert.False(rvgi.IsHot);
|
||||
}
|
||||
|
||||
rvgi.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
Assert.True(rvgi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesPeriod()
|
||||
{
|
||||
Assert.Equal(10, new Rvgi(period: 10).WarmupPeriod);
|
||||
Assert.Equal(14, new Rvgi(period: 14).WarmupPeriod);
|
||||
}
|
||||
|
||||
// ───── E) Robustness (NaN/Infinity) ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_High_DoesNotPropagate()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
rvgi.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var nanBar = new TBar(DateTime.UtcNow, 100, double.NaN, 95, 102, 1000);
|
||||
rvgi.Update(nanBar, isNew: true);
|
||||
Assert.True(double.IsFinite(rvgi.RvgiValue));
|
||||
Assert.True(double.IsFinite(rvgi.Signal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityClose_DoesNotPropagate()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
rvgi.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var infBar = new TBar(DateTime.UtcNow, 100, 110, 90, double.PositiveInfinity, 1000);
|
||||
rvgi.Update(infBar, isNew: true);
|
||||
Assert.True(double.IsFinite(rvgi.RvgiValue));
|
||||
Assert.True(double.IsFinite(rvgi.Signal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BatchNaN_Safe()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
// Feed a run of NaN bars — should not throw or produce non-finite output
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rvgi.Update(new TBar(DateTime.UtcNow.AddMinutes(i),
|
||||
double.NaN, double.NaN, double.NaN, double.NaN, 0), isNew: true);
|
||||
Assert.True(double.IsFinite(rvgi.RvgiValue));
|
||||
}
|
||||
}
|
||||
|
||||
// ───── F) Consistency (all modes match) ─────
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Consistency_Streaming_Vs_Batch_Match()
|
||||
{
|
||||
const int N = 100;
|
||||
const int period = 14;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 123);
|
||||
var bars = new TBar[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var rvgi = new Rvgi(period);
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
rvgi.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamRvgi = rvgi.RvgiValue;
|
||||
double streamSig = rvgi.Signal;
|
||||
|
||||
// Batch span
|
||||
var opens = new double[N]; var highs = new double[N];
|
||||
var lows = new double[N]; var closes = new double[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
opens[i] = bars[i].Open; highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low; closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
var rvgiBatch = new double[N];
|
||||
var sigBatch = new double[N];
|
||||
Rvgi.Batch(opens, highs, lows, closes, rvgiBatch, sigBatch, period);
|
||||
|
||||
Assert.Equal(streamRvgi, rvgiBatch[N - 1], Tolerance);
|
||||
Assert.Equal(streamSig, sigBatch[N - 1], Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Consistency_UpdateAll_Vs_Batch_Match()
|
||||
{
|
||||
const int N = 80;
|
||||
const int period = 10;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 456);
|
||||
var series = new TBarSeries();
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
series.Add(gbm.Next(isNew: true));
|
||||
}
|
||||
|
||||
var rvgiInst = new Rvgi(period);
|
||||
var (rvgiSeries, sigSeries) = rvgiInst.UpdateAll(series);
|
||||
|
||||
var rvgiBatch = new double[N];
|
||||
var sigBatch = new double[N];
|
||||
Rvgi.Batch(series.OpenValues, series.HighValues, series.LowValues, series.CloseValues,
|
||||
rvgiBatch, sigBatch, period);
|
||||
|
||||
Assert.Equal(rvgiSeries.Last.Value, rvgiBatch[N - 1], Tolerance);
|
||||
Assert.Equal(sigSeries.Last.Value, sigBatch[N - 1], Tolerance);
|
||||
}
|
||||
|
||||
// ───── G) Span API validation ─────
|
||||
|
||||
[Fact]
|
||||
public void Batch_ZeroPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rvgi.Batch(
|
||||
new double[5], new double[5], new double[5], new double[5],
|
||||
new double[5], new double[5], period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedHighLength_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rvgi.Batch(
|
||||
new double[5], new double[6], new double[5], new double[5],
|
||||
new double[5], new double[5], period: 3));
|
||||
Assert.Equal("high", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedOutputLength_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rvgi.Batch(
|
||||
new double[5], new double[5], new double[5], new double[5],
|
||||
new double[4], new double[5], period: 3));
|
||||
Assert.Equal("rvgiOutput", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedSignalOutputLength_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rvgi.Batch(
|
||||
new double[5], new double[5], new double[5], new double[5],
|
||||
new double[5], new double[4], period: 3));
|
||||
Assert.Equal("signalOutput", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInputs_NoThrow()
|
||||
{
|
||||
var ex = Record.Exception(() =>
|
||||
Rvgi.Batch(
|
||||
ReadOnlySpan<double>.Empty, ReadOnlySpan<double>.Empty,
|
||||
ReadOnlySpan<double>.Empty, ReadOnlySpan<double>.Empty,
|
||||
Span<double>.Empty, Span<double>.Empty, period: 5));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargePeriod_UsesArrayPool()
|
||||
{
|
||||
const int period = 300;
|
||||
const int N = 500;
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 99);
|
||||
var opens = new double[N]; var highs = new double[N];
|
||||
var lows = new double[N]; var closes = new double[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
var b = gbm.Next(isNew: true);
|
||||
opens[i] = b.Open; highs[i] = b.High;
|
||||
lows[i] = b.Low; closes[i] = b.Close;
|
||||
}
|
||||
var rvgiOut = new double[N];
|
||||
var sigOut = new double[N];
|
||||
Rvgi.Batch(opens, highs, lows, closes, rvgiOut, sigOut, period);
|
||||
|
||||
Assert.True(double.IsFinite(rvgiOut[N - 1]));
|
||||
Assert.True(double.IsFinite(sigOut[N - 1]));
|
||||
}
|
||||
|
||||
// ───── H) Chainability / events ─────
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires_OnUpdate()
|
||||
{
|
||||
var rvgi = new Rvgi(period: 5);
|
||||
int fired = 0;
|
||||
rvgi.Pub += (_, in _) => fired++;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rvgi.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(5, fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_TBarSeries_Chains()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var rvgi = new Rvgi(series, period: 3);
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
series.Add(_gbm.Next(isNew: true));
|
||||
}
|
||||
|
||||
Assert.True(rvgi.IsHot);
|
||||
Assert.True(double.IsFinite(rvgi.RvgiValue));
|
||||
Assert.True(double.IsFinite(rvgi.Signal));
|
||||
}
|
||||
|
||||
// ───── Known-value tests ─────
|
||||
|
||||
[Fact]
|
||||
public void KnownValue_AllUpBars_PositiveRvgi()
|
||||
{
|
||||
// Constant up bars: O=100, H=106, L=98, C=105 (C-O=5, H-L=8)
|
||||
// SWMA(C-O) = (5+2*5+2*5+5)/6 = 5, SWMA(H-L) = (8+2*8+2*8+8)/6 = 8
|
||||
// SMA ratio = 5/8 = 0.625
|
||||
var rvgi = new Rvgi(period: 3);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rvgi.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100, 106, 98, 105, 1000), isNew: true);
|
||||
}
|
||||
// After many identical bars, RVGI should converge to 5/8
|
||||
Assert.Equal(5.0 / 8.0, rvgi.RvgiValue, 1e-6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownValue_SymmetricBars_ZeroRvgi()
|
||||
{
|
||||
// Bars where close == open (doji-like but with range) → C-O = 0 → RVGI = 0
|
||||
var rvgi = new Rvgi(period: 3);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rvgi.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100, 105, 95, 100, 1000), isNew: true);
|
||||
}
|
||||
Assert.Equal(0.0, rvgi.RvgiValue, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownValue_SignalConverges_ToRvgi_WhenConstant()
|
||||
{
|
||||
// When RVGI is constant, signal SWMA converges to the same value
|
||||
var rvgi = new Rvgi(period: 3);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
rvgi.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100, 106, 98, 105, 1000), isNew: true);
|
||||
}
|
||||
// After many identical bars, signal should equal RVGI (SWMA of constant = constant)
|
||||
Assert.Equal(rvgi.RvgiValue, rvgi.Signal, 1e-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Self-consistency validation for RVGI.
|
||||
/// RVGI is not implemented by TA-Lib, Skender, Tulip, or Ooples,
|
||||
/// so validation uses streaming == batch == span mode consistency
|
||||
/// plus mathematical identity checks.
|
||||
/// </summary>
|
||||
public sealed class RvgiValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
private readonly ITestOutputHelper _output = output;
|
||||
private const double Tolerance = 1e-12;
|
||||
|
||||
// ───── Self-consistency: streaming == batch span ─────
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch_Period10()
|
||||
{
|
||||
const int N = 200;
|
||||
const int period = 10;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 1001);
|
||||
var opens = new double[N]; var highs = new double[N];
|
||||
var lows = new double[N]; var closes = new double[N];
|
||||
var bars = new TBar[N];
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
opens[i] = bars[i].Open; highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low; closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var rvgi = new Rvgi(period);
|
||||
for (int i = 0; i < N; i++) { rvgi.Update(bars[i], isNew: true); }
|
||||
double streamRvgi = rvgi.RvgiValue;
|
||||
double streamSig = rvgi.Signal;
|
||||
|
||||
// Batch span
|
||||
var rvgiBatch = new double[N];
|
||||
var sigBatch = new double[N];
|
||||
Rvgi.Batch(opens, highs, lows, closes, rvgiBatch, sigBatch, period);
|
||||
|
||||
_output.WriteLine($"Streaming RVGI={streamRvgi:F8}, Batch RVGI={rvgiBatch[N-1]:F8}");
|
||||
_output.WriteLine($"Streaming Signal={streamSig:F8}, Batch Signal={sigBatch[N-1]:F8}");
|
||||
|
||||
Assert.Equal(streamRvgi, rvgiBatch[N - 1], Tolerance);
|
||||
Assert.Equal(streamSig, sigBatch[N - 1], Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch_Period20()
|
||||
{
|
||||
const int N = 300;
|
||||
const int period = 20;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.3, seed: 2002);
|
||||
var opens = new double[N]; var highs = new double[N];
|
||||
var lows = new double[N]; var closes = new double[N];
|
||||
var bars = new TBar[N];
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
opens[i] = bars[i].Open; highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low; closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
var rvgi = new Rvgi(period);
|
||||
for (int i = 0; i < N; i++) { rvgi.Update(bars[i], isNew: true); }
|
||||
|
||||
var rvgiBatch = new double[N];
|
||||
var sigBatch = new double[N];
|
||||
Rvgi.Batch(opens, highs, lows, closes, rvgiBatch, sigBatch, period);
|
||||
|
||||
Assert.Equal(rvgi.RvgiValue, rvgiBatch[N - 1], Tolerance);
|
||||
Assert.Equal(rvgi.Signal, sigBatch[N - 1], Tolerance);
|
||||
}
|
||||
|
||||
// ───── Mathematical identity checks ─────
|
||||
|
||||
[Fact]
|
||||
public void Validate_ConstantUpBars_RvgiConvergesToRatio()
|
||||
{
|
||||
// Constant bars: O=100, H=106, L=98, C=105 → C-O=5, H-L=8
|
||||
// SWMA(5) = 5, SWMA(8) = 8, SMA(5)/SMA(8) = 5/8 = 0.625
|
||||
const int N = 50;
|
||||
const int period = 5;
|
||||
|
||||
var rvgi = new Rvgi(period);
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
rvgi.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: 100.0, high: 106.0, low: 98.0, close: 105.0, volume: 1000), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(5.0 / 8.0, rvgi.RvgiValue, 1e-9);
|
||||
_output.WriteLine($"Constant up RVGI (expect 0.625): {rvgi.RvgiValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZeroCloseOpenDiff_RvgiIsZero()
|
||||
{
|
||||
// Close == Open → numerator always 0 → RVGI = 0
|
||||
const int N = 50;
|
||||
const int period = 10;
|
||||
|
||||
var rvgi = new Rvgi(period);
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
rvgi.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: 100.0, high: 105.0, low: 95.0, close: 100.0, volume: 1000), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, rvgi.RvgiValue, Tolerance);
|
||||
_output.WriteLine($"Zero C-O RVGI (expect 0): {rvgi.RvgiValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DojiBars_ZeroDenominator_ReturnsZero()
|
||||
{
|
||||
// High == Low → denominator = 0 → RVGI = 0 (defensive division)
|
||||
const int N = 50;
|
||||
const int period = 10;
|
||||
|
||||
var rvgi = new Rvgi(period);
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
rvgi.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: 100.0, high: 100.0, low: 100.0, close: 105.0, volume: 0), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, rvgi.RvgiValue, Tolerance);
|
||||
_output.WriteLine($"Zero range (doji) RVGI (expect 0): {rvgi.RvgiValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_SignalConverges_WhenConstantRvgi()
|
||||
{
|
||||
// When RVGI is constant, SWMA signal converges to that constant
|
||||
const int N = 50;
|
||||
const int period = 5;
|
||||
|
||||
var rvgi = new Rvgi(period);
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
rvgi.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: 100.0, high: 106.0, low: 98.0, close: 105.0, volume: 1000), isNew: true);
|
||||
}
|
||||
|
||||
// Signal = SWMA(RVGI, 4) — when RVGI is constant, SWMA(constant) = constant
|
||||
Assert.Equal(rvgi.RvgiValue, rvgi.Signal, 1e-9);
|
||||
_output.WriteLine($"Signal converges to RVGI: {rvgi.Signal} == {rvgi.RvgiValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllBars_Streaming_Vs_Batch_Match()
|
||||
{
|
||||
const int N = 100;
|
||||
const int period = 10;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 3333);
|
||||
var opens = new double[N]; var highs = new double[N];
|
||||
var lows = new double[N]; var closes = new double[N];
|
||||
var bars = new TBar[N];
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
opens[i] = bars[i].Open; highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low; closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
var rvgiBatch = new double[N];
|
||||
var sigBatch = new double[N];
|
||||
Rvgi.Batch(opens, highs, lows, closes, rvgiBatch, sigBatch, period);
|
||||
|
||||
var rvgi = new Rvgi(period);
|
||||
int mismatches = 0;
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
rvgi.Update(bars[i], isNew: true);
|
||||
double diffRvgi = Math.Abs(rvgi.RvgiValue - rvgiBatch[i]);
|
||||
double diffSig = Math.Abs(rvgi.Signal - sigBatch[i]);
|
||||
if (diffRvgi > Tolerance || diffSig > Tolerance)
|
||||
{
|
||||
mismatches++;
|
||||
_output.WriteLine($"Mismatch at i={i}: RVGI stream={rvgi.RvgiValue}, batch={rvgiBatch[i]}, diff={diffRvgi:E3}; Signal stream={rvgi.Signal}, batch={sigBatch[i]}, diffSig={diffSig:E3}");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(0, mismatches);
|
||||
_output.WriteLine($"All {N} bars match between streaming and batch");
|
||||
}
|
||||
|
||||
// ───── Determinism ─────
|
||||
|
||||
[Fact]
|
||||
public void Validate_Deterministic_SameSeed_SameResult()
|
||||
{
|
||||
const int N = 150;
|
||||
const int period = 14;
|
||||
|
||||
static (double rvgi, double sig) Compute(int n, int p, int seed)
|
||||
{
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: seed);
|
||||
var ind = new Rvgi(p);
|
||||
for (int i = 0; i < n; i++) { ind.Update(gbm.Next(isNew: true), isNew: true); }
|
||||
return (ind.RvgiValue, ind.Signal);
|
||||
}
|
||||
|
||||
var (rv1, sg1) = Compute(N, period, 777);
|
||||
var (rv2, sg2) = Compute(N, period, 777);
|
||||
|
||||
Assert.Equal(rv1, rv2, Tolerance);
|
||||
Assert.Equal(sg1, sg2, Tolerance);
|
||||
_output.WriteLine($"Deterministic RVGI: {rv1}, Signal: {sg1}");
|
||||
}
|
||||
|
||||
// ───── Directional correctness ─────
|
||||
|
||||
[Fact]
|
||||
public void Validate_PersistentUpTrend_PositiveRvgi()
|
||||
{
|
||||
// Persistent strong up bars: RVGI must be positive
|
||||
const int period = 10;
|
||||
var rvgi = new Rvgi(period);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double basePrice = 100.0 + i * 0.5;
|
||||
rvgi.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: basePrice,
|
||||
high: basePrice + 3.0,
|
||||
low: basePrice - 1.0,
|
||||
close: basePrice + 2.0, volume: 1000), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(rvgi.RvgiValue > 0.0, $"Expected RVGI > 0 in uptrend, got {rvgi.RvgiValue}");
|
||||
_output.WriteLine($"Uptrend RVGI: {rvgi.RvgiValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_PersistentDownTrend_NegativeRvgi()
|
||||
{
|
||||
// Persistent down bars: RVGI must be negative
|
||||
const int period = 10;
|
||||
var rvgi = new Rvgi(period);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double basePrice = 200.0 - i * 0.5;
|
||||
rvgi.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: basePrice + 2.0,
|
||||
high: basePrice + 3.0,
|
||||
low: basePrice - 1.0,
|
||||
close: basePrice, volume: 1000), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(rvgi.RvgiValue < 0.0, $"Expected RVGI < 0 in downtrend, got {rvgi.RvgiValue}");
|
||||
_output.WriteLine($"Downtrend RVGI: {rvgi.RvgiValue}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// RVGI: Relative Vigor Index
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dual-output oscillator built in four stages:
|
||||
/// <list type="number">
|
||||
/// <item>SWMA(close−open, 4 bars) with weights [1,2,2,1]/6 → numerator per bar</item>
|
||||
/// <item>SWMA(high−low, 4 bars) with same weights → denominator per bar</item>
|
||||
/// <item>SMA(numerator, period) / SMA(denominator, period) → RVGI line</item>
|
||||
/// <item>SWMA(RVGI, 4 bars) → Signal line</item>
|
||||
/// </list>
|
||||
/// 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
|
||||
/// </remarks>
|
||||
[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;
|
||||
|
||||
/// <summary>Display name for the indicator.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>Bars required for the first valid output.</summary>
|
||||
public int WarmupPeriod { get; }
|
||||
|
||||
/// <summary>True once the SMA window is fully populated.</summary>
|
||||
public bool IsHot => _s.Count >= _period;
|
||||
|
||||
/// <summary>Primary output: the RVGI line value.</summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>RVGI line (same as Last.Value).</summary>
|
||||
public double RvgiValue => _s.RvgiValue;
|
||||
|
||||
/// <summary>Signal line: 4-bar SWMA of RVGI.</summary>
|
||||
public double Signal => _s.SignalValue;
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Creates RVGI with the specified SMA smoothing period.
|
||||
/// </summary>
|
||||
/// <param name="period">SMA period (must be > 0, default 10)</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates RVGI chained to a TBarSeries source.
|
||||
/// </summary>
|
||||
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 });
|
||||
|
||||
/// <summary>Resets all state to initial conditions.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates RVGI with a new bar.
|
||||
/// </summary>
|
||||
/// <param name="input">OHLCV bar data</param>
|
||||
/// <param name="isNew">True to advance state; false to rewrite the latest bar</param>
|
||||
/// <returns>Current RVGI value as TValue (primary output)</returns>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates RVGI from a TValue (creates a synthetic bar with all OHLC == value).
|
||||
/// </summary>
|
||||
[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);
|
||||
|
||||
/// <summary>
|
||||
/// Updates RVGI from a TBarSeries, computing RVGI and Signal series.
|
||||
/// </summary>
|
||||
public (TSeries Rvgi, TSeries Signal) UpdateAll(TBarSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
if (len == 0)
|
||||
{
|
||||
return (new TSeries([], []), new TSeries([], []));
|
||||
}
|
||||
|
||||
var rvgiList = new List<double>(len);
|
||||
var sigList = new List<double>(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<long>(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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch-computes RVGI over raw OHLC spans. Zero-allocation path for large datasets.
|
||||
/// </summary>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> open,
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> rvgiOutput,
|
||||
Span<double> 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<double> numBuf;
|
||||
scoped Span<double> denBuf;
|
||||
|
||||
if (period <= StackallocThreshold)
|
||||
{
|
||||
numBuf = stackalloc double[period];
|
||||
denBuf = stackalloc double[period];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedNum = ArrayPool<double>.Shared.Rent(period);
|
||||
rentedDen = ArrayPool<double>.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<double>.Shared.Return(rentedNum); }
|
||||
if (rentedDen != null) { ArrayPool<double>.Shared.Return(rentedDen); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Primes the indicator by replaying historical data without firing events.</summary>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
foreach (var bar in source)
|
||||
{
|
||||
Update(bar, isNew: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user