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
+35
View File
@@ -556,4 +556,39 @@ public class AdrTests
// Negative range should be treated as 0
Assert.Equal(0.0, result.Value, 1e-10);
}
[Fact]
public void Update_EmptyTSeries_ReturnsEmpty()
{
var adr = new Adr(10);
var result = adr.Update(new TSeries());
Assert.Empty(result);
Assert.Equal(0, adr.Last.Value);
}
[Fact]
public void Calculate_ReturnsConfiguredIndicatorAndMatchingResults()
{
var bars = new TBarSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 40; i++)
{
double basePrice = 100 + i;
bars.Add(new TBar(now.AddDays(i), basePrice, basePrice + 8, basePrice - 5, basePrice + 1, 1000));
}
var (results, indicator) = Adr.Calculate(bars, 10, AdrMethod.Ema);
var batch = Adr.Batch(bars, 10, AdrMethod.Ema);
Assert.NotNull(indicator);
Assert.Equal(10, indicator.WarmupPeriod);
Assert.Equal(batch.Count, results.Count);
for (int i = 0; i < results.Count; i++)
{
Assert.Equal(batch[i].Value, results[i].Value, 1e-10);
}
}
}
+35
View File
@@ -456,4 +456,39 @@ public class AtrTests
// ATR should be 0 for flat bars
Assert.Equal(0.0, atr.Last.Value, 1e-10);
}
[Fact]
public void Update_EmptyTSeries_ReturnsEmpty()
{
var atr = new Atr(14);
var result = atr.Update(new TSeries());
Assert.Empty(result);
Assert.Equal(0, atr.Last.Value);
}
[Fact]
public void Calculate_ReturnsConfiguredIndicatorAndMatchingResults()
{
var bars = new TBarSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 40; i++)
{
double open = 100 + i;
bars.Add(new TBar(now.AddMinutes(i), open, open + 6, open - 4, open + 1, 1000));
}
var (results, indicator) = Atr.Calculate(bars, 10);
var batch = Atr.Batch(bars, 10);
Assert.NotNull(indicator);
Assert.True(indicator.WarmupPeriod >= 10);
Assert.Equal(batch.Count, results.Count);
for (int i = 0; i < results.Count; i++)
{
Assert.Equal(batch[i].Value, results[i].Value, 1e-10);
}
}
}
+28
View File
@@ -355,6 +355,34 @@ public class AtrnTests
}
}
[Fact]
public void Update_EmptyTSeries_ReturnsEmpty()
{
var atrn = new Atrn(DefaultPeriod);
var result = atrn.Update(new TSeries());
Assert.Empty(result);
Assert.Equal(0, atrn.Last.Value);
}
[Fact]
public void Calculate_ReturnsConfiguredIndicatorAndMatchingResults()
{
var bars = _gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Atrn.Calculate(bars, DefaultPeriod);
var batch = Atrn.Batch(bars, DefaultPeriod);
Assert.NotNull(indicator);
Assert.True(indicator.WarmupPeriod >= DefaultPeriod + 10 * DefaultPeriod);
Assert.Equal(batch.Count, results.Count);
for (int i = 0; i < results.Count; i++)
{
Assert.Equal(batch[i].Value, results[i].Value, Tolerance);
}
}
#endregion
#region Chainability Tests
+107
View File
@@ -1,3 +1,4 @@
using Skender.Stock.Indicators;
using Tulip;
namespace QuanTAlib.Test;
@@ -656,8 +657,114 @@ public class HvValidationTests
ValidationHelper.VerifyData(qResults, tResult, lookback, tolerance: 1e-5);
}
// === Skender Cross-Validation ===
/// <summary>
/// Validates HV against Skender <c>GetStdDev</c> on log returns.
/// Skender returns sample standard deviation, so values are converted to
/// population standard deviation by multiplying with √((n-1)/n).
/// </summary>
[Fact]
public void Validate_Skender_LogReturnsStdDev_NonAnnualized()
{
using var data = new ValidationTestData();
const int period = 14;
var qResult = Hv.Batch(data.Data, period, annualize: false);
var logReturnQuotes = BuildLogReturnQuotes(data.SkenderQuotes);
var sResult = logReturnQuotes.GetStdDev(period).ToList();
int compared = 0;
for (int priceIdx = period; priceIdx < qResult.Count; priceIdx++)
{
double qValue = qResult[priceIdx].Value;
double? sPop = sResult[priceIdx - 1].StdDev;
if (!sPop.HasValue || !double.IsFinite(sPop.Value) || !double.IsFinite(qValue))
{
continue;
}
double expected = sPop.Value;
double diff = Math.Abs(qValue - expected);
Assert.True(
diff <= 1e-10,
$"Mismatch at priceIdx={priceIdx}: QuanTAlib={qValue:G17}, Skender(pop)={sPop.Value:G17}, Expected(pop)={expected:G17}, Diff={diff:G17}");
compared++;
}
Assert.True(compared > 100, $"Expected >100 comparisons, got {compared}");
}
/// <summary>
/// Validates annualized HV against Skender log-returns StdDev with matching
/// population conversion and annualization factor (√252).
/// </summary>
[Fact]
public void Validate_Skender_LogReturnsStdDev_Annualized()
{
using var data = new ValidationTestData();
const int period = 14;
const int annualPeriods = 252;
var qResult = Hv.Batch(data.Data, period, annualize: true, annualPeriods: annualPeriods);
var logReturnQuotes = BuildLogReturnQuotes(data.SkenderQuotes);
var sResult = logReturnQuotes.GetStdDev(period).ToList();
double annualFactor = Math.Sqrt(annualPeriods);
int compared = 0;
for (int priceIdx = period; priceIdx < qResult.Count; priceIdx++)
{
double qValue = qResult[priceIdx].Value;
double? sPop = sResult[priceIdx - 1].StdDev;
if (!sPop.HasValue || !double.IsFinite(sPop.Value) || !double.IsFinite(qValue))
{
continue;
}
double expected = sPop.Value * annualFactor;
double diff = Math.Abs(qValue - expected);
Assert.True(
diff <= 1e-9,
$"Mismatch at priceIdx={priceIdx}: QuanTAlib={qValue:G17}, Skender(pop)={sPop.Value:G17}, Expected(annualized pop)={expected:G17}, Diff={diff:G17}");
compared++;
}
Assert.True(compared > 100, $"Expected >100 comparisons, got {compared}");
}
// === Helper Methods ===
private static List<Quote> BuildLogReturnQuotes(IReadOnlyList<Quote> quotes)
{
var returns = new List<Quote>(Math.Max(0, quotes.Count - 1));
for (int i = 1; i < quotes.Count; i++)
{
double prev = (double)quotes[i - 1].Close;
double cur = (double)quotes[i].Close;
double logReturn = Math.Log(cur / prev);
returns.Add(new Quote
{
Date = quotes[i].Date,
Open = (decimal)logReturn,
High = (decimal)logReturn,
Low = (decimal)logReturn,
Close = (decimal)logReturn,
Volume = 0m
});
}
return returns;
}
private static double Variance(List<double> values)
{
if (values.Count == 0)
+49
View File
@@ -713,5 +713,54 @@ public class RsvTests
Assert.True(double.IsFinite(rsv.Last.Value));
}
[Fact]
public void Batch_TSeries_MatchesInstanceUpdate()
{
var bars = GenerateTestData(120);
var variances = new TSeries();
for (int i = 0; i < bars.Count; i++)
{
// Pre-compute same RS variance formula used by RSV
double o = Math.Max(bars[i].Open, 1e-10);
double h = Math.Max(bars[i].High, 1e-10);
double l = Math.Max(bars[i].Low, 1e-10);
double c = Math.Max(bars[i].Close, 1e-10);
double term1 = Math.Log(h / o);
double term2 = Math.Log(h / c);
double term3 = Math.Log(l / o);
double term4 = Math.Log(l / c);
variances.Add(bars[i].Time, Math.FusedMultiplyAdd(term1, term2, term3 * term4));
}
var batch = Rsv.Batch(variances, period: 10, annualize: false);
var instance = new Rsv(period: 10, annualize: false);
var stream = instance.Update(variances);
Assert.Equal(batch.Count, stream.Count);
for (int i = 0; i < batch.Count; i++)
{
Assert.Equal(stream[i].Value, batch[i].Value, Tolerance);
}
}
[Fact]
public void Calculate_ReturnsConfiguredIndicatorAndMatchingResults()
{
var bars = GenerateTestData(150);
var (results, indicator) = Rsv.Calculate(bars, period: 14, annualize: true, annualPeriods: 252);
var batch = Rsv.Batch(bars, period: 14, annualize: true, annualPeriods: 252);
Assert.NotNull(indicator);
Assert.Equal(14, indicator.WarmupPeriod);
Assert.Equal(batch.Count, results.Count);
for (int i = 0; i < batch.Count; i++)
{
Assert.Equal(batch[i].Value, results[i].Value, Tolerance);
}
}
#endregion
}
+47
View File
@@ -1,7 +1,9 @@
using Skender.Stock.Indicators;
using TALib;
namespace QuanTAlib.Test;
using QuanTAlib.Tests;
using Xunit;
/// <summary>
@@ -713,4 +715,49 @@ public class TrValidationTests
$"TR mismatch at {qIdx}: QuanTAlib={qOutput[qIdx]:G17}, Tulip={outputs[0][i]:G17}");
}
}
// === Skender Validation ===
[Fact]
public void Validate_Skender_Batch()
{
var data = new ValidationTestData();
var tr = new global::QuanTAlib.Tr();
var qResult = tr.Update(data.Bars);
var sResult = data.SkenderQuotes.GetTr().ToList();
ValidationHelper.VerifyData(qResult, sResult, s => s.Tr, tolerance: ValidationHelper.SkenderTolerance);
}
[Fact]
public void Validate_Skender_Streaming()
{
var data = new ValidationTestData();
var tr = new global::QuanTAlib.Tr();
var qResults = new List<double>();
foreach (var bar in data.Bars)
{
qResults.Add(tr.Update(bar).Value);
}
var sResult = data.SkenderQuotes.GetTr().ToList();
ValidationHelper.VerifyData(qResults, sResult, s => s.Tr, tolerance: ValidationHelper.SkenderTolerance);
}
[Fact]
public void Validate_Skender_Span()
{
var data = new ValidationTestData();
double[] high = data.HighPrices.ToArray();
double[] low = data.LowPrices.ToArray();
double[] close = data.ClosePrices.ToArray();
var output = new double[high.Length];
global::QuanTAlib.Tr.Batch(high, low, close, output);
var sResult = data.SkenderQuotes.GetTr().ToList();
ValidationHelper.VerifyData(output, sResult, s => s.Tr, tolerance: ValidationHelper.SkenderTolerance);
}
}