mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 21:18: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,326 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RsvIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RsvIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new RsvIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.True(indicator.Annualize);
|
||||
Assert.Equal(252, indicator.AnnualPeriods);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("RSV - Rogers-Satchell Volatility", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new RsvIndicator { Period = 14 };
|
||||
Assert.Contains("RSV", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new RsvIndicator();
|
||||
|
||||
Assert.Equal(0, RsvIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_Initialize_CreatesInternalRsv()
|
||||
{
|
||||
var indicator = new RsvIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RsvIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with varying volatility
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
double range = 2 + (i % 5); // Varying ranges
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + range, basePrice - range, basePrice + 1, 1000);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val >= 0, "Volatility should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RsvIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; 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 with larger range
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 120, 135, 105, 125, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var indicator = new RsvIndicator { Period = period };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
double range = 3 + (i % 4);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + range, basePrice - range, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"Period {period} should produce finite value");
|
||||
Assert.True(val >= 0, $"Period {period} should produce non-negative value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new RsvIndicator();
|
||||
Assert.Equal(20, indicator.Period);
|
||||
|
||||
indicator.Period = 14;
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 10;
|
||||
Assert.Equal(10, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_Annualize_CanBeToggled()
|
||||
{
|
||||
var indicator = new RsvIndicator();
|
||||
Assert.True(indicator.Annualize);
|
||||
|
||||
indicator.Annualize = false;
|
||||
Assert.False(indicator.Annualize);
|
||||
|
||||
indicator.Annualize = true;
|
||||
Assert.True(indicator.Annualize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_AnnualPeriods_CanBeChanged()
|
||||
{
|
||||
var indicator = new RsvIndicator();
|
||||
Assert.Equal(252, indicator.AnnualPeriods);
|
||||
|
||||
indicator.AnnualPeriods = 365;
|
||||
Assert.Equal(365, indicator.AnnualPeriods);
|
||||
|
||||
indicator.AnnualPeriods = 52;
|
||||
Assert.Equal(52, indicator.AnnualPeriods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new RsvIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new RsvIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Rsv.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_HighVolatility_ProducesHigherValue()
|
||||
{
|
||||
var indicator1 = new RsvIndicator { Period = 10, Annualize = false };
|
||||
var indicator2 = new RsvIndicator { Period = 10, Annualize = false };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Indicator 1: low volatility (narrow range)
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100;
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 1, basePrice - 1, basePrice + 0.5, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Indicator 2: high volatility (wide range)
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100;
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 10, basePrice - 10, basePrice + 2, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lowVol = indicator1.LinesSeries[0].GetValue(0);
|
||||
double highVol = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(lowVol));
|
||||
Assert.True(double.IsFinite(highVol));
|
||||
Assert.True(highVol > lowVol, "Higher volatility bars should produce higher RSV value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_AnnualizedValue_IsScaled()
|
||||
{
|
||||
var indicatorRaw = new RsvIndicator { Period = 10, Annualize = false };
|
||||
var indicatorAnn = new RsvIndicator { Period = 10, Annualize = true, AnnualPeriods = 252 };
|
||||
indicatorRaw.Initialize();
|
||||
indicatorAnn.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Same data for both
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100 + i * 0.5;
|
||||
indicatorRaw.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 3, basePrice - 3, basePrice + 1, 1000);
|
||||
indicatorRaw.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicatorAnn.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 3, basePrice - 3, basePrice + 1, 1000);
|
||||
indicatorAnn.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double rawValue = indicatorRaw.LinesSeries[0].GetValue(0);
|
||||
double annValue = indicatorAnn.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(rawValue));
|
||||
Assert.True(double.IsFinite(annValue));
|
||||
|
||||
// Annualized should be approximately sqrt(252) times larger
|
||||
double expectedRatio = Math.Sqrt(252);
|
||||
double actualRatio = annValue / rawValue;
|
||||
|
||||
Assert.True(Math.Abs(actualRatio - expectedRatio) < 0.01,
|
||||
$"Annualized value should be ~{expectedRatio:F2}× raw, got {actualRatio:F2}×");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_UsesAllOhlc_SensitiveToOpenClose()
|
||||
{
|
||||
// Test that RSV uses all OHLC prices (unlike HLV which only uses H-L)
|
||||
var indicator1 = new RsvIndicator { Period = 10, Annualize = false };
|
||||
var indicator2 = new RsvIndicator { Period = 10, Annualize = false };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Same high/low range but different open/close
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
// Indicator 1: open = close (doji pattern at center)
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Indicator 2: open and close at extremes (strong directional move)
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), 95.5, 105, 95, 104.5, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val1 = indicator1.LinesSeries[0].GetValue(0);
|
||||
double val2 = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val1));
|
||||
Assert.True(double.IsFinite(val2));
|
||||
// RSV should be different since it uses all OHLC prices
|
||||
Assert.NotEqual(val1, val2, 5); // Values should differ significantly
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_ConstantPrice_ProducesZeroVolatility()
|
||||
{
|
||||
var indicator = new RsvIndicator { Period = 10, Annualize = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Constant price (no volatility) - but need small spread to avoid log(1) issues
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 100.01, 99.99, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val < 0.01, "Near-constant price should produce near-zero volatility");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RsvIndicator_DriftAdjusted_HandlesUptrend()
|
||||
{
|
||||
// RSV is drift-adjusted, so should handle trending markets well
|
||||
var indicator = new RsvIndicator { Period = 10, Annualize = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Strong uptrend with consistent volatility
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100 + i * 2; // Trending up
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 3, basePrice - 2, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val > 0, "Trending market with volatility should produce positive RSV");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,766 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
using Xunit;
|
||||
|
||||
public class RsvTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
private static TBarSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var rsv = new Rsv();
|
||||
Assert.Equal(20, rsv.Period);
|
||||
Assert.True(rsv.Annualize);
|
||||
Assert.Equal(252, rsv.AnnualPeriods);
|
||||
Assert.Equal("Rsv(20)", rsv.Name);
|
||||
Assert.Equal(20, rsv.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParameters_SetsCorrectValues()
|
||||
{
|
||||
var rsv = new Rsv(period: 10, annualize: false, annualPeriods: 365);
|
||||
Assert.Equal(10, rsv.Period);
|
||||
Assert.False(rsv.Annualize);
|
||||
Assert.Equal(365, rsv.AnnualPeriods);
|
||||
Assert.Equal("Rsv(10)", rsv.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rsv(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rsv(period: -1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroAnnualPeriodsWhenAnnualizing_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rsv(period: 10, annualize: true, annualPeriods: 0));
|
||||
Assert.Equal("annualPeriods", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroAnnualPeriodsWhenNotAnnualizing_DoesNotThrow()
|
||||
{
|
||||
var rsv = new Rsv(period: 10, annualize: false, annualPeriods: 0);
|
||||
Assert.Equal(0, rsv.AnnualPeriods);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_SingleBar_ReturnsNonNegativeValue()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bar = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
|
||||
var result = rsv.Update(bar);
|
||||
|
||||
Assert.True(result.Value >= 0, "RSV should return non-negative values");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleBars_ReturnsCorrectCount()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bars = GenerateTestData(10);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(rsv.IsHot, "Indicator should be hot after warmup period");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsLastValue()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bar = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
|
||||
var result = rsv.Update(bar);
|
||||
|
||||
Assert.Equal(result.Value, rsv.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithoutAnnualization_ReturnsSmallerValues()
|
||||
{
|
||||
var rsvAnnual = new Rsv(period: 10, annualize: true, annualPeriods: 252);
|
||||
var rsvNoAnnual = new Rsv(period: 10, annualize: false);
|
||||
var bars = GenerateTestData(20);
|
||||
|
||||
double lastAnnual = 0;
|
||||
double lastNoAnnual = 0;
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
lastAnnual = rsvAnnual.Update(bars[i]).Value;
|
||||
lastNoAnnual = rsvNoAnnual.Update(bars[i]).Value;
|
||||
}
|
||||
|
||||
// Annualized values should be larger by factor of sqrt(252)
|
||||
Assert.True(lastAnnual > lastNoAnnual, "Annualized values should be larger");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Management Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102.0, 107.0, 100.0, 105.0, 1000);
|
||||
|
||||
rsv.Update(bar1, isNew: true);
|
||||
var result1 = rsv.Last.Value;
|
||||
|
||||
rsv.Update(bar2, isNew: true);
|
||||
var result2 = rsv.Last.Value;
|
||||
|
||||
Assert.NotEqual(result1, result2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
|
||||
|
||||
rsv.Update(bar1, isNew: true);
|
||||
var firstValue = rsv.Last.Value;
|
||||
|
||||
// Update the same bar with different OHLC values
|
||||
var bar1Updated = new TBar(DateTime.UtcNow, 99.0, 110.0, 95.0, 108.0, 1000);
|
||||
rsv.Update(bar1Updated, isNew: false);
|
||||
var updatedValue = rsv.Last.Value;
|
||||
|
||||
Assert.NotEqual(firstValue, updatedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoresState()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bars = GenerateTestData(10);
|
||||
|
||||
// Process first 5 bars
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rsv.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Add bar 6 and correct multiple times
|
||||
rsv.Update(bars[5], isNew: true);
|
||||
rsv.Update(bars[5], isNew: false);
|
||||
rsv.Update(bars[5], isNew: false);
|
||||
rsv.Update(bars[5], isNew: false);
|
||||
|
||||
// Now continue with bar 7
|
||||
rsv.Update(bars[6], isNew: true);
|
||||
|
||||
// Create new instance and process same data
|
||||
var rsv2 = new Rsv(period: 5);
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
rsv2.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(rsv.Last.Value, rsv2.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHot and Warmup Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BeforeWarmup_ReturnsFalse()
|
||||
{
|
||||
var rsv = new Rsv(period: 10);
|
||||
var bars = GenerateTestData(5);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.False(rsv.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterWarmup_ReturnsTrue()
|
||||
{
|
||||
var rsv = new Rsv(period: 10);
|
||||
var bars = GenerateTestData(15);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(rsv.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_ExactlyAtWarmup_ReturnsTrue()
|
||||
{
|
||||
var rsv = new Rsv(period: 10);
|
||||
var bars = GenerateTestData(10);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(rsv.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bars = GenerateTestData(10);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
|
||||
rsv.Reset();
|
||||
|
||||
Assert.False(rsv.IsHot);
|
||||
Assert.Equal(0, rsv.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReprocessing()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bars = GenerateTestData(10);
|
||||
|
||||
// First pass
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
var firstResult = rsv.Last.Value;
|
||||
|
||||
// Reset and second pass
|
||||
rsv.Reset();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
var secondResult = rsv.Last.Value;
|
||||
|
||||
Assert.Equal(firstResult, secondResult, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Robustness Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNaNValues_UsesLastValidVariance()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bars = GenerateTestData(10);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rsv.Last.Value;
|
||||
|
||||
// Bar with NaN high - should use last valid RS variance
|
||||
var nanBar = new TBar(DateTime.UtcNow, 100.0, double.NaN, 98.0, 102.0, 1000);
|
||||
var result = rsv.Update(nanBar);
|
||||
|
||||
// Result should be finite and close to previous (SMA smoothed)
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid variance");
|
||||
Assert.True(result.Value >= 0, "Volatility should be non-negative");
|
||||
double relativeDiff = Math.Abs(result.Value - valueBeforeInvalid) / valueBeforeInvalid;
|
||||
Assert.True(relativeDiff < 0.2, $"Value should be similar to previous: {valueBeforeInvalid} vs {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithInfinityValues_UsesLastValidVariance()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bars = GenerateTestData(10);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rsv.Last.Value;
|
||||
|
||||
// Bar with infinity - should use last valid RS variance
|
||||
var infBar = new TBar(DateTime.UtcNow, 100.0, double.PositiveInfinity, 98.0, 102.0, 1000);
|
||||
var result = rsv.Update(infBar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid variance");
|
||||
Assert.True(result.Value >= 0, "Volatility should be non-negative");
|
||||
double relativeDiff = Math.Abs(result.Value - valueBeforeInvalid) / valueBeforeInvalid;
|
||||
Assert.True(relativeDiff < 0.2, $"Value should be similar to previous: {valueBeforeInvalid} vs {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithZeroPrices_UsesLastValidVariance()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bars = GenerateTestData(10);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rsv.Last.Value;
|
||||
|
||||
// Bar with zero low (invalid for log) - should use last valid RS variance
|
||||
var zeroBar = new TBar(DateTime.UtcNow, 100.0, 105.0, 0.0, 102.0, 1000);
|
||||
var result = rsv.Update(zeroBar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid variance");
|
||||
Assert.True(result.Value >= 0, "Volatility should be non-negative");
|
||||
double relativeDiff = Math.Abs(result.Value - valueBeforeInvalid) / valueBeforeInvalid;
|
||||
Assert.True(relativeDiff < 0.2, $"Value should be similar to previous: {valueBeforeInvalid} vs {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNegativePrices_UsesLastValidVariance()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var bars = GenerateTestData(10);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
var valueBeforeInvalid = rsv.Last.Value;
|
||||
|
||||
// Bar with negative price - should use last valid RS variance
|
||||
var negBar = new TBar(DateTime.UtcNow, 100.0, 105.0, -98.0, 102.0, 1000);
|
||||
var result = rsv.Update(negBar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite when using last valid variance");
|
||||
Assert.True(result.Value >= 0, "Volatility should be non-negative");
|
||||
double relativeDiff = Math.Abs(result.Value - valueBeforeInvalid) / valueBeforeInvalid;
|
||||
Assert.True(relativeDiff < 0.2, $"Value should be similar to previous: {valueBeforeInvalid} vs {result.Value}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch and Series Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_MatchesStreamingResults()
|
||||
{
|
||||
const int dataCount = 100;
|
||||
var bars = GenerateTestData(dataCount);
|
||||
|
||||
// Streaming
|
||||
var rsvStreaming = new Rsv(period: 10);
|
||||
var streamingResults = new double[dataCount];
|
||||
for (int i = 0; i < dataCount; i++)
|
||||
{
|
||||
streamingResults[i] = rsvStreaming.Update(bars[i]).Value;
|
||||
}
|
||||
|
||||
// Batch (RSV uses all OHLC)
|
||||
var opens = new double[dataCount];
|
||||
var highs = new double[dataCount];
|
||||
var lows = new double[dataCount];
|
||||
var closes = new double[dataCount];
|
||||
var batchResults = new double[dataCount];
|
||||
|
||||
for (int i = 0; i < dataCount; i++)
|
||||
{
|
||||
opens[i] = bars[i].Open;
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
Rsv.Batch(opens, highs, lows, closes, batchResults, period: 10);
|
||||
|
||||
// Compare last 50 values (after warmup)
|
||||
for (int i = 50; i < dataCount; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResults[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_TBarSeries_ReturnsCorrectLength()
|
||||
{
|
||||
const int dataCount = 50;
|
||||
var barSeries = GenerateTestData(dataCount);
|
||||
|
||||
var result = Rsv.Batch(barSeries, period: 10);
|
||||
|
||||
Assert.Equal(dataCount, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TBarSeries_MatchesStreamingResults()
|
||||
{
|
||||
const int dataCount = 50;
|
||||
var barSeries = GenerateTestData(dataCount);
|
||||
|
||||
// Series update
|
||||
var rsvSeries = new Rsv(period: 10);
|
||||
var seriesResult = rsvSeries.Update(barSeries);
|
||||
|
||||
// Streaming
|
||||
var rsvStreaming = new Rsv(period: 10);
|
||||
var streamingResults = new double[dataCount];
|
||||
for (int i = 0; i < dataCount; i++)
|
||||
{
|
||||
streamingResults[i] = rsvStreaming.Update(barSeries[i]).Value;
|
||||
}
|
||||
|
||||
// Compare last 30 values
|
||||
for (int i = 20; i < dataCount; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], seriesResult.Values[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_DoesNotThrow()
|
||||
{
|
||||
var opens = Array.Empty<double>();
|
||||
var highs = Array.Empty<double>();
|
||||
var lows = Array.Empty<double>();
|
||||
var closes = Array.Empty<double>();
|
||||
var output = Array.Empty<double>();
|
||||
|
||||
// Should not throw
|
||||
Rsv.Batch(opens, highs, lows, closes, output, period: 10);
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
var opens = new double[10];
|
||||
var highs = new double[10];
|
||||
var lows = new double[5]; // Mismatched
|
||||
var closes = new double[10];
|
||||
var output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rsv.Batch(opens, highs, lows, closes, output, period: 10));
|
||||
Assert.Equal("close", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooShort_ThrowsArgumentException()
|
||||
{
|
||||
var opens = new double[10];
|
||||
var highs = new double[10];
|
||||
var lows = new double[10];
|
||||
var closes = new double[10];
|
||||
var output = new double[5]; // Too short
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rsv.Batch(opens, highs, lows, closes, output, period: 10));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var opens = new double[10];
|
||||
var highs = new double[10];
|
||||
var lows = new double[10];
|
||||
var closes = new double[10];
|
||||
var output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Rsv.Batch(opens, highs, lows, closes, output, period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Publishing Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_PublishesEvent()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
bool eventFired = false;
|
||||
rsv.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
|
||||
rsv.Update(bar);
|
||||
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChainedIndicator_ReceivesValues()
|
||||
{
|
||||
var source = new Rsv(period: 5);
|
||||
var downstream = new Sma(source, period: 3);
|
||||
|
||||
var bars = GenerateTestData(10);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
source.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(downstream.Last.Value > 0, "Downstream indicator should receive values");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TValue Update Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_TreatsAsPrecomputedVariance()
|
||||
{
|
||||
var rsv1 = new Rsv(period: 5);
|
||||
var rsv2 = new Rsv(period: 5);
|
||||
|
||||
// For rsv1, use bar data
|
||||
var bar = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
|
||||
rsv1.Update(bar);
|
||||
|
||||
// For rsv2, use pre-computed RS variance value
|
||||
// Compute manually following the formula
|
||||
double o = 100.0, h = 105.0, l = 98.0, c = 102.0;
|
||||
double term1 = Math.Log(h / o);
|
||||
double term2 = Math.Log(h / c);
|
||||
double term3 = Math.Log(l / o);
|
||||
double term4 = Math.Log(l / c);
|
||||
double rsVariance = (term1 * term2) + (term3 * term4);
|
||||
|
||||
var tvalue = new TValue(bar.Time, rsVariance);
|
||||
rsv2.Update(tvalue);
|
||||
|
||||
Assert.Equal(rsv1.Last.Value, rsv2.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RSV-Specific Tests
|
||||
|
||||
[Fact]
|
||||
public void Rsv_UsesAllOhlcPrices()
|
||||
{
|
||||
// RSV uses all OHLC, so changing Open should affect result (unlike HLV)
|
||||
var rsv1 = new Rsv(period: 5);
|
||||
var rsv2 = new Rsv(period: 5);
|
||||
|
||||
// Bars with same H-L-C but different Open
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100.0, 105.0, 98.0, 102.0, 1000);
|
||||
var bar2 = new TBar(DateTime.UtcNow, 99.0, 105.0, 98.0, 102.0, 1000); // Different Open
|
||||
|
||||
var result1 = rsv1.Update(bar1).Value;
|
||||
var result2 = rsv2.Update(bar2).Value;
|
||||
|
||||
// Results should be different since Open matters for RSV
|
||||
Assert.NotEqual(result1, result2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rsv_UsesSmaMakesSmoothTransitions()
|
||||
{
|
||||
// SMA should produce smoother transitions than RMA
|
||||
var rsv = new Rsv(period: 10);
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
var results = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
results[i] = rsv.Update(bars[i]).Value;
|
||||
}
|
||||
|
||||
// Check that results don't have extreme jumps after warmup
|
||||
for (int i = 11; i < bars.Count; i++)
|
||||
{
|
||||
double change = Math.Abs(results[i] - results[i - 1]);
|
||||
double avg = (results[i] + results[i - 1]) / 2;
|
||||
if (avg > 0.001) // Avoid division by very small numbers
|
||||
{
|
||||
double relativeChange = change / avg;
|
||||
Assert.True(relativeChange < 0.5, $"SMA should produce smooth transitions: change={relativeChange:P} at index {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rsv_DriftAdjusted_HandlesTrendingMarket()
|
||||
{
|
||||
// RSV is drift-adjusted, so trending markets should still produce reasonable volatility
|
||||
var rsv = new Rsv(period: 10, annualize: false);
|
||||
|
||||
// Create trending bars (each bar higher than previous)
|
||||
var bars = new TBarSeries();
|
||||
double basePrice = 100.0;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double trend = i * 0.5; // Upward trend
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
basePrice + trend,
|
||||
basePrice + trend + 2.0,
|
||||
basePrice + trend - 1.0,
|
||||
basePrice + trend + 1.5,
|
||||
1000
|
||||
);
|
||||
bars.Add(bar);
|
||||
rsv.Update(bar);
|
||||
}
|
||||
|
||||
// RSV should still produce reasonable (non-inflated) volatility despite drift
|
||||
Assert.True(rsv.Last.Value > 0, "RSV should be positive");
|
||||
Assert.True(rsv.Last.Value < 1.0, "RSV (non-annualized) should be reasonable despite trending market");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_Performance()
|
||||
{
|
||||
var rsv = new Rsv(period: 20);
|
||||
var bars = GenerateTestData(5000);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = rsv.Update(bars[i]);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentParameters_ProduceDistinctValues()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
var rsv1 = new Rsv(period: 10);
|
||||
var rsv2 = new Rsv(period: 20);
|
||||
var rsv3 = new Rsv(period: 10, annualize: false);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv1.Update(bars[i]);
|
||||
rsv2.Update(bars[i]);
|
||||
rsv3.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rsv1.Last.Value));
|
||||
Assert.True(double.IsFinite(rsv2.Last.Value));
|
||||
Assert.True(double.IsFinite(rsv3.Last.Value));
|
||||
// Different parameters should produce different values
|
||||
Assert.NotEqual(rsv1.Last.Value, rsv2.Last.Value);
|
||||
Assert.NotEqual(rsv1.Last.Value, rsv3.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticCalculate_Works()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
var result = Rsv.Batch(bars, period: 14);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
Assert.True(double.IsFinite(result[result.Count - 1].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticCalculate_ValidatesInput()
|
||||
{
|
||||
var bars = GenerateTestData(10);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Rsv.Batch(bars, period: 0));
|
||||
Assert.Throws<ArgumentException>(() => Rsv.Batch(bars, period: -1));
|
||||
Assert.Throws<ArgumentException>(() => Rsv.Batch(bars, period: 10, annualize: true, annualPeriods: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_Works()
|
||||
{
|
||||
var rsv = new Rsv(period: 5);
|
||||
var values = new double[] { 0.001, 0.002, 0.0015, 0.0018, 0.0012, 0.0022 };
|
||||
|
||||
rsv.Prime(values);
|
||||
|
||||
Assert.True(rsv.IsHot);
|
||||
Assert.True(double.IsFinite(rsv.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_MatchesInstanceUpdate()
|
||||
{
|
||||
var bars = GenerateTestData(120);
|
||||
var variances = new TSeries();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
// Pre-compute same RS variance formula used by RSV
|
||||
double o = Math.Max(bars[i].Open, 1e-10);
|
||||
double h = Math.Max(bars[i].High, 1e-10);
|
||||
double l = Math.Max(bars[i].Low, 1e-10);
|
||||
double c = Math.Max(bars[i].Close, 1e-10);
|
||||
double term1 = Math.Log(h / o);
|
||||
double term2 = Math.Log(h / c);
|
||||
double term3 = Math.Log(l / o);
|
||||
double term4 = Math.Log(l / c);
|
||||
variances.Add(bars[i].Time, Math.FusedMultiplyAdd(term1, term2, term3 * term4));
|
||||
}
|
||||
|
||||
var batch = Rsv.Batch(variances, period: 10, annualize: false);
|
||||
var instance = new Rsv(period: 10, annualize: false);
|
||||
var stream = instance.Update(variances);
|
||||
|
||||
Assert.Equal(batch.Count, stream.Count);
|
||||
for (int i = 0; i < batch.Count; i++)
|
||||
{
|
||||
Assert.Equal(stream[i].Value, batch[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsConfiguredIndicatorAndMatchingResults()
|
||||
{
|
||||
var bars = GenerateTestData(150);
|
||||
|
||||
var (results, indicator) = Rsv.Calculate(bars, period: 14, annualize: true, annualPeriods: 252);
|
||||
var batch = Rsv.Batch(bars, period: 14, annualize: true, annualPeriods: 252);
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
Assert.Equal(14, indicator.WarmupPeriod);
|
||||
Assert.Equal(batch.Count, results.Count);
|
||||
|
||||
for (int i = 0; i < batch.Count; i++)
|
||||
{
|
||||
Assert.Equal(batch[i].Value, results[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
namespace QuanTAlib.Test;
|
||||
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for RSV (Rogers-Satchell Volatility).
|
||||
/// RSV is an OHLC-based volatility estimator with drift adjustment.
|
||||
/// Formula: rs_variance = log(H/O)*log(H/C) + log(L/O)*log(L/C)
|
||||
/// SMA smoothing applied (not RMA like HLV).
|
||||
/// </summary>
|
||||
public class RsvValidationTests
|
||||
{
|
||||
private static TBarSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
// === Mathematical Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates the Rogers-Satchell variance formula:
|
||||
/// rs_variance = log(H/O)*log(H/C) + log(L/O)*log(L/C)
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_RsVarianceFormula_IsCorrect()
|
||||
{
|
||||
double open = 100.0;
|
||||
double high = 105.0;
|
||||
double low = 95.0;
|
||||
double close = 102.0;
|
||||
|
||||
double lnHO = Math.Log(high / open); // log(105/100) ≈ 0.04879
|
||||
double lnHC = Math.Log(high / close); // log(105/102) ≈ 0.02899
|
||||
double lnLO = Math.Log(low / open); // log(95/100) ≈ -0.05129
|
||||
double lnLC = Math.Log(low / close); // log(95/102) ≈ -0.07115
|
||||
|
||||
double term1 = lnHO * lnHC; // positive * positive = positive
|
||||
double term2 = lnLO * lnLC; // negative * negative = positive
|
||||
|
||||
double rsVariance = term1 + term2;
|
||||
|
||||
Assert.True(rsVariance >= 0, "RS variance should be non-negative for valid OHLC");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates RS variance is zero for flat bar (O=H=L=C).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_FlatBar_ProducesZeroVariance()
|
||||
{
|
||||
double price = 100.0;
|
||||
|
||||
double lnHO = Math.Log(price / price); // log(1) = 0
|
||||
double lnHC = Math.Log(price / price); // log(1) = 0
|
||||
double lnLO = Math.Log(price / price); // log(1) = 0
|
||||
double lnLC = Math.Log(price / price); // log(1) = 0
|
||||
|
||||
double rsVariance = lnHO * lnHC + lnLO * lnLC; // 0
|
||||
|
||||
Assert.Equal(0.0, rsVariance, 15);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates SMA smoothing formula (unlike RMA used in HLV).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_UsesSmaSmoothing_NotRma()
|
||||
{
|
||||
// SMA sums values and divides by period
|
||||
// RMA uses exponential decay
|
||||
double[] values = { 1, 2, 3, 4, 5 };
|
||||
int period = 5;
|
||||
|
||||
double smaExpected = values.Average();
|
||||
Assert.Equal(3.0, smaExpected, 10);
|
||||
|
||||
// SMA is simple mean, not weighted
|
||||
double sum = values.Sum();
|
||||
double smaManual = sum / period;
|
||||
Assert.Equal(smaExpected, smaManual, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates annualization factor: √(annualPeriods)
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(252, 15.8745078663875)] // Daily trading days
|
||||
[InlineData(365, 19.1049731745428)] // Calendar days
|
||||
[InlineData(52, 7.21110255092798)] // Weekly
|
||||
[InlineData(12, 3.46410161513775)] // Monthly
|
||||
public void Rsv_AnnualizationFactor_IsCorrect(int annualPeriods, double expectedFactor)
|
||||
{
|
||||
double factor = Math.Sqrt(annualPeriods);
|
||||
Assert.Equal(expectedFactor, factor, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that wider range produces higher RS variance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_WiderRange_ProducesHigherVariance()
|
||||
{
|
||||
// Narrow range bar
|
||||
double narrowVar = ComputeRsVariance(100, 101, 99, 100);
|
||||
|
||||
// Wide range bar
|
||||
double wideVar = ComputeRsVariance(100, 110, 90, 100);
|
||||
|
||||
Assert.True(wideVar > narrowVar,
|
||||
"Wider range should produce higher RS variance");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that RSV uses all OHLC prices (unlike HLV which only uses H-L).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_UsesAllOhlc_SensitiveToOpenClose()
|
||||
{
|
||||
var rsv1 = new Rsv(14, annualize: false);
|
||||
var rsv2 = new Rsv(14, annualize: false);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
// Same high/low range but different open/close
|
||||
// Indicator 1: doji pattern (open ≈ close at center)
|
||||
var bar1 = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 105.0, 95.0, 100.0, 1000.0
|
||||
);
|
||||
rsv1.Update(bar1);
|
||||
|
||||
// Indicator 2: open and close at extremes
|
||||
var bar2 = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
95.5, 105.0, 95.0, 104.5, 1000.0
|
||||
);
|
||||
rsv2.Update(bar2);
|
||||
}
|
||||
|
||||
// RSV should be different since it uses all OHLC prices
|
||||
Assert.NotEqual(rsv1.Last.Value, rsv2.Last.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates drift adjustment property: RSV handles trending markets.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_DriftAdjusted_HandlesTrendingMarket()
|
||||
{
|
||||
var rsv = new Rsv(14, annualize: false);
|
||||
|
||||
// Strongly trending market (continuous up moves)
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100 + i * 2; // Strong uptrend
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
basePrice, basePrice + 3, basePrice - 2, basePrice + 2, 1000.0
|
||||
);
|
||||
rsv.Update(bar);
|
||||
}
|
||||
|
||||
// RSV should still produce valid volatility estimate
|
||||
Assert.True(double.IsFinite(rsv.Last.Value));
|
||||
Assert.True(rsv.Last.Value > 0, "Trending market with volatility should have positive RSV");
|
||||
}
|
||||
|
||||
// === Consistency Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates streaming and batch produce identical results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_StreamingMatchesBatch()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming calculation
|
||||
var streamingRsv = new Rsv(14);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingRsv.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Batch calculation
|
||||
var batchResult = Rsv.Batch(bars, 14);
|
||||
|
||||
// Compare last values
|
||||
Assert.Equal(batchResult.Last.Value, streamingRsv.Last.Value, 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TBarSeries input matches TBar streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_TBarSeriesInput_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingRsv = new Rsv(14);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingRsv.Update(bars[i]);
|
||||
}
|
||||
|
||||
// TBarSeries batch
|
||||
var batchRsv = new Rsv(14);
|
||||
var batchResult = batchRsv.Update(bars);
|
||||
|
||||
Assert.Equal(batchResult.Last.Value, streamingRsv.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates Span batch matches streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingRsv = new Rsv(14);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingRsv.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Extract OHLC arrays
|
||||
var opens = new double[bars.Count];
|
||||
var highs = new double[bars.Count];
|
||||
var lows = new double[bars.Count];
|
||||
var closes = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
opens[i] = bars[i].Open;
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
var output = new double[bars.Count];
|
||||
Rsv.Batch(opens, highs, lows, closes, output, 14);
|
||||
|
||||
Assert.Equal(output[^1], streamingRsv.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates annualized output is scaled correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_Annualized_ScaledCorrectly()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
// Non-annualized
|
||||
var rsvRaw = new Rsv(14, annualize: false);
|
||||
|
||||
// Annualized (default 252 periods)
|
||||
var rsvAnn = new Rsv(14, annualize: true, annualPeriods: 252);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsvRaw.Update(bars[i]);
|
||||
rsvAnn.Update(bars[i]);
|
||||
}
|
||||
|
||||
double expectedRatio = Math.Sqrt(252);
|
||||
double actualRatio = rsvAnn.Last.Value / rsvRaw.Last.Value;
|
||||
|
||||
Assert.Equal(expectedRatio, actualRatio, 6);
|
||||
}
|
||||
|
||||
// === Parameter Sensitivity ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates shorter period produces more responsive volatility.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_ShorterPeriod_MoreResponsive()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
var rsvShort = new Rsv(5);
|
||||
var rsvLong = new Rsv(20);
|
||||
|
||||
var shortResults = new List<double>();
|
||||
var longResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsvShort.Update(bars[i]);
|
||||
rsvLong.Update(bars[i]);
|
||||
|
||||
if (rsvShort.IsHot && rsvLong.IsHot)
|
||||
{
|
||||
shortResults.Add(rsvShort.Last.Value);
|
||||
longResults.Add(rsvLong.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Shorter period should have higher variance in results
|
||||
double shortVar = Variance(shortResults);
|
||||
double longVar = Variance(longResults);
|
||||
|
||||
Assert.True(shortResults.Count > 0, "Should have hot results");
|
||||
Assert.True(shortVar > longVar * 0.5,
|
||||
"Shorter period should generally be more variable");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates different periods produce different results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
var rsv10 = new Rsv(10);
|
||||
var rsv14 = new Rsv(14);
|
||||
var rsv20 = new Rsv(20);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv10.Update(bars[i]);
|
||||
rsv14.Update(bars[i]);
|
||||
rsv20.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.NotEqual(rsv10.Last.Value, rsv14.Last.Value);
|
||||
Assert.NotEqual(rsv14.Last.Value, rsv20.Last.Value);
|
||||
}
|
||||
|
||||
// === Edge Cases ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of very small ranges (tight consolidation).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_VerySmallRanges_HandledCorrectly()
|
||||
{
|
||||
var rsv = new Rsv(14);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 100.001, 99.999, 100.0, 1000.0
|
||||
);
|
||||
rsv.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rsv.Last.Value));
|
||||
Assert.True(rsv.Last.Value >= 0, "Volatility should be non-negative");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of very large ranges (high volatility).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_VeryLargeRanges_HandledCorrectly()
|
||||
{
|
||||
var rsv = new Rsv(14);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 200.0, 50.0, 150.0, 1000.0
|
||||
);
|
||||
rsv.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rsv.Last.Value));
|
||||
Assert.True(rsv.Last.Value > 0, "High volatility should produce positive value");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of constant bars (zero volatility).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_ConstantBars_ProducesMinimalVolatility()
|
||||
{
|
||||
var rsv = new Rsv(14);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
// Near-constant bars (small epsilon to avoid log issues)
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 100.001, 99.999, 100.0, 1000.0
|
||||
);
|
||||
rsv.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rsv.Last.Value));
|
||||
Assert.True(rsv.Last.Value < 0.01, "Near-constant price should produce near-zero volatility");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates warmup period calculation.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(10)]
|
||||
[InlineData(14)]
|
||||
[InlineData(20)]
|
||||
public void Rsv_WarmupPeriod_IsCorrect(int period)
|
||||
{
|
||||
var rsv = new Rsv(period);
|
||||
Assert.Equal(period, rsv.WarmupPeriod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates output is always non-negative (volatility property).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_Output_IsNonNegative()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var rsv = new Rsv(14);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
if (rsv.IsHot)
|
||||
{
|
||||
Assert.True(rsv.Last.Value >= 0,
|
||||
$"Volatility should be non-negative at bar {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates bar correction works correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_BarCorrection_WorksCorrectly()
|
||||
{
|
||||
var rsv = new Rsv(14);
|
||||
var bars = GenerateTestData(30);
|
||||
|
||||
// Feed initial bars
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rsv.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Add new bar
|
||||
rsv.Update(bars[20], isNew: true);
|
||||
double afterNew = rsv.Last.Value;
|
||||
|
||||
// Correct with different bar (much higher volatility)
|
||||
var correctedBar = new TBar(
|
||||
bars[20].Time,
|
||||
100, 200, 50, 150, 1000
|
||||
);
|
||||
rsv.Update(correctedBar, isNew: false);
|
||||
double afterCorrection = rsv.Last.Value;
|
||||
|
||||
// Restore original
|
||||
rsv.Update(bars[20], isNew: false);
|
||||
double afterRestore = rsv.Last.Value;
|
||||
|
||||
Assert.NotEqual(afterNew, afterCorrection);
|
||||
Assert.Equal(afterNew, afterRestore, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates iterative corrections converge to same result.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_IterativeCorrections_Converge()
|
||||
{
|
||||
var rsv = new Rsv(14);
|
||||
var bars = GenerateTestData(30);
|
||||
|
||||
// Feed bars and make corrections
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rsv.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Multiple corrections on same bar
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
var tempBar = new TBar(
|
||||
bars[19].Time,
|
||||
100 + j, 110 + j, 90 + j, 105 + j, 1000
|
||||
);
|
||||
rsv.Update(tempBar, isNew: false);
|
||||
}
|
||||
|
||||
// Final correction back to original
|
||||
rsv.Update(bars[19], isNew: false);
|
||||
double afterCorrections = rsv.Last.Value;
|
||||
|
||||
// Fresh calculation
|
||||
var rsvFresh = new Rsv(14);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rsvFresh.Update(bars[i], isNew: true);
|
||||
}
|
||||
double freshValue = rsvFresh.Last.Value;
|
||||
|
||||
Assert.Equal(freshValue, afterCorrections, 10);
|
||||
}
|
||||
|
||||
// === Comparison with Other Volatility Estimators ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates RSV vs HLV: RSV uses O-C, HLV ignores O-C.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_VsHlv_DifferentBehavior()
|
||||
{
|
||||
var rsv = new Rsv(14, annualize: false);
|
||||
var hlv = new Hlv(14, annualize: false);
|
||||
|
||||
// Same bars
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
// Directional bar (O != C)
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 105.0, 95.0, 104.0, 1000.0
|
||||
);
|
||||
rsv.Update(bar);
|
||||
hlv.Update(bar);
|
||||
}
|
||||
|
||||
// Both should produce positive values
|
||||
Assert.True(rsv.Last.Value > 0);
|
||||
Assert.True(hlv.Last.Value > 0);
|
||||
|
||||
// They should be different since RSV uses O-C while HLV ignores it
|
||||
Assert.NotEqual(rsv.Last.Value, hlv.Last.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates RSV vs GKV: both use OHLC but different formulas.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_VsGkv_DifferentValues()
|
||||
{
|
||||
var rsv = new Rsv(14, annualize: false);
|
||||
var gkv = new Gkv(14, annualize: false);
|
||||
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
gkv.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Both should produce positive values
|
||||
Assert.True(rsv.Last.Value > 0);
|
||||
Assert.True(gkv.Last.Value > 0);
|
||||
|
||||
// They should be similar but not identical (different formulas)
|
||||
Assert.NotEqual(rsv.Last.Value, gkv.Last.Value);
|
||||
}
|
||||
|
||||
// === Stability Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates RSV stability over repeated runs with same seed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_Stability_ConsistentOverRepeatedRuns()
|
||||
{
|
||||
// Multiple runs with same seed should produce identical results
|
||||
var results = new List<double>();
|
||||
|
||||
for (int run = 0; run < 3; run++)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var rsv = new Rsv(14);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
results.Add(rsv.Last.Value);
|
||||
}
|
||||
|
||||
// All runs should be identical
|
||||
Assert.Equal(results[0], results[1], 15);
|
||||
Assert.Equal(results[1], results[2], 15);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates RSV responds to volatility regime changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_RespondsToVolatilityRegimeChange()
|
||||
{
|
||||
var rsv = new Rsv(10);
|
||||
|
||||
// Low volatility regime
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 101.0, 99.0, 100.5, 1000.0 // 2% range
|
||||
);
|
||||
rsv.Update(bar);
|
||||
}
|
||||
double lowVolValue = rsv.Last.Value;
|
||||
|
||||
// High volatility regime
|
||||
for (int i = 20; i < 40; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 110.0, 90.0, 105.0, 1000.0 // 20% range
|
||||
);
|
||||
rsv.Update(bar);
|
||||
}
|
||||
double highVolValue = rsv.Last.Value;
|
||||
|
||||
Assert.True(highVolValue > lowVolValue * 2,
|
||||
"RSV should significantly increase with higher volatility regime");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates RSV produces reasonable volatility estimate.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_ProducesReasonableVolatilityEstimate()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var rsv = new Rsv(14, annualize: false);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rsv.Update(bars[i]);
|
||||
}
|
||||
|
||||
// RSV should be positive and finite
|
||||
Assert.True(double.IsFinite(rsv.Last.Value));
|
||||
Assert.True(rsv.Last.Value > 0);
|
||||
Assert.True(rsv.Last.Value < 10, "Raw volatility should be reasonable (< 1000%)");
|
||||
}
|
||||
|
||||
// === SMA vs RMA Smoothing Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates that RSV uses SMA (not RMA like HLV).
|
||||
/// SMA should adapt faster to changes when period is small.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rsv_SmaSmoothing_AdaptsToChange()
|
||||
{
|
||||
var rsv = new Rsv(5, annualize: false);
|
||||
|
||||
// Low volatility phase
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 101.0, 99.0, 100.0, 1000.0
|
||||
);
|
||||
rsv.Update(bar);
|
||||
}
|
||||
double lowVolValue = rsv.Last.Value;
|
||||
|
||||
// Sudden high volatility (5 bars = full SMA window)
|
||||
for (int i = 10; i < 15; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 120.0, 80.0, 100.0, 1000.0
|
||||
);
|
||||
rsv.Update(bar);
|
||||
}
|
||||
double afterHighVolSma = rsv.Last.Value;
|
||||
|
||||
// With SMA (period=5), after 5 high-vol bars the old low-vol values should be gone
|
||||
// Value should be significantly higher
|
||||
Assert.True(afterHighVolSma > lowVolValue * 3,
|
||||
"SMA should fully adapt after period bars");
|
||||
}
|
||||
|
||||
// === Helper Methods ===
|
||||
|
||||
private static double ComputeRsVariance(double open, double high, double low, double close)
|
||||
{
|
||||
// Protect against division by zero
|
||||
open = Math.Max(open, 1e-10);
|
||||
close = Math.Max(close, 1e-10);
|
||||
|
||||
double lnHO = Math.Log(high / open);
|
||||
double lnHC = Math.Log(high / close);
|
||||
double lnLO = Math.Log(low / open);
|
||||
double lnLC = Math.Log(low / close);
|
||||
|
||||
return lnHO * lnHC + lnLO * lnLC;
|
||||
}
|
||||
|
||||
private static double Variance(List<double> values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
double mean = values.Average();
|
||||
return values.Average(v => Math.Pow(v - mean, 2));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user