mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 10:08:05 +00:00
adding missing validations
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using TALib;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation for Avgprice (Average Price) = (O+H+L+C)/4.
|
||||
/// Cross-validated against TA-Lib AVGPRICE (exact match expected).
|
||||
/// Skender, Tulip, and Ooples do not implement AVGPRICE as a standalone function.
|
||||
/// </summary>
|
||||
public sealed class AvgpriceValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data = new();
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public AvgpriceValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_data.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── A) Cross-validate with TA-Lib AVGPRICE ────────────────────────────────
|
||||
[Fact]
|
||||
public void TALib_AvgPrice_Batch_Validates()
|
||||
{
|
||||
double[] open = _data.OpenPrices.ToArray();
|
||||
double[] high = _data.HighPrices.ToArray();
|
||||
double[] low = _data.LowPrices.ToArray();
|
||||
double[] close = _data.ClosePrices.ToArray();
|
||||
|
||||
// TA-Lib AvgPrice
|
||||
var taOut = new double[open.Length];
|
||||
var retCode = Functions.AvgPrice(open.AsSpan(), high.AsSpan(), low.AsSpan(), close.AsSpan(),
|
||||
0..^0, taOut, out var outRange);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
|
||||
|
||||
// QuanTAlib batch span
|
||||
var qlOut = new double[open.Length];
|
||||
Avgprice.Batch(open.AsSpan(), high.AsSpan(), low.AsSpan(), close.AsSpan(), qlOut.AsSpan());
|
||||
|
||||
int mismatches = 0;
|
||||
for (int j = 0; j < length; j++)
|
||||
{
|
||||
int qi = j + offset;
|
||||
double err = Math.Abs(qlOut[qi] - taOut[j]);
|
||||
if (err > ValidationHelper.TalibTolerance) { mismatches++; }
|
||||
}
|
||||
|
||||
double mismatchRate = (double)mismatches / length;
|
||||
_output.WriteLine($"TALib AVGPRICE: {length} compared, {mismatches} mismatches ({mismatchRate:P2})");
|
||||
Assert.Equal(0, mismatches);
|
||||
}
|
||||
|
||||
// ── B) Streaming == Batch span ────────────────────────────────────────────
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch()
|
||||
{
|
||||
const int N = 200;
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 1001);
|
||||
var bars = new TBar[N];
|
||||
for (int i = 0; i < N; i++) { bars[i] = gbm.Next(isNew: true); }
|
||||
|
||||
// Streaming
|
||||
var ind = new Avgprice();
|
||||
for (int i = 0; i < N; i++) { ind.Update(bars[i], isNew: true); }
|
||||
double streamVal = ind.Last.Value;
|
||||
|
||||
// Batch span
|
||||
double[] o = new double[N], h = new double[N], l = new double[N], c = new double[N];
|
||||
for (int i = 0; i < N; i++) { o[i] = bars[i].Open; h[i] = bars[i].High; l[i] = bars[i].Low; c[i] = bars[i].Close; }
|
||||
var qlOut = new double[N];
|
||||
Avgprice.Batch(o.AsSpan(), h.AsSpan(), l.AsSpan(), c.AsSpan(), qlOut.AsSpan());
|
||||
|
||||
_output.WriteLine($"Streaming={streamVal:F10}, Batch={qlOut[N - 1]:F10}");
|
||||
Assert.Equal(streamVal, qlOut[N - 1], 1e-12);
|
||||
}
|
||||
|
||||
// ── C) Formula verification: (O+H+L+C)/4 ─────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_Formula_Manual()
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow, open: 10.0, high: 20.0, low: 5.0, close: 15.0, volume: 1000);
|
||||
var ind = new Avgprice();
|
||||
var result = ind.Update(bar, isNew: true);
|
||||
double expected = (10.0 + 20.0 + 5.0 + 15.0) / 4.0; // = 12.5
|
||||
Assert.Equal(expected, result.Value, 1e-12);
|
||||
_output.WriteLine($"AVGPRICE formula: expected={expected}, actual={result.Value}: PASSED");
|
||||
}
|
||||
|
||||
// ── D) Batch(TBarSeries) == Calculate ─────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_BatchBarSeries_Equals_Calculate()
|
||||
{
|
||||
var (results, _) = Avgprice.Calculate(_data.Bars);
|
||||
var batchResult = Avgprice.Batch(_data.Bars);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], results.Values[i], 1e-12);
|
||||
}
|
||||
_output.WriteLine("AVGPRICE Batch(TBarSeries) == Calculate: PASSED");
|
||||
}
|
||||
|
||||
// ── E) Determinism ────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_Deterministic()
|
||||
{
|
||||
var r1 = Avgprice.Batch(_data.Bars);
|
||||
var r2 = Avgprice.Batch(_data.Bars);
|
||||
for (int i = 0; i < r1.Count; i++) { Assert.Equal(r1.Values[i], r2.Values[i], 15); }
|
||||
_output.WriteLine("AVGPRICE determinism: PASSED");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using TALib;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation for Medprice (Median Price) = (H+L)/2.
|
||||
/// Cross-validated against TA-Lib MEDPRICE (exact match expected).
|
||||
/// Skender, Tulip, and Ooples do not implement MEDPRICE as a standalone function.
|
||||
/// </summary>
|
||||
public sealed class MedpriceValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data = new();
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public MedpriceValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_data.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── A) Cross-validate with TA-Lib MEDPRICE ────────────────────────────────
|
||||
[Fact]
|
||||
public void TALib_MedPrice_Batch_Validates()
|
||||
{
|
||||
double[] high = _data.HighPrices.ToArray();
|
||||
double[] low = _data.LowPrices.ToArray();
|
||||
|
||||
// TA-Lib MedPrice
|
||||
var taOut = new double[high.Length];
|
||||
var retCode = Functions.MedPrice(high.AsSpan(), low.AsSpan(), 0..^0, taOut, out var outRange);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
|
||||
|
||||
// QuanTAlib batch via TBarSeries
|
||||
var qlOut = new double[high.Length];
|
||||
Medprice.Batch(_data.Bars, qlOut.AsSpan());
|
||||
|
||||
int mismatches = 0;
|
||||
for (int j = 0; j < length; j++)
|
||||
{
|
||||
int qi = j + offset;
|
||||
double err = Math.Abs(qlOut[qi] - taOut[j]);
|
||||
if (err > ValidationHelper.TalibTolerance) { mismatches++; }
|
||||
}
|
||||
|
||||
double mismatchRate = (double)mismatches / length;
|
||||
_output.WriteLine($"TALib MEDPRICE: {length} compared, {mismatches} mismatches ({mismatchRate:P2})");
|
||||
Assert.Equal(0, mismatches);
|
||||
}
|
||||
|
||||
// ── B) Streaming == Batch span ────────────────────────────────────────────
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch()
|
||||
{
|
||||
const int N = 200;
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 1001);
|
||||
var bars = new TBar[N];
|
||||
for (int i = 0; i < N; i++) { bars[i] = gbm.Next(isNew: true); }
|
||||
|
||||
// Streaming
|
||||
var ind = new Medprice();
|
||||
for (int i = 0; i < N; i++) { ind.Update(bars[i], isNew: true); }
|
||||
double streamVal = ind.Last.Value;
|
||||
|
||||
// Batch span
|
||||
double[] h = new double[N], l = new double[N];
|
||||
for (int i = 0; i < N; i++) { h[i] = bars[i].High; l[i] = bars[i].Low; }
|
||||
var qlOut = new double[N];
|
||||
Medprice.Batch(h.AsSpan(), l.AsSpan(), qlOut.AsSpan());
|
||||
|
||||
_output.WriteLine($"Streaming={streamVal:F10}, Batch={qlOut[N - 1]:F10}");
|
||||
Assert.Equal(streamVal, qlOut[N - 1], 1e-12);
|
||||
}
|
||||
|
||||
// ── C) Formula verification: (H+L)/2 ──────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_Formula_Manual()
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow, open: 10.0, high: 20.0, low: 5.0, close: 15.0, volume: 1000);
|
||||
var ind = new Medprice();
|
||||
var result = ind.Update(bar, isNew: true);
|
||||
double expected = (20.0 + 5.0) / 2.0; // = 12.5
|
||||
Assert.Equal(expected, result.Value, 1e-12);
|
||||
_output.WriteLine($"MEDPRICE formula: expected={expected}, actual={result.Value}: PASSED");
|
||||
}
|
||||
|
||||
// ── D) Always hot after first bar ─────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_AlwaysHotAfterFirstBar()
|
||||
{
|
||||
var ind = new Medprice();
|
||||
Assert.False(ind.IsHot);
|
||||
ind.Update(new TBar(DateTime.UtcNow, 10, 12, 8, 11, 1000), isNew: true);
|
||||
Assert.True(ind.IsHot);
|
||||
_output.WriteLine("MEDPRICE always hot after first bar: PASSED");
|
||||
}
|
||||
|
||||
// ── E) Batch(TBarSeries) == Calculate ─────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_BatchBarSeries_Equals_Calculate()
|
||||
{
|
||||
var (results, _) = Medprice.Calculate(_data.Bars);
|
||||
var batchResult = Medprice.Batch(_data.Bars);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], results.Values[i], 1e-12);
|
||||
}
|
||||
_output.WriteLine("MEDPRICE Batch(TBarSeries) == Calculate: PASSED");
|
||||
}
|
||||
|
||||
// ── F) Determinism ────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_Deterministic()
|
||||
{
|
||||
var r1 = Medprice.Batch(_data.Bars);
|
||||
var r2 = Medprice.Batch(_data.Bars);
|
||||
for (int i = 0; i < r1.Count; i++) { Assert.Equal(r1.Values[i], r2.Values[i], 15); }
|
||||
_output.WriteLine("MEDPRICE determinism: PASSED");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using TALib;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation for Midprice (Midpoint Price) = (Highest(H,N) + Lowest(L,N)) / 2.
|
||||
/// Cross-validated against TA-Lib MIDPRICE (exact match expected).
|
||||
/// Skender, Tulip, and Ooples do not implement MIDPRICE as a standalone function.
|
||||
/// </summary>
|
||||
public sealed class MidpriceValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data = new();
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public MidpriceValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_data.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── A) Cross-validate with TA-Lib MIDPRICE ────────────────────────────────
|
||||
[Fact]
|
||||
public void TALib_MidPrice_Batch_Validates_Period14()
|
||||
{
|
||||
const int period = 14;
|
||||
double[] high = _data.HighPrices.ToArray();
|
||||
double[] low = _data.LowPrices.ToArray();
|
||||
|
||||
// TA-Lib MidPrice
|
||||
var taOut = new double[high.Length];
|
||||
var retCode = Functions.MidPrice(high.AsSpan(), low.AsSpan(), 0..^0, taOut, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
|
||||
|
||||
// QuanTAlib batch span
|
||||
var qlOut = new double[high.Length];
|
||||
Midprice.Batch(high.AsSpan(), low.AsSpan(), qlOut.AsSpan(), period);
|
||||
|
||||
int mismatches = 0;
|
||||
for (int j = 0; j < length; j++)
|
||||
{
|
||||
int qi = j + offset;
|
||||
double err = Math.Abs(qlOut[qi] - taOut[j]);
|
||||
if (err > ValidationHelper.TalibTolerance) { mismatches++; }
|
||||
}
|
||||
|
||||
double mismatchRate = (double)mismatches / length;
|
||||
_output.WriteLine($"TALib MIDPRICE(14): {length} compared, {mismatches} mismatches ({mismatchRate:P2})");
|
||||
Assert.Equal(0, mismatches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TALib_MidPrice_Batch_Validates_Period5()
|
||||
{
|
||||
const int period = 5;
|
||||
double[] high = _data.HighPrices.ToArray();
|
||||
double[] low = _data.LowPrices.ToArray();
|
||||
|
||||
var taOut = new double[high.Length];
|
||||
var retCode = Functions.MidPrice(high.AsSpan(), low.AsSpan(), 0..^0, taOut, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
|
||||
|
||||
var qlOut = new double[high.Length];
|
||||
Midprice.Batch(high.AsSpan(), low.AsSpan(), qlOut.AsSpan(), period);
|
||||
|
||||
int mismatches = 0;
|
||||
for (int j = 0; j < length; j++)
|
||||
{
|
||||
int qi = j + offset;
|
||||
double err = Math.Abs(qlOut[qi] - taOut[j]);
|
||||
if (err > ValidationHelper.TalibTolerance) { mismatches++; }
|
||||
}
|
||||
|
||||
_output.WriteLine($"TALib MIDPRICE(5): {length} compared, {mismatches} mismatches");
|
||||
Assert.Equal(0, mismatches);
|
||||
}
|
||||
|
||||
// ── B) Streaming == Batch span ────────────────────────────────────────────
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch()
|
||||
{
|
||||
const int N = 200;
|
||||
const int period = 14;
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 1001);
|
||||
var bars = new TBar[N];
|
||||
for (int i = 0; i < N; i++) { bars[i] = gbm.Next(isNew: true); }
|
||||
|
||||
// Streaming
|
||||
var ind = new Midprice(period);
|
||||
for (int i = 0; i < N; i++) { ind.Update(bars[i], isNew: true); }
|
||||
double streamVal = ind.Last.Value;
|
||||
|
||||
// Batch span
|
||||
double[] h = new double[N], l = new double[N];
|
||||
for (int i = 0; i < N; i++) { h[i] = bars[i].High; l[i] = bars[i].Low; }
|
||||
var qlOut = new double[N];
|
||||
Midprice.Batch(h.AsSpan(), l.AsSpan(), qlOut.AsSpan(), period);
|
||||
|
||||
_output.WriteLine($"Streaming={streamVal:F10}, Batch={qlOut[N - 1]:F10}");
|
||||
Assert.Equal(streamVal, qlOut[N - 1], 1e-12);
|
||||
}
|
||||
|
||||
// ── C) Formula verification: (HH5 + LL5) / 2 ─────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_Formula_Manual()
|
||||
{
|
||||
// Prices for 5 bars: H=[10,12,15,11,13], L=[8,9,10,7,9]
|
||||
// Highest H over 5 = 15, Lowest L over 5 = 7 → midprice = (15+7)/2 = 11
|
||||
const int period = 5;
|
||||
double[] highs = [10.0, 12.0, 15.0, 11.0, 13.0];
|
||||
double[] lows = [8.0, 9.0, 10.0, 7.0, 9.0];
|
||||
|
||||
var output = new double[5];
|
||||
Midprice.Batch(highs.AsSpan(), lows.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
double expected = (15.0 + 7.0) / 2.0;
|
||||
Assert.Equal(expected, output[4], 1e-12);
|
||||
_output.WriteLine($"MIDPRICE formula: expected={expected}, actual={output[4]}: PASSED");
|
||||
}
|
||||
|
||||
// ── D) Batch(TBarSeries) == Calculate ─────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_BatchBarSeries_Equals_Calculate()
|
||||
{
|
||||
const int period = 14;
|
||||
var (results, _) = Midprice.Calculate(_data.Bars, period);
|
||||
var batchResult = Midprice.Batch(_data.Bars, period);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], results.Values[i], 1e-12);
|
||||
}
|
||||
_output.WriteLine("MIDPRICE Batch(TBarSeries) == Calculate: PASSED");
|
||||
}
|
||||
|
||||
// ── E) Determinism ────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_Deterministic()
|
||||
{
|
||||
const int period = 14;
|
||||
var r1 = Midprice.Batch(_data.Bars, period);
|
||||
var r2 = Midprice.Batch(_data.Bars, period);
|
||||
for (int i = 0; i < r1.Count; i++) { Assert.Equal(r1.Values[i], r2.Values[i], 15); }
|
||||
_output.WriteLine("MIDPRICE determinism: PASSED");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using TALib;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation for Typprice (Typical Price) = (H+L+C)/3.
|
||||
/// Cross-validated against TA-Lib TYPPRICE (exact match expected).
|
||||
/// Skender, Tulip, and Ooples do not implement TYPPRICE as a standalone function.
|
||||
/// </summary>
|
||||
public sealed class TyppriceValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data = new();
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public TyppriceValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_data.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── A) Cross-validate with TA-Lib TYPPRICE ────────────────────────────────
|
||||
[Fact]
|
||||
public void TALib_TypPrice_Batch_Validates()
|
||||
{
|
||||
double[] high = _data.HighPrices.ToArray();
|
||||
double[] low = _data.LowPrices.ToArray();
|
||||
double[] close = _data.ClosePrices.ToArray();
|
||||
|
||||
// TA-Lib TypPrice
|
||||
var taOut = new double[high.Length];
|
||||
var retCode = Functions.TypPrice(high.AsSpan(), low.AsSpan(), close.AsSpan(),
|
||||
0..^0, taOut, out var outRange);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
|
||||
|
||||
// QuanTAlib batch span
|
||||
var qlOut = new double[high.Length];
|
||||
Typprice.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), qlOut.AsSpan());
|
||||
|
||||
int mismatches = 0;
|
||||
for (int j = 0; j < length; j++)
|
||||
{
|
||||
int qi = j + offset;
|
||||
double err = Math.Abs(qlOut[qi] - taOut[j]);
|
||||
if (err > ValidationHelper.TalibTolerance) { mismatches++; }
|
||||
}
|
||||
|
||||
double mismatchRate = (double)mismatches / length;
|
||||
_output.WriteLine($"TALib TYPPRICE: {length} compared, {mismatches} mismatches ({mismatchRate:P2})");
|
||||
Assert.Equal(0, mismatches);
|
||||
}
|
||||
|
||||
// ── B) Streaming == Batch span ────────────────────────────────────────────
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch()
|
||||
{
|
||||
const int N = 200;
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 1002);
|
||||
var bars = new TBar[N];
|
||||
for (int i = 0; i < N; i++) { bars[i] = gbm.Next(isNew: true); }
|
||||
|
||||
// Streaming
|
||||
var ind = new Typprice();
|
||||
for (int i = 0; i < N; i++) { ind.Update(bars[i], isNew: true); }
|
||||
double streamVal = ind.Last.Value;
|
||||
|
||||
// Batch span
|
||||
double[] h = new double[N], l = new double[N], c = new double[N];
|
||||
for (int i = 0; i < N; i++) { h[i] = bars[i].High; l[i] = bars[i].Low; c[i] = bars[i].Close; }
|
||||
var qlOut = new double[N];
|
||||
Typprice.Batch(h.AsSpan(), l.AsSpan(), c.AsSpan(), qlOut.AsSpan());
|
||||
|
||||
_output.WriteLine($"Streaming={streamVal:F10}, Batch={qlOut[N - 1]:F10}");
|
||||
Assert.Equal(streamVal, qlOut[N - 1], 1e-12);
|
||||
}
|
||||
|
||||
// ── C) Formula verification: (H+L+C)/3 ───────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_Formula_Manual()
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow, open: 10.0, high: 18.0, low: 6.0, close: 15.0, volume: 1000);
|
||||
var ind = new Typprice();
|
||||
var result = ind.Update(bar, isNew: true);
|
||||
double expected = (18.0 + 6.0 + 15.0) / 3.0; // = 13.0
|
||||
Assert.Equal(expected, result.Value, 1e-12);
|
||||
_output.WriteLine($"TYPPRICE formula: expected={expected}, actual={result.Value}: PASSED");
|
||||
}
|
||||
|
||||
// ── D) Batch(TBarSeries) == Calculate ─────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_BatchBarSeries_Equals_Calculate()
|
||||
{
|
||||
var (results, _) = Typprice.Calculate(_data.Bars);
|
||||
var batchResult = Typprice.Batch(_data.Bars);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], results.Values[i], 1e-12);
|
||||
}
|
||||
_output.WriteLine("TYPPRICE Batch(TBarSeries) == Calculate: PASSED");
|
||||
}
|
||||
|
||||
// ── E) Determinism ────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_Deterministic()
|
||||
{
|
||||
var r1 = Typprice.Batch(_data.Bars);
|
||||
var r2 = Typprice.Batch(_data.Bars);
|
||||
for (int i = 0; i < r1.Count; i++) { Assert.Equal(r1.Values[i], r2.Values[i], 15); }
|
||||
_output.WriteLine("TYPPRICE determinism: PASSED");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using TALib;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation for Wclprice (Weighted Close Price) = (H+L+2*C)/4.
|
||||
/// Cross-validated against TA-Lib WCLPRICE (exact match expected).
|
||||
/// Skender, Tulip, and Ooples do not implement WCLPRICE as a standalone function.
|
||||
/// </summary>
|
||||
public sealed class WclpriceValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data = new();
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public WclpriceValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_data.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── A) Cross-validate with TA-Lib WCLPRICE ────────────────────────────────
|
||||
[Fact]
|
||||
public void TALib_WclPrice_Batch_Validates()
|
||||
{
|
||||
double[] high = _data.HighPrices.ToArray();
|
||||
double[] low = _data.LowPrices.ToArray();
|
||||
double[] close = _data.ClosePrices.ToArray();
|
||||
|
||||
// TA-Lib WclPrice
|
||||
var taOut = new double[high.Length];
|
||||
var retCode = Functions.WclPrice(high.AsSpan(), low.AsSpan(), close.AsSpan(),
|
||||
0..^0, taOut, out var outRange);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
|
||||
|
||||
// QuanTAlib batch span
|
||||
var qlOut = new double[high.Length];
|
||||
Wclprice.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), qlOut.AsSpan());
|
||||
|
||||
int mismatches = 0;
|
||||
for (int j = 0; j < length; j++)
|
||||
{
|
||||
int qi = j + offset;
|
||||
double err = Math.Abs(qlOut[qi] - taOut[j]);
|
||||
if (err > ValidationHelper.TalibTolerance) { mismatches++; }
|
||||
}
|
||||
|
||||
double mismatchRate = (double)mismatches / length;
|
||||
_output.WriteLine($"TALib WCLPRICE: {length} compared, {mismatches} mismatches ({mismatchRate:P2})");
|
||||
Assert.Equal(0, mismatches);
|
||||
}
|
||||
|
||||
// ── B) Streaming == Batch span ────────────────────────────────────────────
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch()
|
||||
{
|
||||
const int N = 200;
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 1003);
|
||||
var bars = new TBar[N];
|
||||
for (int i = 0; i < N; i++) { bars[i] = gbm.Next(isNew: true); }
|
||||
|
||||
// Streaming
|
||||
var ind = new Wclprice();
|
||||
for (int i = 0; i < N; i++) { ind.Update(bars[i], isNew: true); }
|
||||
double streamVal = ind.Last.Value;
|
||||
|
||||
// Batch span
|
||||
double[] h = new double[N], l = new double[N], c = new double[N];
|
||||
for (int i = 0; i < N; i++) { h[i] = bars[i].High; l[i] = bars[i].Low; c[i] = bars[i].Close; }
|
||||
var qlOut = new double[N];
|
||||
Wclprice.Batch(h.AsSpan(), l.AsSpan(), c.AsSpan(), qlOut.AsSpan());
|
||||
|
||||
_output.WriteLine($"Streaming={streamVal:F10}, Batch={qlOut[N - 1]:F10}");
|
||||
Assert.Equal(streamVal, qlOut[N - 1], 1e-12);
|
||||
}
|
||||
|
||||
// ── C) Formula verification: (H+L+2*C)/4 ─────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_Formula_Manual()
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow, open: 10.0, high: 20.0, low: 8.0, close: 16.0, volume: 1000);
|
||||
var ind = new Wclprice();
|
||||
var result = ind.Update(bar, isNew: true);
|
||||
double expected = (20.0 + 8.0 + 2.0 * 16.0) / 4.0; // = 15.0
|
||||
Assert.Equal(expected, result.Value, 1e-12);
|
||||
_output.WriteLine($"WCLPRICE formula: expected={expected}, actual={result.Value}: PASSED");
|
||||
}
|
||||
|
||||
// ── D) Batch(TBarSeries) == Calculate ─────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_BatchBarSeries_Equals_Calculate()
|
||||
{
|
||||
var (results, _) = Wclprice.Calculate(_data.Bars);
|
||||
var batchResult = Wclprice.Batch(_data.Bars);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], results.Values[i], 1e-12);
|
||||
}
|
||||
_output.WriteLine("WCLPRICE Batch(TBarSeries) == Calculate: PASSED");
|
||||
}
|
||||
|
||||
// ── E) Determinism ────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Validate_Deterministic()
|
||||
{
|
||||
var r1 = Wclprice.Batch(_data.Bars);
|
||||
var r2 = Wclprice.Batch(_data.Bars);
|
||||
for (int i = 0; i < r1.Count; i++) { Assert.Equal(r1.Values[i], r2.Values[i], 15); }
|
||||
_output.WriteLine("WCLPRICE determinism: PASSED");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user