mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-05 04:27:43 +00:00
060649192f
- 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
230 lines
7.7 KiB
C#
230 lines
7.7 KiB
C#
using Xunit;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
/// <summary>
|
|
/// MSTOCH self-consistency validation tests.
|
|
/// No external library implements Ehlers MESA Stochastic, so we validate
|
|
/// streaming==batch==span consistency, range enforcement, and directional
|
|
/// correctness against known deterministic inputs.
|
|
/// </summary>
|
|
public sealed class MstochValidationTests
|
|
{
|
|
private static double[] GeneratePrices(int count, int seed = 42)
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
|
|
var prices = new double[count];
|
|
for (int i = 0; i < count; i++) { prices[i] = gbm.Next(isNew: true).Close; }
|
|
return prices;
|
|
}
|
|
|
|
private static TSeries MakeSeries(double[] vals)
|
|
{
|
|
var times = new List<long>(vals.Length);
|
|
var values = new List<double>(vals.Length);
|
|
var t0 = DateTime.UtcNow;
|
|
for (int i = 0; i < vals.Length; i++)
|
|
{
|
|
times.Add(t0.AddSeconds(i).Ticks);
|
|
values.Add(vals[i]);
|
|
}
|
|
return new TSeries(times, values);
|
|
}
|
|
|
|
// --- A) Streaming == Batch(TSeries) ---
|
|
|
|
[Fact]
|
|
public void Streaming_Matches_Batch_TSeries()
|
|
{
|
|
var prices = GeneratePrices(300);
|
|
var series = MakeSeries(prices);
|
|
const int stochLength = 20;
|
|
const int hpLength = 48;
|
|
const int ssLength = 10;
|
|
|
|
// Streaming
|
|
var mstoch = new Mstoch(stochLength, hpLength, ssLength);
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
mstoch.Update(series[i]);
|
|
}
|
|
|
|
// Batch
|
|
TSeries batchResult = Mstoch.Batch(series, stochLength, hpLength, ssLength);
|
|
|
|
Assert.Equal(mstoch.Last.Value, batchResult[^1].Value, 6);
|
|
}
|
|
|
|
// --- B) Batch(TSeries) == Batch(Span) ---
|
|
|
|
[Fact]
|
|
public void Batch_TSeries_Matches_Span()
|
|
{
|
|
var prices = GeneratePrices(200);
|
|
var series = MakeSeries(prices);
|
|
const int stochLength = 15;
|
|
const int hpLength = 30;
|
|
const int ssLength = 7;
|
|
|
|
TSeries tsBatch = Mstoch.Batch(series, stochLength, hpLength, ssLength);
|
|
|
|
var spanOut = new double[prices.Length];
|
|
Mstoch.Batch(prices.AsSpan(), spanOut.AsSpan(), stochLength, hpLength, ssLength);
|
|
|
|
for (int i = 0; i < prices.Length; i++)
|
|
{
|
|
Assert.Equal(tsBatch.Values[i], spanOut[i], 12);
|
|
}
|
|
}
|
|
|
|
// --- C) Output always in [0,1] ---
|
|
|
|
[Fact]
|
|
public void AllOutputs_InRange_Zero_To_One_Streaming()
|
|
{
|
|
var prices = GeneratePrices(500, seed: 123);
|
|
var t0 = DateTime.UtcNow;
|
|
var mstoch = new Mstoch(stochLength: 20, hpLength: 48, ssLength: 10);
|
|
for (int i = 0; i < prices.Length; i++)
|
|
{
|
|
TValue result = mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]));
|
|
Assert.True(result.Value >= 0.0 && result.Value <= 1.0,
|
|
$"Bar {i}: value {result.Value} out of [0,1]");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AllOutputs_InRange_Zero_To_One_Batch()
|
|
{
|
|
var prices = GeneratePrices(500, seed: 456);
|
|
var out_ = new double[prices.Length];
|
|
Mstoch.Batch(prices.AsSpan(), out_.AsSpan(), stochLength: 20, hpLength: 48, ssLength: 10);
|
|
for (int i = 0; i < out_.Length; i++)
|
|
{
|
|
Assert.True(out_[i] >= 0.0 && out_[i] <= 1.0,
|
|
$"Bar {i}: value {out_[i]} out of [0,1]");
|
|
}
|
|
}
|
|
|
|
// --- D) Constant input produces finite output (zero range -> midpoint) ---
|
|
|
|
[Fact]
|
|
public void ConstantInput_ProducesFiniteOutput()
|
|
{
|
|
double[] prices = Enumerable.Repeat(100.0, 100).ToArray();
|
|
var out_ = new double[100];
|
|
Mstoch.Batch(prices.AsSpan(), out_.AsSpan(), stochLength: 20, hpLength: 48, ssLength: 10);
|
|
for (int i = 0; i < out_.Length; i++)
|
|
{
|
|
Assert.True(double.IsFinite(out_[i]), $"Output[{i}] = {out_[i]} is not finite");
|
|
}
|
|
}
|
|
|
|
// --- E) Update(TSeries) matches Batch(TSeries) ---
|
|
|
|
[Fact]
|
|
public void Update_TSeries_Matches_Batch_TSeries()
|
|
{
|
|
var prices = GeneratePrices(150);
|
|
var series = MakeSeries(prices);
|
|
const int stochLength = 10;
|
|
const int hpLength = 20;
|
|
const int ssLength = 5;
|
|
|
|
var indicator = new Mstoch(stochLength, hpLength, ssLength);
|
|
TSeries updateResult = indicator.Update(series);
|
|
|
|
TSeries batchResult = Mstoch.Batch(series, stochLength, hpLength, ssLength);
|
|
|
|
// All values should match
|
|
for (int i = 0; i < prices.Length; i++)
|
|
{
|
|
Assert.Equal(batchResult.Values[i], updateResult.Values[i], 6);
|
|
}
|
|
}
|
|
|
|
// --- F) Calculate static factory returns consistent result ---
|
|
|
|
[Fact]
|
|
public void Calculate_Matches_Batch()
|
|
{
|
|
var prices = GeneratePrices(200, seed: 99);
|
|
var series = MakeSeries(prices);
|
|
const int stochLength = 20;
|
|
const int hpLength = 48;
|
|
const int ssLength = 10;
|
|
|
|
var (calcResult, _) = Mstoch.Calculate(series, stochLength, hpLength, ssLength);
|
|
TSeries batchResult = Mstoch.Batch(series, stochLength, hpLength, ssLength);
|
|
|
|
Assert.Equal(batchResult[^1].Value, calcResult[^1].Value, 6);
|
|
}
|
|
|
|
// --- G) Directional correctness ---
|
|
|
|
[Fact]
|
|
public void Rising_Then_Falling_Prices_ShowsDirectionalResponse()
|
|
{
|
|
// After enough rising prices, MSTOCH should be above midpoint (0.5)
|
|
var mstoch = new Mstoch(stochLength: 10, hpLength: 20, ssLength: 5);
|
|
var t0 = DateTime.UtcNow;
|
|
|
|
// Feed 100 warmup bars at constant 100
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
mstoch.Update(new TValue(t0.AddSeconds(i), 100.0));
|
|
}
|
|
|
|
// Feed 50 strongly rising bars
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
mstoch.Update(new TValue(t0.AddSeconds(100 + i), 100.0 + i * 2.0));
|
|
}
|
|
double risingVal = mstoch.Last.Value;
|
|
|
|
// Feed 50 strongly falling bars from a new instance reset
|
|
mstoch.Reset();
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
mstoch.Update(new TValue(t0.AddSeconds(i), 100.0));
|
|
}
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
mstoch.Update(new TValue(t0.AddSeconds(100 + i), 100.0 - i * 2.0));
|
|
}
|
|
double fallingVal = mstoch.Last.Value;
|
|
|
|
// MSTOCH is a cycle indicator based on HP-filtered (detrended) data.
|
|
// During a strong uptrend, the HP filter output is near its recent high → stochastic near 1.
|
|
// During a strong downtrend, the HP filter output is near its recent low → stochastic near 0.
|
|
// The two scenarios must produce distinctly different readings.
|
|
Assert.NotEqual(risingVal, fallingVal);
|
|
Assert.True(double.IsFinite(risingVal) && double.IsFinite(fallingVal),
|
|
$"Both values must be finite: rising={risingVal}, falling={fallingVal}");
|
|
// Validate they diverge significantly (opposite ends of [0,1])
|
|
Assert.True(Math.Abs(risingVal - fallingVal) > 0.5,
|
|
$"Rising ({risingVal}) and falling ({fallingVal}) should diverge by >0.5");
|
|
}
|
|
|
|
// --- H) NaN input self-consistency ---
|
|
|
|
[Fact]
|
|
public void SparseNaN_Streaming_OutputFinite()
|
|
{
|
|
var prices = GeneratePrices(100);
|
|
// Inject some NaNs
|
|
prices[10] = double.NaN;
|
|
prices[25] = double.NaN;
|
|
prices[50] = double.PositiveInfinity;
|
|
|
|
var t0 = DateTime.UtcNow;
|
|
var mstoch = new Mstoch(stochLength: 10, hpLength: 20, ssLength: 5);
|
|
for (int i = 0; i < prices.Length; i++)
|
|
{
|
|
TValue result = mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]));
|
|
Assert.True(double.IsFinite(result.Value),
|
|
$"Bar {i}: NaN/Inf input produced non-finite output {result.Value}");
|
|
}
|
|
}
|
|
}
|