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

- Remove 'C# Implementation Considerations' sections from 34 indicator .md files
- Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.)
- Move test files into tests/ subdirectories for consistent project structure
- Add trader-focused bullet points to indicator documentation
This commit is contained in:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -0,0 +1,340 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class YzvIndicatorTests
{
[Fact]
public void YzvIndicator_Constructor_SetsDefaults()
{
var indicator = new YzvIndicator();
Assert.Equal(20, indicator.Period);
Assert.True(indicator.ShowColdValues);
Assert.Equal("YZV - Yang-Zhang Volatility", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void YzvIndicator_ShortName_IncludesParameters()
{
var indicator = new YzvIndicator { Period = 30 };
Assert.Contains("YZV", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("30", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void YzvIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new YzvIndicator();
Assert.Equal(0, YzvIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void YzvIndicator_Initialize_CreatesInternalYzv()
{
var indicator = new YzvIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void YzvIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new YzvIndicator { Period = 10 };
indicator.Initialize();
// Add historical data with varying volatility
var now = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
// Create price movement that generates volatility
double basePrice = 100 + Math.Sin(i * 0.3) * (5 + i * 0.1);
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 2, basePrice - 2, basePrice, 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, "YZV should be non-negative");
}
[Fact]
public void YzvIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new YzvIndicator { 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 + 2, basePrice - 2, basePrice + 1, 1000);
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar
indicator.HistoricalData.AddBar(now.AddMinutes(30), 130, 135, 125, 132, 1500);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void YzvIndicator_DifferentPeriods_Work()
{
var periods = new[] { 5, 10, 20, 30 };
foreach (int period in periods)
{
var indicator = new YzvIndicator { Period = period };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 60; i++)
{
// Create price movement with varying amplitude
double basePrice = 100 + Math.Sin(i * 0.2) * 5;
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 2, basePrice - 2, basePrice, 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 YzvIndicator_Period_CanBeChanged()
{
var indicator = new YzvIndicator();
Assert.Equal(20, indicator.Period);
indicator.Period = 30;
Assert.Equal(30, indicator.Period);
indicator.Period = 10;
Assert.Equal(10, indicator.Period);
}
[Fact]
public void YzvIndicator_ShowColdValues_CanBeToggled()
{
var indicator = new YzvIndicator();
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
indicator.ShowColdValues = true;
Assert.True(indicator.ShowColdValues);
}
[Fact]
public void YzvIndicator_SourceCodeLink_IsValid()
{
var indicator = new YzvIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Yzv.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void YzvIndicator_ConstantPrice_ProducesNearZero()
{
var indicator = new YzvIndicator { Period = 10 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Constant price - no volatility
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, "Constant price should produce near-zero YZV");
}
[Fact]
public void YzvIndicator_HighVolatility_ProducesPositiveValue()
{
var indicator = new YzvIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
// High volatility with large price swings
for (int i = 0; i < 30; i++)
{
double price = 100 + (i % 2 == 0 ? 10 : -10); // Large oscillations
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 5, price - 5, price, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val));
Assert.True(val > 0, "High volatility should produce positive YZV value");
}
[Fact]
public void YzvIndicator_UsesOHLC_ForCalculation()
{
// YZV uses full OHLC for calculation (overnight + intraday components)
var indicator = new YzvIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Price with varying OHLC
for (int i = 0; i < 20; i++)
{
double open = 100 + Math.Sin(i * 0.3) * 3;
double high = open + 2 + Math.Abs(Math.Sin(i * 0.5));
double low = open - 2 - Math.Abs(Math.Cos(i * 0.5));
double close = open + Math.Sin(i * 0.4) * 2;
indicator.HistoricalData.AddBar(now.AddMinutes(i), open, high, low, close, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val));
Assert.True(val >= 0, "YZV should be non-negative");
}
[Fact]
public void YzvIndicator_LargerPeriod_SmootherOutput()
{
var indicator1 = new YzvIndicator { Period = 5 };
var indicator2 = new YzvIndicator { Period = 20 };
indicator1.Initialize();
indicator2.Initialize();
var now = DateTime.UtcNow;
var results1 = new List<double>();
var results2 = new List<double>();
for (int i = 0; i < 60; i++)
{
double price = 100 + Math.Sin(i * 0.3) * 5;
indicator1.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price, 1000);
indicator2.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price, 1000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
if (i >= 25) // After both are fully warmed up
{
results1.Add(indicator1.LinesSeries[0].GetValue(0));
results2.Add(indicator2.LinesSeries[0].GetValue(0));
}
}
// Calculate variance of changes
double variance1 = CalculateChangeVariance(results1);
double variance2 = CalculateChangeVariance(results2);
// Longer period should be smoother
Assert.True(variance2 <= variance1 * 1.5, // Allow some tolerance
$"Longer period should be smoother: short variance={variance1:F6}, long variance={variance2:F6}");
}
private static double CalculateChangeVariance(List<double> values)
{
if (values.Count < 2)
{
return 0;
}
var changes = new List<double>();
for (int i = 1; i < values.Count; i++)
{
changes.Add(values[i] - values[i - 1]);
}
double mean = changes.Average();
double variance = changes.Select(c => (c - mean) * (c - mean)).Average();
return variance;
}
[Fact]
public void YzvIndicator_GapUp_AffectsVolatility()
{
var indicator = new YzvIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Normal trading
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 101, 99, 100, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double beforeGap = indicator.LinesSeries[0].GetValue(0);
// Large gap up (open much higher than previous close)
for (int i = 10; i < 20; i++)
{
double open = 120 + (i - 10) * 2; // Large gaps
indicator.HistoricalData.AddBar(now.AddMinutes(i), open, open + 2, open - 2, open + 1, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double afterGap = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(beforeGap));
Assert.True(double.IsFinite(afterGap));
// Gap should increase volatility measurement
Assert.True(afterGap > beforeGap * 0.5, "Gap up should affect volatility");
}
[Fact]
public void YzvIndicator_VolatilityRegimeChange_RespondsCorrectly()
{
var indicator = new YzvIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Low volatility regime
for (int i = 0; i < 20; i++)
{
double price = 100 + Math.Sin(i * 0.5) * 0.5; // Small movements
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 0.2, price - 0.2, price, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double lowVolVal = indicator.LinesSeries[0].GetValue(0);
// High volatility regime
for (int i = 20; i < 40; i++)
{
double price = 100 + Math.Sin(i * 0.5) * 10; // Large movements
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 5, price - 5, price, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double highVolVal = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(lowVolVal));
Assert.True(double.IsFinite(highVolVal));
Assert.True(highVolVal > lowVolVal, "High volatility regime should produce higher YZV");
}
}
+611
View File
@@ -0,0 +1,611 @@
// Yang-Zhang Volatility (YZV) Unit Tests
using Xunit;
namespace QuanTAlib.Tests;
public class YzvTests
{
private readonly GBM _gbm;
private const double Tolerance = 1e-10;
private const int DefaultPeriod = 20;
public YzvTests()
{
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
}
private TBarSeries GenerateBarData(int count)
{
_gbm.Reset(DateTime.UtcNow.Ticks);
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
#region Constructor Tests
[Fact]
public void Constructor_DefaultParameters_SetsCorrectValues()
{
var yzv = new Yzv();
Assert.Equal(DefaultPeriod, yzv.Period);
Assert.Equal($"Yzv({DefaultPeriod})", yzv.Name);
Assert.Equal(DefaultPeriod, yzv.WarmupPeriod);
}
[Fact]
public void Constructor_CustomPeriod_SetsCorrectValues()
{
var yzv = new Yzv(period: 30);
Assert.Equal(30, yzv.Period);
Assert.Equal("Yzv(30)", yzv.Name);
Assert.Equal(30, yzv.WarmupPeriod);
}
[Fact]
public void Constructor_ZeroPeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Yzv(period: 0));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_NegativePeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Yzv(period: -5));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_WithTBarSeriesSource_PrimesIndicator()
{
var bars = GenerateBarData(50);
var yzv = new Yzv(bars, period: 10);
Assert.True(yzv.IsHot);
Assert.True(double.IsFinite(yzv.Last.Value));
}
#endregion
#region Basic Calculation Tests
[Fact]
public void Update_SingleBar_ReturnsNonNegativeValue()
{
var yzv = new Yzv();
var bar = new TBar(DateTime.UtcNow, 100.0, 102.0, 98.0, 101.0, 1000);
var result = yzv.Update(bar);
Assert.True(result.Value >= 0);
}
[Fact]
public void Update_ConstantPrices_ProducesLowVolatility()
{
var yzv = new Yzv(period: 5);
for (int i = 0; i < 30; i++)
{
// Constant OHLC = no volatility components
yzv.Update(new TBar(DateTime.UtcNow, 100.0, 100.0, 100.0, 100.0, 1000));
}
// With constant prices, volatility should be very low
Assert.True(yzv.Last.Value < 0.001, $"Expected near zero, got {yzv.Last.Value}");
}
[Fact]
public void Update_ReturnsNonNegativeValue()
{
var yzv = new Yzv();
var bars = GenerateBarData(100);
for (int i = 0; i < bars.Count; i++)
{
var result = yzv.Update(bars[i]);
Assert.True(result.Value >= 0, $"YZV should be non-negative, got {result.Value}");
}
}
[Fact]
public void Update_HighVolatility_ProducesHigherValues()
{
var yzvLow = new Yzv(period: 10);
var yzvHigh = new Yzv(period: 10);
// Low volatility: small H-L range
for (int i = 0; i < 30; i++)
{
double price = 100.0 + (i % 2) * 0.1;
yzvLow.Update(new TBar(DateTime.UtcNow, price, price + 0.05, price - 0.05, price, 1000));
}
// High volatility: large H-L range
for (int i = 0; i < 30; i++)
{
double price = 100.0 + (i % 2) * 5.0;
yzvHigh.Update(new TBar(DateTime.UtcNow, price, price + 5.0, price - 5.0, price + 2.0, 1000));
}
Assert.True(yzvHigh.Last.Value > yzvLow.Last.Value,
$"High vol ({yzvHigh.Last.Value}) should exceed low vol ({yzvLow.Last.Value})");
}
[Fact]
public void Update_OvernightGaps_IncorporatesGapVolatility()
{
var yzvNoGap = new Yzv(period: 10);
var yzvWithGap = new Yzv(period: 10);
// No gaps: open = prev close
double prevClose = 100.0;
for (int i = 0; i < 30; i++)
{
yzvNoGap.Update(new TBar(DateTime.UtcNow, prevClose, prevClose + 1, prevClose - 1, prevClose + 0.5, 1000));
prevClose = prevClose + 0.5;
}
// With gaps: open != prev close
prevClose = 100.0;
for (int i = 0; i < 30; i++)
{
double open = prevClose + (i % 2 == 0 ? 2.0 : -2.0); // Gap up or down
yzvWithGap.Update(new TBar(DateTime.UtcNow, open, open + 1, open - 1, open + 0.5, 1000));
prevClose = open + 0.5;
}
// YZV with gaps should show higher volatility due to overnight component
Assert.True(yzvWithGap.Last.Value > yzvNoGap.Last.Value,
$"Gap YZV ({yzvWithGap.Last.Value}) should exceed no-gap YZV ({yzvNoGap.Last.Value})");
}
#endregion
#region IsHot and Warmup Tests
[Fact]
public void IsHot_BeforeWarmup_ReturnsFalse()
{
var yzv = new Yzv(period: 10);
for (int i = 0; i < 5; i++)
{
yzv.Update(new TBar(DateTime.UtcNow, 100.0 + i, 102.0 + i, 98.0 + i, 101.0 + i, 1000));
}
Assert.False(yzv.IsHot);
}
[Fact]
public void IsHot_AfterWarmup_ReturnsTrue()
{
var yzv = new Yzv(period: 10);
for (int i = 0; i < 15; i++)
{
yzv.Update(new TBar(DateTime.UtcNow, 100.0 + i, 102.0 + i, 98.0 + i, 101.0 + i, 1000));
}
Assert.True(yzv.IsHot);
}
[Fact]
public void WarmupPeriod_EqualsToPeriod()
{
var yzv = new Yzv(period: 15);
Assert.Equal(15, yzv.WarmupPeriod);
}
#endregion
#region Bar Correction (isNew) Tests
[Fact]
public void Update_IsNewTrue_AdvancesState()
{
var yzv = new Yzv(period: 5);
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
yzv.Update(new TBar(time.AddSeconds(i), 100 + i, 102 + i, 98 + i, 101 + i, 1000), isNew: true);
}
double valueBeforeNew = yzv.Last.Value;
yzv.Update(new TBar(time.AddSeconds(10), 150, 155, 145, 152, 1000), isNew: true);
Assert.NotEqual(valueBeforeNew, yzv.Last.Value);
}
[Fact]
public void Update_IsNewFalse_UpdatesCurrentBar()
{
var yzv = new Yzv(period: 5);
var time = DateTime.UtcNow;
for (int i = 0; i < 15; i++)
{
yzv.Update(new TBar(time.AddSeconds(i), 100 + i, 102 + i, 98 + i, 101 + i, 1000), isNew: true);
}
double valueBeforeCorrection = yzv.Last.Value;
// First correction
yzv.Update(new TBar(time.AddSeconds(15), 200, 210, 190, 205, 1000), isNew: false);
double valueAfterCorrection1 = yzv.Last.Value;
// Second correction to different value
yzv.Update(new TBar(time.AddSeconds(15), 50, 55, 45, 52, 1000), isNew: false);
double valueAfterCorrection2 = yzv.Last.Value;
Assert.NotEqual(valueBeforeCorrection, valueAfterCorrection1);
Assert.NotEqual(valueAfterCorrection1, valueAfterCorrection2);
}
[Fact]
public void Update_MultipleCorrections_RestoresPreviousState()
{
var yzv = new Yzv(period: 5);
var time = DateTime.UtcNow;
for (int i = 0; i < 15; i++)
{
yzv.Update(new TBar(time.AddSeconds(i), 100 + i, 102 + i, 98 + i, 101 + i, 1000), isNew: true);
}
// Add a new bar
var newBar = new TBar(time.AddSeconds(15), 115, 117, 113, 116, 1000);
yzv.Update(newBar, isNew: true);
double baseValue = yzv.Last.Value;
// Multiple corrections should all restore to same base state
yzv.Update(new TBar(time.AddSeconds(15), 200, 210, 190, 205, 1000), isNew: false);
yzv.Update(newBar, isNew: false);
double restoredValue = yzv.Last.Value;
Assert.Equal(baseValue, restoredValue, 10);
}
#endregion
#region Reset Tests
[Fact]
public void Reset_ClearsAllState()
{
var yzv = new Yzv(period: 5);
var bars = GenerateBarData(20);
for (int i = 0; i < bars.Count; i++)
{
yzv.Update(bars[i]);
}
Assert.True(yzv.IsHot);
yzv.Reset();
Assert.False(yzv.IsHot);
Assert.Equal(default, yzv.Last);
}
[Fact]
public void Reset_AllowsReuse()
{
var yzv = new Yzv(period: 5);
var bars = GenerateBarData(20);
for (int i = 0; i < bars.Count; i++)
{
yzv.Update(bars[i]);
}
double firstRunValue = yzv.Last.Value;
yzv.Reset();
for (int i = 0; i < bars.Count; i++)
{
yzv.Update(bars[i]);
}
double secondRunValue = yzv.Last.Value;
Assert.Equal(firstRunValue, secondRunValue, 10);
}
#endregion
#region NaN and Infinity Handling Tests
[Fact]
public void Update_NaNInput_UsesLastValidValue()
{
var yzv = new Yzv(period: 5);
for (int i = 0; i < 15; i++)
{
yzv.Update(new TBar(DateTime.UtcNow, 100 + i, 102 + i, 98 + i, 101 + i, 1000));
}
// Update with NaN
yzv.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
Assert.True(double.IsFinite(yzv.Last.Value));
}
[Fact]
public void Update_InfinityInput_UsesLastValidValue()
{
var yzv = new Yzv(period: 5);
for (int i = 0; i < 15; i++)
{
yzv.Update(new TBar(DateTime.UtcNow, 100 + i, 102 + i, 98 + i, 101 + i, 1000));
}
yzv.Update(new TBar(DateTime.UtcNow, double.PositiveInfinity, double.PositiveInfinity, 98, 101, 1000));
Assert.True(double.IsFinite(yzv.Last.Value));
}
[Fact]
public void Update_MultipleNaNs_StaysFinite()
{
var yzv = new Yzv(period: 5);
for (int i = 0; i < 15; i++)
{
yzv.Update(new TBar(DateTime.UtcNow, 100 + i, 102 + i, 98 + i, 101 + i, 1000));
}
for (int i = 0; i < 5; i++)
{
yzv.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
}
Assert.True(double.IsFinite(yzv.Last.Value));
}
#endregion
#region TBarSeries and Batch Tests
[Fact]
public void Update_TBarSeries_ReturnsCorrectLength()
{
var yzv = new Yzv();
var bars = GenerateBarData(100);
var result = yzv.Update(bars);
Assert.Equal(bars.Count, result.Count);
}
[Fact]
public void Calculate_Static_ProducesValidResults()
{
var bars = GenerateBarData(100);
var result = Yzv.Batch(bars, period: 10);
Assert.Equal(bars.Count, result.Count);
for (int i = 0; i < result.Count; i++)
{
Assert.True(double.IsFinite(result.Values[i]));
Assert.True(result.Values[i] >= 0);
}
}
[Fact]
public void Batch_ProducesConsistentResults()
{
var bars = GenerateBarData(100);
double[] output = new double[100];
Yzv.Batch(bars, output, period: 10);
for (int i = 0; i < output.Length; i++)
{
Assert.True(double.IsFinite(output[i]));
Assert.True(output[i] >= 0);
}
}
[Fact]
public void Batch_ZeroPeriod_ThrowsArgumentException()
{
var bars = GenerateBarData(10);
double[] output = new double[10];
var ex = Assert.Throws<ArgumentException>(() => Yzv.Batch(bars, output, period: 0));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Batch_OutputTooSmall_ThrowsArgumentException()
{
var bars = GenerateBarData(10);
double[] output = new double[5];
var ex = Assert.Throws<ArgumentException>(() => Yzv.Batch(bars, output));
Assert.Equal("output", ex.ParamName);
}
[Fact]
public void Batch_EmptySource_DoesNotThrow()
{
var bars = new TBarSeries();
double[] output = [];
Yzv.Batch(bars, output);
Assert.Empty(output);
}
[Fact]
public void Batch_OhlcArrays_ProducesValidResults()
{
int len = 50;
double[] open = new double[len];
double[] high = new double[len];
double[] low = new double[len];
double[] close = new double[len];
double[] output = new double[len];
for (int i = 0; i < len; i++)
{
open[i] = 100 + i;
high[i] = 102 + i;
low[i] = 98 + i;
close[i] = 101 + i;
}
Yzv.Batch(open, high, low, close, output, period: 10);
for (int i = 0; i < output.Length; i++)
{
Assert.True(double.IsFinite(output[i]));
Assert.True(output[i] >= 0);
}
}
#endregion
#region Mode Consistency Tests
[Fact]
public void AllModes_ProduceSameResults()
{
var bars = GenerateBarData(100);
int period = 10;
// Mode 1: Streaming
var streamingYzv = new Yzv(period);
for (int i = 0; i < bars.Count; i++)
{
streamingYzv.Update(bars[i], isNew: true);
}
// Mode 2: TBarSeries batch
var batchResult = Yzv.Batch(bars, period);
// Mode 3: Span batch
double[] spanOutput = new double[bars.Count];
Yzv.Batch(bars, spanOutput, period);
// Compare last 50 values (after warmup)
int compareStart = bars.Count - 50;
for (int i = compareStart; i < bars.Count; i++)
{
double batch = batchResult[i].Value;
double span = spanOutput[i];
Assert.Equal(batch, span, Tolerance);
}
// Final values should match
Assert.Equal(streamingYzv.Last.Value, batchResult[bars.Count - 1].Value, 1e-8);
Assert.Equal(streamingYzv.Last.Value, spanOutput[bars.Count - 1], 1e-8);
}
#endregion
#region Event Tests
[Fact]
public void Pub_FiresOnUpdate()
{
var yzv = new Yzv(period: 5);
int eventCount = 0;
yzv.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
var time = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
yzv.Update(new TBar(time.AddSeconds(i), 100 + i, 102 + i, 98 + i, 101 + i, 1000));
}
Assert.Equal(5, eventCount);
}
#endregion
#region TValue Input Tests
[Fact]
public void Update_TValue_CreatesSyntheticBar()
{
var yzv1 = new Yzv(period: 5);
var yzv2 = new Yzv(period: 5);
var time = DateTime.UtcNow;
for (int i = 0; i < 15; i++)
{
// TValue input creates bar with O=H=L=C
yzv1.Update(new TValue(time.AddSeconds(i), 100.0 + i));
yzv2.Update(new TBar(time.AddSeconds(i), 100.0 + i, 100.0 + i, 100.0 + i, 100.0 + i, 0));
}
Assert.Equal(yzv1.Last.Value, yzv2.Last.Value, Tolerance);
}
#endregion
#region Large Period Tests
[Fact]
public void LargeDataset_NoStackOverflow()
{
var bars = GenerateBarData(10000);
double[] output = new double[10000];
Yzv.Batch(bars, output, period: 20);
for (int i = 0; i < output.Length; i++)
{
Assert.True(double.IsFinite(output[i]));
Assert.True(output[i] >= 0);
}
}
#endregion
#region Prime Tests
[Fact]
public void Prime_SetsInitialState()
{
var yzv = new Yzv(period: 5);
double[] warmupData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
yzv.Prime(warmupData);
Assert.True(yzv.IsHot);
}
#endregion
#region Yang-Zhang Specific Tests
[Fact]
public void Update_RogersStatchellComponent_ContributesToResult()
{
// Test that intraday high-low movement contributes to volatility
var yzvSmallRange = new Yzv(period: 10);
var yzvLargeRange = new Yzv(period: 10);
for (int i = 0; i < 30; i++)
{
double basePrice = 100.0;
// Small H-L range
yzvSmallRange.Update(new TBar(DateTime.UtcNow, basePrice, basePrice + 0.1, basePrice - 0.1, basePrice, 1000));
// Large H-L range (same open/close)
yzvLargeRange.Update(new TBar(DateTime.UtcNow, basePrice, basePrice + 5.0, basePrice - 5.0, basePrice, 1000));
}
Assert.True(yzvLargeRange.Last.Value > yzvSmallRange.Last.Value,
$"Large range YZV ({yzvLargeRange.Last.Value}) should exceed small range ({yzvSmallRange.Last.Value})");
}
[Fact]
public void Update_BiasCorrection_WorksDuringWarmup()
{
var yzv = new Yzv(period: 20);
var bars = GenerateBarData(5);
// During warmup, bias correction should prevent extreme values
for (int i = 0; i < bars.Count; i++)
{
var result = yzv.Update(bars[i]);
Assert.True(double.IsFinite(result.Value), $"Value at index {i} should be finite");
Assert.True(result.Value >= 0, $"Value at index {i} should be non-negative");
}
}
#endregion
}
@@ -0,0 +1,317 @@
// Yang-Zhang Volatility (YZV) Validation Tests
// Validates against the PineScript reference implementation
using Xunit;
namespace QuanTAlib.Tests;
public class YzvValidationTests
{
private readonly GBM _gbm;
private const double PineScriptTolerance = 1e-6;
public YzvValidationTests()
{
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
}
private TBarSeries GenerateBarData(int count)
{
_gbm.Reset(DateTime.UtcNow.Ticks);
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
#region PineScript Algorithm Validation
[Fact]
public void Yzv_MatchesPineScriptAlgorithm_SingleBar()
{
// Test with known values to verify algorithm implementation
// Using the exact formulas from the PineScript
int period = 20;
double o = 100.0, h = 105.0, l = 95.0, c = 102.0;
double prevClose = 99.0; // Previous close
// Manual calculation following PineScript
double ro = Math.Log(o / prevClose); // Overnight return
double rc = Math.Log(c / o); // Close-to-open return
double rh = Math.Log(h / o); // High-to-open
double rl = Math.Log(l / o); // Low-to-open
double sOSq = ro * ro;
double sCSq = rc * rc;
double sRsSq = rh * (rh - rc) + rl * (rl - rc);
double ratioN = (double)(period + 1) / (period - 1);
double kYz = 0.34 / (1.34 + ratioN);
double sSqDaily = sOSq + kYz * sCSq + (1.0 - kYz) * sRsSq;
// First bar: RMA = value, eComp = 1 - alpha
double alpha = 1.0 / period;
double rawRma = sSqDaily;
double eComp = 1.0 - alpha;
// Bias correction
const double epsilon = 1e-10;
double smoothedSSq = eComp > epsilon ? rawRma / (1.0 - eComp) : rawRma;
_ = Math.Sqrt(smoothedSSq); // YZV = sqrt(smoothed variance) - validated below via impl
// Now test with our implementation
var yzv = new Yzv(period);
// First bar with prevClose = open (first bar behavior)
var firstBar = new TBar(DateTime.UtcNow, prevClose, prevClose + 1, prevClose - 1, prevClose, 1000);
yzv.Update(firstBar, isNew: true);
// Second bar with the test values
var testBar = new TBar(DateTime.UtcNow, o, h, l, c, 1000);
var result = yzv.Update(testBar, isNew: true);
// The result should be close to our manual calculation
// (not exact match due to state from first bar)
Assert.True(double.IsFinite(result.Value));
Assert.True(result.Value > 0);
}
[Fact]
public void Yzv_YangZhangWeightingFactor_IsCorrect()
{
// Verify k_yz calculation: k = 0.34 / (1.34 + (N+1)/(N-1))
// For period = 20: ratioN = 21/19 = 1.1053, k = 0.34 / (1.34 + 1.1053) = 0.34 / 2.4453 = 0.1391
int period = 20;
double ratioN = (double)(period + 1) / (period - 1);
double kYz = 0.34 / (1.34 + ratioN);
double expectedK = 0.34 / (1.34 + 21.0 / 19.0);
Assert.Equal(expectedK, kYz, 10);
// Verify k is in reasonable range (0 < k < 0.5)
Assert.True(kYz > 0);
Assert.True(kYz < 0.5);
}
[Fact]
public void Yzv_RogersStatchellComponent_IsCorrect()
{
// Verify Rogers-Satchell formula: rh*(rh-rc) + rl*(rl-rc)
double open = 100.0, high = 105.0, low = 95.0, close = 102.0;
double rc = Math.Log(close / open);
double rh = Math.Log(high / open);
double rl = Math.Log(low / open);
double sRsSq = rh * (rh - rc) + rl * (rl - rc);
// Verify this is positive for typical bar
Assert.True(sRsSq >= 0, "Rogers-Satchell should be non-negative for valid OHLC");
}
[Fact]
public void Yzv_BiasCorrection_MatchesPineScript()
{
// Verify bias correction formula: smoothed = raw / (1 - eComp)
// where eComp = (1 - alpha)^n for n bars
int period = 10;
double alpha = 1.0 / period;
// After 1 bar: eComp = 0.9
double eComp1 = 1.0 - alpha;
Assert.Equal(0.9, eComp1, 10);
// After 2 bars: eComp = 0.81
double eComp2 = (1.0 - alpha) * eComp1;
Assert.Equal(0.81, eComp2, 10);
// After 3 bars: eComp = 0.729
double eComp3 = (1.0 - alpha) * eComp2;
Assert.Equal(0.729, eComp3, 10);
}
#endregion
#region Streaming vs Batch Consistency
[Fact]
public void Yzv_StreamingMatchesBatch_AllPeriods()
{
int[] periods = [5, 10, 14, 20, 50];
foreach (int period in periods)
{
var bars = GenerateBarData(100);
// Streaming
var streamingYzv = new Yzv(period);
for (int i = 0; i < bars.Count; i++)
{
streamingYzv.Update(bars[i], isNew: true);
}
// Batch
double[] batchOutput = new double[bars.Count];
Yzv.Batch(bars, batchOutput, period);
// Compare final value
Assert.Equal(streamingYzv.Last.Value, batchOutput[bars.Count - 1], PineScriptTolerance);
}
}
[Fact]
public void Yzv_BatchMatchesCalculate_AllValues()
{
var bars = GenerateBarData(100);
int period = 14;
// Using static Calculate
var calculateResult = Yzv.Batch(bars, period);
// Using Batch
double[] batchOutput = new double[bars.Count];
Yzv.Batch(bars, batchOutput, period);
for (int i = 0; i < bars.Count; i++)
{
Assert.Equal(calculateResult[i].Value, batchOutput[i], PineScriptTolerance);
}
}
#endregion
#region Mathematical Properties
[Fact]
public void Yzv_AlwaysNonNegative()
{
var bars = GenerateBarData(500);
var yzv = new Yzv(20);
for (int i = 0; i < bars.Count; i++)
{
var result = yzv.Update(bars[i]);
Assert.True(result.Value >= 0, $"YZV at index {i} should be non-negative: {result.Value}");
}
}
[Fact]
public void Yzv_ConstantPrices_ApproachesZero()
{
var yzv = new Yzv(10);
// Feed constant OHLC bars
for (int i = 0; i < 100; i++)
{
yzv.Update(new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000));
}
// Should be very close to zero
Assert.True(yzv.Last.Value < 1e-10, $"Constant prices should yield near-zero YZV: {yzv.Last.Value}");
}
[Fact]
public void Yzv_ScalesWithVolatility()
{
// YZV should scale proportionally with price movement magnitude
var yzvSmall = new Yzv(10);
var yzvLarge = new Yzv(10);
for (int i = 0; i < 50; i++)
{
double baseSmall = 100.0;
double baseLarge = 100.0;
double moveSmall = 1.0;
double moveLarge = 10.0;
yzvSmall.Update(new TBar(DateTime.UtcNow, baseSmall, baseSmall + moveSmall, baseSmall - moveSmall, baseSmall + (i % 2) * moveSmall, 1000));
yzvLarge.Update(new TBar(DateTime.UtcNow, baseLarge, baseLarge + moveLarge, baseLarge - moveLarge, baseLarge + (i % 2) * moveLarge, 1000));
}
// Larger moves should produce larger YZV (roughly 10x)
double ratio = yzvLarge.Last.Value / yzvSmall.Last.Value;
Assert.True(ratio > 5 && ratio < 15, $"YZV ratio should be around 10, got {ratio}");
}
#endregion
#region Edge Cases
[Fact]
public void Yzv_Period1_HandlesCorrectly()
{
var yzv = new Yzv(1);
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
var result = yzv.Update(bar);
Assert.True(double.IsFinite(result.Value));
Assert.True(result.Value >= 0);
}
[Fact]
public void Yzv_LargePeriod_HandlesCorrectly()
{
var yzv = new Yzv(200);
var bars = GenerateBarData(300);
for (int i = 0; i < bars.Count; i++)
{
var result = yzv.Update(bars[i]);
Assert.True(double.IsFinite(result.Value));
Assert.True(result.Value >= 0);
}
}
[Fact]
public void Yzv_GapUp_IncreasesVolatility()
{
var yzvNoGap = new Yzv(10);
var yzvGapUp = new Yzv(10);
// No gap scenario
for (int i = 0; i < 30; i++)
{
double close = 100 + i * 0.1;
yzvNoGap.Update(new TBar(DateTime.UtcNow, close, close + 1, close - 1, close, 1000));
}
// Gap up scenario
for (int i = 0; i < 30; i++)
{
double open = 100 + i + 2; // Gap up each day
yzvGapUp.Update(new TBar(DateTime.UtcNow, open, open + 1, open - 1, open, 1000));
}
// Gap scenario should have higher volatility due to overnight component
Assert.True(yzvGapUp.Last.Value > yzvNoGap.Last.Value,
$"Gap YZV ({yzvGapUp.Last.Value}) should exceed no-gap YZV ({yzvNoGap.Last.Value})");
}
[Fact]
public void Yzv_GapDown_IncreasesVolatility()
{
var yzvNoGap = new Yzv(10);
var yzvGapDown = new Yzv(10);
// No gap scenario
for (int i = 0; i < 30; i++)
{
double close = 100 - i * 0.1;
yzvNoGap.Update(new TBar(DateTime.UtcNow, close, close + 1, close - 1, close, 1000));
}
// Gap down scenario
for (int i = 0; i < 30; i++)
{
double open = 100 - i - 2; // Gap down each day
yzvGapDown.Update(new TBar(DateTime.UtcNow, open, open + 1, open - 1, open, 1000));
}
// Gap scenario should have higher volatility due to overnight component
Assert.True(yzvGapDown.Last.Value > yzvNoGap.Last.Value,
$"Gap YZV ({yzvGapDown.Last.Value}) should exceed no-gap YZV ({yzvNoGap.Last.Value})");
}
#endregion
}