mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 12:08:05 +00:00
python wrapper
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user