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,156 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Quantower.Tests;
public class NmaIndicatorTests
{
[Fact]
public void NmaIndicator_Constructor_SetsDefaults()
{
var indicator = new NmaIndicator();
Assert.Equal(40, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("NMA - Natural Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void NmaIndicator_MinHistoryDepths_IsZero()
{
var indicator = new NmaIndicator { Period = 20 };
Assert.Equal(0, NmaIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void NmaIndicator_ShortName_IncludesPeriodAndSource()
{
var indicator = new NmaIndicator { Period = 15 };
Assert.Contains("NMA", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void NmaIndicator_Initialize_CreatesInternalNma()
{
var indicator = new NmaIndicator { Period = 10 };
indicator.Initialize();
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void NmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new NmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.True(indicator.LinesSeries[0].Count > 0);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void NmaIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new NmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void NmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new NmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double firstValue = indicator.LinesSeries[0].GetValue(0);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double secondValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(firstValue));
Assert.True(double.IsFinite(secondValue));
}
[Fact]
public void NmaIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new NmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105, 107, 106 };
foreach (var close in closes)
{
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
now = now.AddMinutes(1);
}
for (int i = 0; i < closes.Length; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
}
double lastNma = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastNma >= 95 && lastNma <= 115);
}
[Fact]
public void NmaIndicator_DifferentSourceTypes_Work()
{
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
foreach (var source in sources)
{
var indicator = new NmaIndicator { Period = 3, Source = source };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
$"Source {source} should produce finite value");
}
}
[Fact]
public void NmaIndicator_Period_CanBeChanged()
{
var indicator = new NmaIndicator { Period = 5 };
Assert.Equal(5, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
}
}
+499
View File
@@ -0,0 +1,499 @@
using Xunit;
namespace QuanTAlib.Tests;
public class NmaTests
{
private const int DefaultPeriod = 40;
private const double Tolerance = 1e-10;
private const long Seed = 12345;
private static readonly TimeSpan Step = TimeSpan.FromMinutes(1);
private static TSeries GetTestSeries(int count = 500)
{
var gbm = new GBM();
var bars = gbm.Fetch(count, Seed, Step);
return bars.Close;
}
// ── A) Constructor validation ──────────────────────────────────────
[Fact]
public void Constructor_PeriodZero_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Nma(0));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_PeriodNegative_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Nma(-1));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_PeriodOne_Valid()
{
var nma = new Nma(1);
Assert.Equal("Nma(1)", nma.Name);
}
[Fact]
public void Constructor_ValidPeriod_SetsName()
{
var nma = new Nma(DefaultPeriod);
Assert.Equal($"Nma({DefaultPeriod})", nma.Name);
}
[Fact]
public void Constructor_ValidPeriod_SetsWarmupPeriod()
{
var nma = new Nma(DefaultPeriod);
Assert.Equal(DefaultPeriod, nma.WarmupPeriod);
}
// ── B) Basic calculation ───────────────────────────────────────────
[Fact]
public void Update_FirstBar_ReturnsPrice()
{
var nma = new Nma(DefaultPeriod);
var result = nma.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(100.0, result.Value);
}
[Fact]
public void Update_ReturnsFiniteValues()
{
var nma = new Nma(DefaultPeriod);
var series = GetTestSeries();
foreach (var tv in series)
{
var result = nma.Update(tv);
Assert.True(double.IsFinite(result.Value), $"Non-finite at {tv.Time}");
}
}
[Fact]
public void Update_Last_MatchesReturnValue()
{
var nma = new Nma(DefaultPeriod);
var series = GetTestSeries(100);
foreach (var tv in series)
{
var result = nma.Update(tv);
Assert.Equal(result.Value, nma.Last.Value);
}
}
// ── C) State + bar correction ──────────────────────────────────────
[Fact]
public void Update_IsNewTrue_AdvancesState()
{
var nma = new Nma(DefaultPeriod);
var series = GetTestSeries(50);
for (int i = 0; i < series.Count; i++)
{
nma.Update(series[i], isNew: true);
}
Assert.True(nma.IsHot);
}
[Fact]
public void Update_IsNewFalse_CorrectionRestores()
{
var nma = new Nma(DefaultPeriod);
var series = GetTestSeries(100);
// Process 98 bars
for (int i = 0; i < 98; i++)
{
nma.Update(series[i]);
}
// Correction path: isNew=true then multiple isNew=false
nma.Update(new TValue(series[98].Time, series[98].Value), true);
nma.Update(new TValue(series[98].Time, series[98].Value + 0.5), false);
nma.Update(new TValue(series[98].Time, series[98].Value + 1.0), false);
var corrected = nma.Update(new TValue(series[98].Time, series[98].Value + 1.5), false);
// Clean path: same data in fresh indicator
var nma2 = new Nma(DefaultPeriod);
for (int i = 0; i < 98; i++)
{
nma2.Update(series[i]);
}
var expected = nma2.Update(new TValue(series[98].Time, series[98].Value + 1.5), true);
Assert.Equal(expected.Value, corrected.Value, 1e-9);
}
[Fact]
public void Update_IterativeCorrections_RestoresExactly()
{
var nma = new Nma(DefaultPeriod);
var series = GetTestSeries(80);
for (int i = 0; i < series.Count - 1; i++)
{
nma.Update(series[i]);
}
// Apply new bar then 5 corrections, final correction to target value
nma.Update(series[^1]);
for (int c = 0; c < 5; c++)
{
nma.Update(new TValue(series[^1].Time, series[^1].Value * (1.0 + c * 0.01)), isNew: false);
}
var corrected = nma.Update(new TValue(series[^1].Time, series[^1].Value + 2.0), isNew: false);
// Clean path
var nma2 = new Nma(DefaultPeriod);
for (int i = 0; i < series.Count - 1; i++)
{
nma2.Update(series[i]);
}
var expected = nma2.Update(new TValue(series[^1].Time, series[^1].Value + 2.0), true);
Assert.Equal(expected.Value, corrected.Value, 1e-9);
}
[Fact]
public void Reset_ClearsState()
{
var nma = new Nma(DefaultPeriod);
var series = GetTestSeries(100);
foreach (var tv in series)
{
nma.Update(tv);
}
nma.Reset();
Assert.False(nma.IsHot);
Assert.Equal(0, nma.Last.Value);
}
// ── D) Warmup/convergence ──────────────────────────────────────────
[Fact]
public void IsHot_FlipsAtPeriod()
{
var nma = new Nma(DefaultPeriod);
for (int i = 0; i < DefaultPeriod; i++)
{
var hot = nma.IsHot;
nma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
if (i < DefaultPeriod - 1)
{
Assert.False(hot);
}
}
Assert.True(nma.IsHot);
}
// ── E) Robustness ──────────────────────────────────────────────────
[Fact]
public void Update_NaN_UsesLastValid()
{
var nma = new Nma(DefaultPeriod);
var series = GetTestSeries(60);
for (int i = 0; i < 50; i++)
{
nma.Update(series[i]);
}
_ = nma.Last.Value;
nma.Update(new TValue(DateTime.UtcNow, double.NaN));
double afterNaN = nma.Last.Value;
Assert.True(double.IsFinite(afterNaN));
}
[Fact]
public void Update_Infinity_UsesLastValid()
{
var nma = new Nma(DefaultPeriod);
var series = GetTestSeries(60);
for (int i = 0; i < 50; i++)
{
nma.Update(series[i]);
}
nma.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(nma.Last.Value));
}
[Fact]
public void Update_BatchNaN_AllFinite()
{
var nma = new Nma(DefaultPeriod);
var series = GetTestSeries(100);
for (int i = 0; i < series.Count; i++)
{
// Inject NaN every 10th bar after warmup
if (i > DefaultPeriod && i % 10 == 0)
{
nma.Update(new TValue(series[i].Time, double.NaN));
}
else
{
nma.Update(series[i]);
}
Assert.True(double.IsFinite(nma.Last.Value));
}
}
// ── F) Consistency (4 modes) ───────────────────────────────────────
[Fact]
public void TSeries_MatchesStreaming()
{
var series = GetTestSeries(200);
// Streaming
var streaming = new Nma(DefaultPeriod);
var streamResults = new double[series.Count];
for (int i = 0; i < series.Count; i++)
{
streamResults[i] = streaming.Update(series[i]).Value;
}
// Batch via TSeries
var batchResults = Nma.Batch(series, DefaultPeriod);
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-7);
}
}
[Fact]
public void Batch_Span_MatchesStreaming()
{
var series = GetTestSeries(200);
// Streaming
var streaming = new Nma(DefaultPeriod);
var streamResults = new double[series.Count];
for (int i = 0; i < series.Count; i++)
{
streamResults[i] = streaming.Update(series[i]).Value;
}
// Span batch
var output = new double[series.Count];
Nma.Batch(series.Values, output, DefaultPeriod);
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(streamResults[i], output[i], 1e-7);
}
}
[Fact]
public void EventDriven_MatchesStreaming()
{
var series = GetTestSeries(200);
// Streaming
var streaming = new Nma(DefaultPeriod);
var streamResults = new double[series.Count];
for (int i = 0; i < series.Count; i++)
{
streamResults[i] = streaming.Update(series[i]).Value;
}
// Event-driven
var source = new TSeries();
var eventNma = new Nma(source, DefaultPeriod);
var eventResults = new double[series.Count];
for (int i = 0; i < series.Count; i++)
{
source.Add(series[i]);
eventResults[i] = eventNma.Last.Value;
}
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(streamResults[i], eventResults[i], 1e-10);
}
}
// ── G) Span API tests ──────────────────────────────────────────────
[Fact]
public void Batch_Span_MismatchedLengths_Throws()
{
var src = new double[10];
var output = new double[5];
var ex = Assert.Throws<ArgumentException>(() => Nma.Batch(src, output, DefaultPeriod));
Assert.Equal("output", ex.ParamName);
}
[Fact]
public void Batch_Span_InvalidPeriod_Throws()
{
var src = new double[10];
var output = new double[10];
var ex = Assert.Throws<ArgumentException>(() => Nma.Batch(src, output, 0));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Batch_Span_Empty_NoOp()
{
var src = ReadOnlySpan<double>.Empty;
var output = Span<double>.Empty;
Nma.Batch(src, output, DefaultPeriod);
Assert.True(true); // S2699 - verifying no exception is the assertion
}
[Fact]
public void Batch_Span_HandlesNaN()
{
var src = new double[] { 100, 101, double.NaN, 103, 104 };
var output = new double[5];
Nma.Batch(src, output, 3);
for (int i = 0; i < output.Length; i++)
{
Assert.True(double.IsFinite(output[i]));
}
}
// ── H) Chainability ────────────────────────────────────────────────
[Fact]
public void PubSub_FiresEvents()
{
var source = new TSeries();
var nma = new Nma(source, DefaultPeriod);
int eventCount = 0;
nma.Pub += (object? _, in TValueEventArgs e) => eventCount++;
for (int i = 0; i < 10; i++)
{
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
}
Assert.Equal(10, eventCount);
}
[Fact]
public void Dispose_UnsubscribesFromSource()
{
var source = new TSeries();
var nma = new Nma(source, DefaultPeriod);
nma.Dispose();
// Adding to source should not affect disposed nma
source.Add(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(0, nma.Last.Value);
}
// ── Additional behavior tests ──────────────────────────────────────
[Fact]
public void ConstantInput_ConvergesToConstant()
{
var nma = new Nma(DefaultPeriod);
double constant = 50.0;
for (int i = 0; i < 200; i++)
{
nma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), constant));
}
Assert.Equal(constant, nma.Last.Value, 1e-6);
}
[Fact]
public void MonotonicInput_TracksTrend()
{
var nma = new Nma(14);
double lastNma = 0;
for (int i = 0; i < 100; i++)
{
double price = 100.0 + i;
lastNma = nma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price)).Value;
}
// NMA should be between first and last price in a monotonic series
Assert.True(lastNma > 100.0);
Assert.True(lastNma < 200.0);
}
[Fact]
public void Ratio_BoundedZeroOne()
{
// The ratio should conceptually be in [0,1] range
// We verify indirectly: NMA should always be between min and max of input
var nma = new Nma(DefaultPeriod);
var series = GetTestSeries(200);
double minPrice = double.MaxValue;
double maxPrice = double.MinValue;
for (int i = 0; i < series.Count; i++)
{
nma.Update(series[i]);
if (series[i].Value < minPrice)
{
minPrice = series[i].Value;
}
if (series[i].Value > maxPrice)
{
maxPrice = series[i].Value;
}
}
// NMA value should be within the range of input data (with some tolerance)
Assert.True(nma.Last.Value >= minPrice * 0.99);
Assert.True(nma.Last.Value <= maxPrice * 1.01);
}
[Theory]
[InlineData(5)]
[InlineData(14)]
[InlineData(40)]
[InlineData(100)]
public void DifferentPeriods_AllValid(int period)
{
var nma = new Nma(period);
var series = GetTestSeries(200);
foreach (var tv in series)
{
var result = nma.Update(tv);
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void Calculate_ReturnsBothResultsAndIndicator()
{
var series = GetTestSeries(100);
var (results, indicator) = Nma.Calculate(series, DefaultPeriod);
Assert.Equal(series.Count, results.Count);
Assert.True(indicator.IsHot);
}
[Fact]
public void Prime_SetsState()
{
var series = GetTestSeries(100);
var nma = new Nma(DefaultPeriod);
nma.Prime(series.Values);
Assert.True(nma.IsHot);
Assert.True(double.IsFinite(nma.Last.Value));
}
}
@@ -0,0 +1,200 @@
using Xunit;
namespace QuanTAlib.Tests;
/// <summary>
/// Self-consistency validation for NMA. No external library supports NMA,
/// so we validate internal consistency: streaming==batch==span, ratio bounds,
/// regime detection, and determinism.
/// </summary>
public class NmaValidationTests
{
private const long Seed = 12345;
private static readonly TimeSpan Step = TimeSpan.FromMinutes(1);
private static TSeries GetTestSeries(int count = 500)
{
var gbm = new GBM();
var bars = gbm.Fetch(count, Seed, Step);
return bars.Close;
}
[Fact]
public void StreamingEqualsBatch_DefaultPeriod()
{
var series = GetTestSeries(500);
int period = 40;
// Streaming
var streaming = new Nma(period);
var streamResults = new double[series.Count];
for (int i = 0; i < series.Count; i++)
{
streamResults[i] = streaming.Update(series[i]).Value;
}
// Batch (span)
var batchResults = new double[series.Count];
Nma.Batch(series.Values, batchResults, period);
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(streamResults[i], batchResults[i], 1e-7);
}
}
[Fact]
public void StreamingEqualsTSeries()
{
var series = GetTestSeries(500);
int period = 40;
// Streaming
var streaming = new Nma(period);
var streamResults = new double[series.Count];
for (int i = 0; i < series.Count; i++)
{
streamResults[i] = streaming.Update(series[i]).Value;
}
// TSeries batch
var batchSeries = Nma.Batch(series, period);
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(streamResults[i], batchSeries.Values[i], 1e-7);
}
}
[Theory]
[InlineData(5)]
[InlineData(14)]
[InlineData(40)]
[InlineData(80)]
public void ConsistencyAcrossPeriods(int period)
{
var series = GetTestSeries(300);
// Streaming
var streaming = new Nma(period);
var streamResults = new double[series.Count];
for (int i = 0; i < series.Count; i++)
{
streamResults[i] = streaming.Update(series[i]).Value;
}
// Batch
var batchResults = new double[series.Count];
Nma.Batch(series.Values, batchResults, period);
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(streamResults[i], batchResults[i], 1e-7);
}
}
[Fact]
public void ConstantInput_NmaEqualsConstant()
{
double constant = 100.0;
int period = 40;
int count = 200;
var nma = new Nma(period);
for (int i = 0; i < count; i++)
{
nma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), constant));
}
// For constant input, volatility is 0 everywhere → ratio = 0
// But first bar seeds NMA = constant, so it should stay constant
Assert.Equal(constant, nma.Last.Value, 1e-8);
}
[Fact]
public void MonotonicRising_NmaFollowsGradually()
{
int period = 14;
var nma = new Nma(period);
double lastNma = 0;
for (int i = 0; i < 100; i++)
{
double price = 100.0 + i * 0.5;
lastNma = nma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price)).Value;
}
// NMA should lag behind the linearly rising price
Assert.True(lastNma > 100.0, "NMA should rise");
Assert.True(lastNma < 150.0, "NMA should lag behind final price");
}
[Fact]
public void DeterministicOutput()
{
var series = GetTestSeries(200);
int period = 40;
var nma1 = new Nma(period);
var nma2 = new Nma(period);
for (int i = 0; i < series.Count; i++)
{
var r1 = nma1.Update(series[i]);
var r2 = nma2.Update(series[i]);
Assert.Equal(r1.Value, r2.Value, 1e-15);
}
}
[Fact]
public void OutputBounded_WithinInputRange()
{
var series = GetTestSeries(500);
int period = 40;
var nma = new Nma(period);
double minInput = double.MaxValue;
double maxInput = double.MinValue;
for (int i = 0; i < series.Count; i++)
{
nma.Update(series[i]);
if (series[i].Value < minInput)
{
minInput = series[i].Value;
}
if (series[i].Value > maxInput)
{
maxInput = series[i].Value;
}
}
// NMA should stay within input range (with small tolerance for FP)
Assert.True(nma.Last.Value >= minInput * 0.99);
Assert.True(nma.Last.Value <= maxInput * 1.01);
}
[Fact]
public void SmallPeriod_MoreResponsive()
{
var series = GetTestSeries(200);
var nmaFast = new Nma(5);
var nmaSlow = new Nma(80);
double sumAbsDiffFast = 0;
double sumAbsDiffSlow = 0;
for (int i = 0; i < series.Count; i++)
{
var fast = nmaFast.Update(series[i]).Value;
var slow = nmaSlow.Update(series[i]).Value;
sumAbsDiffFast += Math.Abs(fast - series[i].Value);
sumAbsDiffSlow += Math.Abs(slow - series[i].Value);
}
// Faster NMA (smaller period) should track price more closely
Assert.True(sumAbsDiffFast < sumAbsDiffSlow,
$"Fast NMA avg deviation ({sumAbsDiffFast / series.Count:F4}) should be less than slow ({sumAbsDiffSlow / series.Count:F4})");
}
}