mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 04:58:08 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user