mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 13:08:04 +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,126 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class DemIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DemIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new DemIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("DEM - DeMarker Oscillator", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DemIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new DemIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, DemIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DemIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new DemIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("DEM", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DemIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new DemIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Dem.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DemIndicator_Initialize_CreatesOneLineSeries()
|
||||
{
|
||||
var indicator = new DemIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DemIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new DemIndicator { 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 demValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(demValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DemIndicator_ProcessUpdate_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new DemIndicator { 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);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.True(indicator.LinesSeries[0].Count >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DemIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new DemIndicator { Period = 21 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(21, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DemIndicator_OhlcInput_ComputesFiniteValues()
|
||||
{
|
||||
var indicator = new DemIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100.0 + i * 0.5;
|
||||
indicator.HistoricalData.AddBar(
|
||||
now.AddMinutes(i),
|
||||
open: basePrice,
|
||||
high: basePrice + 3.0,
|
||||
low: basePrice - 2.0,
|
||||
close: basePrice + 1.0);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class DemTests
|
||||
{
|
||||
private readonly GBM _gbm = new(100.0, 0.05, 0.2, seed: 42);
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
// ───── A) Constructor validation ─────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriod_IsValid()
|
||||
{
|
||||
var dem = new Dem();
|
||||
Assert.Equal("Dem(14)", dem.Name);
|
||||
Assert.Equal(15, dem.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Dem(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Dem(period: -1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period1_IsValid()
|
||||
{
|
||||
var dem = new Dem(period: 1);
|
||||
Assert.Equal("Dem(1)", dem.Name);
|
||||
Assert.Equal(2, dem.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomPeriod_SetsCorrectly()
|
||||
{
|
||||
var dem = new Dem(period: 14);
|
||||
Assert.Equal("Dem(14)", dem.Name);
|
||||
Assert.Equal(15, dem.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ───── B) Basic calculation ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
var result = dem.Update(bar);
|
||||
Assert.IsType<TValue>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_IsAccessible()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
dem.Update(bar);
|
||||
Assert.True(double.IsFinite(dem.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_KnownValue_Period1_DeMaxOnly()
|
||||
{
|
||||
// period=1: SMA_DeMax=DeMax, SMA_DeMin=DeMin for that single bar
|
||||
// Bar 1: prevH=100, prevL=90; H=110, L=80 → DeMax=10, DeMin=10
|
||||
// DEM = 10/(10+10) = 0.5
|
||||
var dem = new Dem(period: 1);
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 90, 95, 1000);
|
||||
dem.Update(bar1, isNew: true); // first bar, prevHigh=High, prevLow=Low → DeMax=DeMin=0
|
||||
// bar2 uses bar1 as prev: prevHigh=100, prevLow=90
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 100, 110, 80, 100, 1000);
|
||||
var result = dem.Update(bar2, isNew: true);
|
||||
// DeMax = max(110-100, 0) = 10; DeMin = max(90-80, 0) = 10 → DEM = 10/20 = 0.5
|
||||
Assert.Equal(0.5, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_KnownValue_Period1_PureBullish()
|
||||
{
|
||||
// Bar 2: High much higher than prevHigh, Low same as prevLow → DeMin=0
|
||||
// DEM = DeMax / (DeMax + 0) = 1.0
|
||||
var dem = new Dem(period: 1);
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 90, 95, 1000);
|
||||
dem.Update(bar1, isNew: true);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 100, 110, 90, 105, 1000);
|
||||
var result = dem.Update(bar2, isNew: true);
|
||||
// DeMax = max(110-100, 0) = 10; DeMin = max(90-90, 0) = 0 → DEM = 10/10 = 1.0
|
||||
Assert.Equal(1.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_KnownValue_Period1_PureBearish()
|
||||
{
|
||||
// Bar 2: Low much lower than prevLow, High same as prevHigh → DeMax=0
|
||||
// DEM = 0 / (0 + DeMin) = 0.0
|
||||
var dem = new Dem(period: 1);
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 90, 95, 1000);
|
||||
dem.Update(bar1, isNew: true);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 100, 100, 80, 85, 1000);
|
||||
var result = dem.Update(bar2, isNew: true);
|
||||
// DeMax = max(100-100, 0) = 0; DeMin = max(90-80, 0) = 10 → DEM = 0/10 = 0.0
|
||||
Assert.Equal(0.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_OutputInRange_0_to_1()
|
||||
{
|
||||
var dem = new Dem(period: 14);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var result = dem.Update(_gbm.Next(isNew: true));
|
||||
if (dem.IsHot)
|
||||
{
|
||||
Assert.True(result.Value >= 0.0, $"DEM below 0: {result.Value}");
|
||||
Assert.True(result.Value <= 1.0, $"DEM above 1: {result.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ───── C) State + bar correction ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_True_AdvancesState()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
dem.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
Assert.True(double.IsFinite(dem.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_False_RollsBack()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
dem.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow, 105, 110, 100, 107, 1000);
|
||||
dem.Update(bar, isNew: false);
|
||||
double corrected1 = dem.Last.Value;
|
||||
|
||||
dem.Update(bar, isNew: false);
|
||||
double corrected2 = dem.Last.Value;
|
||||
|
||||
Assert.Equal(corrected1, corrected2, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_Restore()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
var bars = new TBar[15];
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
{
|
||||
bars[i] = _gbm.Next(isNew: true);
|
||||
}
|
||||
|
||||
foreach (var b in bars)
|
||||
{
|
||||
dem.Update(b, isNew: true);
|
||||
}
|
||||
|
||||
double baseline = dem.Last.Value;
|
||||
|
||||
// Corrupt with wildly different values
|
||||
dem.Update(new TBar(DateTime.UtcNow, 200, 250, 150, 220, 5000), isNew: false);
|
||||
dem.Update(new TBar(DateTime.UtcNow, 999, 1050, 900, 1000, 9999), isNew: false);
|
||||
// Restore with original last bar
|
||||
dem.Update(bars[^1], isNew: false);
|
||||
|
||||
Assert.Equal(baseline, dem.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
dem.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
dem.Reset();
|
||||
|
||||
Assert.False(dem.IsHot);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
dem.Update(bar, isNew: true);
|
||||
Assert.False(dem.IsHot);
|
||||
}
|
||||
|
||||
// ───── D) Warmup / IsHot ─────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BeforeWarmup_False()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
// period+1 = 6 bars needed; first 6 bars should NOT yet be hot (needs > period bars)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
dem.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
Assert.False(dem.IsHot, $"Should not be hot after {i + 1} bar(s)");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterWarmup_True()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
dem.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
Assert.True(dem.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsPeriodPlusOne()
|
||||
{
|
||||
Assert.Equal(15, new Dem(14).WarmupPeriod);
|
||||
Assert.Equal(2, new Dem(1).WarmupPeriod);
|
||||
Assert.Equal(27, new Dem(26).WarmupPeriod);
|
||||
}
|
||||
|
||||
// ───── E) Robustness ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNInput_UsesLastValid()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
dem.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var nanBar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 0);
|
||||
var result = dem.Update(nanBar, isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value), "NaN input should not produce NaN output");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityInput_Handled()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
dem.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var infBar = new TBar(DateTime.UtcNow, 100, double.PositiveInfinity, 90, 100, 0);
|
||||
var result = dem.Update(infBar, isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value), "Infinity input should not propagate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BatchNaN_Safe()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
dem.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var nanBar = new TBar(DateTime.UtcNow.AddMinutes(i), double.NaN, double.NaN, double.NaN, double.NaN, 0);
|
||||
var result = dem.Update(nanBar, isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value), $"Batch NaN failed at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
// ───── F) Consistency ─────
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Consistency_Streaming_Equals_Batch()
|
||||
{
|
||||
const int N = 200;
|
||||
const int period = 14;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 1234);
|
||||
var highs = new double[N];
|
||||
var lows = new double[N];
|
||||
var bars = new TBar[N];
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var dem = new Dem(period);
|
||||
for (int i = 0; i < N; i++) { dem.Update(bars[i], isNew: true); }
|
||||
double streamVal = dem.Last.Value;
|
||||
|
||||
// Batch span
|
||||
var batchOut = new double[N];
|
||||
Dem.Batch(highs, lows, batchOut, period);
|
||||
|
||||
Assert.Equal(streamVal, batchOut[N - 1], Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Consistency_Deterministic_SameSeed()
|
||||
{
|
||||
const int period = 14;
|
||||
const int N = 100;
|
||||
|
||||
double run1, run2;
|
||||
|
||||
var gbm1 = new GBM(100.0, 0.05, 0.2, seed: 7);
|
||||
var dem1 = new Dem(period);
|
||||
for (int i = 0; i < N; i++) { dem1.Update(gbm1.Next(isNew: true), isNew: true); }
|
||||
run1 = dem1.Last.Value;
|
||||
|
||||
var gbm2 = new GBM(100.0, 0.05, 0.2, seed: 7);
|
||||
var dem2 = new Dem(period);
|
||||
for (int i = 0; i < N; i++) { dem2.Update(gbm2.Next(isNew: true), isNew: true); }
|
||||
run2 = dem2.Last.Value;
|
||||
|
||||
Assert.Equal(run1, run2, Tolerance);
|
||||
}
|
||||
|
||||
// ───── G) Span API tests ─────
|
||||
|
||||
[Fact]
|
||||
public void Batch_ZeroPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Dem.Batch(new double[10], new double[10], new double[10], period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLow_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Dem.Batch(new double[10], new double[5], new double[10], period: 3));
|
||||
Assert.Equal("low", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedOutput_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Dem.Batch(new double[10], new double[10], new double[5], period: 3));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_NoThrow()
|
||||
{
|
||||
var emptyOut = Array.Empty<double>();
|
||||
Dem.Batch([], [], emptyOut, period: 5);
|
||||
Assert.Empty(emptyOut);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputInRange_0_to_1()
|
||||
{
|
||||
const int N = 100;
|
||||
const int period = 14;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.3, seed: 99);
|
||||
var highs = new double[N];
|
||||
var lows = new double[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
highs[i] = bar.High;
|
||||
lows[i] = bar.Low;
|
||||
}
|
||||
|
||||
var output = new double[N];
|
||||
Dem.Batch(highs, lows, output, period);
|
||||
|
||||
for (int i = period; i < N; i++)
|
||||
{
|
||||
Assert.True(output[i] >= 0.0, $"Batch DEM[{i}] = {output[i]} below 0");
|
||||
Assert.True(output[i] <= 1.0, $"Batch DEM[{i}] = {output[i]} above 1");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargePeriod_UsesArrayPool()
|
||||
{
|
||||
// period > 256 forces ArrayPool path
|
||||
const int N = 500;
|
||||
const int period = 300;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 11);
|
||||
var highs = new double[N];
|
||||
var lows = new double[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
highs[i] = bar.High;
|
||||
lows[i] = bar.Low;
|
||||
}
|
||||
|
||||
var output = new double[N];
|
||||
// Should not throw
|
||||
Dem.Batch(highs, lows, output, period);
|
||||
Assert.True(double.IsFinite(output[N - 1]));
|
||||
}
|
||||
|
||||
// ───── H) Chainability ─────
|
||||
|
||||
[Fact]
|
||||
public void PubEvent_Fires_OnUpdate()
|
||||
{
|
||||
var dem = new Dem(period: 5);
|
||||
int count = 0;
|
||||
dem.Pub += (object? _, in TValueEventArgs e) => count++;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
dem.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(10, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBarSeries_Chaining_Works()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
var dem = new Dem(source, period: 5);
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 55);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
source.Add(gbm.Next(isNew: true));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(dem.Last.Value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Self-consistency validation for DEM (DeMarker Oscillator).
|
||||
/// No external library (TA-Lib, Skender, Tulip, Ooples) implements the DeMarker Oscillator,
|
||||
/// so validation uses: streaming == batch span consistency, mathematical identity checks,
|
||||
/// and directional correctness proofs.
|
||||
/// </summary>
|
||||
public sealed class DemValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
private readonly ITestOutputHelper _output = output;
|
||||
private const double Tolerance = 1e-12;
|
||||
|
||||
// ───── Self-consistency: streaming == batch span ─────
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch_Period14()
|
||||
{
|
||||
const int N = 200;
|
||||
const int period = 14;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 1001);
|
||||
var highs = new double[N];
|
||||
var lows = new double[N];
|
||||
var bars = new TBar[N];
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var dem = new Dem(period);
|
||||
for (int i = 0; i < N; i++) { dem.Update(bars[i], isNew: true); }
|
||||
double streamVal = dem.Last.Value;
|
||||
|
||||
// Batch span
|
||||
var batchOut = new double[N];
|
||||
Dem.Batch(highs, lows, batchOut, period);
|
||||
|
||||
_output.WriteLine($"Streaming DEM={streamVal:F10}, Batch DEM={batchOut[N - 1]:F10}");
|
||||
Assert.Equal(streamVal, batchOut[N - 1], Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch_Period1()
|
||||
{
|
||||
const int N = 100;
|
||||
const int period = 1;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.3, seed: 2002);
|
||||
var highs = new double[N];
|
||||
var lows = new double[N];
|
||||
var bars = new TBar[N];
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
}
|
||||
|
||||
var dem = new Dem(period);
|
||||
for (int i = 0; i < N; i++) { dem.Update(bars[i], isNew: true); }
|
||||
|
||||
var batchOut = new double[N];
|
||||
Dem.Batch(highs, lows, batchOut, period);
|
||||
|
||||
Assert.Equal(dem.Last.Value, batchOut[N - 1], Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch_Period5()
|
||||
{
|
||||
const int N = 150;
|
||||
const int period = 5;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.25, seed: 3003);
|
||||
var highs = new double[N];
|
||||
var lows = new double[N];
|
||||
var bars = new TBar[N];
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
}
|
||||
|
||||
var dem = new Dem(period);
|
||||
for (int i = 0; i < N; i++) { dem.Update(bars[i], isNew: true); }
|
||||
|
||||
var batchOut = new double[N];
|
||||
Dem.Batch(highs, lows, batchOut, period);
|
||||
|
||||
Assert.Equal(dem.Last.Value, batchOut[N - 1], Tolerance);
|
||||
}
|
||||
|
||||
// ───── Mathematical identity checks ─────
|
||||
|
||||
[Fact]
|
||||
public void Validate_ConstantPrice_ZeroDerivatives_Neutral()
|
||||
{
|
||||
// Constant prices → DeMax=0, DeMin=0 every bar (from bar 2 onward)
|
||||
// → denominator=0 → DEM=0.5 (neutral guard)
|
||||
const int N = 30;
|
||||
const int period = 5;
|
||||
|
||||
var dem = new Dem(period);
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
dem.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: 100.0, high: 105.0, low: 95.0, close: 100.0, volume: 1000), isNew: true);
|
||||
}
|
||||
|
||||
_output.WriteLine($"Constant price DEM (expect 0.5): {dem.Last.Value}");
|
||||
Assert.Equal(0.5, dem.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_StrictlyRising_HighsOnly_DemEquals1()
|
||||
{
|
||||
// Every bar: High strictly above prevHigh, Low = prevLow or higher
|
||||
// → DeMax > 0 every bar, DeMin = 0 every bar → DEM = 1.0
|
||||
const int N = 30;
|
||||
const int period = 5;
|
||||
|
||||
var dem = new Dem(period);
|
||||
double h = 100.0;
|
||||
double l = 90.0;
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
dem.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: h, high: h + 1.0, low: l, close: h + 0.5, volume: 1000), isNew: true);
|
||||
h += 1.0;
|
||||
}
|
||||
|
||||
_output.WriteLine($"All-rising DEM (expect 1.0): {dem.Last.Value}");
|
||||
Assert.Equal(1.0, dem.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_StrictlyFalling_LowsOnly_DemEquals0()
|
||||
{
|
||||
// Every bar: Low strictly below prevLow, High = prevHigh or lower
|
||||
// → DeMax = 0 every bar, DeMin > 0 every bar → DEM = 0.0
|
||||
const int N = 30;
|
||||
const int period = 5;
|
||||
|
||||
var dem = new Dem(period);
|
||||
double h = 100.0;
|
||||
double l = 90.0;
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
dem.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: h, high: h, low: l - 1.0, close: h - 0.5, volume: 1000), isNew: true);
|
||||
l -= 1.0;
|
||||
}
|
||||
|
||||
_output.WriteLine($"All-falling DEM (expect 0.0): {dem.Last.Value}");
|
||||
Assert.Equal(0.0, dem.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_SymmetricBars_DemNear05()
|
||||
{
|
||||
// Alternating up/down bars of equal magnitude → DeMax ≈ DeMin → DEM ≈ 0.5
|
||||
const int N = 60;
|
||||
const int period = 14;
|
||||
|
||||
var dem = new Dem(period);
|
||||
double h = 100.0;
|
||||
double step = 1.0;
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
double high = h + step;
|
||||
double low = h - step;
|
||||
dem.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: h, high: high, low: low, close: h, volume: 1000), isNew: true);
|
||||
// Alternate sign to keep DeMax and DeMin balanced
|
||||
step = -step;
|
||||
}
|
||||
|
||||
_output.WriteLine($"Symmetric DEM (expect ~0.5): {dem.Last.Value}");
|
||||
// With alternating bars the sums balance, so DEM ~ 0.5
|
||||
Assert.True(dem.Last.Value is >= 0.0 and <= 1.0);
|
||||
}
|
||||
|
||||
// ───── Mathematical identity: DEM = SMADeMax / (SMADeMax + SMADeMin) ─────
|
||||
|
||||
[Fact]
|
||||
public void Validate_MathIdentity_DEM_Times_Denom_Equals_DeMaxSum()
|
||||
{
|
||||
// DEM × (SMADeMax + SMADeMin) == SMADeMax
|
||||
// We verify by recomputing components manually and checking the formula
|
||||
const int period = 5;
|
||||
const int N = 30;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 5050);
|
||||
var highs = new double[N];
|
||||
var lows = new double[N];
|
||||
var bars = new TBar[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
}
|
||||
|
||||
// Compute DEM values
|
||||
var demOut = new double[N];
|
||||
Dem.Batch(highs, lows, demOut, period);
|
||||
|
||||
// Manually compute DeMax and DeMin per bar
|
||||
var deMaxArr = new double[N];
|
||||
var deMinArr = new double[N];
|
||||
deMaxArr[0] = 0.0;
|
||||
deMinArr[0] = 0.0;
|
||||
for (int i = 1; i < N; i++)
|
||||
{
|
||||
deMaxArr[i] = Math.Max(highs[i] - highs[i - 1], 0.0);
|
||||
deMinArr[i] = Math.Max(lows[i - 1] - lows[i], 0.0);
|
||||
}
|
||||
|
||||
// Verify identity at last hot bar
|
||||
int last = N - 1;
|
||||
double smaDeMax = 0.0;
|
||||
double smaDeMin = 0.0;
|
||||
for (int j = last - period + 1; j <= last; j++)
|
||||
{
|
||||
smaDeMax += deMaxArr[j];
|
||||
smaDeMin += deMinArr[j];
|
||||
}
|
||||
smaDeMax /= period;
|
||||
smaDeMin /= period;
|
||||
|
||||
double expectedDem = (smaDeMax + smaDeMin) != 0.0
|
||||
? smaDeMax / (smaDeMax + smaDeMin)
|
||||
: 0.5;
|
||||
|
||||
_output.WriteLine($"Manual DEM={expectedDem:F10}, Batch DEM={demOut[last]:F10}");
|
||||
Assert.Equal(expectedDem, demOut[last], 1e-9);
|
||||
}
|
||||
|
||||
// ───── Output range validation ─────
|
||||
|
||||
[Fact]
|
||||
public void Validate_OutputAlwaysInRange_0_1()
|
||||
{
|
||||
const int N = 500;
|
||||
const int period = 14;
|
||||
|
||||
var gbm = new GBM(100.0, 0.1, 0.4, seed: 7777);
|
||||
var highs = new double[N];
|
||||
var lows = new double[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
highs[i] = bar.High;
|
||||
lows[i] = bar.Low;
|
||||
}
|
||||
|
||||
var batchOutput = new double[N];
|
||||
Dem.Batch(highs, lows, batchOutput, period);
|
||||
|
||||
int violations = 0;
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
if (batchOutput[i] < 0.0 || batchOutput[i] > 1.0)
|
||||
{
|
||||
violations++;
|
||||
_output.WriteLine($"Range violation at i={i}: DEM={batchOutput[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(0, violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dem_MatchesOoples_Structural()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var ooplesData = bars.Select(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 result = new StockData(ooplesData).CalculateDemarker();
|
||||
var values = result.CustomValuesList;
|
||||
int finiteCount = values.Count(v => double.IsFinite(v));
|
||||
Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user