mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-12 15:48:05 +00:00
- 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
215 lines
6.7 KiB
C#
215 lines
6.7 KiB
C#
using QuanTAlib;
|
|
using Xunit;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
/// <summary>
|
|
/// Validation tests for Decaychannel - internal consistency tests only.
|
|
/// No external library validation available (N/A in TA-Lib, Skender, Tulip, Ooples).
|
|
/// </summary>
|
|
public sealed class DecaychannelValidationTests : IDisposable
|
|
{
|
|
private readonly GBM _gbm;
|
|
private readonly TBarSeries _series;
|
|
private const int Period = 14;
|
|
private const int DataLength = 500;
|
|
|
|
public DecaychannelValidationTests()
|
|
{
|
|
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
|
_series = new TBarSeries();
|
|
for (int i = 0; i < DataLength; i++)
|
|
{
|
|
_series.Add(_gbm.Next(isNew: true));
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Cleanup if needed
|
|
}
|
|
|
|
[Fact]
|
|
public void Streaming_Batch_Span_Consistency()
|
|
{
|
|
// Streaming mode
|
|
var dStream = new Decaychannel(Period);
|
|
for (int i = 0; i < _series.Count; i++)
|
|
{
|
|
dStream.Update(_series[i], isNew: true);
|
|
}
|
|
|
|
// Batch mode (TBarSeries overload)
|
|
var (midBatch, upBatch, loBatch) = Decaychannel.Batch(_series, Period);
|
|
|
|
// Span mode
|
|
int len = _series.Count;
|
|
double[] midSpan = new double[len];
|
|
double[] upSpan = new double[len];
|
|
double[] loSpan = new double[len];
|
|
Decaychannel.Batch(_series.HighValues, _series.LowValues, midSpan.AsSpan(), upSpan.AsSpan(), loSpan.AsSpan(), Period);
|
|
|
|
// Compare last 100 values across all modes
|
|
int checkStart = len - 100;
|
|
for (int i = checkStart; i < len; i++)
|
|
{
|
|
Assert.Equal(midBatch[i].Value, midSpan[i], 1e-10);
|
|
Assert.Equal(upBatch[i].Value, upSpan[i], 1e-10);
|
|
Assert.Equal(loBatch[i].Value, loSpan[i], 1e-10);
|
|
}
|
|
|
|
// Streaming vs batch final values
|
|
Assert.Equal(dStream.Last.Value, midBatch.Last.Value, 1e-10);
|
|
Assert.Equal(dStream.Upper.Value, upBatch.Last.Value, 1e-10);
|
|
Assert.Equal(dStream.Lower.Value, loBatch.Last.Value, 1e-10);
|
|
}
|
|
|
|
[Fact]
|
|
public void AllModes_ProduceFiniteResults()
|
|
{
|
|
// Streaming
|
|
var dStream = new Decaychannel(Period);
|
|
for (int i = 0; i < _series.Count; i++)
|
|
{
|
|
var result = dStream.Update(_series[i], isNew: true);
|
|
if (i >= Period - 1)
|
|
{
|
|
Assert.True(double.IsFinite(result.Value), $"Streaming: bar {i} should be finite");
|
|
Assert.True(double.IsFinite(dStream.Upper.Value), $"Streaming upper: bar {i} should be finite");
|
|
Assert.True(double.IsFinite(dStream.Lower.Value), $"Streaming lower: bar {i} should be finite");
|
|
}
|
|
}
|
|
|
|
// Batch
|
|
var (mid, up, lo) = Decaychannel.Batch(_series, Period);
|
|
for (int i = Period - 1; i < mid.Count; i++)
|
|
{
|
|
Assert.True(double.IsFinite(mid[i].Value), $"Batch middle: index {i} should be finite");
|
|
Assert.True(double.IsFinite(up[i].Value), $"Batch upper: index {i} should be finite");
|
|
Assert.True(double.IsFinite(lo[i].Value), $"Batch lower: index {i} should be finite");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void BandOrdering_AlwaysValid()
|
|
{
|
|
var d = new Decaychannel(Period);
|
|
for (int i = 0; i < _series.Count; i++)
|
|
{
|
|
d.Update(_series[i], isNew: true);
|
|
|
|
Assert.True(d.Upper.Value >= d.Lower.Value, $"Bar {i}: Upper >= Lower");
|
|
Assert.True(d.Last.Value >= d.Lower.Value - 1e-10, $"Bar {i}: Middle >= Lower");
|
|
Assert.True(d.Last.Value <= d.Upper.Value + 1e-10, $"Bar {i}: Middle <= Upper");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Calculate_Static_ReturnsValidIndicator()
|
|
{
|
|
var ((mid, up, lo), indicator) = Decaychannel.Calculate(_series, Period);
|
|
|
|
Assert.True(indicator.IsHot);
|
|
Assert.Equal(mid.Count, _series.Count);
|
|
Assert.Equal(up.Count, _series.Count);
|
|
Assert.Equal(lo.Count, _series.Count);
|
|
|
|
// Can continue streaming
|
|
var newBar = _gbm.Next(isNew: true);
|
|
indicator.Update(newBar, isNew: true);
|
|
Assert.True(double.IsFinite(indicator.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void DecayBehavior_ChannelContractsWithoutNewExtremes()
|
|
{
|
|
var d = new Decaychannel(Period);
|
|
|
|
// Prime with data
|
|
for (int i = 0; i < Period * 2; i++)
|
|
{
|
|
d.Update(_series[i], isNew: true);
|
|
}
|
|
|
|
double widthBefore = d.Upper.Value - d.Lower.Value;
|
|
|
|
// Feed flat bars at midpoint
|
|
double mid = (d.Upper.Value + d.Lower.Value) * 0.5;
|
|
for (int i = 0; i < Period; i++)
|
|
{
|
|
d.Update(new TBar(DateTime.UtcNow, mid, mid, mid, mid, 1000), isNew: true);
|
|
}
|
|
|
|
double widthAfter = d.Upper.Value - d.Lower.Value;
|
|
|
|
Assert.True(widthAfter < widthBefore, "Channel should contract when no new extremes");
|
|
}
|
|
|
|
[Fact]
|
|
public void BarCorrection_RestoresState()
|
|
{
|
|
var d = new Decaychannel(Period);
|
|
|
|
// Build up state
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
d.Update(_series[i], isNew: true);
|
|
}
|
|
|
|
double midBefore = d.Last.Value;
|
|
double upBefore = d.Upper.Value;
|
|
double loBefore = d.Lower.Value;
|
|
|
|
// Apply correction
|
|
var correctionBar = new TBar(DateTime.UtcNow, 200, 250, 150, 200, 1000);
|
|
d.Update(correctionBar, isNew: false);
|
|
|
|
// Values should change
|
|
Assert.NotEqual(midBefore, d.Last.Value);
|
|
|
|
// Apply another correction back to original-ish
|
|
d.Update(_series[49], isNew: false);
|
|
|
|
// Should restore
|
|
Assert.Equal(midBefore, d.Last.Value, 1e-10);
|
|
Assert.Equal(upBefore, d.Upper.Value, 1e-10);
|
|
Assert.Equal(loBefore, d.Lower.Value, 1e-10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Event_FiresOnUpdate()
|
|
{
|
|
var src = new TBarSeries();
|
|
var d = new Decaychannel(src, Period);
|
|
|
|
int eventCount = 0;
|
|
d.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
src.Add(_series[i]);
|
|
}
|
|
|
|
Assert.Equal(10, eventCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void MultiPeriod_Consistency()
|
|
{
|
|
int[] periods = [5, 10, 20, 50];
|
|
|
|
foreach (int period in periods)
|
|
{
|
|
var d = new Decaychannel(period);
|
|
for (int i = 0; i < _series.Count; i++)
|
|
{
|
|
d.Update(_series[i], isNew: true);
|
|
}
|
|
|
|
Assert.True(d.IsHot, $"Period {period} should be hot");
|
|
Assert.True(double.IsFinite(d.Last.Value), $"Period {period} middle should be finite");
|
|
Assert.True(d.Upper.Value >= d.Lower.Value, $"Period {period} upper >= lower");
|
|
}
|
|
}
|
|
}
|