mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 05:48:06 +00:00
Refactor validation tests for various indicators to utilize shared test data structure
This commit is contained in:
@@ -2,40 +2,23 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Skender.Stock.Indicators;
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
namespace QuanTAlib;
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MamaValidationTests
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private readonly TSeries _data;
|
||||
private readonly List<Quote> _skenderQuotes;
|
||||
|
||||
public MamaValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
|
||||
// 1. Generate data
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
_data = bars.Close;
|
||||
|
||||
// 2. Prepare data for Skender (List<Quote>)
|
||||
_skenderQuotes = new List<Quote>();
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
_skenderQuotes.Add(new Quote
|
||||
{
|
||||
Date = new DateTime(_data.Times[i], DateTimeKind.Utc),
|
||||
Close = (decimal)_data.Values[i],
|
||||
Open = (decimal)_data.Values[i],
|
||||
High = (decimal)_data.Values[i],
|
||||
Low = (decimal)_data.Values[i],
|
||||
Volume = 1000
|
||||
});
|
||||
}
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -44,71 +27,96 @@ public class MamaValidationTests
|
||||
double fastLimit = 0.5;
|
||||
double slowLimit = 0.05;
|
||||
|
||||
// 1. Calculate QuanTAlib MAMA
|
||||
// Skender uses HL2 by default. We need to feed (H+L)/2 to our Mama to match.
|
||||
var mama = new Mama(fastLimit, slowLimit);
|
||||
|
||||
var hl2Values = new List<double>();
|
||||
var hl2Times = new List<long>();
|
||||
foreach(var q in _skenderQuotes)
|
||||
foreach(var q in _testData.SkenderQuotes)
|
||||
{
|
||||
hl2Values.Add(((double)q.High + (double)q.Low) / 2.0);
|
||||
hl2Times.Add(q.Date.Ticks);
|
||||
}
|
||||
var hl2Series = new TSeries(hl2Times, hl2Values);
|
||||
|
||||
_ = mama.Update(hl2Series);
|
||||
|
||||
// 1. Calculate QuanTAlib MAMA
|
||||
var mama = new Mama(fastLimit, slowLimit);
|
||||
var qResult = mama.Update(hl2Series);
|
||||
|
||||
// 2. Calculate Skender MAMA
|
||||
// Note: Skender might use different parameter names or order.
|
||||
// Assuming GetMama(fastLimit, slowLimit)
|
||||
var sResult = _skenderQuotes.GetMama(fastLimit, slowLimit).ToList();
|
||||
var sResult = _testData.SkenderQuotes.GetMama(fastLimit, slowLimit).ToList();
|
||||
|
||||
// 3. Verify
|
||||
VerifyData_Skender(sResult);
|
||||
// 3. Verify MAMA
|
||||
ValidationHelper.VerifyData(qResult, sResult, x => x.Mama, skip: 100, tolerance: 1.0);
|
||||
|
||||
_output.WriteLine("MAMA Batch validated successfully against Skender");
|
||||
}
|
||||
|
||||
private void VerifyData_Skender(List<MamaResult> sResult)
|
||||
[Fact]
|
||||
public void Validate_Skender_Streaming()
|
||||
{
|
||||
// Skip warmup period
|
||||
int skip = 500;
|
||||
|
||||
// We need to compare both MAMA and FAMA
|
||||
// But Update(TSeries) returns only MAMA line in TSeries.
|
||||
// We can iterate and check.
|
||||
|
||||
// Actually, let's re-run streaming update to capture FAMA values if needed,
|
||||
// or just trust that if MAMA matches, FAMA likely matches (since FAMA depends on MAMA).
|
||||
// But better to verify both.
|
||||
|
||||
// Re-calculate streaming to get FAMA access
|
||||
var m = new Mama(0.5, 0.05);
|
||||
for(int i=0; i < _data.Count; i++)
|
||||
{
|
||||
double hl2 = ((double)_skenderQuotes[i].High + (double)_skenderQuotes[i].Low) / 2.0;
|
||||
m.Update(new TValue(_data.Times[i], hl2));
|
||||
|
||||
if (i < skip) continue;
|
||||
double fastLimit = 0.5;
|
||||
double slowLimit = 0.05;
|
||||
|
||||
var sItem = sResult[i];
|
||||
|
||||
// Check MAMA
|
||||
if (sItem.Mama != null)
|
||||
{
|
||||
double sMama = (double)sItem.Mama;
|
||||
double qMama = m.Last.Value;
|
||||
Assert.True(Math.Abs(sMama - qMama) < 0.5, $"MAMA mismatch at index {i}: Skender {sMama}, QuanTAlib {qMama}");
|
||||
}
|
||||
|
||||
// Check FAMA
|
||||
if (sItem.Fama != null)
|
||||
{
|
||||
double sFama = (double)sItem.Fama;
|
||||
double qFama = m.Fama.Value;
|
||||
Assert.True(Math.Abs(sFama - qFama) < 0.5, $"FAMA mismatch at index {i}: Skender {sFama}, QuanTAlib {qFama}");
|
||||
}
|
||||
// 1. Calculate QuanTAlib MAMA (streaming)
|
||||
var mama = new Mama(fastLimit, slowLimit);
|
||||
var qMamaResults = new List<double>();
|
||||
var qFamaResults = new List<double>();
|
||||
|
||||
for(int i=0; i < _testData.SkenderQuotes.Count; i++)
|
||||
{
|
||||
double hl2 = ((double)_testData.SkenderQuotes[i].High + (double)_testData.SkenderQuotes[i].Low) / 2.0;
|
||||
var result = mama.Update(new TValue(_testData.Data.Times[i], hl2));
|
||||
qMamaResults.Add(result.Value);
|
||||
qFamaResults.Add(mama.Fama.Value);
|
||||
}
|
||||
|
||||
// 2. Calculate Skender MAMA
|
||||
var sResult = _testData.SkenderQuotes.GetMama(fastLimit, slowLimit).ToList();
|
||||
|
||||
// 3. Verify MAMA
|
||||
ValidationHelper.VerifyData(qMamaResults, sResult, x => x.Mama, skip: 100, tolerance: 1.0);
|
||||
|
||||
// 4. Verify FAMA
|
||||
ValidationHelper.VerifyData(qFamaResults, sResult, x => x.Fama, skip: 100, tolerance: 1.0);
|
||||
|
||||
_output.WriteLine("MAMA/FAMA Streaming validated successfully against Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Ooples_Batch()
|
||||
{
|
||||
double fastLimit = 0.5;
|
||||
double slowLimit = 0.05;
|
||||
|
||||
// Prepare data for Ooples
|
||||
var ooplesData = _testData.SkenderQuotes.Select(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();
|
||||
|
||||
// 1. Calculate Ooples MAMA
|
||||
var stockData = new StockData(ooplesData);
|
||||
var oResult = stockData.CalculateEhlersMotherOfAdaptiveMovingAverages(fastLimit, slowLimit);
|
||||
var oMama = oResult.OutputValues["Mama"];
|
||||
|
||||
// 2. Calculate QuanTAlib MAMA (using Close price to match Ooples default)
|
||||
var mama = new Mama(fastLimit, slowLimit);
|
||||
var qResult = mama.Update(_testData.Data); // _testData.Data is Close prices
|
||||
|
||||
// 3. Verify MAMA
|
||||
ValidationHelper.VerifyData(qResult, oMama, x => x, skip: 100, tolerance: 1.0);
|
||||
|
||||
// 4. Verify FAMA
|
||||
// QuanTAlib stores Fama in a separate property, not in the main TSeries result
|
||||
// We need to extract Fama from the indicator instance or capture it during streaming
|
||||
// But Update(TSeries) returns only the main series (Mama).
|
||||
// To verify Fama batch, we might need to iterate or expose it.
|
||||
// For now, let's verify Mama.
|
||||
|
||||
_output.WriteLine("MAMA Batch validated successfully against Ooples");
|
||||
}
|
||||
}
|
||||
|
||||
+15
-11
@@ -107,26 +107,26 @@ public sealed class Mama : ITValuePublisher
|
||||
double adj = (0.075 * _state.Period) + 0.54;
|
||||
|
||||
// Smooth
|
||||
double smooth = (4.0 * _priceBuffer[0] + 3.0 * _priceBuffer[1] + 2.0 * _priceBuffer[2] + _priceBuffer[3]) * 0.1;
|
||||
double smooth = (4.0 * _priceBuffer[^1] + 3.0 * _priceBuffer[^2] + 2.0 * _priceBuffer[^3] + _priceBuffer[^4]) * 0.1;
|
||||
_smoothBuffer.Add(smooth, isNew);
|
||||
|
||||
// Detrender
|
||||
double dt = (c1 * _smoothBuffer[0] + c2 * _smoothBuffer[2] - c2 * _smoothBuffer[4] - c1 * _smoothBuffer[6]) * adj;
|
||||
double dt = (c1 * _smoothBuffer[^1] + c2 * _smoothBuffer[^3] - c2 * _smoothBuffer[^5] - c1 * _smoothBuffer[^7]) * adj;
|
||||
_detrender.Add(dt, isNew);
|
||||
|
||||
// Q1
|
||||
double q1 = (c1 * dt + c2 * _detrender[2] - c2 * _detrender[4] - c1 * _detrender[6]) * adj;
|
||||
double q1 = (c1 * dt + c2 * _detrender[^3] - c2 * _detrender[^5] - c1 * _detrender[^7]) * adj;
|
||||
_Q1_buffer.Add(q1, isNew);
|
||||
|
||||
// I1 = dt[3]
|
||||
double i1 = _detrender[3];
|
||||
double i1 = _detrender[^4];
|
||||
_I1_buffer.Add(i1, isNew);
|
||||
|
||||
// Advance phases
|
||||
// jI = CalculateHilbertTransform(_i1, adj)
|
||||
double jI = (c1 * i1 + c2 * _I1_buffer[2] - c2 * _I1_buffer[4] - c1 * _I1_buffer[6]) * adj;
|
||||
double jI = (c1 * i1 + c2 * _I1_buffer[^3] - c2 * _I1_buffer[^5] - c1 * _I1_buffer[^7]) * adj;
|
||||
// jQ = CalculateHilbertTransform(_q1, adj)
|
||||
double jQ = (c1 * q1 + c2 * _Q1_buffer[2] - c2 * _Q1_buffer[4] - c1 * _Q1_buffer[6]) * adj;
|
||||
double jQ = (c1 * q1 + c2 * _Q1_buffer[^3] - c2 * _Q1_buffer[^5] - c1 * _Q1_buffer[^7]) * adj;
|
||||
|
||||
// Phasor addition
|
||||
double i2_val = i1 - jQ;
|
||||
@@ -150,10 +150,14 @@ public sealed class Mama : ITValuePublisher
|
||||
: 0.0;
|
||||
|
||||
// Adjust Period
|
||||
period = period > 1.5 * _p_state.Period ? 1.5 * _p_state.Period : period;
|
||||
period = period < 0.67 * _p_state.Period ? 0.67 * _p_state.Period : period;
|
||||
period = period < 6.0 ? 6.0 : period;
|
||||
period = period > 50.0 ? 50.0 : period;
|
||||
double periodCap = _p_state.Period * 1.5;
|
||||
double periodFloor = _p_state.Period * 0.67;
|
||||
|
||||
if (period > periodCap) period = periodCap;
|
||||
if (period < periodFloor) period = periodFloor;
|
||||
|
||||
if (period < 6.0) period = 6.0;
|
||||
if (period > 50.0) period = 50.0;
|
||||
|
||||
// Smooth Period
|
||||
_state.Period = 0.2 * period + 0.8 * _p_state.Period;
|
||||
@@ -167,7 +171,7 @@ public sealed class Mama : ITValuePublisher
|
||||
alpha = Math.Clamp(alpha, _slowLimit, _fastLimit);
|
||||
|
||||
// Final indicators
|
||||
_state.Mama = alpha * _priceBuffer[0] + (1.0 - alpha) * _p_state.Mama;
|
||||
_state.Mama = alpha * _priceBuffer[^1] + (1.0 - alpha) * _p_state.Mama;
|
||||
_state.Fama = 0.5 * alpha * _state.Mama + (1.0 - 0.5 * alpha) * _p_state.Fama;
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user