mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +00:00
f10baa6dfb
- Add docs/license.md with Apache 2.0 rationale and patent protection analysis - Add docs/python.md and docs/pinescript.md platform guides - Expand README license section with disclosure and link to rationale - Update docs/api.md and docs/architecture.md - Update Python bindings: helpers, all indicator modules, pyproject.toml - Add Python tests for Arrow and Polars integration - Update TValue core type and documentation - Add fix_length_to_period tooling script
345 lines
12 KiB
Python
345 lines
12 KiB
Python
"""Shared wrapper helpers for quantalib indicator modules.
|
|
|
|
Auto-generated by generate_category_modules.py — DO NOT EDIT.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
from numpy.typing import NDArray
|
|
|
|
from ._bridge import _lib, _check, _dp, _ci, _cd
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Optional dataframe library imports (all optional; numpy is the only hard dep)
|
|
# ---------------------------------------------------------------------------
|
|
try:
|
|
import pandas as pd # type: ignore[import-untyped]
|
|
except ImportError: # pragma: no cover
|
|
pd = None # type: ignore[assignment]
|
|
|
|
try:
|
|
import polars as pl # type: ignore[import-untyped]
|
|
except ImportError: # pragma: no cover
|
|
pl = None # type: ignore[assignment]
|
|
|
|
try:
|
|
import pyarrow as pa # type: ignore[import-untyped]
|
|
except ImportError: # pragma: no cover
|
|
pa = None # type: ignore[assignment]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Origin token — opaque marker passed from _arr() → _wrap()/_wrap_multi()
|
|
#
|
|
# Callers in category modules simply do:
|
|
# src, origin = _arr(close)
|
|
# ...
|
|
# return _wrap(dst, origin, name, category, offset)
|
|
#
|
|
# The token carries enough info to reconstruct the original container type.
|
|
# ---------------------------------------------------------------------------
|
|
_ORIGIN_NUMPY = "numpy"
|
|
_ORIGIN_PANDAS = "pandas"
|
|
_ORIGIN_POLARS = "polars"
|
|
_ORIGIN_PYARROW = "pyarrow"
|
|
|
|
|
|
class _Origin:
|
|
"""Lightweight tag recording the input container type + metadata."""
|
|
|
|
__slots__ = ("kind", "meta")
|
|
|
|
def __init__(self, kind: str, meta: object = None) -> None:
|
|
self.kind = kind
|
|
self.meta = meta # pandas.Index | polars series name | None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal helpers
|
|
# ---------------------------------------------------------------------------
|
|
_F64 = np.float64
|
|
|
|
|
|
def _arr(x: object) -> tuple[NDArray[np.float64], _Origin | None]:
|
|
"""Return *(contiguous float64 array, origin_token)*.
|
|
|
|
Supported input types
|
|
---------------------
|
|
* ``numpy.ndarray``
|
|
* ``pandas.Series`` / ``pandas.DataFrame`` (first column)
|
|
* ``polars.Series`` / ``polars.DataFrame`` (first column)
|
|
* ``pyarrow.Array`` / ``pyarrow.ChunkedArray``
|
|
* Any sequence coercible via ``np.asarray``
|
|
"""
|
|
if x is None:
|
|
raise ValueError("Input array must not be None")
|
|
|
|
origin: _Origin | None = None
|
|
|
|
# ── pandas ──────────────────────────────────────────────────────────
|
|
if pd is not None and isinstance(x, (pd.Series, pd.DataFrame)):
|
|
if isinstance(x, pd.DataFrame):
|
|
origin = _Origin(_ORIGIN_PANDAS, x.index)
|
|
x = x.iloc[:, 0].to_numpy(dtype=_F64, copy=False)
|
|
else:
|
|
origin = _Origin(_ORIGIN_PANDAS, x.index)
|
|
x = x.to_numpy(dtype=_F64, copy=False)
|
|
|
|
# ── polars ──────────────────────────────────────────────────────────
|
|
elif pl is not None and isinstance(x, (pl.Series, pl.DataFrame)):
|
|
if isinstance(x, pl.DataFrame):
|
|
col = x.get_column(x.columns[0])
|
|
origin = _Origin(_ORIGIN_POLARS, col.name)
|
|
x = col.cast(pl.Float64).to_numpy(allow_copy=True)
|
|
else:
|
|
origin = _Origin(_ORIGIN_POLARS, x.name)
|
|
x = x.cast(pl.Float64).to_numpy(allow_copy=True)
|
|
|
|
# ── pyarrow ─────────────────────────────────────────────────────────
|
|
elif pa is not None and isinstance(x, (pa.Array, pa.ChunkedArray)):
|
|
origin = _Origin(_ORIGIN_PYARROW)
|
|
x = x.to_numpy(zero_copy_only=False).astype(_F64, copy=False)
|
|
|
|
# ── coerce to numpy ────────────────────────────────────────────────
|
|
arr = np.asarray(x, dtype=_F64)
|
|
if not arr.flags["C_CONTIGUOUS"]:
|
|
arr = np.ascontiguousarray(arr)
|
|
if arr.ndim == 0 or len(arr) == 0:
|
|
raise ValueError("Input array must not be empty")
|
|
return arr, origin
|
|
|
|
|
|
def _ptr(a: NDArray[np.float64]): # noqa: ANN202
|
|
"""Get ctypes double* from array."""
|
|
return a.ctypes.data_as(_dp)
|
|
|
|
|
|
def _out(n: int) -> NDArray[np.float64]:
|
|
"""Allocate output array."""
|
|
return np.empty(n, dtype=_F64)
|
|
|
|
|
|
def _offset(arr: NDArray[np.float64], off: int) -> NDArray[np.float64]:
|
|
"""Apply offset (roll + NaN fill)."""
|
|
if off != 0:
|
|
arr = np.roll(arr, off)
|
|
if off > 0:
|
|
arr[:off] = np.nan
|
|
else:
|
|
arr[off:] = np.nan
|
|
return arr
|
|
|
|
|
|
def _wrap(
|
|
arr: NDArray[np.float64],
|
|
origin: _Origin | None,
|
|
name: str,
|
|
category: str,
|
|
offset: int = 0,
|
|
):
|
|
"""Wrap result: apply offset, reconstruct the original container type.
|
|
|
|
Return types by origin
|
|
----------------------
|
|
* numpy → ``np.ndarray``
|
|
* pandas → ``pd.Series`` with original index
|
|
* polars → ``pl.Series``
|
|
* pyarrow → ``pa.Array`` (float64)
|
|
"""
|
|
arr = _offset(arr, offset)
|
|
|
|
if origin is None:
|
|
return arr
|
|
|
|
if origin.kind == _ORIGIN_PANDAS and pd is not None:
|
|
s = pd.Series(arr, index=origin.meta, name=name)
|
|
s.attrs["category"] = category
|
|
return s
|
|
|
|
if origin.kind == _ORIGIN_POLARS and pl is not None:
|
|
return pl.Series(name=name, values=arr)
|
|
|
|
if origin.kind == _ORIGIN_PYARROW and pa is not None:
|
|
return pa.array(arr, type=pa.float64())
|
|
|
|
return arr
|
|
|
|
|
|
def _wrap_multi(
|
|
arrays: dict[str, NDArray[np.float64]],
|
|
origin: _Origin | None,
|
|
category: str,
|
|
offset: int = 0,
|
|
):
|
|
"""Wrap multi-output result into the original container type.
|
|
|
|
Return types by origin
|
|
----------------------
|
|
* numpy → ``tuple[np.ndarray, ...]``
|
|
* pandas → ``pd.DataFrame`` with original index
|
|
* polars → ``pl.DataFrame``
|
|
* pyarrow → ``dict[str, pa.Array]``
|
|
"""
|
|
for k in arrays:
|
|
arrays[k] = _offset(arrays[k], offset)
|
|
|
|
if origin is None:
|
|
return tuple(arrays.values())
|
|
|
|
if origin.kind == _ORIGIN_PANDAS and pd is not None:
|
|
df = pd.DataFrame(arrays, index=origin.meta)
|
|
df.attrs["category"] = category
|
|
return df
|
|
|
|
if origin.kind == _ORIGIN_POLARS and pl is not None:
|
|
return pl.DataFrame(
|
|
{k: pl.Series(name=k, values=v) for k, v in arrays.items()}
|
|
)
|
|
|
|
if origin.kind == _ORIGIN_PYARROW and pa is not None:
|
|
return {k: pa.array(v, type=pa.float64()) for k, v in arrays.items()}
|
|
|
|
return tuple(arrays.values())
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Generic pattern helpers
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
def _pa(
|
|
fn_name: str, close: object, period: int, offset: int,
|
|
default_period: int, label: str, category: str,
|
|
) -> object:
|
|
"""Generic Pattern A wrapper: single-input + period."""
|
|
period = int(period) if period is not None else default_period
|
|
offset = int(offset) if offset is not None else 0
|
|
src, idx = _arr(close)
|
|
n = len(src)
|
|
dst = _out(n)
|
|
_check(getattr(_lib, fn_name)(_ptr(src), n, _ptr(dst), period))
|
|
return _wrap(dst, idx, f"{label}_{period}", category, offset)
|
|
|
|
|
|
def _pa3(
|
|
fn_name: str, close: object, offset: int,
|
|
label: str, category: str,
|
|
) -> object:
|
|
"""Generic Pattern A3 wrapper: single-input, no params."""
|
|
offset = int(offset) if offset is not None else 0
|
|
src, idx = _arr(close)
|
|
n = len(src)
|
|
dst = _out(n)
|
|
_check(getattr(_lib, fn_name)(_ptr(src), n, _ptr(dst)))
|
|
return _wrap(dst, idx, label, category, offset)
|
|
|
|
|
|
def _pf(
|
|
fn_name: str, actual: object, predicted: object,
|
|
period: int, offset: int, default_period: int,
|
|
label: str, category: str,
|
|
) -> object:
|
|
"""Generic Pattern F wrapper: actual+predicted+period."""
|
|
period = int(period) if period is not None else default_period
|
|
offset = int(offset) if offset is not None else 0
|
|
a, idx = _arr(actual)
|
|
p, _ = _arr(predicted)
|
|
n = len(a)
|
|
dst = _out(n)
|
|
_check(getattr(_lib, fn_name)(_ptr(a), _ptr(p), n, _ptr(dst), period))
|
|
return _wrap(dst, idx, f"{label}_{period}", category, offset)
|
|
|
|
|
|
def _pg(
|
|
fn_name: str, close: object, volume: object,
|
|
offset: int, label: str, category: str,
|
|
) -> object:
|
|
"""Pattern G: source+volume, no period."""
|
|
offset = int(offset) if offset is not None else 0
|
|
c, idx = _arr(close)
|
|
v, _ = _arr(volume)
|
|
n = len(c)
|
|
dst = _out(n)
|
|
_check(getattr(_lib, fn_name)(_ptr(c), _ptr(v), n, _ptr(dst)))
|
|
return _wrap(dst, idx, label, category, offset)
|
|
|
|
|
|
def _pg2(
|
|
fn_name: str, close: object, volume: object, period: int,
|
|
offset: int, default_period: int, label: str, category: str,
|
|
) -> object:
|
|
"""Pattern G2: source+volume+period."""
|
|
period = int(period) if period is not None else default_period
|
|
offset = int(offset) if offset is not None else 0
|
|
c, idx = _arr(close)
|
|
v, _ = _arr(volume)
|
|
n = len(c)
|
|
dst = _out(n)
|
|
_check(getattr(_lib, fn_name)(_ptr(c), _ptr(v), n, _ptr(dst), period))
|
|
return _wrap(dst, idx, f"{label}_{period}", category, offset)
|
|
|
|
|
|
def _ph(
|
|
fn_name: str, x: object, y: object, period: int,
|
|
offset: int, default_period: int, label: str, category: str,
|
|
) -> object:
|
|
"""Pattern H: X+Y+period."""
|
|
period = int(period) if period is not None else default_period
|
|
offset = int(offset) if offset is not None else 0
|
|
xarr, idx = _arr(x)
|
|
yarr, _ = _arr(y)
|
|
n = len(xarr)
|
|
dst = _out(n)
|
|
_check(getattr(_lib, fn_name)(_ptr(xarr), _ptr(yarr), n, _ptr(dst), period))
|
|
return _wrap(dst, idx, f"{label}_{period}", category, offset)
|
|
|
|
|
|
def _ohlcv_bars_period(
|
|
fn_name: str, open: object, high: object, low: object,
|
|
close: object, volume: object, period: int,
|
|
offset: int, default_period: int, label: str, category: str,
|
|
) -> object:
|
|
"""OHLCV bars + period → single output (BuildBars pattern)."""
|
|
period = int(period) if period is not None else default_period
|
|
offset = int(offset) if offset is not None else 0
|
|
o, idx = _arr(open)
|
|
h, _ = _arr(high)
|
|
l, _ = _arr(low)
|
|
c, _ = _arr(close)
|
|
v, _ = _arr(volume)
|
|
n = len(o)
|
|
dst = _out(n)
|
|
_check(getattr(_lib, fn_name)(
|
|
_ptr(o), _ptr(h), _ptr(l), _ptr(c), _ptr(v), period, n, _ptr(dst)))
|
|
return _wrap(dst, idx, f"{label}_{period}", category, offset)
|
|
|
|
|
|
def _hlc_period(
|
|
fn_name: str, high: object, low: object, close: object,
|
|
period: int, offset: int, default_period: int,
|
|
label: str, category: str,
|
|
) -> object:
|
|
"""HLC + period → single output."""
|
|
period = int(period) if period is not None else default_period
|
|
offset = int(offset) if offset is not None else 0
|
|
h, idx = _arr(high)
|
|
l, _ = _arr(low)
|
|
c, _ = _arr(close)
|
|
n = len(h)
|
|
dst = _out(n)
|
|
_check(getattr(_lib, fn_name)(
|
|
_ptr(h), _ptr(l), _ptr(c), period, n, _ptr(dst)))
|
|
return _wrap(dst, idx, f"{label}_{period}", category, offset)
|
|
|
|
|
|
def _src_period(
|
|
fn_name: str, source: object, period: int,
|
|
offset: int, default_period: int, label: str, category: str,
|
|
) -> object:
|
|
"""source + period → single output (BuildSeries pattern, src,period,n,dst)."""
|
|
period = int(period) if period is not None else default_period
|
|
offset = int(offset) if offset is not None else 0
|
|
src, idx = _arr(source)
|
|
n = len(src)
|
|
dst = _out(n)
|
|
_check(getattr(_lib, fn_name)(_ptr(src), period, n, _ptr(dst)))
|
|
return _wrap(dst, idx, f"{label}_{period}", category, offset)
|