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,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);
}
}
+393
View File
@@ -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;
}
}