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,210 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class StcIndicatorTests
{
[Fact]
public void StcIndicator_Constructor_SetsDefaults()
{
var indicator = new StcIndicator();
Assert.Equal(12, indicator.CycleLength);
Assert.Equal(26, indicator.FastLength);
Assert.Equal(50, indicator.SlowLength);
Assert.Equal(StcSmoothing.Sigmoid, indicator.Smoothing);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("STC - Schaff Trend Cycle", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void StcIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new StcIndicator();
Assert.Equal(0, StcIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void StcIndicator_ShortName_IncludesParameters()
{
var indicator = new StcIndicator
{
CycleLength = 10,
FastLength = 23,
SlowLength = 50,
Smoothing = StcSmoothing.Ema,
};
// Format is "STC {CycleLength}:{FastLength}:{SlowLength}:{Smoothing}:{Source}"
// e.g. "STC 10:23:50:Ema:Close"
string shortName = indicator.ShortName;
Assert.Contains("STC", shortName, StringComparison.Ordinal);
Assert.Contains("10", shortName, StringComparison.Ordinal);
Assert.Contains("23", shortName, StringComparison.Ordinal);
Assert.Contains("50", shortName, StringComparison.Ordinal);
Assert.Contains("Ema", shortName, StringComparison.Ordinal);
}
[Fact]
public void StcIndicator_Initialize_CreatesInternalStc()
{
var indicator = new StcIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
Assert.Equal("STC", indicator.LinesSeries[0].Name);
}
[Fact]
public void StcIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new StcIndicator { CycleLength = 5, FastLength = 10, SlowLength = 20 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
// We must feed bars one by one to simulate history for stateful indicators
for (int i = 0; i < 50; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Line series should have values
Assert.Equal(50, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0))); // GetValue(0) is the most recent
}
[Fact]
public void StcIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new StcIndicator { CycleLength = 5, FastLength = 10, SlowLength = 20 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Feed enough history to warm up
for (int i = 0; i < 50; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
indicator.HistoricalData.AddBar(now.AddMinutes(50), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.True(indicator.LinesSeries[0].Count > 0);
}
[Fact]
public void StcIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new StcIndicator { CycleLength = 5, FastLength = 10, SlowLength = 20 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Feed warmup bars
for (int i = 0; i < 50; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double firstValue = indicator.LinesSeries[0].GetValue(0);
// Update with NewTick (same bar, new price potentially, but reusing last bar in this mock)
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 StcIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new StcIndicator { CycleLength = 10, FastLength = 12, SlowLength = 26 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Generate enough price action to clear warmup (SlowLength + 2*CycleLength = 26 + 20 = 46)
// We'll generate 100 bars to be safe
double[] closes = new double[100];
for (int i = 0; i < 100; i++)
{
closes[i] = 100 + Math.Sin(i * 0.1) * 10;
}
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);
}
// The last value should be finite (we are well past 46)
double lastVal = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(lastVal));
}
[Fact]
public void StcIndicator_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 StcIndicator
{
CycleLength = 10,
FastLength = 23,
SlowLength = 50,
Source = source,
};
indicator.Initialize();
var now = DateTime.UtcNow;
// Feed enough bars to produce a value
// Warmup = 50 + 20 = 70 approx
for (int i = 0; i < 80; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 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 StcIndicator_Parameters_CanBeChanged()
{
var indicator = new StcIndicator();
indicator.CycleLength = 20;
Assert.Equal(20, indicator.CycleLength);
indicator.FastLength = 12;
Assert.Equal(12, indicator.FastLength);
indicator.SlowLength = 26;
Assert.Equal(26, indicator.SlowLength);
indicator.Smoothing = StcSmoothing.Digital;
Assert.Equal(StcSmoothing.Digital, indicator.Smoothing);
}
}
+132
View File
@@ -0,0 +1,132 @@
using System;
using Xunit;
namespace QuanTAlib;
public class StcTests
{
private const int CycleLength = 12;
private const int FastLength = 26;
private const int SlowLength = 50;
private static Stc CreateDefaultStc() => new(kPeriod: CycleLength, dPeriod: CycleLength, fastLength: FastLength, slowLength: SlowLength, smoothing: StcSmoothing.Sigmoid);
[Fact]
public void Constructor_ValidatesInput()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Stc(kPeriod: 1));
Assert.Throws<ArgumentOutOfRangeException>(() => new Stc(dPeriod: 0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Stc(fastLength: 1));
Assert.Throws<ArgumentOutOfRangeException>(() => new Stc(slowLength: 1));
}
[Fact]
public void Calc_ReturnsValue()
{
var stc = CreateDefaultStc();
var result = stc.Update(new TValue(DateTime.UtcNow, 100));
// Expect NaN during warmup
Assert.True(double.IsNaN(result.Value) || double.IsFinite(result.Value));
}
[Fact]
public void Properties_Accessible()
{
var stc = CreateDefaultStc();
Assert.Equal(0, stc.Last.Value); // Initial value before updates
Assert.False(stc.IsHot);
Assert.Contains("Stc", stc.Name, StringComparison.Ordinal);
}
[Fact]
public void IsHot_BecomesTrueWhenBufferFull()
{
var stc = CreateDefaultStc();
int warmup = stc.WarmupPeriod;
for (int i = 0; i < warmup - 1; i++)
{
stc.Update(new TValue(DateTime.UtcNow, 100));
Assert.False(stc.IsHot);
}
stc.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(stc.IsHot);
}
[Fact]
public void BatchCalc_MatchesIterativeCalc()
{
var iterativeStc = CreateDefaultStc();
var batchStc = CreateDefaultStc();
var series = new TSeries();
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
for (int i = 0; i < 200; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
iterativeStc.Update(new TValue(bar.Time, bar.Close));
}
var batchResult = batchStc.Update(series);
Assert.Equal(iterativeStc.Last.Value, batchResult.Last.Value, 1e-9);
}
[Fact]
public void SpanBatch_MatchesTSeriesBatch()
{
// Use default parameters for static calculation
var series = new TSeries();
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
double[] input = new double[200];
double[] output = new double[200];
for (int i = 0; i < 200; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
input[i] = bar.Close;
}
var batchStc = CreateDefaultStc();
var tseriesResult = batchStc.Update(series);
Stc.Batch(input.AsSpan(), output.AsSpan(), kPeriod: CycleLength, dPeriod: CycleLength, fastLength: FastLength, slowLength: SlowLength, smoothing: StcSmoothing.Sigmoid);
// Compare last value
Assert.Equal(tseriesResult.Last.Value, output[^1], 1e-9);
}
[Fact]
public void NaN_Input_HandledSafely()
{
var stc = CreateDefaultStc();
stc.Update(new TValue(DateTime.UtcNow, 100));
var result = stc.Update(new TValue(DateTime.UtcNow, double.NaN));
// Should be NaN during warmup
Assert.True(double.IsNaN(result.Value) || double.IsFinite(result.Value));
}
[Fact]
public void SmoothingOptions_ProduceDifferentResults()
{
var stcSigmoid = new Stc(kPeriod: 10, dPeriod: 10, fastLength: 20, slowLength: 40, smoothing: StcSmoothing.Sigmoid);
var stcEma = new Stc(kPeriod: 10, dPeriod: 10, fastLength: 20, slowLength: 40, smoothing: StcSmoothing.Ema);
var stcDigital = new Stc(kPeriod: 10, dPeriod: 10, fastLength: 20, slowLength: 40, smoothing: StcSmoothing.Digital);
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
for (int i = 0; i < 100; i++)
{
double val = gbm.Next().Close;
stcSigmoid.Update(new TValue(DateTime.UtcNow, val));
stcEma.Update(new TValue(DateTime.UtcNow, val));
stcDigital.Update(new TValue(DateTime.UtcNow, val));
}
Assert.NotEqual(stcSigmoid.Last.Value, stcEma.Last.Value);
Assert.NotEqual(stcSigmoid.Last.Value, stcDigital.Last.Value);
}
}
@@ -0,0 +1,117 @@
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using System;
using System.Linq;
using Skender.Stock.Indicators;
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public sealed class StcValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
public StcValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
public void Dispose()
{
_testData.Dispose();
}
[Fact]
public void Validate_Skender_Stc_Deviation()
{
// Skender's STC implementation uses a "Single Smoothed" approach (Stoch of MACD).
// QuanTAlib implements the standard "Double Smoothed" approach (Stoch of Stoch of MACD),
// as originally defined by Schaff.
//
// Example mismatch at index 333:
// QuanTAlib (Double Smoothed) = 50.0
// Skender (Single Smoothed) = 97.05
//
// This test documents this known deviation rather than failing on it.
const int cycle = 10;
int fast = 23;
int slow = 50;
var sResult = _testData.SkenderQuotes.GetStc(cycle, fast, slow).ToList();
var qStc = new Stc(kPeriod: cycle, dPeriod: 3, fastLength: fast, slowLength: slow, smoothing: StcSmoothing.Ema);
var qResult = qStc.Update(_testData.Data);
// Skender recommends S+C+250 warmup. 50+10+250 = 310.
int skip = 310;
double sumSq = 0;
int count = 0;
for (int i = skip; i < qResult.Count; i++)
{
double sVal = sResult[i].Stc ?? double.NaN;
double qVal = qResult[i].Value;
if (!double.IsNaN(sVal) && !double.IsNaN(qVal))
{
sumSq += (sVal - qVal) * (sVal - qVal);
count++;
}
}
double rmse = Math.Sqrt(sumSq / count);
_output.WriteLine($"Known Methodology Deviation - RMSE: {rmse:F4}");
// Assert that we are essentially different (RMSE > 5.0 implies significant deviation)
// If they accidentally matched (e.g. if we broke our logic to match Skender), this should fail.
Assert.True(rmse > 5.0, "QuanTAlib STC matches Skender STC, which suggests regression to Single Smoothed logic.");
// Assert values are valid
for (int i = skip; i < qResult.Count; i++)
{
Assert.True(double.IsFinite(qResult[i].Value));
Assert.InRange(qResult[i].Value, 0, 100);
}
}
// ── Cross-library: OoplesFinance ──────────────────────────────────────────
[Fact]
public void Stc_MatchesOoples_Structural()
{
var ooplesData = _testData.SkenderQuotes.Select(static q => new TickerData
{
Date = q.Date,
Open = (double)q.Open,
High = (double)q.High,
Low = (double)q.Low,
Close = (double)q.Close,
Volume = (double)q.Volume
}).ToList();
var stockData = new StockData(ooplesData);
var oResult = stockData.CalculateSchaffTrendCycle();
var oValues = oResult.OutputValues.Values.First();
var stc = new Stc(kPeriod: 10, dPeriod: 3, fastLength: 23, slowLength: 50, smoothing: StcSmoothing.Ema);
var qValues = new List<double>();
foreach (var item in _testData.Data)
{
qValues.Add(stc.Update(item).Value);
}
Assert.True(oValues.Count > 0, "Ooples STC must produce output");
int finiteCount = 0;
for (int i = 50; i < Math.Min(oValues.Count, qValues.Count); i++)
{
if (double.IsFinite(oValues[i]) && double.IsFinite(qValues[i]))
{
finiteCount++;
}
}
Assert.True(finiteCount > 100, $"Expected >100 finite STC pairs, got {finiteCount}");
_output.WriteLine($"STC Ooples structural: {finiteCount} finite pairs verified.");
}
}