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,153 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public sealed class TtmWaveIndicatorTests
{
[Fact]
public void TtmWaveIndicator_Constructor_SetsDefaults()
{
var indicator = new TtmWaveIndicator();
Assert.True(indicator.ShowColdValues);
Assert.Contains("TTM Wave", indicator.Name, StringComparison.Ordinal);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void TtmWaveIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new TtmWaveIndicator();
Assert.Equal(0, TtmWaveIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void TtmWaveIndicator_ShortName_IncludesIdentifier()
{
var indicator = new TtmWaveIndicator();
indicator.Initialize();
Assert.Contains("TTM_Wave", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void TtmWaveIndicator_SourceCodeLink_IsValid()
{
var indicator = new TtmWaveIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("TtmWave", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void TtmWaveIndicator_Initialize_CreatesLineSeries()
{
var indicator = new TtmWaveIndicator();
indicator.Initialize();
// 6 wave histograms + 1 zero line = 7 series
Assert.Equal(7, indicator.LinesSeries.Count);
}
[Fact]
public void TtmWaveIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new TtmWaveIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 800; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i * 0.1, 110 + i * 0.1, 90 + i * 0.1, 105 + i * 0.1);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Wave A1 (index 4 — added 5th in constructor order: C1,C2,B1,B2,A1,A2,Zero)
double waveA1 = indicator.LinesSeries[4].GetValue(0);
Assert.True(double.IsFinite(waveA1));
}
[Fact]
public void TtmWaveIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new TtmWaveIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 800; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i * 0.1, 110 + i * 0.1, 90 + i * 0.1, 105 + i * 0.1);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Simulate a new bar
indicator.HistoricalData.AddBar(now.AddMinutes(800), 180, 190, 170, 185);
var newArgs = new UpdateArgs(UpdateReason.NewBar);
indicator.ProcessUpdate(newArgs);
double waveA1 = indicator.LinesSeries[4].GetValue(0);
Assert.True(double.IsFinite(waveA1));
}
[Fact]
public void TtmWaveIndicator_ZeroLine_IsSet()
{
var indicator = new TtmWaveIndicator();
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);
}
// Zero line is the last series (index 6)
double zero = indicator.LinesSeries[6].GetValue(0);
Assert.Equal(0.0, zero, 1e-10);
}
[Fact]
public void TtmWaveIndicator_Description_IsSet()
{
var indicator = new TtmWaveIndicator();
Assert.NotNull(indicator.Description);
Assert.NotEmpty(indicator.Description);
Assert.Contains("TTM", indicator.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void TtmWaveIndicator_AllSeries_ProduceFiniteValues()
{
var indicator = new TtmWaveIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 800; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i * 0.1, 110 + i * 0.1, 90 + i * 0.1, 105 + i * 0.1);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// All 7 series should have finite values
for (int s = 0; s < 7; s++)
{
double val = indicator.LinesSeries[s].GetValue(0);
Assert.True(double.IsFinite(val), $"Series {s} value not finite: {val}");
}
}
}
@@ -0,0 +1,735 @@
using System.Runtime.InteropServices;
using Xunit;
namespace QuanTAlib.Tests;
// ══════════════════════════════════════════════════════════════
// A) Constructor Validation
// ══════════════════════════════════════════════════════════════
public sealed class TtmWaveConstructorTests
{
[Fact]
public void Constructor_Default_SetsName()
{
var wave = new TtmWave();
Assert.Equal("TtmWave", wave.Name);
}
[Fact]
public void Constructor_Default_NotHot()
{
var wave = new TtmWave();
Assert.False(wave.IsHot);
}
[Fact]
public void Constructor_WarmupPeriod_Is752()
{
var wave = new TtmWave();
// max(8, 377) + 377 - 2 = 752
Assert.Equal(752, wave.WarmupPeriod);
}
[Fact]
public void Constructor_Chaining_SubscribesToSource()
{
var source = new Ema(10);
using var wave = new TtmWave(source);
Assert.Equal("TtmWave", wave.Name);
}
[Fact]
public void Constructor_DefaultOutputs_AreDefault()
{
var wave = new TtmWave();
Assert.Equal(0, wave.WaveA1.Value);
Assert.Equal(0, wave.WaveA2.Value);
Assert.Equal(0, wave.WaveB1.Value);
Assert.Equal(0, wave.WaveB2.Value);
Assert.Equal(0, wave.WaveC1.Value);
Assert.Equal(0, wave.WaveC2.Value);
}
}
// ══════════════════════════════════════════════════════════════
// B) Basic Calculation
// ══════════════════════════════════════════════════════════════
public sealed class TtmWaveBasicTests
{
private static TSeries GenerateSeries(int count, int seed = 42)
{
var gbm = new GBM(seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
}
[Fact]
public void Update_ReturnsTValue()
{
var wave = new TtmWave();
var input = new TValue(DateTime.UtcNow, 100.0);
var result = wave.Update(input);
Assert.IsType<TValue>(result);
}
[Fact]
public void Update_Last_IsAccessible()
{
var wave = new TtmWave();
var input = new TValue(DateTime.UtcNow, 100.0);
wave.Update(input);
Assert.Equal(wave.Wave1.Value, wave.Last.Value);
}
[Fact]
public void Update_AllWaves_PopulatedAfterUpdate()
{
var wave = new TtmWave();
var series = GenerateSeries(100);
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
// After 100 bars, waves should have non-default values
// (A wave should be non-zero since warmup for channel 1 is only 66)
Assert.NotEqual(0, wave.WaveA2.Value);
}
[Fact]
public void Update_Wave1_EqualsWaveA2()
{
var wave = new TtmWave();
var series = GenerateSeries(100);
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
Assert.Equal(wave.WaveA2.Value, wave.Wave1.Value);
Assert.Equal(wave.WaveA2.Time, wave.Wave1.Time);
}
[Fact]
public void Update_Wave2High_IsMaxOfC()
{
var wave = new TtmWave();
var series = GenerateSeries(800);
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
Assert.Equal(Math.Max(wave.WaveC1.Value, wave.WaveC2.Value), wave.Wave2High);
}
[Fact]
public void Update_Wave2Low_IsMinOfC()
{
var wave = new TtmWave();
var series = GenerateSeries(800);
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
Assert.Equal(Math.Min(wave.WaveC1.Value, wave.WaveC2.Value), wave.Wave2Low);
}
}
// ══════════════════════════════════════════════════════════════
// C) State + Bar Correction
// ══════════════════════════════════════════════════════════════
public sealed class TtmWaveBarCorrectionTests
{
private static TSeries GenerateSeries(int count, int seed = 42)
{
var gbm = new GBM(seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
}
[Fact]
public void IsNew_True_AdvancesState()
{
var wave = new TtmWave();
var series = GenerateSeries(200);
for (int i = 0; i < 100; i++)
{
wave.Update(series[i], isNew: true);
}
double valBefore = wave.Last.Value;
wave.Update(series[100], isNew: true);
Assert.NotEqual(valBefore, wave.Last.Value);
}
[Fact]
public void IsNew_False_RewritesCurrentBar()
{
var wave = new TtmWave();
var series = GenerateSeries(200);
for (int i = 0; i < 100; i++)
{
wave.Update(series[i], isNew: true);
}
// First update as new bar
wave.Update(series[100], isNew: true);
double afterNew = wave.Last.Value;
// Update same bar with different value
var modified = new TValue(series[100].Time, series[100].Value + 5.0);
wave.Update(modified, isNew: false);
// Re-update with original value should restore
wave.Update(series[100], isNew: false);
double afterRestore = wave.Last.Value;
Assert.Equal(afterNew, afterRestore, 10);
}
[Fact]
public void IterativeCorrections_Restore()
{
var wave = new TtmWave();
var series = GenerateSeries(200);
for (int i = 0; i < 100; i++)
{
wave.Update(series[i], isNew: true);
}
// Multiple rewrites followed by same-value restore
wave.Update(series[100], isNew: true);
double baseline = wave.Last.Value;
for (int j = 0; j < 5; j++)
{
var tick = new TValue(series[100].Time, series[100].Value + (j * 2.0));
wave.Update(tick, isNew: false);
}
wave.Update(series[100], isNew: false);
Assert.Equal(baseline, wave.Last.Value, 10);
}
}
// ══════════════════════════════════════════════════════════════
// D) Warmup / Convergence
// ══════════════════════════════════════════════════════════════
public sealed class TtmWaveWarmupTests
{
private static TSeries GenerateSeries(int count, int seed = 42)
{
var gbm = new GBM(seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
}
[Fact]
public void IsHot_FlipsAfterWarmup()
{
var wave = new TtmWave();
var series = GenerateSeries(800);
bool wasHot = false;
int hotAt = -1;
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
if (wave.IsHot && !wasHot)
{
wasHot = true;
hotAt = i;
}
}
Assert.True(wasHot, "Indicator never became hot");
// Should become hot at or near WarmupPeriod (752)
Assert.True(hotAt <= wave.WarmupPeriod, $"Became hot at {hotAt}, expected <= {wave.WarmupPeriod}");
}
[Fact]
public void IsHot_StaysCold_BeforeWarmup()
{
var wave = new TtmWave();
var series = GenerateSeries(100);
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
// 100 bars is not enough for 752 warmup
Assert.False(wave.IsHot);
}
}
// ══════════════════════════════════════════════════════════════
// E) Robustness (NaN / Infinity)
// ══════════════════════════════════════════════════════════════
public sealed class TtmWaveRobustnessTests
{
private static TSeries GenerateSeries(int count, int seed = 42)
{
var gbm = new GBM(seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
}
[Fact]
public void NaN_Input_ProducesFiniteOutput()
{
var wave = new TtmWave();
var series = GenerateSeries(100);
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
// Feed NaN
var nanInput = new TValue(DateTime.UtcNow, double.NaN);
wave.Update(nanInput, isNew: true);
// MACD internally handles NaN via Ema which substitutes last valid
Assert.True(double.IsFinite(wave.Last.Value) || wave.Last.Value == 0,
"NaN input should not propagate to output");
}
[Fact]
public void Infinity_Input_ProducesFiniteOutput()
{
var wave = new TtmWave();
var series = GenerateSeries(100);
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
var infInput = new TValue(DateTime.UtcNow, double.PositiveInfinity);
wave.Update(infInput, isNew: true);
// Should handle gracefully
Assert.True(double.IsFinite(wave.Last.Value) || wave.Last.Value == 0,
"Infinity input should not propagate to output");
}
[Fact]
public void BatchNaN_Safe()
{
var wave = new TtmWave();
// Feed mixture of valid and NaN
for (int i = 0; i < 50; i++)
{
double val = (i % 10 == 0) ? double.NaN : 100.0 + i;
wave.Update(new TValue(DateTime.UtcNow.AddMinutes(i), val), isNew: true);
}
// Should not throw
Assert.True(double.IsFinite(wave.Last.Value) || wave.Last.Value == 0);
}
}
// ══════════════════════════════════════════════════════════════
// F) Consistency (Batch == Streaming == Eventing)
// ══════════════════════════════════════════════════════════════
public sealed class TtmWaveConsistencyTests
{
private static TSeries GenerateSeries(int count, int seed = 42)
{
var gbm = new GBM(seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
}
[Fact]
public void BatchCalc_EqualsStreaming()
{
var series = GenerateSeries(200);
// Batch
var batchResults = TtmWave.Batch(series);
// Streaming
var streamWave = new TtmWave();
var streamResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamWave.Update(series[i], isNew: true);
streamResults.Add(streamWave.Last.Value);
}
Assert.Equal(batchResults.Count, streamResults.Count);
for (int i = 0; i < batchResults.Count; i++)
{
Assert.Equal(batchResults.Values[i], streamResults[i], 10);
}
}
[Fact]
public void Calculate_ReturnsBothResults()
{
var series = GenerateSeries(200);
var (results, indicator) = TtmWave.Calculate(series);
Assert.NotNull(results);
Assert.NotNull(indicator);
Assert.Equal(200, results.Count);
Assert.Equal("TtmWave", indicator.Name);
}
[Fact]
public void EventBased_MatchesStreaming()
{
var series = GenerateSeries(200);
// Streaming
var streamWave = new TtmWave();
var streamResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamWave.Update(series[i], isNew: true);
streamResults.Add(streamWave.Last.Value);
}
// Event-based
var eventSource = new Ema(1); // Pass-through: EMA(1) = identity
using var eventWave = new TtmWave(eventSource);
var eventResults = new List<double>();
eventWave.Pub += (object? _, in TValueEventArgs args) => eventResults.Add(args.Value.Value);
for (int i = 0; i < series.Count; i++)
{
eventSource.Update(series[i], isNew: true);
}
Assert.Equal(streamResults.Count, eventResults.Count);
for (int i = 0; i < streamResults.Count; i++)
{
Assert.Equal(streamResults[i], eventResults[i], 10);
}
}
[Fact]
public void Update_TSeries_MatchesStreaming()
{
var series = GenerateSeries(200);
// TSeries batch via Update
var batchWave = new TtmWave();
var batchResults = batchWave.Update(series);
// Streaming
var streamWave = new TtmWave();
for (int i = 0; i < series.Count; i++)
{
streamWave.Update(series[i], isNew: true);
}
Assert.Equal(series.Count, batchResults.Count);
// Last values should match
Assert.Equal(streamWave.Last.Value, batchResults.Values[^1], 10);
}
}
// ══════════════════════════════════════════════════════════════
// G) Reset Tests
// ══════════════════════════════════════════════════════════════
public sealed class TtmWaveResetTests
{
private static TSeries GenerateSeries(int count, int seed = 42)
{
var gbm = new GBM(seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
}
[Fact]
public void Reset_ClearsAllState()
{
var wave = new TtmWave();
var series = GenerateSeries(200);
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
wave.Reset();
Assert.False(wave.IsHot);
Assert.Equal(0, wave.WaveA1.Value);
Assert.Equal(0, wave.WaveA2.Value);
Assert.Equal(0, wave.WaveB1.Value);
Assert.Equal(0, wave.WaveB2.Value);
Assert.Equal(0, wave.WaveC1.Value);
Assert.Equal(0, wave.WaveC2.Value);
}
[Fact]
public void Reset_ThenReprocess_MatchesOriginal()
{
var wave = new TtmWave();
var series = GenerateSeries(200);
// First pass
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
double firstPassLast = wave.Last.Value;
// Reset and reprocess
wave.Reset();
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
double secondPassLast = wave.Last.Value;
Assert.Equal(firstPassLast, secondPassLast, 10);
}
}
// ══════════════════════════════════════════════════════════════
// H) Batch / Static API Tests
// ══════════════════════════════════════════════════════════════
public sealed class TtmWaveBatchTests
{
private static TSeries GenerateSeries(int count, int seed = 42)
{
var gbm = new GBM(seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
}
[Fact]
public void Batch_ReturnsCorrectLength()
{
var series = GenerateSeries(200);
var result = TtmWave.Batch(series);
Assert.Equal(200, result.Count);
}
[Fact]
public void Batch_EmptyInput_ReturnsEmpty()
{
var series = new TSeries([], []);
var result = TtmWave.Batch(series);
Assert.True(result.Count == 0);
}
[Fact]
public void Calculate_ReturnsWarmIndicator()
{
var series = GenerateSeries(800);
var (results, indicator) = TtmWave.Calculate(series);
Assert.Equal(800, results.Count);
Assert.True(indicator.IsHot);
}
}
// ══════════════════════════════════════════════════════════════
// I) Prime Tests
// ══════════════════════════════════════════════════════════════
public sealed class TtmWavePrimeTests
{
private static TSeries GenerateSeries(int count, int seed = 42)
{
var gbm = new GBM(seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
}
[Fact]
public void Prime_SetsState()
{
var wave = new TtmWave();
var series = GenerateSeries(200);
wave.Prime(series);
// After priming, wave should have processed all data
Assert.NotEqual(0, wave.Last.Value);
}
[Fact]
public void Prime_ThenUpdate_ContinuesCorrectly()
{
var series = GenerateSeries(300);
// Reference: process all 300 bars
var refWave = new TtmWave();
for (int i = 0; i < 300; i++)
{
refWave.Update(series[i], isNew: true);
}
// Prime with first 200, then stream remaining 100
var primeWave = new TtmWave();
var tList = new List<long>(200);
var vList = new List<double>(200);
for (int i = 0; i < 200; i++)
{
tList.Add(series.Times[i]);
vList.Add(series.Values[i]);
}
var primeSeries = new TSeries(tList, vList);
primeWave.Prime(primeSeries);
for (int i = 200; i < 300; i++)
{
primeWave.Update(series[i], isNew: true);
}
Assert.Equal(refWave.Last.Value, primeWave.Last.Value, 10);
}
[Fact]
public void Prime_EmptySeries_NoOp()
{
var wave = new TtmWave();
var empty = new TSeries([], []);
wave.Prime(empty);
Assert.False(wave.IsHot);
}
}
// ══════════════════════════════════════════════════════════════
// J) Event / Chainability Tests
// ══════════════════════════════════════════════════════════════
public sealed class TtmWaveEventTests
{
[Fact]
public void Pub_Fires_OnUpdate()
{
var wave = new TtmWave();
int fireCount = 0;
wave.Pub += (object? _, in TValueEventArgs _a) => fireCount++;
for (int i = 0; i < 10; i++)
{
wave.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i), isNew: true);
}
Assert.Equal(10, fireCount);
}
[Fact]
public void Chaining_PropagatesValues()
{
var source = new Ema(1);
using var wave = new TtmWave(source);
var received = new List<double>();
wave.Pub += (object? _, in TValueEventArgs args) => received.Add(args.Value.Value);
for (int i = 0; i < 50; i++)
{
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i), isNew: true);
}
Assert.Equal(50, received.Count);
}
[Fact]
public void Dispose_UnsubscribesFromSource()
{
var source = new Ema(1);
var wave = new TtmWave(source);
int fireCount = 0;
wave.Pub += (object? _, in TValueEventArgs _a) => fireCount++;
source.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
Assert.Equal(1, fireCount);
wave.Dispose();
source.Update(new TValue(DateTime.UtcNow, 101.0), isNew: true);
Assert.Equal(1, fireCount); // Should not fire again
}
}
// ══════════════════════════════════════════════════════════════
// K) Multi-Output Verification
// ══════════════════════════════════════════════════════════════
public sealed class TtmWaveMultiOutputTests
{
private static TSeries GenerateSeries(int count, int seed = 42)
{
var gbm = new GBM(seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
}
[Fact]
public void AllSixWaves_HaveSameTimestamp()
{
var wave = new TtmWave();
var series = GenerateSeries(100);
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
long t = wave.WaveA1.Time;
Assert.Equal(t, wave.WaveA2.Time);
Assert.Equal(t, wave.WaveB1.Time);
Assert.Equal(t, wave.WaveB2.Time);
Assert.Equal(t, wave.WaveC1.Time);
Assert.Equal(t, wave.WaveC2.Time);
}
[Fact]
public void WaveAmplitudes_IncreaseWithPeriod()
{
// Longer-period waves tend to have larger absolute values
// after sufficient warmup, because they capture more price movement.
// This is a soft heuristic test, not a hard rule.
var wave = new TtmWave();
var series = GenerateSeries(1000);
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
// Just verify all waves are finite and output different values
Assert.True(double.IsFinite(wave.WaveA1.Value));
Assert.True(double.IsFinite(wave.WaveA2.Value));
Assert.True(double.IsFinite(wave.WaveB1.Value));
Assert.True(double.IsFinite(wave.WaveB2.Value));
Assert.True(double.IsFinite(wave.WaveC1.Value));
Assert.True(double.IsFinite(wave.WaveC2.Value));
}
[Fact]
public void Waves_IndependentValues()
{
var wave = new TtmWave();
var series = GenerateSeries(800);
for (int i = 0; i < series.Count; i++)
{
wave.Update(series[i], isNew: true);
}
// Different channels should produce different values
// (extremely unlikely for all 6 to be identical with random data)
var values = new HashSet<double>
{
wave.WaveA1.Value,
wave.WaveA2.Value,
wave.WaveB1.Value,
wave.WaveB2.Value,
wave.WaveC1.Value,
wave.WaveC2.Value
};
Assert.True(values.Count >= 3, "At least 3 of 6 wave values should be distinct");
}
}
@@ -0,0 +1,344 @@
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// TTM Wave validation tests.
/// No external libraries (Skender/TA-Lib/Tulip/Ooples) implement TTM Wave,
/// so validation is self-consistency: streaming vs batch, prime vs cold,
/// deterministic reproducibility, and multi-wave coherence checks.
/// </summary>
public sealed class TtmWaveValidationTests
{
private readonly ITestOutputHelper _output;
public TtmWaveValidationTests(ITestOutputHelper output)
{
_output = output;
}
private static TSeries GenerateSeries(int count, int seed = 42)
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Extract close prices into TSeries for TtmWave (which operates on single values)
var t = new List<long>(count);
var v = new List<double>(count);
for (int i = 0; i < bars.Count; i++)
{
t.Add(bars[i].Time);
v.Add(bars[i].Close); // Close price
}
return new TSeries(t, v);
}
// --- A) Streaming vs Batch agreement ---
[Fact]
public void Streaming_Matches_Batch()
{
var series = GenerateSeries(1000);
var wave = new TtmWave();
for (int i = 0; i < series.Count; i++)
{
wave.Update(new TValue(new DateTime(series.Times[i], DateTimeKind.Utc), series.Values[i]));
}
var batch = TtmWave.Batch(series);
Assert.Equal(wave.Last.Value, batch[^1].Value, 1e-10);
_output.WriteLine($"Streaming last={wave.Last.Value:F10}, Batch last={batch[^1].Value:F10}");
}
[Fact]
public void Streaming_Matches_Batch_AllValues()
{
var series = GenerateSeries(1000);
int warmup = 752;
var wave = new TtmWave();
var streamValues = new double[series.Count];
for (int i = 0; i < series.Count; i++)
{
wave.Update(new TValue(new DateTime(series.Times[i], DateTimeKind.Utc), series.Values[i]));
streamValues[i] = wave.Last.Value;
}
var batch = TtmWave.Batch(series);
int mismatches = 0;
for (int i = warmup; i < series.Count; i++)
{
double diff = Math.Abs(streamValues[i] - batch[i].Value);
if (diff > 1e-8)
{
mismatches++;
if (mismatches <= 5)
{
_output.WriteLine($"Mismatch at i={i}: stream={streamValues[i]:F10}, batch={batch[i].Value:F10}, diff={diff:E3}");
}
}
}
Assert.Equal(0, mismatches);
}
// --- B) Primed vs Cold start agreement ---
[Fact]
public void Primed_Matches_Cold_Start()
{
var series = GenerateSeries(1000);
int splitAt = 800;
// Cold: process all at once
var cold = new TtmWave();
for (int i = 0; i < series.Count; i++)
{
cold.Update(new TValue(new DateTime(series.Times[i], DateTimeKind.Utc), series.Values[i]));
}
// Primed: prime with first chunk, then stream remainder
var primed = new TtmWave();
var primeSeries = GenerateSubSeries(series, splitAt);
primed.Prime(primeSeries);
for (int i = splitAt; i < series.Count; i++)
{
primed.Update(new TValue(new DateTime(series.Times[i], DateTimeKind.Utc), series.Values[i]));
}
double diff = Math.Abs(cold.Last.Value - primed.Last.Value);
_output.WriteLine($"Cold={cold.Last.Value:F10}, Primed={primed.Last.Value:F10}, diff={diff:E3}");
Assert.True(diff < 1e-8, $"Primed vs cold diff={diff:E3} exceeds tolerance");
}
// --- C) Deterministic reproducibility ---
[Fact]
public void Same_Input_Produces_Same_Output()
{
var series1 = GenerateSeries(1000, seed: 99);
var series2 = GenerateSeries(1000, seed: 99);
var batch1 = TtmWave.Batch(series1);
var batch2 = TtmWave.Batch(series2);
for (int i = 0; i < batch1.Count; i++)
{
Assert.Equal(batch1[i].Value, batch2[i].Value, 1e-15);
}
}
[Fact]
public void Different_Seed_Produces_Different_Output()
{
var series1 = GenerateSeries(1000, seed: 42);
var series2 = GenerateSeries(1000, seed: 99);
var batch1 = TtmWave.Batch(series1);
var batch2 = TtmWave.Batch(series2);
bool anyDifferent = false;
for (int i = 800; i < batch1.Count; i++)
{
if (Math.Abs(batch1[i].Value - batch2[i].Value) > 1e-6)
{
anyDifferent = true;
break;
}
}
Assert.True(anyDifferent, "Different seeds should produce different outputs");
}
// --- D) Multi-wave coherence ---
[Fact]
public void All_Six_Waves_Produce_Finite_Values()
{
var series = GenerateSeries(1000);
var wave = new TtmWave();
for (int i = 0; i < series.Count; i++)
{
wave.Update(new TValue(new DateTime(series.Times[i], DateTimeKind.Utc), series.Values[i]));
}
Assert.True(double.IsFinite(wave.WaveA1.Value), "WaveA1 not finite");
Assert.True(double.IsFinite(wave.WaveA2.Value), "WaveA2 not finite");
Assert.True(double.IsFinite(wave.WaveB1.Value), "WaveB1 not finite");
Assert.True(double.IsFinite(wave.WaveB2.Value), "WaveB2 not finite");
Assert.True(double.IsFinite(wave.WaveC1.Value), "WaveC1 not finite");
Assert.True(double.IsFinite(wave.WaveC2.Value), "WaveC2 not finite");
_output.WriteLine($"A1={wave.WaveA1.Value:F6}, A2={wave.WaveA2.Value:F6}");
_output.WriteLine($"B1={wave.WaveB1.Value:F6}, B2={wave.WaveB2.Value:F6}");
_output.WriteLine($"C1={wave.WaveC1.Value:F6}, C2={wave.WaveC2.Value:F6}");
}
[Fact]
public void Wave_Magnitudes_Follow_Expected_Ordering()
{
// Longer-period MACD channels should generally have larger absolute histograms
// (wider slow EMA separation from fast). Not guaranteed per-bar, but on average.
var series = GenerateSeries(2000);
var wave = new TtmWave();
double sumAbsA = 0, sumAbsB = 0, sumAbsC = 0;
int hotBars = 0;
for (int i = 0; i < series.Count; i++)
{
wave.Update(new TValue(new DateTime(series.Times[i], DateTimeKind.Utc), series.Values[i]));
if (wave.IsHot)
{
sumAbsA += Math.Abs(wave.WaveA1.Value) + Math.Abs(wave.WaveA2.Value);
sumAbsB += Math.Abs(wave.WaveB1.Value) + Math.Abs(wave.WaveB2.Value);
sumAbsC += Math.Abs(wave.WaveC1.Value) + Math.Abs(wave.WaveC2.Value);
hotBars++;
}
}
double avgA = sumAbsA / (2 * hotBars);
double avgB = sumAbsB / (2 * hotBars);
double avgC = sumAbsC / (2 * hotBars);
_output.WriteLine($"Avg |A|={avgA:F6}, |B|={avgB:F6}, |C|={avgC:F6}, hotBars={hotBars}");
// Longer periods tend to produce larger histogram deviations on trending GBM data
Assert.True(avgC > avgA * 0.5, $"Wave C avg ({avgC:F6}) should not be drastically smaller than A ({avgA:F6})");
}
[Fact]
public void TOS_Compatibility_Properties_Are_Consistent()
{
var series = GenerateSeries(1000);
var wave = new TtmWave();
for (int i = 0; i < series.Count; i++)
{
wave.Update(new TValue(new DateTime(series.Times[i], DateTimeKind.Utc), series.Values[i]));
}
// Wave1 == WaveA2 (per TOS mapping)
Assert.Equal(wave.WaveA2.Value, wave.Wave1.Value, 1e-15);
// Wave2High = max(C1, C2)
Assert.Equal(Math.Max(wave.WaveC1.Value, wave.WaveC2.Value), wave.Wave2High, 1e-15);
// Wave2Low = min(C1, C2)
Assert.Equal(Math.Min(wave.WaveC1.Value, wave.WaveC2.Value), wave.Wave2Low, 1e-15);
// Last == Wave1
Assert.Equal(wave.Wave1.Value, wave.Last.Value, 1e-15);
}
// --- E) Calculate returns warm indicator ---
[Fact]
public void Calculate_Returns_Warm_Indicator()
{
var series = GenerateSeries(1000);
var (results, indicator) = TtmWave.Calculate(series);
Assert.Equal(series.Count, results.Count);
Assert.True(indicator.IsHot);
Assert.Equal(results[^1].Value, indicator.Last.Value, 1e-10);
}
// --- F) Reset produces clean slate ---
[Fact]
public void Reset_Then_Replay_Matches_Fresh()
{
var series = GenerateSeries(1000);
var wave = new TtmWave();
for (int i = 0; i < series.Count; i++)
{
wave.Update(new TValue(new DateTime(series.Times[i], DateTimeKind.Utc), series.Values[i]));
}
double firstRun = wave.Last.Value;
wave.Reset();
for (int i = 0; i < series.Count; i++)
{
wave.Update(new TValue(new DateTime(series.Times[i], DateTimeKind.Utc), series.Values[i]));
}
double secondRun = wave.Last.Value;
Assert.Equal(firstRun, secondRun, 1e-15);
}
// --- G) Large dataset stability ---
[Fact]
public void Large_Dataset_No_Overflow()
{
var series = GenerateSeries(5000);
var wave = new TtmWave();
for (int i = 0; i < series.Count; i++)
{
wave.Update(new TValue(new DateTime(series.Times[i], DateTimeKind.Utc), series.Values[i]));
}
Assert.True(wave.IsHot);
Assert.True(double.IsFinite(wave.Last.Value), "Last value should be finite after 5000 bars");
Assert.True(double.IsFinite(wave.WaveC1.Value), "WaveC1 should be finite after 5000 bars");
Assert.True(double.IsFinite(wave.WaveC2.Value), "WaveC2 should be finite after 5000 bars");
}
// --- H) Warm-up period validation ---
[Fact]
public void WarmupPeriod_Is_752()
{
var wave = new TtmWave();
Assert.Equal(752, wave.WarmupPeriod);
}
[Fact]
public void IsHot_False_Before_Warmup_True_After()
{
var series = GenerateSeries(1000);
var wave = new TtmWave();
bool wasHot = false;
int firstHotBar = -1;
for (int i = 0; i < series.Count; i++)
{
wave.Update(new TValue(new DateTime(series.Times[i], DateTimeKind.Utc), series.Values[i]));
if (wave.IsHot && !wasHot)
{
firstHotBar = i;
wasHot = true;
}
}
Assert.True(wasHot, "Should become hot before 1000 bars");
_output.WriteLine($"First hot bar index: {firstHotBar}");
// IsHot should engage roughly around the warmup period
Assert.True(firstHotBar > 0, "Should not be hot immediately");
Assert.True(firstHotBar <= wave.WarmupPeriod, $"First hot bar {firstHotBar} should be <= WarmupPeriod {wave.WarmupPeriod}");
}
// --- helper ---
private static TSeries GenerateSubSeries(TSeries source, int count)
{
var t = new List<long>(count);
var v = new List<double>(count);
for (int i = 0; i < count && i < source.Count; i++)
{
t.Add(source.Times[i]);
v.Add(source.Values[i]);
}
return new TSeries(t, v);
}
}