mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08:05 +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,159 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RainIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RainIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new RainIndicator();
|
||||
|
||||
Assert.Equal(2, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("RAIN - Rainbow Moving Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RainIndicator_MinHistoryDepths_IsZero()
|
||||
{
|
||||
var indicator = new RainIndicator { Period = 10 };
|
||||
|
||||
Assert.Equal(0, RainIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RainIndicator_ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new RainIndicator { Period = 6 };
|
||||
|
||||
Assert.Contains("RAIN", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("6", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RainIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new RainIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Rain.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RainIndicator_Initialize_CreatesInternalRain()
|
||||
{
|
||||
var indicator = new RainIndicator { Period = 4 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RainIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RainIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RainIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RainIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RainIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new RainIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RainIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new RainIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 104, 103, 105, 107, 106 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RainIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new RainIndicator { Period = 3, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RainIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new RainIndicator { Period = 4 };
|
||||
Assert.Equal(4, indicator.Period);
|
||||
|
||||
indicator.Period = 10;
|
||||
Assert.Equal(10, indicator.Period);
|
||||
Assert.Equal(0, RainIndicator.MinHistoryDepths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class RainTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rain(0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
|
||||
var ex2 = Assert.Throws<ArgumentException>(() => new Rain(-1));
|
||||
Assert.Equal("period", ex2.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidPeriod_SetsProperties()
|
||||
{
|
||||
var rain = new Rain(5);
|
||||
|
||||
Assert.Equal("Rain(5)", rain.Name);
|
||||
Assert.Equal(50, rain.WarmupPeriod); // 5 * 10 layers
|
||||
Assert.False(rain.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var rain = new Rain(2);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rain.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rain.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAfterWarmup()
|
||||
{
|
||||
const int period = 3;
|
||||
var rain = new Rain(period);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rain.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
|
||||
if (i < period - 1)
|
||||
{
|
||||
// First layer not yet hot
|
||||
Assert.False(rain.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
// After 100 bars with period=3, all layers should be hot
|
||||
Assert.True(rain.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_BarCorrection_Works()
|
||||
{
|
||||
var rain = new Rain(5);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed first 99
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
rain.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
// Update with 100th point (isNew=true)
|
||||
rain.Update(new TValue(bars[99].Time, bars[99].Close), true);
|
||||
|
||||
// Update with modified 100th point (isNew=false)
|
||||
var val2 = rain.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), false);
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var rain2 = new Rain(5);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
rain2.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
var val3 = rain2.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), true);
|
||||
|
||||
Assert.Equal(val3.Value, val2.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrection_RestoresState()
|
||||
{
|
||||
var rain = new Rain(3);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed all bars
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rain.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
double afterAll = rain.Last.Value;
|
||||
|
||||
// Now update last bar with isNew=false using same value
|
||||
rain.Update(new TValue(bars[^1].Time, bars[^1].Close), false);
|
||||
double afterCorrection = rain.Last.Value;
|
||||
|
||||
Assert.Equal(afterAll, afterCorrection, 1e-12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var rain = new Rain(5);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rain.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
Assert.True(rain.IsHot);
|
||||
|
||||
rain.Reset();
|
||||
|
||||
Assert.False(rain.IsHot);
|
||||
Assert.Equal(default, rain.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_HandledGracefully()
|
||||
{
|
||||
var rain = new Rain(3);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed some valid data first
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rain.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
// Feed NaN
|
||||
rain.Update(new TValue(bars[20].Time, double.NaN));
|
||||
Assert.True(double.IsFinite(rain.Last.Value));
|
||||
|
||||
// Feed Infinity
|
||||
rain.Update(new TValue(bars[21].Time, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(rain.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_Safe()
|
||||
{
|
||||
var rain = new Rain(3);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed some valid, then batch of NaN
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
rain.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
for (int i = 10; i < 15; i++)
|
||||
{
|
||||
rain.Update(new TValue(bars[i].Time, double.NaN));
|
||||
}
|
||||
|
||||
for (int i = 15; i < 50; i++)
|
||||
{
|
||||
rain.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rain.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModeConsistency_BatchMatchesStreaming()
|
||||
{
|
||||
const int period = 3;
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Build TSeries from bars
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
series.Add(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
// Mode 1: Streaming
|
||||
var rain1 = new Rain(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rain1.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
// Mode 2: Batch (TSeries)
|
||||
var batchResult = Rain.Batch(series, period);
|
||||
|
||||
// Mode 3: Span
|
||||
Span<double> spanOut = new double[series.Count];
|
||||
Rain.Batch(series.Values, spanOut, period);
|
||||
|
||||
// All should match at the last value
|
||||
Assert.Equal(rain1.Last.Value, batchResult[^1].Value, 1e-9);
|
||||
Assert.Equal(rain1.Last.Value, spanOut[^1], 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModeConsistency_EventMatchesStreaming()
|
||||
{
|
||||
const int period = 3;
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var series = new TSeries();
|
||||
var rain = new Rain(series, period);
|
||||
|
||||
// Feed via events
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
series.Add(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
// Create fresh streaming
|
||||
var rain2 = new Rain(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rain2.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
Assert.Equal(rain2.Last.Value, rain.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ArgumentValidation()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] output = new double[5]; // Wrong length
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Rain.Batch((ReadOnlySpan<double>)src, output, 3));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_InvalidPeriod_Throws()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Rain.Batch((ReadOnlySpan<double>)src, output, 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_EmptyInput_NoOp()
|
||||
{
|
||||
Span<double> src = [];
|
||||
Span<double> output = [];
|
||||
|
||||
Rain.Batch((ReadOnlySpan<double>)src, output, 3); // Should not throw
|
||||
Assert.True(true); // Explicit assertion for S2699
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesTSeries()
|
||||
{
|
||||
const int period = 4;
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
series.Add(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
var batchResult = Rain.Batch(series, period);
|
||||
|
||||
Span<double> spanOut = new double[series.Count];
|
||||
Rain.Batch(series.Values, spanOut, period);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, spanOut[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_PubFires()
|
||||
{
|
||||
var rain = new Rain(3);
|
||||
int pubCount = 0;
|
||||
rain.Pub += Handler;
|
||||
|
||||
// skipcq: CS-R1140 - S2123 false positive: pubCount is captured and read below
|
||||
void Handler(object? sender, in TValueEventArgs args) { pubCount++; }
|
||||
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
rain.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
Assert.Equal(10, pubCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
const int period = 2;
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
series.Add(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
var (results, indicator) = Rain.Calculate(series, period);
|
||||
|
||||
Assert.Equal(series.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(results[^1].Value, indicator.Last.Value, 1e-12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Period1_ReturnsPriceItself()
|
||||
{
|
||||
// With period=1, each SMA(x, 1) = x, so all 10 layers return the input.
|
||||
// Weighted average of same value = that value.
|
||||
var rain = new Rain(1);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = rain.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
Assert.Equal(bars[i].Close, result.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_LargeData_NoStackOverflow()
|
||||
{
|
||||
const int period = 10;
|
||||
const int size = 10000;
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(size, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
double[] src = new double[size];
|
||||
double[] output = new double[size];
|
||||
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
src[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
Rain.Batch((ReadOnlySpan<double>)src, output, period);
|
||||
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_UnsubscribesFromSource()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var rain = new Rain(series, 3);
|
||||
|
||||
rain.Dispose();
|
||||
|
||||
// Adding to series after dispose should not affect the disposed indicator
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
double lastValue = rain.Last.Value;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
series.Add(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
Assert.Equal(lastValue, rain.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user