mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-07 21:47:43 +00:00
060649192f
- 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
273 lines
9.4 KiB
C#
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);
|
|
}
|
|
}
|