mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-07 13:37:44 +00:00
060649192f
- 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
194 lines
5.9 KiB
C#
194 lines
5.9 KiB
C#
using Xunit.Abstractions;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public sealed class RainValidationTests : IDisposable
|
|
{
|
|
private readonly ITestOutputHelper _output;
|
|
private readonly GBM _gbm;
|
|
private readonly TBarSeries _bars;
|
|
private const int BarCount = 1000;
|
|
private const int DefaultPeriod = 10;
|
|
private const double Tolerance = 1e-9;
|
|
private bool _disposed;
|
|
|
|
public RainValidationTests(ITestOutputHelper output)
|
|
{
|
|
_output = output;
|
|
_gbm = new GBM();
|
|
_bars = _gbm.Fetch(BarCount, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private void Dispose(bool disposing)
|
|
{
|
|
if (!_disposed && disposing)
|
|
{
|
|
_disposed = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates RAIN against a naive reference implementation:
|
|
/// 10 cascaded SMAs with weighted average [5,4,3,2,1,1,1,1,1,1]/20
|
|
/// </summary>
|
|
[Fact]
|
|
public void Rain_MatchesNaiveReference_Batch()
|
|
{
|
|
double[] closes = new double[BarCount];
|
|
for (int i = 0; i < BarCount; i++)
|
|
{
|
|
closes[i] = _bars[i].Close;
|
|
}
|
|
|
|
// QuanTAlib RAIN
|
|
double[] rainOutput = new double[BarCount];
|
|
Rain.Batch((ReadOnlySpan<double>)closes, rainOutput, DefaultPeriod);
|
|
|
|
// Naive reference: 10 cascaded SMAs
|
|
double[] layer0 = NaiveSma(closes, DefaultPeriod);
|
|
double[] layer1 = NaiveSma(layer0, DefaultPeriod);
|
|
double[] layer2 = NaiveSma(layer1, DefaultPeriod);
|
|
double[] layer3 = NaiveSma(layer2, DefaultPeriod);
|
|
double[] layer4 = NaiveSma(layer3, DefaultPeriod);
|
|
double[] layer5 = NaiveSma(layer4, DefaultPeriod);
|
|
double[] layer6 = NaiveSma(layer5, DefaultPeriod);
|
|
double[] layer7 = NaiveSma(layer6, DefaultPeriod);
|
|
double[] layer8 = NaiveSma(layer7, DefaultPeriod);
|
|
double[] layer9 = NaiveSma(layer8, DefaultPeriod);
|
|
|
|
// Weighted average: [5,4,3,2,1,1,1,1,1,1]/20
|
|
double[] expected = new double[BarCount];
|
|
for (int i = 0; i < BarCount; i++)
|
|
{
|
|
expected[i] = (5.0 * layer0[i] + 4.0 * layer1[i] + 3.0 * layer2[i] + 2.0 * layer3[i]
|
|
+ layer4[i] + layer5[i] + layer6[i] + layer7[i] + layer8[i] + layer9[i]) / 20.0;
|
|
}
|
|
|
|
// Compare after all layers are fully warmed (10 * period = 100)
|
|
int warmup = DefaultPeriod * 10;
|
|
double maxDiff = 0;
|
|
for (int i = warmup; i < BarCount; i++)
|
|
{
|
|
double diff = Math.Abs(rainOutput[i] - expected[i]);
|
|
if (diff > maxDiff)
|
|
{
|
|
maxDiff = diff;
|
|
}
|
|
|
|
Assert.True(diff < Tolerance,
|
|
$"Bar {i}: RAIN={rainOutput[i]:F12}, Expected={expected[i]:F12}, Diff={diff:E3}");
|
|
}
|
|
|
|
_output.WriteLine($"RAIN vs Naive Reference: maxDiff={maxDiff:E3} (tolerance={Tolerance:E1})");
|
|
}
|
|
|
|
[Fact]
|
|
public void Rain_StreamingMatchesBatch()
|
|
{
|
|
double[] closes = new double[BarCount];
|
|
for (int i = 0; i < BarCount; i++)
|
|
{
|
|
closes[i] = _bars[i].Close;
|
|
}
|
|
|
|
// Batch
|
|
double[] batchOutput = new double[BarCount];
|
|
Rain.Batch((ReadOnlySpan<double>)closes, batchOutput, DefaultPeriod);
|
|
|
|
// Streaming
|
|
var rain = new Rain(DefaultPeriod);
|
|
double[] streamOutput = new double[BarCount];
|
|
for (int i = 0; i < BarCount; i++)
|
|
{
|
|
var result = rain.Update(new TValue(_bars[i].Time, closes[i]));
|
|
streamOutput[i] = result.Value;
|
|
}
|
|
|
|
double maxDiff = 0;
|
|
for (int i = 0; i < BarCount; i++)
|
|
{
|
|
double diff = Math.Abs(batchOutput[i] - streamOutput[i]);
|
|
if (diff > maxDiff)
|
|
{
|
|
maxDiff = diff;
|
|
}
|
|
|
|
Assert.True(diff < Tolerance,
|
|
$"Bar {i}: Batch={batchOutput[i]:F12}, Stream={streamOutput[i]:F12}, Diff={diff:E3}");
|
|
}
|
|
|
|
_output.WriteLine($"RAIN Batch vs Streaming: maxDiff={maxDiff:E3} (tolerance={Tolerance:E1})");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(2)]
|
|
[InlineData(5)]
|
|
[InlineData(10)]
|
|
[InlineData(20)]
|
|
[InlineData(50)]
|
|
public void Rain_DifferentPeriods_AllConsistent(int period)
|
|
{
|
|
double[] closes = new double[BarCount];
|
|
for (int i = 0; i < BarCount; i++)
|
|
{
|
|
closes[i] = _bars[i].Close;
|
|
}
|
|
|
|
double[] batchOutput = new double[BarCount];
|
|
Rain.Batch((ReadOnlySpan<double>)closes, batchOutput, period);
|
|
|
|
var rain = new Rain(period);
|
|
for (int i = 0; i < BarCount; i++)
|
|
{
|
|
rain.Update(new TValue(_bars[i].Time, closes[i]));
|
|
}
|
|
|
|
Assert.Equal(rain.Last.Value, batchOutput[^1], Tolerance);
|
|
_output.WriteLine($"Period {period}: Last={rain.Last.Value:F10}");
|
|
}
|
|
|
|
[Fact]
|
|
public void Rain_ConstantInput_ConvergesToConstant()
|
|
{
|
|
const double constant = 42.0;
|
|
const int period = 5;
|
|
|
|
var rain = new Rain(period);
|
|
for (int i = 0; i < 200; i++)
|
|
{
|
|
rain.Update(new TValue(DateTime.UtcNow.AddMinutes(i), constant));
|
|
}
|
|
|
|
// After convergence, RAIN of a constant should be the constant
|
|
Assert.Equal(constant, rain.Last.Value, 1e-10);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Naive SMA (N-point) for validation. Uses expanding window during warmup.
|
|
/// </summary>
|
|
private static double[] NaiveSma(double[] source, int period)
|
|
{
|
|
double[] result = new double[source.Length];
|
|
|
|
for (int i = 0; i < source.Length; i++)
|
|
{
|
|
int start = Math.Max(0, i - period + 1);
|
|
int count = i - start + 1;
|
|
double sum = 0;
|
|
for (int j = start; j <= i; j++)
|
|
{
|
|
sum += source[j];
|
|
}
|
|
result[i] = sum / count;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|