feat: add wickra to Python benchmark and fix correlation export

Added wickra (Rust/PyO3) as a comparison library across all 7 indicators. Fixed the correlation export name mismatch (qtl_correlation -> qtl_correl) and the benchmark's qtl.correlation -> qtl.correl reference so Pearson correlation is benchmarkable.
This commit is contained in:
Miha Kralj
2026-09-27 18:00:15 -07:00
parent cd000c3772
commit c5c93a0350
2 changed files with 52 additions and 2 deletions
+1 -1
View File
@@ -1307,7 +1307,7 @@ public static unsafe partial class Exports
}
// Correlation: Pattern H (x, y, out, int period)
[UnmanagedCallersOnly(EntryPoint = "qtl_correlation")]
[UnmanagedCallersOnly(EntryPoint = "qtl_correl")]
public static int QtlCorrelation(double* x, double* y, int n, double* dst, int period)
{
int v = Chk2(x, y, dst, n); if (v != 0) return v;
+51 -1
View File
@@ -3,6 +3,7 @@
Compares:
- quantalib (NativeAOT via ctypes FFI)
- wickra (Rust core via PyO3 FFI)
- pandas-ta (pure Python / numpy)
- pandas (rolling baseline where applicable)
@@ -46,6 +47,11 @@ try:
except ImportError:
ta = None # type: ignore[assignment]
try:
import wickra as wk # type: ignore[import-untyped]
except ImportError:
wk = None # type: ignore[assignment]
try:
import quantalib as qtl
except (ImportError, OSError) as _err:
@@ -157,6 +163,8 @@ def run_benchmarks(
print(f" | pandas {pd.__version__}", end="")
if ta is not None:
print(f" | pandas-ta {ta.version}", end="")
if wk is not None:
print(f" | wickra {getattr(wk, '__version__', '?')}", end="")
if qtl is not None:
print(f" | quantalib {getattr(qtl, '__version__', '?')}", end="")
print(f"\n{'=' * 76}\n")
@@ -166,6 +174,7 @@ def run_benchmarks(
close_np, close_pd, df_ta,
qtl_fn=lambda: qtl.sma(close_np, period=period) if qtl else None,
pta_fn=lambda: ta.sma(close_pd, length=period) if (ta and close_pd is not None) else None,
wk_fn=lambda: wk.SMA(period).batch(close_np) if wk else None,
pd_fn=lambda: close_pd.rolling(period).mean() if close_pd is not None else None,
pd_label="pandas rolling")
@@ -174,6 +183,7 @@ def run_benchmarks(
close_np, close_pd, df_ta,
qtl_fn=lambda: qtl.ema(close_np, period=period) if qtl else None,
pta_fn=lambda: ta.ema(close_pd, length=period) if (ta and close_pd is not None) else None,
wk_fn=lambda: wk.EMA(period).batch(close_np) if wk else None,
pd_fn=lambda: close_pd.ewm(span=period, adjust=False).mean() if close_pd is not None else None,
pd_label="pandas ewm")
@@ -190,6 +200,7 @@ def run_benchmarks(
close_np, close_pd, df_ta,
qtl_fn=lambda: qtl.wma(close_np, period=period) if qtl else None,
pta_fn=lambda: ta.wma(close_pd, length=period) if (ta and close_pd is not None) else None,
wk_fn=lambda: wk.WMA(period).batch(close_np) if wk else None,
pd_fn=_pd_wma,
pd_label="pandas rolling+apply")
@@ -198,6 +209,7 @@ def run_benchmarks(
close_np, close_pd, df_ta,
qtl_fn=lambda: qtl.hma(close_np, period=period) if qtl else None,
pta_fn=lambda: ta.hma(close_pd, length=period) if (ta and close_pd is not None) else None,
wk_fn=lambda: wk.HMA(period).batch(close_np) if wk else None,
pd_fn=None,
pd_label=None)
@@ -214,10 +226,16 @@ def run_benchmarks(
return ta.adosc(df_ta["high"], df_ta["low"], df_ta["close"],
df_ta["volume"], fast=3, slow=10)
def _wk_adosc():
if wk is None:
return None
return wk.ChaikinOscillator(3, 10).batch(high_np, low_np, close_np, vol_np)
_category("ADOSC", results, bars, period, iterations,
close_np, close_pd, df_ta,
qtl_fn=_qtl_adosc,
pta_fn=_pta_adosc,
wk_fn=_wk_adosc,
pd_fn=None,
pd_label=None)
@@ -225,7 +243,7 @@ def run_benchmarks(
def _qtl_corr():
if qtl is None:
return None
return qtl.correlation(close_np, open_np, period=period)
return qtl.correl(close_np, open_np, period=period)
def _pd_corr():
if close_pd is None or open_pd is None:
@@ -236,6 +254,7 @@ def run_benchmarks(
close_np, close_pd, df_ta,
qtl_fn=_qtl_corr,
pta_fn=None, # pandas-ta has no rolling correlation
wk_fn=lambda: wk.PearsonCorrelation(period).batch(close_np, open_np) if wk else None,
pd_fn=_pd_corr,
pd_label="pandas rolling.corr")
@@ -249,6 +268,7 @@ def run_benchmarks(
close_np, close_pd, df_ta,
qtl_fn=lambda: qtl.skew(close_np, period=period) if qtl else None,
pta_fn=lambda: ta.skew(close_pd, length=period) if (ta and close_pd is not None) else None,
wk_fn=lambda: wk.Skewness(period).batch(close_np) if wk else None,
pd_fn=_pd_skew,
pd_label="pandas rolling.skew")
@@ -266,6 +286,7 @@ def _category(
df_ta,
qtl_fn,
pta_fn,
wk_fn,
pd_fn,
pd_label,
):
@@ -299,6 +320,20 @@ def _category(
else:
print(f" pandas-ta : not installed")
# wickra (Rust core)
if wk is not None and wk_fn is not None:
try:
mean, std = _bench(wk_fn, iterations)
r = BenchResult(name, "wickra (Rust)", mean, std)
results.append(r)
print(f" wickra : {mean:>12,.1f} us +/- {std:>8,.1f} us")
except Exception as e:
print(f" wickra : FAILED -- {e}")
elif wk_fn is None:
print(f" wickra : N/A (no equivalent)")
else:
print(f" wickra : not installed")
# pandas baseline
if pd is not None and pd_fn is not None:
try:
@@ -344,12 +379,15 @@ def print_markdown(results: list[BenchResult], bars: int, period: int):
# Summary comparison table: quantalib vs pandas-ta
qtl_map: dict[str, float] = {}
pta_map: dict[str, float] = {}
wk_map: dict[str, float] = {}
pd_map: dict[str, float] = {}
for r in results:
if "quantalib" in r.library:
qtl_map[r.name] = r.mean_us
elif "pandas-ta" in r.library:
pta_map[r.name] = r.mean_us
elif "wickra" in r.library:
wk_map[r.name] = r.mean_us
elif "pandas" in r.library:
pd_map[r.name] = r.mean_us
@@ -365,6 +403,18 @@ def print_markdown(results: list[BenchResult], bars: int, period: int):
print(f"| {name:<11s} | {q:>13,.1f} | {p:>13,.1f} | {speedup:>6.1f}x |")
print()
if qtl_map and wk_map:
print("### quantalib vs wickra Speedup\n")
print("| Indicator | quantalib (us) | wickra (us) | Speedup |")
print("|-----------|---------------:|------------:|--------:|")
for name in qtl_map:
if name in wk_map:
q = qtl_map[name]
w = wk_map[name]
speedup = w / q if q > 0 else float("inf")
print(f"| {name:<11s} | {q:>13,.1f} | {w:>11,.1f} | {speedup:>6.1f}x |")
print()
if qtl_map and pd_map:
print("### quantalib vs pandas Speedup\n")
print("| Indicator | quantalib (us) | pandas (us) | Speedup |")