Files
Miha Kralj 060649192f 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
2026-03-12 12:34:16 -07:00

273 lines
9.4 KiB
C#

using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using Skender.Stock.Indicators;
using TALib;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public sealed class MacdValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public MacdValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
public void Validate_Skender_Batch()
{
// Standard MACD parameters
const int fastPeriod = 12;
const int slowPeriod = 26;
const int signalPeriod = 9;
// Calculate QuanTAlib MACD (batch TSeries)
var macd = new global::QuanTAlib.Macd(fastPeriod, slowPeriod, signalPeriod);
var qResult = macd.Update(_testData.Data);
// Calculate Skender MACD
var sResult = _testData.SkenderQuotes.GetMacd(fastPeriod, slowPeriod, signalPeriod).ToList();
// Compare last 100 records
// MACD Line
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Macd);
// Signal Line
// We need to extract Signal line from QuanTAlib result.
// Since Update returns TSeries of MACD line, we need to access Signal property from the indicator instance
// But for batch update, we need to re-run or capture signal.
// The Macd.Update(TSeries) returns the MACD line series.
// To validate Signal and Histogram, we should use the streaming approach or modify Macd to return all lines.
// For now, let's validate MACD line here, and do full validation in Streaming test.
}
[Fact]
public void Validate_Skender_Streaming()
{
const int fastPeriod = 12;
const int slowPeriod = 26;
const int signalPeriod = 9;
// Calculate QuanTAlib MACD (streaming)
var macd = new global::QuanTAlib.Macd(fastPeriod, slowPeriod, signalPeriod);
var qMacd = new List<double>();
var qSignal = new List<double>();
var qHist = new List<double>();
foreach (var item in _testData.Data)
{
macd.Update(item);
qMacd.Add(macd.Last.Value);
qSignal.Add(macd.Signal.Value);
qHist.Add(macd.Histogram.Value);
}
// Calculate Skender MACD
var sResult = _testData.SkenderQuotes.GetMacd(fastPeriod, slowPeriod, signalPeriod).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qMacd, sResult, (s) => s.Macd);
ValidationHelper.VerifyData(qSignal, sResult, (s) => s.Signal);
ValidationHelper.VerifyData(qHist, sResult, (s) => s.Histogram);
_output.WriteLine("MACD Streaming validated successfully against Skender");
}
[Fact]
public void Validate_Talib_Streaming()
{
const int fastPeriod = 12;
const int slowPeriod = 26;
const int signalPeriod = 9;
// Prepare data for TA-Lib (double[])
double[] tData = _testData.RawData.ToArray();
double[] outMacd = new double[tData.Length];
double[] outSignal = new double[tData.Length];
double[] outHist = new double[tData.Length];
// Calculate QuanTAlib MACD (streaming)
var macd = new global::QuanTAlib.Macd(fastPeriod, slowPeriod, signalPeriod);
var qMacd = new List<double>();
var qSignal = new List<double>();
var qHist = new List<double>();
foreach (var item in _testData.Data)
{
macd.Update(item);
qMacd.Add(macd.Last.Value);
qSignal.Add(macd.Signal.Value);
qHist.Add(macd.Histogram.Value);
}
// Calculate TA-Lib MACD
var retCode = TALib.Functions.Macd<double>(tData, 0..^0, outMacd, outSignal, outHist, out var outRange, fastPeriod, slowPeriod, signalPeriod);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
int lookback = TALib.Functions.MacdLookback(fastPeriod, slowPeriod, signalPeriod);
// Compare last 100 records
ValidationHelper.VerifyData(qMacd, outMacd, outRange, lookback);
ValidationHelper.VerifyData(qSignal, outSignal, outRange, lookback);
ValidationHelper.VerifyData(qHist, outHist, outRange, lookback);
_output.WriteLine("MACD Streaming validated successfully against TA-Lib");
}
[Fact]
public void Validate_Against_Ooples()
{
const int fastPeriod = 12;
const int slowPeriod = 26;
const int signalPeriod = 9;
// Prepare data for Ooples (List<TickerData>)
var ooplesData = _testData.SkenderQuotes.Select(q => new TickerData
{
Date = q.Date,
Close = (double)q.Close,
High = (double)q.High,
Low = (double)q.Low,
Open = (double)q.Open,
Volume = (double)q.Volume
}).ToList();
// Calculate QuanTAlib MACD (streaming)
var macd = new global::QuanTAlib.Macd(fastPeriod, slowPeriod, signalPeriod);
var qMacd = new List<double>();
var qSignal = new List<double>();
var qHist = new List<double>();
foreach (var item in _testData.Data)
{
macd.Update(item);
qMacd.Add(macd.Last.Value);
qSignal.Add(macd.Signal.Value);
qHist.Add(macd.Histogram.Value);
}
// Calculate Ooples MACD
var stockData = new StockData(ooplesData);
var oResult = stockData.CalculateMovingAverageConvergenceDivergence(fastLength: fastPeriod, slowLength: slowPeriod, signalLength: signalPeriod);
var oMacd = oResult.OutputValues["Macd"];
var oSignal = oResult.OutputValues["Signal"];
var oHist = oResult.OutputValues["Histogram"];
// Compare
ValidationHelper.VerifyData(qMacd, oMacd, (s) => s, tolerance: ValidationHelper.OoplesTolerance);
ValidationHelper.VerifyData(qSignal, oSignal, (s) => s, tolerance: ValidationHelper.OoplesTolerance);
ValidationHelper.VerifyData(qHist, oHist, (s) => s, tolerance: ValidationHelper.OoplesTolerance);
_output.WriteLine("MACD validated successfully against Ooples");
}
[Fact]
public void Validate_Tulip_Streaming()
{
// Tulip has a hardcoded override for 12/26 that uses 0.15 and 0.075 instead of standard alpha
// We use different periods to validate the algorithm correctness without this quirk
const int fastPeriod = 10;
const int slowPeriod = 20;
const int signalPeriod = 9;
// Prepare data for Tulip (double[])
double[] tData = _testData.RawData.ToArray();
// Calculate QuanTAlib MACD (streaming)
var macd = new global::QuanTAlib.Macd(fastPeriod, slowPeriod, signalPeriod);
var qMacd = new List<double>();
var qSignal = new List<double>();
var qHist = new List<double>();
foreach (var item in _testData.Data)
{
macd.Update(item);
qMacd.Add(macd.Last.Value);
qSignal.Add(macd.Signal.Value);
qHist.Add(macd.Histogram.Value);
}
// Calculate Tulip MACD
var macdIndicator = Tulip.Indicators.macd;
double[][] inputs = { tData };
double[] options = { fastPeriod, slowPeriod, signalPeriod };
// Tulip MACD lookback
int lookback = macdIndicator.Start(options);
double[][] outputs = {
new double[tData.Length - lookback], // MACD
new double[tData.Length - lookback], // Signal
new double[tData.Length - lookback] // Histogram
};
macdIndicator.Run(inputs, options, outputs);
var tMacd = outputs[0];
var tSignal = outputs[1];
var tHist = outputs[2];
// Compare last 100 records
ValidationHelper.VerifyData(qMacd, tMacd, lookback);
ValidationHelper.VerifyData(qSignal, tSignal, lookback);
ValidationHelper.VerifyData(qHist, tHist, lookback);
_output.WriteLine("MACD Streaming validated successfully against Tulip");
}
[Fact]
public void Macd_Correction_Recomputes()
{
var ind = new Macd();
var t0 = DateTime.MinValue;
// Build state well past warmup
for (int i = 0; i < 50; i++)
{
ind.Update(new TValue(t0.AddSeconds(i), 100.0 + (i * 0.5)));
}
// Anchor bar
var anchorTime = t0.AddSeconds(50);
const double anchorPrice = 125.0;
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
double anchorMacd = ind.Last.Value;
double anchorSignal = ind.Signal.Value;
double anchorHistogram = ind.Histogram.Value;
// Correction with dramatically different value
ind.Update(new TValue(anchorTime, anchorPrice * 10), isNew: false);
Assert.NotEqual(anchorMacd, ind.Last.Value);
// Correction back to original — all three outputs must restore exactly
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
Assert.Equal(anchorMacd, ind.Last.Value, 1e-9);
Assert.Equal(anchorSignal, ind.Signal.Value, 1e-9);
Assert.Equal(anchorHistogram, ind.Histogram.Value, 1e-9);
}
}