mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
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:
@@ -0,0 +1,161 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RaviIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RaviIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new RaviIndicator();
|
||||
|
||||
Assert.Equal(7, indicator.ShortPeriod);
|
||||
Assert.Equal(65, indicator.LongPeriod);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("RAVI - Chande Range Action Verification Index", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaviIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new RaviIndicator { ShortPeriod = 5, LongPeriod = 50 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("RAVI", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("5", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("50", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaviIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new RaviIndicator();
|
||||
|
||||
Assert.Equal(0, RaviIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaviIndicator_Initialize_CreatesInternalRavi()
|
||||
{
|
||||
var indicator = new RaviIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (single RAVI line)
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaviIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RaviIndicator { ShortPeriod = 3, LongPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double raviVal = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(raviVal));
|
||||
Assert.True(raviVal >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaviIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RaviIndicator { ShortPeriod = 3, LongPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 128, 115, 125, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaviIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[][] paramSets = { new[] { 3, 10 }, new[] { 5, 20 }, new[] { 7, 65 } };
|
||||
|
||||
foreach (var ps in paramSets)
|
||||
{
|
||||
var indicator = new RaviIndicator { ShortPeriod = ps[0], LongPeriod = ps[1] };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double raviVal = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(raviVal), $"Periods ({ps[0]},{ps[1]}) should produce finite RAVI");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaviIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new RaviIndicator();
|
||||
Assert.Equal(7, indicator.ShortPeriod);
|
||||
Assert.Equal(65, indicator.LongPeriod);
|
||||
|
||||
indicator.ShortPeriod = 5;
|
||||
indicator.LongPeriod = 50;
|
||||
Assert.Equal(5, indicator.ShortPeriod);
|
||||
Assert.Equal(50, indicator.LongPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaviIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new RaviIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaviIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new RaviIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Ravi.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaviIndicator_HasOneLineSeries_WithCorrectName()
|
||||
{
|
||||
var indicator = new RaviIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("RAVI", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,683 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RaviTests
|
||||
{
|
||||
// ============== A) Constructor & Parameter Validation ==============
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesShortPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ravi(0, 65));
|
||||
Assert.Throws<ArgumentException>(() => new Ravi(-1, 65));
|
||||
Assert.Throws<ArgumentException>(() => new Ravi(-100, 65));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesLongPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ravi(7, 0));
|
||||
Assert.Throws<ArgumentException>(() => new Ravi(7, -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesShortLessThanLong()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ravi(10, 10));
|
||||
Assert.Throws<ArgumentException>(() => new Ravi(20, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriods_Work()
|
||||
{
|
||||
var ravi = new Ravi();
|
||||
Assert.Contains("7", ravi.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("65", ravi.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomPeriods_Work()
|
||||
{
|
||||
var ravi = new Ravi(5, 50);
|
||||
Assert.Contains("5", ravi.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("50", ravi.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period1Short_Works()
|
||||
{
|
||||
var ravi = new Ravi(1, 2);
|
||||
Assert.NotNull(ravi);
|
||||
}
|
||||
|
||||
// ============== B) Basic Calculation ==============
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var ravi = new Ravi(7, 65);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ravi.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(ravi.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
|
||||
Assert.Equal(0, ravi.Last.Value);
|
||||
|
||||
var result = ravi.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(result.Value, ravi.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var ravi = new Ravi(7, 65);
|
||||
|
||||
Assert.Equal(0, ravi.Last.Value);
|
||||
Assert.False(ravi.IsHot);
|
||||
Assert.Contains("Ravi", ravi.Name, StringComparison.Ordinal);
|
||||
Assert.True(ravi.WarmupPeriod > 0);
|
||||
Assert.Equal(65, ravi.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantPrice_ReturnsZeroAfterWarmup()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
ravi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100));
|
||||
}
|
||||
|
||||
Assert.True(ravi.IsHot);
|
||||
Assert.Equal(0.0, ravi.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputAlwaysNonNegative()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: -0.5, sigma: 1.0);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = ravi.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.True(result.Value >= 0, $"RAVI must be non-negative, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
// ============== C) State Management & Bar Correction ==============
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
|
||||
ravi.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
ravi.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 105), isNew: true);
|
||||
|
||||
Assert.True(ravi.Last.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed 15 bars to get past warmup
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
ravi.Update(new TValue(bars[i].Time, bars[i].Close), isNew: true);
|
||||
}
|
||||
|
||||
double beforeUpdate = ravi.Last.Value;
|
||||
|
||||
// Correct with a very different value
|
||||
ravi.Update(new TValue(bars[14].Time, bars[14].Close * 2), isNew: false);
|
||||
double afterUpdate = ravi.Last.Value;
|
||||
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed first 19
|
||||
for (int i = 0; i < 19; i++)
|
||||
{
|
||||
ravi.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
// Feed 20th bar (isNew=true)
|
||||
ravi.Update(new TValue(bars[19].Time, bars[19].Close), true);
|
||||
|
||||
// Correct with modified value (isNew=false)
|
||||
double modifiedClose = bars[19].Close + 50.0;
|
||||
double val2 = ravi.Update(new TValue(bars[19].Time, modifiedClose), false).Value;
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var ravi2 = new Ravi(3, 10);
|
||||
for (int i = 0; i < 19; i++)
|
||||
{
|
||||
ravi2.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
double val3 = ravi2.Update(new TValue(bars[19].Time, modifiedClose), true).Value;
|
||||
|
||||
Assert.Equal(val3, val2, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed 15 new values
|
||||
TValue tenthValue = default;
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
tenthValue = new TValue(bars[i].Time, bars[i].Close);
|
||||
ravi.Update(tenthValue, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 15 values
|
||||
double stateAfter15 = ravi.Last.Value;
|
||||
|
||||
// Generate corrections with isNew=false (different values)
|
||||
for (int i = 15; i < 25; i++)
|
||||
{
|
||||
ravi.Update(new TValue(bars[i].Time, bars[i].Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 15th value again with isNew=false
|
||||
TValue finalResult = ravi.Update(tenthValue, isNew: false);
|
||||
|
||||
// State should match the original state after 15 values
|
||||
Assert.Equal(stateAfter15, finalResult.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_Works()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ravi.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(ravi.IsHot);
|
||||
|
||||
ravi.Reset();
|
||||
Assert.Equal(0, ravi.Last.Value);
|
||||
Assert.False(ravi.IsHot);
|
||||
|
||||
// After reset, should accept new values
|
||||
ravi.Update(new TValue(bars[0].Time, bars[0].Close));
|
||||
Assert.True(double.IsFinite(ravi.Last.Value));
|
||||
}
|
||||
|
||||
// ============== D) Warmup & Convergence ==============
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
|
||||
Assert.False(ravi.IsHot);
|
||||
|
||||
var baseTime = DateTime.UtcNow;
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
ravi.Update(new TValue(baseTime.AddMinutes(i), 100 + i));
|
||||
Assert.False(ravi.IsHot);
|
||||
}
|
||||
|
||||
// 10th value should make it hot (long period = 10)
|
||||
ravi.Update(new TValue(baseTime.AddMinutes(9), 109));
|
||||
Assert.True(ravi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_IsPeriodDependent()
|
||||
{
|
||||
var ravi7_65 = new Ravi(7, 65);
|
||||
var ravi3_10 = new Ravi(3, 10);
|
||||
|
||||
Assert.Equal(65, ravi7_65.WarmupPeriod);
|
||||
Assert.Equal(10, ravi3_10.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ============== E) NaN/Infinity Handling ==============
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
ravi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
// Feed NaN
|
||||
var resultAfterNaN = ravi.Update(new TValue(DateTime.UtcNow.AddMinutes(12), double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
ravi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
var resultAfterInf = ravi.Update(new TValue(DateTime.UtcNow.AddMinutes(12), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterInf.Value));
|
||||
|
||||
var resultAfterNegInf = ravi.Update(new TValue(DateTime.UtcNow.AddMinutes(13), double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleNaN_ContinuesWithLastValid()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
ravi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
// Feed several NaN values
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var result = ravi.Update(new TValue(DateTime.UtcNow.AddMinutes(12 + i), double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_Safe()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed normal values
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
ravi.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
// Feed NaN values
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var result = ravi.Update(new TValue(DateTime.UtcNow.AddHours(i + 1), double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
// Resume normal
|
||||
for (int i = 15; i < 25; i++)
|
||||
{
|
||||
var result = ravi.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// ============== F) Consistency Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var raviIterative = new Ravi(5, 20);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// Iterative
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var tv in series)
|
||||
{
|
||||
iterativeResults.Add(raviIterative.Update(tv));
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = Ravi.Batch(series, 5, 20);
|
||||
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TSeries_Update_MatchesStreaming()
|
||||
{
|
||||
var ravi1 = new Ravi(5, 20);
|
||||
var ravi2 = new Ravi(5, 20);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// Streaming
|
||||
foreach (var tv in series)
|
||||
{
|
||||
ravi1.Update(tv);
|
||||
}
|
||||
|
||||
// Batch via Update(TSeries)
|
||||
ravi2.Update(series);
|
||||
|
||||
Assert.Equal(ravi1.Last.Value, ravi2.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var ravi = new Ravi(5, 20);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// Streaming
|
||||
var streamResults = new double[100];
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
streamResults[i] = ravi.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
var values = series.Values.ToArray();
|
||||
var spanResults = new double[100];
|
||||
Ravi.Batch(values, spanResults, 5, 20);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], spanResults[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventBased_MatchesStreaming()
|
||||
{
|
||||
var ravi1 = new Ravi(5, 20);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// Collect event-based results
|
||||
var eventResults = new List<double>();
|
||||
ravi1.Pub += (object? _, in TValueEventArgs e) => eventResults.Add(e.Value.Value);
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
ravi1.Update(tv);
|
||||
}
|
||||
|
||||
// Collect streaming results
|
||||
var ravi2 = new Ravi(5, 20);
|
||||
var streamResults = new List<double>();
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
streamResults.Add(ravi2.Update(tv).Value);
|
||||
}
|
||||
|
||||
Assert.Equal(streamResults.Count, eventResults.Count);
|
||||
for (int i = 0; i < streamResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], eventResults[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
int shortP = 5;
|
||||
int longP = 20;
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch
|
||||
var batchSeries = Ravi.Batch(series, shortP, longP);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span
|
||||
var values = series.Values.ToArray();
|
||||
var spanOutput = new double[values.Length];
|
||||
Ravi.Batch(values, spanOutput, shortP, longP);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming
|
||||
var streamingInd = new Ravi(shortP, longP);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 4. Eventing
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Ravi(pubSource, shortP, longP);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
double eventingResult = eventingInd.Last.Value;
|
||||
|
||||
Assert.Equal(expected, spanResult, 1e-9);
|
||||
Assert.Equal(expected, streamingResult, 1e-9);
|
||||
Assert.Equal(expected, eventingResult, 1e-9);
|
||||
}
|
||||
|
||||
// ============== G) Span API Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesLengths()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[5]; // too small
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Ravi.Batch(source, output, 3, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesShortPeriod()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Ravi.Batch(source, output, 0, 10));
|
||||
Assert.Equal("shortPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesLongPeriod()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Ravi.Batch(source, output, 3, 0));
|
||||
Assert.Equal("longPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesShortLessThanLong()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Ravi.Batch(source, output, 10, 5));
|
||||
Assert.Equal("shortPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_EmptyInput_NoOp()
|
||||
{
|
||||
double[] source = Array.Empty<double>();
|
||||
double[] output = Array.Empty<double>();
|
||||
|
||||
var ex = Record.Exception(() => Ravi.Batch(source, output, 3, 10));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_NaN_HandledGracefully()
|
||||
{
|
||||
double[] source = { 100, 101, double.NaN, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112 };
|
||||
double[] output = new double[source.Length];
|
||||
|
||||
Ravi.Batch(source, output, 3, 10);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Output[{i}] should be finite but was {output[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesTSeriesCalc()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// TSeries path
|
||||
var tsResults = Ravi.Batch(series, 5, 20);
|
||||
|
||||
// Span path
|
||||
var values = series.Values.ToArray();
|
||||
var spanOutput = new double[values.Length];
|
||||
Ravi.Batch(values, spanOutput, 5, 20);
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
Assert.Equal(tsResults[i].Value, spanOutput[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// ============== H) Chainability ==============
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var ravi = new Ravi(5, 20);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var result = ravi.Update(series);
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.Equal(ravi.Last.Value, result.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PubEvent_Fires()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
int eventCount = 0;
|
||||
ravi.Pub += (object? _, in TValueEventArgs _) => eventCount++;
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
ravi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.Equal(15, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chaining_ViaConstructor_Works()
|
||||
{
|
||||
// Create a source SMA
|
||||
var sma = new Sma(5);
|
||||
var ravi = new Ravi(sma, 3, 10);
|
||||
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// When SMA updates, chained RAVI should also update
|
||||
foreach (var tv in series)
|
||||
{
|
||||
sma.Update(tv);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(ravi.Last.Value));
|
||||
}
|
||||
|
||||
// ============== RAVI-Specific Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void MonotonicallyIncreasing_ProducesPositiveRavi()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
// Feed monotonically increasing prices
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
ravi.Update(new TValue(baseTime.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.True(ravi.IsHot);
|
||||
Assert.True(ravi.Last.Value > 0, $"RAVI should be positive for trending market, got {ravi.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_Works()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var results = Ravi.Batch(series, 7, 65);
|
||||
|
||||
Assert.Equal(100, results.Count);
|
||||
Assert.True(double.IsFinite(results.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var (results, indicator) = Ravi.Calculate(series, 5, 20);
|
||||
|
||||
Assert.Equal(100, results.Count);
|
||||
Assert.NotNull(indicator);
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// RAVI Validation Tests — Self-consistency validation.
|
||||
/// No external library (TA-Lib, Skender, Tulip, Ooples) implements RAVI.
|
||||
/// Validation focuses on internal consistency and mathematical correctness.
|
||||
/// </summary>
|
||||
public sealed class RaviValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private bool _disposed;
|
||||
|
||||
public RaviValidationTests()
|
||||
{
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Self-Consistency ==============
|
||||
|
||||
[Fact]
|
||||
public void Validation_BatchMatchesStreaming()
|
||||
{
|
||||
int[][] paramSets = { new[] { 3, 10 }, new[] { 5, 20 }, new[] { 7, 65 } };
|
||||
var series = _testData.Data;
|
||||
|
||||
foreach (var ps in paramSets)
|
||||
{
|
||||
int shortP = ps[0];
|
||||
int longP = ps[1];
|
||||
|
||||
// Streaming
|
||||
var raviStream = new Ravi(shortP, longP);
|
||||
var streamResults = new List<double>();
|
||||
foreach (var tv in series)
|
||||
{
|
||||
streamResults.Add(raviStream.Update(tv).Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = Ravi.Batch(series, shortP, longP);
|
||||
|
||||
Assert.Equal(streamResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < streamResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_SpanMatchesStreaming()
|
||||
{
|
||||
int[][] paramSets = { new[] { 3, 10 }, new[] { 5, 20 }, new[] { 7, 65 } };
|
||||
var series = _testData.Data;
|
||||
int len = series.Count;
|
||||
|
||||
double[] values = series.Values.ToArray();
|
||||
|
||||
foreach (var ps in paramSets)
|
||||
{
|
||||
int shortP = ps[0];
|
||||
int longP = ps[1];
|
||||
|
||||
// Streaming
|
||||
var raviStream = new Ravi(shortP, longP);
|
||||
var streamResults = new double[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
streamResults[i] = raviStream.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
double[] spanResults = new double[len];
|
||||
Ravi.Batch(values, spanResults, shortP, longP);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], spanResults[i], 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Known-Value Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Validation_ConstantPrice_ZeroRavi()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var result = ravi.Update(new TValue(baseTime.AddMinutes(i), 100));
|
||||
if (ravi.IsHot)
|
||||
{
|
||||
Assert.Equal(0.0, result.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_EqualPeriods_ThrowsException()
|
||||
{
|
||||
// Short must be strictly less than long — equal throws
|
||||
Assert.Throws<ArgumentException>(() => new Ravi(10, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_WarmupBarsReturnZero()
|
||||
{
|
||||
var ravi = new Ravi(3, 10);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
// First 9 bars (before long SMA is full) should return 0
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var result = ravi.Update(new TValue(baseTime.AddMinutes(i), 100 + i));
|
||||
Assert.Equal(0.0, result.Value, 1e-10);
|
||||
Assert.False(ravi.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_DivByZero_ReturnsZero()
|
||||
{
|
||||
// If all prices are 0, SMA_long = 0 → division guard should produce 0
|
||||
var ravi = new Ravi(3, 10);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
var result = ravi.Update(new TValue(baseTime.AddMinutes(i), 0));
|
||||
Assert.Equal(0.0, result.Value, 1e-10);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Different Periods ==============
|
||||
|
||||
[Fact]
|
||||
public void Validation_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var ravi_3_10 = new Ravi(3, 10);
|
||||
var ravi_5_20 = new Ravi(5, 20);
|
||||
var ravi_7_65 = new Ravi(7, 65);
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.1, sigma: 0.3);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
ravi_3_10.Update(tv);
|
||||
ravi_5_20.Update(tv);
|
||||
ravi_7_65.Update(tv);
|
||||
}
|
||||
|
||||
// All should be finite and non-negative
|
||||
Assert.True(double.IsFinite(ravi_3_10.Last.Value));
|
||||
Assert.True(double.IsFinite(ravi_5_20.Last.Value));
|
||||
Assert.True(double.IsFinite(ravi_7_65.Last.Value));
|
||||
Assert.True(ravi_3_10.Last.Value >= 0);
|
||||
Assert.True(ravi_5_20.Last.Value >= 0);
|
||||
Assert.True(ravi_7_65.Last.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.3);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var (results, indicator) = Ravi.Calculate(series, 5, 20);
|
||||
|
||||
Assert.Equal(series.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_BarCorrection_Consistent()
|
||||
{
|
||||
var ravi1 = new Ravi(5, 20);
|
||||
var ravi2 = new Ravi(5, 20);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.3);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// Ravi1: feed all values normally
|
||||
foreach (var tv in series)
|
||||
{
|
||||
ravi1.Update(tv, isNew: true);
|
||||
}
|
||||
|
||||
// Ravi2: feed values with correction on last bar
|
||||
for (int i = 0; i < series.Count - 1; i++)
|
||||
{
|
||||
ravi2.Update(series[i], isNew: true);
|
||||
}
|
||||
// Feed wrong last value first
|
||||
ravi2.Update(new TValue(series[^1].Time, 999999), isNew: true);
|
||||
// Correct it
|
||||
ravi2.Update(series[^1], isNew: false);
|
||||
|
||||
Assert.Equal(ravi1.Last.Value, ravi2.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_Ravi_AlwaysNonNegative()
|
||||
{
|
||||
var ravi = new Ravi(7, 65);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 1.0);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
var result = ravi.Update(tv);
|
||||
Assert.True(result.Value >= 0, $"RAVI must be non-negative, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_Symmetry_UpAndDownTrends()
|
||||
{
|
||||
// A monotonic rise of +1/bar and a monotonic fall of -1/bar
|
||||
// should produce equal RAVI after warmup
|
||||
var raviUp = new Ravi(3, 10);
|
||||
var raviDown = new Ravi(3, 10);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
double basePrice = 1000;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
raviUp.Update(new TValue(baseTime.AddMinutes(i), basePrice + i));
|
||||
raviDown.Update(new TValue(baseTime.AddMinutes(i), basePrice - i));
|
||||
}
|
||||
|
||||
// Not exactly equal because normalization denominator differs,
|
||||
// but both should be positive and finite
|
||||
Assert.True(raviUp.Last.Value > 0);
|
||||
Assert.True(raviDown.Last.Value > 0);
|
||||
Assert.True(double.IsFinite(raviUp.Last.Value));
|
||||
Assert.True(double.IsFinite(raviDown.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_ManualKnownValue()
|
||||
{
|
||||
// Manual calculation: 5 bars, shortPeriod=2, longPeriod=5
|
||||
// Prices: 100, 102, 104, 106, 108
|
||||
// After 5 bars:
|
||||
// SMA_short(2) = (106 + 108) / 2 = 107
|
||||
// SMA_long(5) = (100 + 102 + 104 + 106 + 108) / 5 = 104
|
||||
// RAVI = |107 - 104| / 104 * 100 = 3/104 * 100 ≈ 2.884615...
|
||||
|
||||
var ravi = new Ravi(2, 5);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
ravi.Update(new TValue(baseTime, 100));
|
||||
ravi.Update(new TValue(baseTime.AddMinutes(1), 102));
|
||||
ravi.Update(new TValue(baseTime.AddMinutes(2), 104));
|
||||
ravi.Update(new TValue(baseTime.AddMinutes(3), 106));
|
||||
ravi.Update(new TValue(baseTime.AddMinutes(4), 108));
|
||||
|
||||
double expected = Math.Abs(107.0 - 104.0) / 104.0 * 100.0;
|
||||
Assert.Equal(expected, ravi.Last.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user