mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 04:28:04 +00:00
python wrapper
This commit is contained in:
@@ -402,4 +402,73 @@ public class LogCoshTests
|
||||
Assert.True(logCosh.Last.Value >= 0, $"LogCosh should be non-negative, got {logCosh.Last.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_EmptyInput_ReturnsWithoutChanges()
|
||||
{
|
||||
double[] actual = [];
|
||||
double[] predicted = [];
|
||||
double[] output = [];
|
||||
|
||||
LogCosh.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 3);
|
||||
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_LargeInput_MatchesIterative()
|
||||
{
|
||||
const int period = 10;
|
||||
const int count = 300; // exceeds stack-alloc threshold branch
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 7);
|
||||
var iterative = new LogCosh(period);
|
||||
|
||||
double[] actual = new double[count];
|
||||
double[] predicted = new double[count];
|
||||
double[] output = new double[count];
|
||||
double[] expected = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
actual[i] = bar.Close;
|
||||
predicted[i] = bar.Close * (1 + (i % 2 == 0 ? 0.015 : -0.012));
|
||||
expected[i] = iterative.Update(actual[i], predicted[i]).Value;
|
||||
}
|
||||
|
||||
LogCosh.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(expected[i], output[i], Precision);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsConfiguredIndicatorAndMatchingResults()
|
||||
{
|
||||
const int period = 7;
|
||||
var actual = new TSeries();
|
||||
var predicted = new TSeries();
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks + i;
|
||||
double value = 100 + i;
|
||||
actual.Add(time, value);
|
||||
predicted.Add(time, value * 0.99);
|
||||
}
|
||||
|
||||
var (results, indicator) = LogCosh.Calculate(actual, predicted, period);
|
||||
var batch = LogCosh.Batch(actual, predicted, period);
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
Assert.Equal(period, indicator.WarmupPeriod);
|
||||
Assert.Equal(batch.Count, results.Count);
|
||||
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
Assert.Equal(batch[i].Value, results[i].Value, Precision);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
namespace QuanTAlib.Validation;
|
||||
|
||||
public sealed class MapeValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data = new();
|
||||
|
||||
public void Dispose() => _data.Dispose();
|
||||
|
||||
[Fact]
|
||||
public void Mape_Matches_MathNetStyle_Computation()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
var quotes = _data.SkenderQuotes.ToList();
|
||||
double[] actual = quotes.Select(q => (double)q.Close).ToArray();
|
||||
double[] predicted = quotes.Select(q => (double)q.Open).ToArray();
|
||||
|
||||
foreach (int period in periods)
|
||||
{
|
||||
var mape = new Mape(period);
|
||||
|
||||
for (int i = 0; i < actual.Length; i++)
|
||||
{
|
||||
var val = mape.Update(
|
||||
new TValue(quotes[i].Date, actual[i]),
|
||||
new TValue(quotes[i].Date, predicted[i]));
|
||||
|
||||
// Validate last 100 bars
|
||||
if (i >= actual.Length - 100 && i >= period - 1)
|
||||
{
|
||||
var windowActual = actual[(i - period + 1)..(i + 1)];
|
||||
var windowPredicted = predicted[(i - period + 1)..(i + 1)];
|
||||
|
||||
double expected = ComputeMape(windowActual, windowPredicted);
|
||||
|
||||
Assert.Equal(expected, val.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mape_Batch_Matches_MathNetStyle_Computation()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
var quotes = _data.SkenderQuotes.ToList();
|
||||
double[] actual = quotes.Select(q => (double)q.Close).ToArray();
|
||||
double[] predicted = quotes.Select(q => (double)q.Open).ToArray();
|
||||
|
||||
foreach (int period in periods)
|
||||
{
|
||||
double[] output = new double[actual.Length];
|
||||
Mape.Batch(actual, predicted, output, period);
|
||||
|
||||
// Validate last 100 bars
|
||||
for (int i = actual.Length - 100; i < actual.Length; i++)
|
||||
{
|
||||
if (i >= period - 1)
|
||||
{
|
||||
var windowActual = actual[(i - period + 1)..(i + 1)];
|
||||
var windowPredicted = predicted[(i - period + 1)..(i + 1)];
|
||||
|
||||
double expected = ComputeMape(windowActual, windowPredicted);
|
||||
|
||||
Assert.Equal(expected, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static double ComputeMape(ReadOnlySpan<double> actual, ReadOnlySpan<double> predicted)
|
||||
{
|
||||
const double epsilon = 1e-10;
|
||||
double sum = 0.0;
|
||||
|
||||
for (int i = 0; i < actual.Length; i++)
|
||||
{
|
||||
double divisor = Math.Abs(actual[i]) < epsilon ? epsilon : actual[i];
|
||||
sum += 100.0 * Math.Abs((actual[i] - predicted[i]) / divisor);
|
||||
}
|
||||
|
||||
return sum / actual.Length;
|
||||
}
|
||||
}
|
||||
@@ -389,4 +389,73 @@ public class MeTests
|
||||
// After resync, result should still be correct
|
||||
Assert.Equal(10.0, me.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_EmptyInput_ReturnsWithoutChanges()
|
||||
{
|
||||
double[] actual = [];
|
||||
double[] predicted = [];
|
||||
double[] output = [];
|
||||
|
||||
Me.Batch(actual.AsSpan(), predicted.AsSpan(), output.AsSpan(), 3);
|
||||
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_LargeInput_MatchesStreaming()
|
||||
{
|
||||
const int period = 9;
|
||||
const int count = 300; // exceeds stack-alloc threshold branch
|
||||
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 321);
|
||||
var me = new Me(period);
|
||||
|
||||
double[] actual = new double[count];
|
||||
double[] predicted = new double[count];
|
||||
double[] streaming = new double[count];
|
||||
double[] batch = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next();
|
||||
actual[i] = bar.Close;
|
||||
predicted[i] = bar.Close * (1 + (i % 2 == 0 ? 0.01 : -0.015));
|
||||
streaming[i] = me.Update(actual[i], predicted[i]).Value;
|
||||
}
|
||||
|
||||
Me.Batch(actual.AsSpan(), predicted.AsSpan(), batch.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(streaming[i], batch[i], 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsConfiguredIndicatorAndMatchingResults()
|
||||
{
|
||||
const int period = 6;
|
||||
var actual = new TSeries();
|
||||
var predicted = new TSeries();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
actual.Add(now.AddSeconds(i), 100 + i);
|
||||
predicted.Add(now.AddSeconds(i), 101 + i);
|
||||
}
|
||||
|
||||
var (results, indicator) = Me.Calculate(actual, predicted, period);
|
||||
var batch = Me.Batch(actual, predicted, period);
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
Assert.Equal(period, indicator.WarmupPeriod);
|
||||
Assert.Equal(batch.Count, results.Count);
|
||||
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
Assert.Equal(batch[i].Value, results[i].Value, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@ public sealed class MseValidationTests : IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using QuanTAlib.Tests;
|
||||
using Skender.Stock.Indicators;
|
||||
|
||||
namespace QuanTAlib.Validation;
|
||||
|
||||
@@ -153,4 +154,44 @@ public sealed class RsquaredValidationTests : IDisposable
|
||||
Assert.Throws<ArgumentException>(() => Rsquared.Batch(actual, predicted, output, 0));
|
||||
Assert.Throws<ArgumentException>(() => Rsquared.Batch(actual, predicted, output, -1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structural validation against Skender <c>GetSlope().RSquared</c>.
|
||||
/// Skender R² measures goodness-of-fit of linear regression on price data.
|
||||
/// QuanTAlib Rsquared compares actual vs predicted values (different concept).
|
||||
/// Both must produce finite output bounded ≤ 1.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_Skender_RSquared_Structural()
|
||||
{
|
||||
const int period = 20;
|
||||
|
||||
// Skender R² from linear regression slope
|
||||
var sResult = _data.SkenderQuotes.GetSlope(period).ToList();
|
||||
|
||||
int finiteCount = sResult.Count(r => r.RSquared is not null && double.IsFinite(r.RSquared.Value));
|
||||
Assert.True(finiteCount > 100, $"Skender should produce >100 finite R² values, got {finiteCount}");
|
||||
|
||||
// All Skender R² values should be in [0, 1] for linear regression
|
||||
foreach (var r in sResult.Where(r => r.RSquared is not null))
|
||||
{
|
||||
Assert.True(r.RSquared!.Value >= -0.01 && r.RSquared.Value <= 1.01,
|
||||
$"Skender R² = {r.RSquared.Value} out of expected [0, 1] range");
|
||||
}
|
||||
|
||||
// QuanTAlib R² (using close as actual, EMA as predicted — same as existing test)
|
||||
var rsq = new Rsquared(period);
|
||||
var ema = new Ema(5);
|
||||
var quotes = _data.SkenderQuotes.ToList();
|
||||
|
||||
for (int i = 0; i < quotes.Count; i++)
|
||||
{
|
||||
double actual = (double)quotes[i].Close;
|
||||
double predicted = ema.Update(new TValue(quotes[i].Date, actual)).Value;
|
||||
rsq.Update(actual, predicted);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(rsq.Last.Value), "QuanTAlib R² last must be finite");
|
||||
Assert.True(rsq.Last.Value <= 1.0 + 1e-9, $"QuanTAlib R² should be ≤ 1, got {rsq.Last.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user