Files
QuanTAlib/lib/trends/butter/Butter.Validation.Tests.cs
T
Miha Kralj d7dbd7078a Refactor event handling and improve argument validation across indicators
- Updated event handler signatures to use TValueEventArgs for consistency in Mama, Mgdi, Pwma, Rma, Sma, Ssf, Super, T3, Tema, Trima, Usf, Vidya, Wma, and Atr classes.
- Enhanced argument validation by specifying parameter names in exceptions for clarity.
- Adjusted tests to align with new event handler signatures.
- Improved code readability and maintainability by using structured records and lambda expressions.
2025-12-27 15:46:28 -08:00

184 lines
5.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
using QuanTAlib;
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
namespace QuanTAlib.Tests;
public class ButterValidationTests
{
private readonly GBM _gbm;
public ButterValidationTests()
{
_gbm = new GBM();
}
[Fact]
public void ValidateAgainstReferenceImplementation()
{
// Generate test data
var bars = _gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
int period = 14;
// 1. QuanTAlib Implementation
var butter = new Butter(period);
var quantalibResult = new List<double>();
foreach (var item in series)
{
quantalibResult.Add(butter.Update(item).Value);
}
// 2. Reference Implementation (PineScript logic)
var referenceResult = CalculateReference(series, period);
// Compare
Assert.Equal(quantalibResult.Count, referenceResult.Count);
for (int i = 0; i < quantalibResult.Count; i++)
{
// Allow small difference due to float precision
Assert.Equal(referenceResult[i], quantalibResult[i], 1e-9);
}
}
[Fact]
public void ValidateAgainstOoples()
{
// Generate test data
var bars = _gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
int period = 14;
// 1. QuanTAlib Implementation
var butter = new Butter(period);
var quantalibResult = new List<double>();
foreach (var item in series)
{
quantalibResult.Add(butter.Update(item).Value);
}
// 2. Ooples Implementation
var ooplesData = bars.Select(b => new TickerData
{
Date = b.AsDateTime,
Open = b.Open,
High = b.High,
Low = b.Low,
Close = b.Close,
Volume = b.Volume
}).ToList();
var stockData = new StockData(ooplesData);
var ooplesResult = stockData.CalculateEhlers2PoleButterworthFilterV2(length: period);
var ooplesValues = ooplesResult.OutputValues.Values.First();
// Compare
Assert.Equal(quantalibResult.Count, ooplesValues.Count);
// Check last 100 bars
for (int i = quantalibResult.Count - 100; i < quantalibResult.Count; i++)
{
// Ooples implementation (Ehlers) deviates slightly from standard Butterworth (PineScript reference)
// Tolerance increased to 0.2 to account for this difference.
Assert.Equal(ooplesValues[i], quantalibResult[i], 2e-1);
}
}
private static IReadOnlyList<double> CalculateReference(TSeries source, int period)
{
var result = new List<double>();
// PineScript logic:
// float pi = math.pi
// int safe_length = math.max(length, 2)
// float omega = 2.0 * pi / safe_length
// float sin_omega = math.sin(omega)
// float cos_omega = math.cos(omega)
// float alpha = sin_omega / math.sqrt(2.0)
// float a0 = 1.0 + alpha
// float a1 = -2.0 * cos_omega
// float a2 = 1.0 - alpha
// float b0 = (1.0 - cos_omega) / 2.0
// float b1 = 1.0 - cos_omega
// float b2 = (1.0 - cos_omega) / 2.0
int safe_length = Math.Max(period, 2);
double omega = 2.0 * Math.PI / safe_length;
double sin_omega = Math.Sin(omega);
double cos_omega = Math.Cos(omega);
double alpha = sin_omega / Math.Sqrt(2.0);
double a0 = 1.0 + alpha;
double a1 = -2.0 * cos_omega;
double a2 = 1.0 - alpha;
double b0 = (1.0 - cos_omega) / 2.0;
double b1 = 1.0 - cos_omega;
double b2 = (1.0 - cos_omega) / 2.0;
double filt = 0;
double filt1 = 0;
double filt2 = 0;
// Need to track history for src[1], src[2]
// In PineScript, src[1] is previous bar's src.
// We iterate through source.
double src1 = 0;
double src2 = 0;
for (int i = 0; i < source.Count; i++)
{
double src = source[i].Value;
// if bar_index < 2
// filt := nz(src, 0.0)
if (i < 2)
{
filt = src;
// Initialize history
// In PineScript, src[1] at index 0 is NaN (nz -> 0.0 or something?)
// Actually, nz(src, 0.0) means if src is NaN, use 0.0.
// But here src is valid.
// At i=0: src[1] is NaN, src[2] is NaN.
// At i=1: src[1] is src[i-1], src[2] is NaN.
// But the PineScript code says:
// if bar_index < 2: filt := nz(src, 0.0)
// else: ... formula ...
// So for i=0 and i=1, filt = src.
}
else
{
// float ssrc = nz(src, src[1]) -> if src is NaN use src[1]. Assuming src is valid.
double ssrc = src;
// float src1 = nz(src[1], ssrc) -> previous src.
// float src2 = nz(src[2], src1) -> 2nd previous src.
// float filt1 = nz(filt[1], ssrc) -> previous filt.
// float filt2 = nz(filt[2], filt1) -> 2nd previous filt.
// filt := (b0 * ssrc + b1 * src1 + b2 * src2 - a1 * filt1 - a2 * filt2) / a0
filt = (b0 * ssrc + b1 * src1 + b2 * src2 - a1 * filt1 - a2 * filt2) / a0;
}
result.Add(filt);
// Update history
src2 = src1;
src1 = src;
filt2 = filt1;
filt1 = filt;
}
return result;
}
}