Files
Miha Kralj 060649192f 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
2026-03-12 12:34:16 -07:00

312 lines
9.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}");
}
}