docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files

- Remove 'C# Implementation Considerations' sections from 34 indicator .md files
- Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.)
- Move test files into tests/ subdirectories for consistent project structure
- Add trader-focused bullet points to indicator documentation
This commit is contained in:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -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}");
}
}
+504
View File
@@ -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}");
}
}