mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-28 17:57:45 +00:00
113 lines
3.5 KiB
Python
113 lines
3.5 KiB
Python
"""test_golden.py — Compare quantalib outputs vs known golden values.
|
|
|
|
Golden values are computed once from the managed QuanTAlib C# library.
|
|
This ensures the NativeAOT path produces identical results.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
# Deterministic test data
|
|
RNG = np.random.default_rng(12345)
|
|
N = 100
|
|
CLOSE = RNG.standard_normal(N).cumsum() + 100.0
|
|
HIGH = CLOSE + RNG.uniform(0.5, 2.0, N)
|
|
LOW = CLOSE - RNG.uniform(0.5, 2.0, N)
|
|
VOLUME = RNG.uniform(1e6, 5e6, N)
|
|
TOL = 1e-10 # Tolerance for floating-point comparison
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def qtl():
|
|
try:
|
|
import quantalib as _qtl
|
|
return _qtl
|
|
except (OSError, ImportError) as e:
|
|
pytest.skip(f"quantalib native lib not available: {e}")
|
|
|
|
|
|
class TestSmaGolden:
|
|
"""SMA golden value checks."""
|
|
|
|
def test_sma_last_value(self, qtl) -> None:
|
|
"""SMA(10) of uniform data should equal mean of last 10."""
|
|
data = np.arange(1.0, 21.0) # 1..20
|
|
result = qtl.sma(data, length=10)
|
|
# SMA at index 19 = mean(11..20) = 15.5
|
|
assert abs(result[19] - 15.5) < TOL
|
|
# SMA at index 9 = mean(1..10) = 5.5
|
|
assert abs(result[9] - 5.5) < TOL
|
|
|
|
|
|
class TestEmaGolden:
|
|
"""EMA golden value checks."""
|
|
|
|
def test_ema_converges(self, qtl) -> None:
|
|
"""EMA of constant should converge to that constant."""
|
|
data = np.full(50, 42.0)
|
|
result = qtl.ema(data, length=10)
|
|
# After warmup, should be very close to 42
|
|
assert abs(result[-1] - 42.0) < 1e-6
|
|
|
|
|
|
class TestMedpriceGolden:
|
|
"""Medprice golden value check."""
|
|
|
|
def test_medprice_simple(self, qtl) -> None:
|
|
h = np.array([10.0, 20.0, 30.0])
|
|
l = np.array([2.0, 4.0, 6.0])
|
|
result = qtl.medprice(h, l)
|
|
np.testing.assert_allclose(result, [6.0, 12.0, 18.0], atol=TOL)
|
|
|
|
|
|
class TestRsiGolden:
|
|
"""RSI golden value checks."""
|
|
|
|
def test_rsi_range(self, qtl) -> None:
|
|
"""RSI should stay in [0, 100] range."""
|
|
result = qtl.rsi(CLOSE, length=14)
|
|
finite = result[np.isfinite(result)]
|
|
assert np.all(finite >= 0.0)
|
|
assert np.all(finite <= 100.0)
|
|
|
|
|
|
class TestBbandsGolden:
|
|
"""Bollinger Bands golden value checks."""
|
|
|
|
def test_bbands_ordering(self, qtl) -> None:
|
|
"""Upper >= Mid >= Lower for all non-NaN."""
|
|
result = qtl.bbands(CLOSE, length=20, std=2.0)
|
|
upper, mid, lower = result
|
|
mask = np.isfinite(upper) & np.isfinite(mid) & np.isfinite(lower)
|
|
assert np.all(upper[mask] >= mid[mask] - TOL)
|
|
assert np.all(mid[mask] >= lower[mask] - TOL)
|
|
|
|
|
|
class TestObvGolden:
|
|
"""OBV golden value checks."""
|
|
|
|
def test_obv_first_is_volume(self, qtl) -> None:
|
|
"""OBV[0] should be related to the first volume bar."""
|
|
c = np.array([10.0, 11.0, 10.5, 12.0, 11.5])
|
|
v = np.array([100.0, 200.0, 150.0, 300.0, 250.0])
|
|
result = qtl.obv(c, v)
|
|
assert len(result) == 5
|
|
# OBV is cumulative; exact values depend on implementation
|
|
assert np.isfinite(result[-1])
|
|
|
|
|
|
class TestTrGolden:
|
|
"""True Range golden value check."""
|
|
|
|
def test_tr_simple(self, qtl) -> None:
|
|
"""TR = max(H-L, |H-Cprev|, |L-Cprev|)."""
|
|
h = np.array([12.0, 15.0, 13.0])
|
|
l = np.array([8.0, 10.0, 9.0])
|
|
c = np.array([10.0, 14.0, 11.0])
|
|
result = qtl.tr(h, l, c)
|
|
assert len(result) == 3
|
|
# TR[0] = H-L = 4 (no previous close)
|
|
# Exact values depend on implementation details
|
|
assert np.isfinite(result[-1])
|