python wrapper

This commit is contained in:
Miha Kralj
2026-02-28 14:14:35 -08:00
parent 82e0248eb0
commit 83e9511261
521 changed files with 62395 additions and 15669 deletions
+69
View File
@@ -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);
}
}
}
+87
View File
@@ -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;
}
}
+69
View File
@@ -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);
}
}
}
+1 -1
View File
@@ -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}");
}
}