mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 04:58:08 +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,147 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class WillrIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void WillrIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new WillrIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Contains("WILLR", indicator.Name, StringComparison.Ordinal);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WillrIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new WillrIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, WillrIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WillrIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new WillrIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("WILLR", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WillrIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new WillrIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Willr", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WillrIndicator_Initialize_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new WillrIndicator { Period = 14 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (WillR + overbought + oversold)
|
||||
Assert.Equal(3, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WillrIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new WillrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double willr = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(willr));
|
||||
Assert.True(willr >= -100.0 && willr <= 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WillrIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new WillrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Simulate a new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 120, 100, 115);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double willr = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(willr));
|
||||
Assert.True(willr >= -100.0 && willr <= 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WillrIndicator_ReferenceLines_AreSet()
|
||||
{
|
||||
var indicator = new WillrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Overbought reference line at -20
|
||||
double overbought = indicator.LinesSeries[1].GetValue(0);
|
||||
Assert.Equal(-20.0, overbought, 1e-10);
|
||||
|
||||
// Oversold reference line at -80
|
||||
double oversold = indicator.LinesSeries[2].GetValue(0);
|
||||
Assert.Equal(-80.0, oversold, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WillrIndicator_CustomPeriod_IsUsed()
|
||||
{
|
||||
var indicator = new WillrIndicator { Period = 7 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("7", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WillrIndicator_Description_IsSet()
|
||||
{
|
||||
var indicator = new WillrIndicator();
|
||||
|
||||
Assert.NotNull(indicator.Description);
|
||||
Assert.NotEmpty(indicator.Description);
|
||||
Assert.Contains("Williams", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class WillrConstructorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultPeriod_Is14()
|
||||
{
|
||||
var w = new Willr();
|
||||
Assert.Equal(14, w.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CustomPeriod_IsStored()
|
||||
{
|
||||
var w = new Willr(period: 20);
|
||||
Assert.Equal(20, w.Period);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
[InlineData(-100)]
|
||||
public void InvalidPeriod_Throws(int period)
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Willr(period));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinimumPeriod_IsOne()
|
||||
{
|
||||
var w = new Willr(period: 1);
|
||||
Assert.Equal(1, w.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_IncludesPeriod()
|
||||
{
|
||||
var w = new Willr(period: 10);
|
||||
Assert.Contains("10", w.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_EqualsPeriod()
|
||||
{
|
||||
Assert.Equal(14, new Willr(14).WarmupPeriod);
|
||||
Assert.Equal(5, new Willr(5).WarmupPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WillrBasicTests
|
||||
{
|
||||
[Fact]
|
||||
public void Update_Returns_TValue()
|
||||
{
|
||||
var w = new Willr();
|
||||
var result = w.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_IsAccessible()
|
||||
{
|
||||
var w = new Willr();
|
||||
_ = w.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
|
||||
Assert.True(double.IsFinite(w.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterWarmup()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
w.Update(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 100), isNew: true);
|
||||
}
|
||||
Assert.False(w.IsHot);
|
||||
|
||||
w.Update(new TBar(time.AddMinutes(4), 104, 109, 99, 106, 100), isNew: true);
|
||||
Assert.True(w.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_IsNotNull()
|
||||
{
|
||||
var w = new Willr();
|
||||
Assert.NotNull(w.Name);
|
||||
Assert.NotEmpty(w.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WillrRangeTests
|
||||
{
|
||||
[Fact]
|
||||
public void CloseAtHighest_ValueIsZero()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
w.Update(new TBar(time.AddMinutes(i), 100, 110, 90, 100, 100), isNew: true);
|
||||
}
|
||||
|
||||
// Close at highest high (110)
|
||||
w.Update(new TBar(time.AddMinutes(5), 110, 110, 90, 110, 100), isNew: true);
|
||||
Assert.Equal(0.0, w.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloseAtLowest_ValueIsNeg100()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
w.Update(new TBar(time.AddMinutes(i), 100, 110, 90, 100, 100), isNew: true);
|
||||
}
|
||||
|
||||
// Close at lowest low (90)
|
||||
w.Update(new TBar(time.AddMinutes(5), 90, 110, 90, 90, 100), isNew: true);
|
||||
Assert.Equal(-100.0, w.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloseAtMidpoint_ValueIsNeg50()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
w.Update(new TBar(time.AddMinutes(i), 100, 110, 90, 100, 100), isNew: true);
|
||||
}
|
||||
|
||||
// Close at midpoint of range (100 = midpoint of 90-110)
|
||||
w.Update(new TBar(time.AddMinutes(5), 100, 110, 90, 100, 100), isNew: true);
|
||||
Assert.Equal(-50.0, w.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantBars_ValueIsNeg50()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
w.Update(new TBar(time.AddMinutes(i), 100, 100, 100, 100, 100), isNew: true);
|
||||
}
|
||||
|
||||
// Range=0, should return -50
|
||||
Assert.Equal(-50.0, w.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rising_Produces_NearZero()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 100.0 + (i * 2.0);
|
||||
w.Update(new TBar(time.AddMinutes(i), price, price + 1, price - 1, price + 1, 100), isNew: true);
|
||||
}
|
||||
|
||||
// Close at recent high → WillR should be near 0 (> -20)
|
||||
Assert.True(w.Last.Value > -20.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Falling_Produces_NearNeg100()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 200.0 - (i * 2.0);
|
||||
w.Update(new TBar(time.AddMinutes(i), price, price + 1, price - 1, price - 1, 100), isNew: true);
|
||||
}
|
||||
|
||||
// Close at recent low → WillR should be near -100 (< -80)
|
||||
Assert.True(w.Last.Value < -80.0);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WillrBarCorrectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsNew_True_AdvancesState()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
var bar1 = new TBar(time, 100, 105, 95, 100, 100);
|
||||
var bar2 = new TBar(time.AddMinutes(1), 102, 108, 98, 104, 100);
|
||||
|
||||
w.Update(bar1, isNew: true);
|
||||
var v1 = w.Last.Value;
|
||||
|
||||
w.Update(bar2, isNew: true);
|
||||
var v2 = w.Last.Value;
|
||||
|
||||
Assert.NotEqual(v1, v2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_RewritesCurrent()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
w.Update(new TBar(time, 100, 105, 95, 100, 100), isNew: true);
|
||||
|
||||
w.Update(new TBar(time.AddMinutes(1), 102, 108, 98, 104, 100), isNew: true);
|
||||
var beforeCorrection = w.Last.Value;
|
||||
|
||||
// Correct current bar (isNew=false)
|
||||
w.Update(new TBar(time.AddMinutes(1), 110, 115, 98, 112, 100), isNew: false);
|
||||
var afterCorrection = w.Last.Value;
|
||||
|
||||
Assert.NotEqual(beforeCorrection, afterCorrection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_Restore()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Feed 3 bars
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
w.Update(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 100), isNew: true);
|
||||
}
|
||||
|
||||
// Add a new bar
|
||||
w.Update(new TBar(time.AddMinutes(3), 103, 108, 98, 105, 100), isNew: true);
|
||||
var original = w.Last.Value;
|
||||
|
||||
// Correct it several times (isNew=false)
|
||||
w.Update(new TBar(time.AddMinutes(3), 110, 115, 98, 112, 100), isNew: false);
|
||||
w.Update(new TBar(time.AddMinutes(3), 90, 115, 85, 88, 100), isNew: false);
|
||||
|
||||
// Correct back to original data
|
||||
w.Update(new TBar(time.AddMinutes(3), 103, 108, 98, 105, 100), isNew: false);
|
||||
var restored = w.Last.Value;
|
||||
|
||||
Assert.Equal(original, restored, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WillrResetTests
|
||||
{
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
w.Update(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 100), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(w.IsHot);
|
||||
|
||||
w.Reset();
|
||||
|
||||
Assert.False(w.IsHot);
|
||||
Assert.Equal(default, w.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuse()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
w.Update(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 100), isNew: true);
|
||||
}
|
||||
|
||||
w.Reset();
|
||||
|
||||
// Should be reusable after reset
|
||||
var result = w.Update(new TBar(time, 100, 105, 95, 100, 100), isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.False(w.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WillrRobustnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void NaN_Uses_LastValid()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Feed valid data
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
w.Update(new TBar(time.AddMinutes(i), 100, 105, 95, 100, 100), isNew: true);
|
||||
}
|
||||
|
||||
_ = w.Last.Value;
|
||||
|
||||
// Feed NaN bar
|
||||
w.Update(new TBar(time.AddMinutes(5), double.NaN, double.NaN, double.NaN, double.NaN, 100), isNew: true);
|
||||
|
||||
Assert.True(double.IsFinite(w.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Uses_LastValid()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
w.Update(new TBar(time.AddMinutes(i), 100, 105, 95, 100, 100), isNew: true);
|
||||
}
|
||||
|
||||
// Feed Infinity bar
|
||||
w.Update(new TBar(time.AddMinutes(5), double.PositiveInfinity, double.PositiveInfinity,
|
||||
double.NegativeInfinity, double.PositiveInfinity, 100), isNew: true);
|
||||
|
||||
Assert.True(double.IsFinite(w.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllNaN_Returns_NaN()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First data is NaN — no last-valid to substitute
|
||||
var result = w.Update(new TBar(time, double.NaN, double.NaN, double.NaN, double.NaN, 100), isNew: true);
|
||||
Assert.True(double.IsNaN(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WillrBatchTests
|
||||
{
|
||||
private static TBarSeries GenerateSeries(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TBarSeries_ProducesOutput()
|
||||
{
|
||||
var bars = GenerateSeries(100);
|
||||
var result = Willr.Batch(bars, period: 14);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
Assert.True(double.IsFinite(result[^1].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Returns_ResultsAndIndicator()
|
||||
{
|
||||
var bars = GenerateSeries(100);
|
||||
var (results, indicator) = Willr.Calculate(bars, period: 14);
|
||||
|
||||
Assert.Equal(100, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Streaming_Matches_Batch()
|
||||
{
|
||||
var bars = GenerateSeries(200);
|
||||
const int period = 14;
|
||||
|
||||
var w = new Willr(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
w.Update(bars[i]);
|
||||
}
|
||||
|
||||
var batch = Willr.Batch(bars, period);
|
||||
|
||||
Assert.Equal(w.Last.Value, batch[^1].Value, 1e-6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_Empty_NoException()
|
||||
{
|
||||
var output = Array.Empty<double>();
|
||||
Willr.Batch(ReadOnlySpan<double>.Empty, ReadOnlySpan<double>.Empty,
|
||||
ReadOnlySpan<double>.Empty, output.AsSpan(), 14);
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_InvalidPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Willr.Batch(new double[10], new double[10], new double[10], new double[10], 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_MismatchedLengths_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Willr.Batch(new double[10], new double[5], new double[10], new double[10], 14));
|
||||
Assert.Equal("high", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_OutputTooSmall_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Willr.Batch(new double[10], new double[10], new double[10], new double[5], 14));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TBarSeries_ProducesOutput()
|
||||
{
|
||||
var bars = GenerateSeries(100);
|
||||
var w = new Willr(14);
|
||||
var result = w.Update(bars);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
Assert.True(w.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_NullSource_ReturnsEmpty()
|
||||
{
|
||||
var result = Willr.Batch(null!, 14);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptySource_ReturnsEmpty()
|
||||
{
|
||||
var result = Willr.Batch(new TBarSeries(), 14);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WillrEventTests
|
||||
{
|
||||
[Fact]
|
||||
public void Pub_Fires_OnUpdate()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
var eventRaised = false;
|
||||
|
||||
w.Pub += (object? _, in TValueEventArgs e) => { eventRaised = true; };
|
||||
|
||||
w.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 100), isNew: true);
|
||||
Assert.True(eventRaised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chaining_Works()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var w = new Willr(bars, period: 5);
|
||||
|
||||
TValue? lastValue = null;
|
||||
w.Pub += (object? _, in TValueEventArgs e) => { lastValue = e.Value; };
|
||||
|
||||
var time = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
bars.Add(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 100));
|
||||
}
|
||||
|
||||
Assert.NotNull(lastValue);
|
||||
Assert.True(double.IsFinite(lastValue.Value.Value));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WillrPrimeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Prime_TBarSeries_SetsState()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
bars.Add(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 100));
|
||||
}
|
||||
|
||||
var w = new Willr(period: 5);
|
||||
w.Prime(bars);
|
||||
|
||||
Assert.True(w.IsHot);
|
||||
Assert.True(double.IsFinite(w.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_Span_SetsState()
|
||||
{
|
||||
var data = new double[50];
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
data[i] = 100.0 + i;
|
||||
}
|
||||
|
||||
var w = new Willr(period: 5);
|
||||
w.Prime(data.AsSpan());
|
||||
|
||||
Assert.True(w.IsHot);
|
||||
Assert.True(double.IsFinite(w.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_EmptySeries_NoError()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
w.Prime(new TBarSeries());
|
||||
|
||||
Assert.False(w.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_EmptySpan_NoError()
|
||||
{
|
||||
var w = new Willr(period: 5);
|
||||
w.Prime(ReadOnlySpan<double>.Empty);
|
||||
|
||||
Assert.False(w.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WillrConsistencyTests
|
||||
{
|
||||
private static TBarSeries GenerateSeries(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Span_Matches_TBarSeries()
|
||||
{
|
||||
var bars = GenerateSeries(200);
|
||||
const int period = 14;
|
||||
|
||||
var batchResult = Willr.Batch(bars, period);
|
||||
|
||||
var output = new double[bars.Count];
|
||||
Willr.Batch(bars.HighValues, bars.LowValues, bars.CloseValues, output.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], output[i], 12);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Different_Periods_Produce_Different_Results()
|
||||
{
|
||||
var bars = GenerateSeries(100);
|
||||
|
||||
var r5 = Willr.Batch(bars, period: 5);
|
||||
var r20 = Willr.Batch(bars, period: 20);
|
||||
|
||||
bool anyDifferent = false;
|
||||
for (int i = 20; i < 100; i++)
|
||||
{
|
||||
if (Math.Abs(r5.Values[i] - r20.Values[i]) > 0.01)
|
||||
{
|
||||
anyDifferent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.True(anyDifferent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deterministic_Across_Runs()
|
||||
{
|
||||
var bars = GenerateSeries(200, seed: 99);
|
||||
const int period = 14;
|
||||
|
||||
var r1 = Willr.Batch(bars, period);
|
||||
var r2 = Willr.Batch(bars, period);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(r1.Values[i], r2.Values[i], 15);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WillR_Is_Inverse_Stoch()
|
||||
{
|
||||
var bars = GenerateSeries(200);
|
||||
const int period = 14;
|
||||
|
||||
var willr = Willr.Batch(bars, period);
|
||||
var (stochK, _) = Stoch.Batch(bars, kLength: period);
|
||||
|
||||
// WillR = -(100 - Stoch%K) = Stoch%K - 100
|
||||
// But only when range>0 (when range=0, Stoch returns 0, WillR returns -50)
|
||||
for (int i = period; i < bars.Count; i++)
|
||||
{
|
||||
double stochVal = stochK.Values[i];
|
||||
double willrVal = willr.Values[i];
|
||||
|
||||
if (Math.Abs(stochVal) > 1e-10 || Math.Abs(willrVal + 50.0) > 1e-10)
|
||||
{
|
||||
// Only compare when not at the degenerate range=0 case
|
||||
Assert.Equal(stochVal - 100.0, willrVal, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using Skender.Stock.Indicators;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Williams %R validation tests.
|
||||
/// Cross-validates against Skender.Stock.Indicators.GetWilliamsR,
|
||||
/// TALib.NETCore, Tulip.NETCore, and self-consistency checks.
|
||||
/// </summary>
|
||||
public sealed class WillrValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data = new();
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public WillrValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_data.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static TBarSeries GenerateSeries(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
// --- A) Streaming vs Batch agreement ---
|
||||
|
||||
[Fact]
|
||||
public void Streaming_Matches_Batch()
|
||||
{
|
||||
var series = GenerateSeries(300);
|
||||
const int period = 14;
|
||||
|
||||
var willr = new Willr(period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
willr.Update(series[i]);
|
||||
}
|
||||
|
||||
var batch = Willr.Batch(series, period);
|
||||
|
||||
Assert.Equal(willr.Last.Value, batch[^1].Value, 1e-6);
|
||||
}
|
||||
|
||||
// --- B) Span matches TBarSeries ---
|
||||
|
||||
[Fact]
|
||||
public void Span_Matches_TBarSeries()
|
||||
{
|
||||
var series = GenerateSeries(200);
|
||||
const int period = 14;
|
||||
|
||||
var batchResult = Willr.Batch(series, period);
|
||||
|
||||
var output = new double[series.Count];
|
||||
Willr.Batch(series.HighValues, series.LowValues, series.CloseValues,
|
||||
output.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], output[i], 12);
|
||||
}
|
||||
}
|
||||
|
||||
// --- C) Constant bars → WillR = -50 ---
|
||||
|
||||
[Fact]
|
||||
public void ConstantBars_ValueIs_Neg50()
|
||||
{
|
||||
const int period = 14;
|
||||
int count = 50;
|
||||
|
||||
var bars = new TBarSeries();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
bars.Add(new TBar(DateTime.UtcNow.AddMinutes(i), 50, 50, 50, 50, 100));
|
||||
}
|
||||
|
||||
var result = Willr.Batch(bars, period);
|
||||
|
||||
// When range=0 for all bars, WillR = -50
|
||||
for (int i = period - 1; i < count; i++)
|
||||
{
|
||||
Assert.Equal(-50.0, result.Values[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// --- D) Directional correctness ---
|
||||
|
||||
[Fact]
|
||||
public void Rising_Produces_NearZero()
|
||||
{
|
||||
const int period = 5;
|
||||
|
||||
var bars = new TBarSeries();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 100.0 + (i * 2.0);
|
||||
bars.Add(new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 1, price - 1, price + 1, 100));
|
||||
}
|
||||
|
||||
var willr = new Willr(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
willr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Close at recent high → WillR near 0 (> -20)
|
||||
Assert.True(willr.Last.Value > -20.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Falling_Produces_NearNeg100()
|
||||
{
|
||||
const int period = 5;
|
||||
|
||||
var bars = new TBarSeries();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 200.0 - (i * 2.0);
|
||||
bars.Add(new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 1, price - 1, price - 1, 100));
|
||||
}
|
||||
|
||||
var willr = new Willr(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
willr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Close at recent low → WillR near -100 (< -80)
|
||||
Assert.True(willr.Last.Value < -80.0);
|
||||
}
|
||||
|
||||
// --- E) Cross-validation with Skender ---
|
||||
|
||||
[Fact]
|
||||
public void Skender_Matches()
|
||||
{
|
||||
const int period = 14;
|
||||
|
||||
var qResult = Willr.Batch(_data.Bars, period);
|
||||
|
||||
var skResults = _data.SkenderQuotes.GetWilliamsR(period).ToList();
|
||||
|
||||
// Compare converged values (skip warmup)
|
||||
int start = period;
|
||||
int totalCompared = 0;
|
||||
int mismatches = 0;
|
||||
|
||||
for (int i = start; i < _data.Bars.Count; i++)
|
||||
{
|
||||
double? skWillR = skResults[i].WilliamsR;
|
||||
|
||||
if (skWillR.HasValue)
|
||||
{
|
||||
totalCompared++;
|
||||
double err = Math.Abs(qResult.Values[i] - skWillR.Value);
|
||||
|
||||
if (err > 1e-9)
|
||||
{
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(totalCompared > 0, "No Skender results to compare");
|
||||
double mismatchRate = (double)mismatches / totalCompared;
|
||||
Assert.True(mismatchRate < 0.01,
|
||||
$"Mismatch rate {mismatchRate:P2} exceeds 1% threshold ({mismatches}/{totalCompared})");
|
||||
|
||||
_output.WriteLine($"Skender validation: {totalCompared} compared, {mismatches} mismatches ({mismatchRate:P2})");
|
||||
}
|
||||
|
||||
// --- F) Cross-validation with TA-Lib ---
|
||||
|
||||
[Fact]
|
||||
public void TALib_Matches()
|
||||
{
|
||||
const int period = 14;
|
||||
int len = _data.Bars.Count;
|
||||
|
||||
var qResult = Willr.Batch(_data.Bars, period);
|
||||
|
||||
double[] taOutput = new double[len];
|
||||
|
||||
var retCode = TALib.Functions.WillR(
|
||||
_data.HighPrices.Span, _data.LowPrices.Span, _data.ClosePrices.Span,
|
||||
0..^0, taOutput, out var outRange, period);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.WillRLookback(period);
|
||||
|
||||
ValidationHelper.VerifyData(qResult, taOutput, outRange, lookback, tolerance: ValidationHelper.TalibTolerance);
|
||||
|
||||
_output.WriteLine("TA-Lib validation passed.");
|
||||
}
|
||||
|
||||
// --- G) Cross-validation with Tulip ---
|
||||
|
||||
[Fact]
|
||||
public void Tulip_Matches()
|
||||
{
|
||||
const int period = 14;
|
||||
int len = _data.Bars.Count;
|
||||
|
||||
var qResult = Willr.Batch(_data.Bars, period);
|
||||
|
||||
double[][] tulipInputs = [_data.HighPrices.ToArray(), _data.LowPrices.ToArray(), _data.ClosePrices.ToArray()];
|
||||
double[][] tulipOutputs = [new double[len - period + 1]];
|
||||
|
||||
_ = Tulip.Indicators.willr.Run(tulipInputs, [period], tulipOutputs);
|
||||
|
||||
int lookback = period - 1;
|
||||
ValidationHelper.VerifyData(qResult, tulipOutputs[0], lookback, tolerance: ValidationHelper.TulipTolerance);
|
||||
|
||||
_output.WriteLine("Tulip validation passed.");
|
||||
}
|
||||
|
||||
// --- H) Inverse Stochastic identity ---
|
||||
|
||||
[Fact]
|
||||
public void WillR_Is_Inverse_Stoch()
|
||||
{
|
||||
var series = GenerateSeries(500, seed: 77);
|
||||
const int period = 14;
|
||||
|
||||
var willr = Willr.Batch(series, period);
|
||||
var (stochK, _) = Stoch.Batch(series, kLength: period);
|
||||
|
||||
// WillR = Stoch%K - 100 when range > 0
|
||||
int totalCompared = 0;
|
||||
for (int i = period; i < series.Count; i++)
|
||||
{
|
||||
double stochVal = stochK.Values[i];
|
||||
double willrVal = willr.Values[i];
|
||||
|
||||
// Skip degenerate range=0 cases (Stoch returns 0, WillR returns -50)
|
||||
if (Math.Abs(stochVal) > 1e-10 || Math.Abs(willrVal + 50.0) > 1e-10)
|
||||
{
|
||||
Assert.Equal(stochVal - 100.0, willrVal, 1e-9);
|
||||
totalCompared++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(totalCompared > 0, "No valid comparison points");
|
||||
_output.WriteLine($"Inverse Stochastic identity: validated {totalCompared} points.");
|
||||
}
|
||||
|
||||
// --- I) Determinism ---
|
||||
|
||||
[Fact]
|
||||
public void Deterministic_Across_Runs()
|
||||
{
|
||||
var series = GenerateSeries(200, seed: 99);
|
||||
const int period = 14;
|
||||
|
||||
var r1 = Willr.Batch(series, period);
|
||||
var r2 = Willr.Batch(series, period);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(r1.Values[i], r2.Values[i], 15);
|
||||
}
|
||||
}
|
||||
|
||||
// --- J) Multi-period consistency ---
|
||||
|
||||
[Fact]
|
||||
public void Different_Periods_Produce_Different_Results()
|
||||
{
|
||||
var series = GenerateSeries(100);
|
||||
|
||||
var r5 = Willr.Batch(series, period: 5);
|
||||
var r20 = Willr.Batch(series, period: 20);
|
||||
|
||||
bool anyDifferent = false;
|
||||
for (int i = 20; i < 100; i++)
|
||||
{
|
||||
if (Math.Abs(r5.Values[i] - r20.Values[i]) > 0.01)
|
||||
{
|
||||
anyDifferent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.True(anyDifferent);
|
||||
}
|
||||
|
||||
// --- K) Calculate returns consistent results ---
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Produces_Consistent_Results()
|
||||
{
|
||||
var series = GenerateSeries(100);
|
||||
const int period = 14;
|
||||
|
||||
var (results, indicator) = Willr.Calculate(series, period);
|
||||
|
||||
Assert.Equal(100, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
// --- L) All outputs finite after warmup ---
|
||||
|
||||
[Fact]
|
||||
public void AllOutputsFinite_AfterWarmup()
|
||||
{
|
||||
const int period = 14;
|
||||
var willr = new Willr(period);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
var result = willr.Update(_data.Bars[i]);
|
||||
|
||||
if (i >= period - 1)
|
||||
{
|
||||
Assert.True(double.IsFinite(result.Value),
|
||||
$"Non-finite output at bar {i}: {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine("All outputs finite after warmup verified.");
|
||||
}
|
||||
|
||||
// --- M) Range bounded ---
|
||||
|
||||
[Fact]
|
||||
public void Output_Bounded_Neg100_To_Zero()
|
||||
{
|
||||
const int period = 14;
|
||||
var result = Willr.Batch(_data.Bars, period);
|
||||
|
||||
for (int i = period - 1; i < _data.Bars.Count; i++)
|
||||
{
|
||||
double val = result.Values[i];
|
||||
Assert.True(val >= -100.0 && val <= 0.0,
|
||||
$"WillR value {val} out of [-100, 0] range at bar {i}");
|
||||
}
|
||||
|
||||
_output.WriteLine("All WillR values within [-100, 0] range.");
|
||||
}
|
||||
|
||||
// ── Cross-library: OoplesFinance ──────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Willr_MatchesOoples_Structural()
|
||||
{
|
||||
const int period = 14;
|
||||
var ooplesData = _data.Bars.Select(static b => new TickerData
|
||||
{
|
||||
Date = new DateTime(b.Time, DateTimeKind.Utc),
|
||||
Open = b.Open,
|
||||
High = b.High,
|
||||
Low = b.Low,
|
||||
Close = b.Close,
|
||||
Volume = b.Volume
|
||||
}).ToList();
|
||||
|
||||
var stockData = new StockData(ooplesData);
|
||||
var oResult = stockData.CalculateWilliamsR(length: period);
|
||||
var oValues = oResult.OutputValues.Values.First();
|
||||
|
||||
var willr = new Willr(period);
|
||||
var qValues = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
qValues.Add(willr.Update(bar).Value);
|
||||
}
|
||||
|
||||
Assert.True(oValues.Count > 0, "Ooples WillR must produce output");
|
||||
int finiteCount = 0;
|
||||
for (int i = period; i < Math.Min(oValues.Count, qValues.Count); i++)
|
||||
{
|
||||
if (double.IsFinite(oValues[i]) && double.IsFinite(qValues[i]))
|
||||
{
|
||||
finiteCount++;
|
||||
}
|
||||
}
|
||||
Assert.True(finiteCount > 100, $"Expected >100 finite WillR pairs, got {finiteCount}");
|
||||
_output.WriteLine($"WillR Ooples structural: {finiteCount} finite pairs verified.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user