quantwave 0.6.1__cp39-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantwave/__init__.py +587 -0
- quantwave/_backtest.pyd +0 -0
- quantwave/_errors.py +46 -0
- quantwave/_metadata.py +372 -0
- quantwave/_metadata_generated.py +231 -0
- quantwave/_quantwave/__init__.py +1 -0
- quantwave/_quantwave/quantwave_python.dll +0 -0
- quantwave/_quantwave/quantwave_python.py +27343 -0
- quantwave/backtest.py +112 -0
- quantwave/backtest_types.py +55 -0
- quantwave/bt_polars.py +623 -0
- quantwave/cli.py +179 -0
- quantwave/features.py +221 -0
- quantwave/options.py +39 -0
- quantwave/pa_flag_strategy.py +16 -0
- quantwave/polars.py +371 -0
- quantwave/results.py +40 -0
- quantwave/talib.py +140 -0
- quantwave/tearsheet.py +24 -0
- quantwave-0.6.1.dist-info/METADATA +185 -0
- quantwave-0.6.1.dist-info/RECORD +38 -0
- quantwave-0.6.1.dist-info/WHEEL +4 -0
- quantwave-0.6.1.dist-info/entry_points.txt +2 -0
- quantwave-0.6.1.dist-info/licenses/LICENSE +21 -0
- quantwave-0.6.1.dist-info/sboms/quantwave-python.cyclonedx.json +6396 -0
- quantwave_plugins/__init__.py +1916 -0
- quantwave_plugins/custom_0.py +101 -0
- quantwave_plugins/custom_1.py +92 -0
- quantwave_plugins/custom_10.py +42 -0
- quantwave_plugins/custom_2.py +63 -0
- quantwave_plugins/custom_3.py +98 -0
- quantwave_plugins/custom_4.py +88 -0
- quantwave_plugins/custom_5.py +104 -0
- quantwave_plugins/custom_6.py +133 -0
- quantwave_plugins/custom_7.py +91 -0
- quantwave_plugins/custom_8.py +91 -0
- quantwave_plugins/custom_9.py +91 -0
- quantwave_plugins/quantwave_plugins.pyd +0 -0
quantwave/__init__.py
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
"""
|
|
2
|
+
quantwave - High-performance technical analysis library (Python bindings).
|
|
3
|
+
|
|
4
|
+
Public surface for 0.5.2 (Python DX improvements from quantwave-p3z9).
|
|
5
|
+
|
|
6
|
+
Recommended modern usage:
|
|
7
|
+
import quantwave as qw
|
|
8
|
+
|
|
9
|
+
# Discovery & introspection
|
|
10
|
+
qw.indicators()
|
|
11
|
+
meta = qw.metadata("supertrend")
|
|
12
|
+
|
|
13
|
+
# Parity verification
|
|
14
|
+
qw.assert_parity("rsi", {"period": 14}, closes)
|
|
15
|
+
|
|
16
|
+
# Streaming with readiness tracking
|
|
17
|
+
cls = qw.streaming_class("rsi")
|
|
18
|
+
inst = qw.wrap_streaming(cls(14))
|
|
19
|
+
|
|
20
|
+
# New recommended namespaces (reduces top-level pollution)
|
|
21
|
+
from quantwave import results, options, talib
|
|
22
|
+
|
|
23
|
+
Legacy top-level access to results (MacdResult, etc.) and options helpers
|
|
24
|
+
still works but is deprecated and will be removed in a future major version.
|
|
25
|
+
|
|
26
|
+
Boundary conditions & error behavior:
|
|
27
|
+
Most indicators return NaN or truncated output during their warmup period.
|
|
28
|
+
Use ``boundary_info(name)`` for curated per-kind semantics. Invalid parameters
|
|
29
|
+
typically raise ``InvalidParameterError`` or ``ValueError``.
|
|
30
|
+
|
|
31
|
+
Exception contract (``quantwave.QuantwaveError`` hierarchy):
|
|
32
|
+
``IndicatorNotFoundError``, ``InvalidParameterError``, ``ParityError``,
|
|
33
|
+
``StreamingError``, and ``InternalError`` (native FFI). See ``quantwave._errors``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from typing import List, Dict, Any
|
|
37
|
+
import warnings
|
|
38
|
+
|
|
39
|
+
# Version
|
|
40
|
+
try:
|
|
41
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
42
|
+
__version__ = version("quantwave")
|
|
43
|
+
except (PackageNotFoundError, Exception):
|
|
44
|
+
__version__ = "0.6.0.dev"
|
|
45
|
+
|
|
46
|
+
# Core compiled extension
|
|
47
|
+
from . import _quantwave # noqa
|
|
48
|
+
|
|
49
|
+
# Polars helpers submodule (optional; core streaming/metadata/DX work without it).
|
|
50
|
+
try:
|
|
51
|
+
from . import polars
|
|
52
|
+
except Exception as _e: # pragma: no cover
|
|
53
|
+
warnings.warn(
|
|
54
|
+
f"quantwave.polars submodule unavailable (polars not installed; "
|
|
55
|
+
f"use `pip install \"quantwave[polars]\"` for Polars layer): {_e}"
|
|
56
|
+
)
|
|
57
|
+
class _DummyNS: pass
|
|
58
|
+
polars = _DummyNS()
|
|
59
|
+
|
|
60
|
+
# Native backtest extension (bundled in unified PyPI wheel as quantwave._backtest).
|
|
61
|
+
_backtest_available = False
|
|
62
|
+
try:
|
|
63
|
+
from . import _backtest # noqa: F401
|
|
64
|
+
_backtest_available = True
|
|
65
|
+
except Exception as _e: # pragma: no cover
|
|
66
|
+
warnings.warn(
|
|
67
|
+
f"quantwave._backtest unavailable (reinstall quantwave wheel; unified build bundles backtest): {_e}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# LazyFrame `.bt` namespace — requires polars + _backtest.
|
|
71
|
+
if _backtest_available:
|
|
72
|
+
try:
|
|
73
|
+
import polars as _pl # noqa: F401
|
|
74
|
+
from . import bt_polars # noqa: F401
|
|
75
|
+
except Exception as _e: # pragma: no cover
|
|
76
|
+
warnings.warn(
|
|
77
|
+
f"quantwave.bt_polars unavailable (requires polars; "
|
|
78
|
+
f"use `pip install \"quantwave[polars]\"`): {_e}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Polars expression plugins — bundled in unified wheel; registers pl.col().ta.
|
|
82
|
+
try:
|
|
83
|
+
import quantwave_plugins # noqa: F401
|
|
84
|
+
except ImportError:
|
|
85
|
+
pass
|
|
86
|
+
except Exception as _e: # pragma: no cover
|
|
87
|
+
warnings.warn(f"quantwave_plugins unavailable: {_e}")
|
|
88
|
+
|
|
89
|
+
# Backtest Python API (PyO3 + pyo3-polars; requires polars extra).
|
|
90
|
+
try:
|
|
91
|
+
from . import backtest
|
|
92
|
+
except Exception as _e: # pragma: no cover
|
|
93
|
+
warnings.warn(
|
|
94
|
+
f"quantwave.backtest submodule unavailable (requires polars; "
|
|
95
|
+
f"use `pip install \"quantwave[polars]\"`): {_e}"
|
|
96
|
+
)
|
|
97
|
+
class _DummyNS: pass
|
|
98
|
+
backtest = _DummyNS()
|
|
99
|
+
|
|
100
|
+
# Popular namespaces (guarded: some may require full maturin build or follow-up on gqem/05q7)
|
|
101
|
+
try:
|
|
102
|
+
from . import results
|
|
103
|
+
from . import options
|
|
104
|
+
from . import talib
|
|
105
|
+
except Exception as _e: # pragma: no cover
|
|
106
|
+
warnings.warn(f"quantwave namespace submodules (results/options/talib) partial load: {_e}")
|
|
107
|
+
# Provide minimal stand-ins so core DX (metadata, discovery, parity) still works
|
|
108
|
+
class _DummyNS: pass
|
|
109
|
+
results = _DummyNS()
|
|
110
|
+
options = _DummyNS()
|
|
111
|
+
talib = _DummyNS()
|
|
112
|
+
|
|
113
|
+
# Public exception types (quantwave-1x2z; must survive partial native load).
|
|
114
|
+
from ._errors import (
|
|
115
|
+
QuantwaveError,
|
|
116
|
+
IndicatorNotFoundError,
|
|
117
|
+
InvalidParameterError,
|
|
118
|
+
ParityError,
|
|
119
|
+
StreamingError,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
_native_internal = getattr(_quantwave, "InternalError", None)
|
|
123
|
+
if _native_internal is None:
|
|
124
|
+
class InternalError(QuantwaveError):
|
|
125
|
+
"""Internal uniffi/FFI error from the native binding."""
|
|
126
|
+
pass
|
|
127
|
+
else:
|
|
128
|
+
class InternalError(QuantwaveError, _native_internal):
|
|
129
|
+
"""Internal uniffi/FFI error from the native binding."""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
globals()["InternalError"] = InternalError
|
|
133
|
+
|
|
134
|
+
# Pull in the clean metadata system (now the robust source for discovery too)
|
|
135
|
+
from ._metadata import (
|
|
136
|
+
IndicatorMeta,
|
|
137
|
+
BoundaryInfo,
|
|
138
|
+
metadata,
|
|
139
|
+
list_metadata,
|
|
140
|
+
warmup_bars,
|
|
141
|
+
get_indicator_signature,
|
|
142
|
+
boundary_info,
|
|
143
|
+
categories,
|
|
144
|
+
indicators_by_category,
|
|
145
|
+
category,
|
|
146
|
+
Param,
|
|
147
|
+
Series,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# DX helpers (defined later in this file for now to avoid circularity during cleanup)
|
|
151
|
+
# We will gradually move more of these out.
|
|
152
|
+
|
|
153
|
+
from .backtest_types import PerformanceMetrics, BacktestStats
|
|
154
|
+
|
|
155
|
+
# Options India helpers — namespaced under quantwave.options (quantwave-05q7).
|
|
156
|
+
# Must NOT pollute top-level indicator discovery or qw.ta.
|
|
157
|
+
_OPTIONS_SYMBOLS = frozenset({
|
|
158
|
+
"bs_call_price", "bs_put_price", "bs_delta", "bs_gamma", "bs_theta",
|
|
159
|
+
"bs_vega", "bs_rho", "implied_vol", "max_pain", "strike_pcr",
|
|
160
|
+
"chain_pcr", "oi_zones", "gex_per_strike", "gex_flip_strike",
|
|
161
|
+
"atm_straddle", "synthetic_futures", "moneyness", "nse_lot_size",
|
|
162
|
+
"nse_risk_free_rate",
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
# --- Dynamic ta namespace population (replaces fragile manual class + "from . import ta") ---
|
|
166
|
+
# This makes indicators(), is_indicator(), streaming_class, and qw.ta.* robust
|
|
167
|
+
# even when some advanced ML/PA feature objects are added in later tasks (tha, gw7s, ej8b).
|
|
168
|
+
# Pulls from the native _quantwave (source of truth for compiled indicators + new uniffi/PyO3 objects)
|
|
169
|
+
# + falls back to metadata keys. Also exposes at top-level for backward compat.
|
|
170
|
+
|
|
171
|
+
class ta:
|
|
172
|
+
"""Dynamic namespace aggregating available indicators (batch + streaming classes)
|
|
173
|
+
and feature/PA helpers. Populated at import time from the compiled extension.
|
|
174
|
+
"""
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
# Populate from native extension.
|
|
178
|
+
# IMPORTANT for gqem (namespace cleanup): Do NOT dump *Result / *Protocol / internal
|
|
179
|
+
# types to top-level globals (that was the ~150 item pollution). Only clean indicator
|
|
180
|
+
# names go to top-level + ta. Result types live in quantwave.results (and ta if useful).
|
|
181
|
+
# This directly advances the "move MacdResult, *Protocol etc out of top-level" goal.
|
|
182
|
+
try:
|
|
183
|
+
if hasattr(_quantwave, "__all__"):
|
|
184
|
+
_native_syms = getattr(_quantwave, "__all__")
|
|
185
|
+
else:
|
|
186
|
+
_native_syms = [x for x in dir(_quantwave) if not x.startswith("_") and not x.startswith("Py")]
|
|
187
|
+
for _sym in _native_syms:
|
|
188
|
+
if _sym in _OPTIONS_SYMBOLS:
|
|
189
|
+
continue # options live in quantwave.options only (05q7)
|
|
190
|
+
try:
|
|
191
|
+
_val = getattr(_quantwave, _sym)
|
|
192
|
+
setattr(ta, _sym, _val) # always available under qw.ta for discovery / advanced use
|
|
193
|
+
is_internal = _sym.endswith("Result") or _sym.endswith("Protocol") or _sym.endswith("Error") or "Protocol" in _sym
|
|
194
|
+
if not is_internal:
|
|
195
|
+
globals()[_sym] = _val # clean top-level exposure only for indicators / public fns
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
except Exception as _e:
|
|
199
|
+
warnings.warn(f"Partial native symbol import into quantwave.ta / top-level: {_e}")
|
|
200
|
+
|
|
201
|
+
# Also ensure any metadata-registered indicators are attached (for pure-Py or future)
|
|
202
|
+
for _meta in list_metadata():
|
|
203
|
+
_n = _meta.name
|
|
204
|
+
if not hasattr(ta, _n):
|
|
205
|
+
for _cand in (_n, _n.capitalize(), _n.replace("_", "")):
|
|
206
|
+
if hasattr(_quantwave, _cand):
|
|
207
|
+
_v = getattr(_quantwave, _cand)
|
|
208
|
+
setattr(ta, _n, _v)
|
|
209
|
+
globals()[_n] = _v
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
# Special recently-wired objects (ML features, PA structs, regime) that may have specific casing
|
|
213
|
+
for _special in [
|
|
214
|
+
"CyberCycleFeatureExtractor", "HurstFeatureExtractor",
|
|
215
|
+
"InstantaneousTrendlineFeatureExtractor", "TrendflexFeatureExtractor",
|
|
216
|
+
"GriffithsDominantCycleFeatureExtractor", "BullBearHMM",
|
|
217
|
+
"GaussianHmmFilterPy", "GaussianHmmDiagnosticsPy",
|
|
218
|
+
"fit_gaussian_hmm", "gaussian_hmm_diagnostics",
|
|
219
|
+
"gaussian_hmm_forecast_state", "gaussian_hmm_forecast_vol",
|
|
220
|
+
"regime_to_features", "griffiths_dominant_cycle_features",
|
|
221
|
+
"MarketStructure", "GeometricPatternScanner", "market_structure_batch",
|
|
222
|
+
]:
|
|
223
|
+
if not hasattr(ta, _special):
|
|
224
|
+
for _cand in (_special, _special.lower().replace("featureextractor", "feature_extractor")):
|
|
225
|
+
if hasattr(_quantwave, _cand):
|
|
226
|
+
_v = getattr(_quantwave, _cand)
|
|
227
|
+
setattr(ta, _special, _v)
|
|
228
|
+
globals()[_special] = _v
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
# --- Basic Discovery API (quantwave-p3z9 / p0s) ---
|
|
232
|
+
# Now backed primarily by metadata (reliable) + dir(ta) for anything extra the native exposes.
|
|
233
|
+
# This closes the "first-pass" + "will be refined as metadata built" gap.
|
|
234
|
+
|
|
235
|
+
def _is_internal_symbol(name: str) -> bool:
|
|
236
|
+
return (
|
|
237
|
+
name.endswith("Result")
|
|
238
|
+
or name.endswith("Protocol")
|
|
239
|
+
or name.endswith("Error")
|
|
240
|
+
or "Protocol" in name
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _build_indicator_names() -> set[str]:
|
|
245
|
+
"""Canonical indicator slugs from Rust metadata export (221); never dir(ta) pollution."""
|
|
246
|
+
try:
|
|
247
|
+
from quantwave._metadata_generated import GENERATED_ENTRIES
|
|
248
|
+
|
|
249
|
+
return set(GENERATED_ENTRIES.keys())
|
|
250
|
+
except ImportError:
|
|
251
|
+
return {m.name for m in list_metadata() if not _is_internal_symbol(m.name)}
|
|
252
|
+
|
|
253
|
+
_INDICATOR_NAMES: set[str] = _build_indicator_names()
|
|
254
|
+
|
|
255
|
+
def indicators() -> list[str]:
|
|
256
|
+
"""Return a sorted list of all available indicator names."""
|
|
257
|
+
return sorted(_INDICATOR_NAMES)
|
|
258
|
+
|
|
259
|
+
def is_indicator(name: str) -> bool:
|
|
260
|
+
"""Check whether the given name is a known indicator."""
|
|
261
|
+
if not name:
|
|
262
|
+
return False
|
|
263
|
+
return name in _INDICATOR_NAMES or name.lower() in _INDICATOR_NAMES
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# =============================================================================
|
|
267
|
+
# Rich Metadata API
|
|
268
|
+
# =============================================================================
|
|
269
|
+
# All rich metadata is provided by the clean implementation in ._metadata
|
|
270
|
+
# (which is the current source of truth, eventually to be generated from Rust).
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# =============================================================================
|
|
274
|
+
# Streaming Class Lookup (quantwave-p3z9)
|
|
275
|
+
# =============================================================================
|
|
276
|
+
|
|
277
|
+
def streaming_class(name: str):
|
|
278
|
+
"""
|
|
279
|
+
Return the streaming (Next<T>) class for a given batch indicator name.
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
cls = quantwave.streaming_class("supertrend")
|
|
283
|
+
st = cls(period=10, multiplier=3.0)
|
|
284
|
+
"""
|
|
285
|
+
if not name:
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
key = name.lower()
|
|
289
|
+
|
|
290
|
+
# Try direct attribute first (common case)
|
|
291
|
+
if hasattr(ta, key):
|
|
292
|
+
candidate = getattr(ta, key)
|
|
293
|
+
if isinstance(candidate, type):
|
|
294
|
+
return candidate
|
|
295
|
+
|
|
296
|
+
# Fallback: try common PascalCase conversions
|
|
297
|
+
pascal = "".join(word.capitalize() for word in key.split("_"))
|
|
298
|
+
if hasattr(ta, pascal) and isinstance(getattr(ta, pascal), type):
|
|
299
|
+
return getattr(ta, pascal)
|
|
300
|
+
|
|
301
|
+
# Last resort: search for anything that looks like a streaming class
|
|
302
|
+
for attr_name in dir(ta):
|
|
303
|
+
if attr_name.lower() == key or attr_name.lower().replace("_", "") == key.replace("_", ""):
|
|
304
|
+
obj = getattr(ta, attr_name)
|
|
305
|
+
if isinstance(obj, type):
|
|
306
|
+
return obj
|
|
307
|
+
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# =============================================================================
|
|
312
|
+
# Parity Testing Helper (quantwave-p3z9 - Critical)
|
|
313
|
+
# =============================================================================
|
|
314
|
+
|
|
315
|
+
def assert_parity(
|
|
316
|
+
indicator_name: str,
|
|
317
|
+
params: Dict[str, Any],
|
|
318
|
+
data: List[float],
|
|
319
|
+
tolerance: float = 1e-10,
|
|
320
|
+
**kwargs,
|
|
321
|
+
) -> bool:
|
|
322
|
+
"""
|
|
323
|
+
Run both batch and streaming versions of an indicator and assert
|
|
324
|
+
they produce (nearly) identical results.
|
|
325
|
+
|
|
326
|
+
Returns True if they match within tolerance.
|
|
327
|
+
Raises AssertionError with details on mismatch.
|
|
328
|
+
|
|
329
|
+
This is the first-class way for users and CI to verify the
|
|
330
|
+
"bit-identical" guarantee.
|
|
331
|
+
"""
|
|
332
|
+
batch_fn = getattr(ta, indicator_name, None)
|
|
333
|
+
if batch_fn is None:
|
|
334
|
+
raise IndicatorNotFoundError(f"Unknown indicator: {indicator_name}")
|
|
335
|
+
|
|
336
|
+
streaming_cls = streaming_class(indicator_name)
|
|
337
|
+
if streaming_cls is None:
|
|
338
|
+
raise IndicatorNotFoundError(f"No streaming class found for: {indicator_name}")
|
|
339
|
+
|
|
340
|
+
# Run batch - native fns usually take explicit params then series= (or data as last positional).
|
|
341
|
+
# Use flexible try to support the generated signatures without hardcoding per-indicator.
|
|
342
|
+
meta = metadata(indicator_name)
|
|
343
|
+
call_params = {k: v for k, v in (params or {}).items()}
|
|
344
|
+
try:
|
|
345
|
+
batch_result = batch_fn(series=data, **call_params, **(kwargs or {}))
|
|
346
|
+
except TypeError:
|
|
347
|
+
try:
|
|
348
|
+
batch_result = batch_fn(data, **call_params, **(kwargs or {}))
|
|
349
|
+
except TypeError:
|
|
350
|
+
raise InvalidParameterError(
|
|
351
|
+
f"Batch call failed for {indicator_name} with params={call_params}. "
|
|
352
|
+
"Check native signature and required parameters."
|
|
353
|
+
) from None
|
|
354
|
+
|
|
355
|
+
# Run streaming
|
|
356
|
+
try:
|
|
357
|
+
streamer = streaming_cls(**params, **kwargs)
|
|
358
|
+
stream_result = [streamer.next(v) for v in data]
|
|
359
|
+
except (TypeError, ValueError) as e:
|
|
360
|
+
raise InvalidParameterError(f"Invalid streaming params for {indicator_name}: {e}") from e
|
|
361
|
+
except Exception as e:
|
|
362
|
+
raise StreamingError(f"Streaming mode failed for {indicator_name}: {e}") from e
|
|
363
|
+
|
|
364
|
+
# Use metadata for warmup awareness (976r) - skip or note initial bars in comparison
|
|
365
|
+
meta = metadata(indicator_name)
|
|
366
|
+
warmup = warmup_bars(indicator_name, params) if meta else 0
|
|
367
|
+
|
|
368
|
+
# Normalize for comparison (handle single vs multi output + result dataclasses)
|
|
369
|
+
def _normalize(x):
|
|
370
|
+
if isinstance(x, (list, tuple)):
|
|
371
|
+
return x
|
|
372
|
+
return [x]
|
|
373
|
+
|
|
374
|
+
def _close_enough(bv, sv):
|
|
375
|
+
import math
|
|
376
|
+
if isinstance(bv, (int, float)) and isinstance(sv, (int, float)):
|
|
377
|
+
if math.isnan(bv) and math.isnan(sv):
|
|
378
|
+
return True
|
|
379
|
+
if math.isinf(bv) and math.isinf(sv) and (bv > 0) == (sv > 0):
|
|
380
|
+
return True
|
|
381
|
+
return abs(bv - sv) <= tolerance
|
|
382
|
+
if repr(bv) == repr(sv):
|
|
383
|
+
return True
|
|
384
|
+
# Dataclass / result object: compare fields with tolerance on floats
|
|
385
|
+
try:
|
|
386
|
+
bdict = vars(bv) if hasattr(bv, "__dict__") else bv._asdict() if hasattr(bv, "_asdict") else None
|
|
387
|
+
sdict = vars(sv) if hasattr(sv, "__dict__") else sv._asdict() if hasattr(sv, "_asdict") else None
|
|
388
|
+
if bdict and sdict and set(bdict.keys()) == set(sdict.keys()):
|
|
389
|
+
for k in bdict:
|
|
390
|
+
if not _close_enough(bdict[k], sdict[k]):
|
|
391
|
+
return False
|
|
392
|
+
return True
|
|
393
|
+
except Exception:
|
|
394
|
+
pass
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
b = _normalize(batch_result)
|
|
398
|
+
s = _normalize(stream_result)
|
|
399
|
+
|
|
400
|
+
if len(b) != len(s):
|
|
401
|
+
raise ParityError(f"Length mismatch: batch={len(b)}, stream={len(s)}")
|
|
402
|
+
|
|
403
|
+
for i, (bv, sv) in enumerate(zip(b, s)):
|
|
404
|
+
if i < warmup:
|
|
405
|
+
# During warmup, both paths should agree on NaN / sentinel values.
|
|
406
|
+
if not _close_enough(bv, sv):
|
|
407
|
+
raise ParityError(
|
|
408
|
+
f"Warmup mismatch at index {i} (warmup={warmup}): batch={bv}, stream={sv}"
|
|
409
|
+
)
|
|
410
|
+
continue
|
|
411
|
+
if not _close_enough(bv, sv):
|
|
412
|
+
raise ParityError(
|
|
413
|
+
f"Mismatch at index {i} (post-warmup, warmup={warmup}): batch={bv}, stream={sv}"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return True
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# =============================================================================
|
|
420
|
+
# Streaming Readiness Helpers (quantwave-p3z9 / 1l79)
|
|
421
|
+
# =============================================================================
|
|
422
|
+
|
|
423
|
+
class StreamingWrapper:
|
|
424
|
+
"""
|
|
425
|
+
Lightweight wrapper that adds is_ready and bars_consumed tracking
|
|
426
|
+
around any streaming (Next<T>) indicator instance.
|
|
427
|
+
|
|
428
|
+
Mirrors Rust ``quantwave_core::TrackedNext`` / ``StreamingReadiness`` (h6xe).
|
|
429
|
+
Uses metadata.warmup_bars for accurate readiness when ``name`` is provided.
|
|
430
|
+
|
|
431
|
+
Usage:
|
|
432
|
+
st = quantwave.streaming_class("supertrend")(period=10, multiplier=3)
|
|
433
|
+
wrapped = quantwave.wrap_streaming(st, name="supertrend")
|
|
434
|
+
for price in data:
|
|
435
|
+
val = wrapped.next(price)
|
|
436
|
+
if wrapped.is_ready:
|
|
437
|
+
...
|
|
438
|
+
"""
|
|
439
|
+
def __init__(self, streaming_instance, name: str = None, warmup_bars_count: int = None):
|
|
440
|
+
self._inner = streaming_instance
|
|
441
|
+
self._bars_consumed = 0
|
|
442
|
+
self._is_ready = False
|
|
443
|
+
self._name = name
|
|
444
|
+
self._warmup = 0
|
|
445
|
+
if warmup_bars_count is not None:
|
|
446
|
+
self._warmup = int(warmup_bars_count)
|
|
447
|
+
elif name:
|
|
448
|
+
try:
|
|
449
|
+
self._warmup = warmup_bars(name)
|
|
450
|
+
except Exception:
|
|
451
|
+
self._warmup = 0
|
|
452
|
+
|
|
453
|
+
def next(self, value):
|
|
454
|
+
result = self._inner.next(value)
|
|
455
|
+
self._bars_consumed += 1
|
|
456
|
+
if self._warmup > 0:
|
|
457
|
+
self._is_ready = self._bars_consumed >= self._warmup
|
|
458
|
+
else:
|
|
459
|
+
# Fallback heuristic for unknown
|
|
460
|
+
self._is_ready = self._bars_consumed > 0
|
|
461
|
+
return result
|
|
462
|
+
|
|
463
|
+
@property
|
|
464
|
+
def is_ready(self) -> bool:
|
|
465
|
+
return self._is_ready
|
|
466
|
+
|
|
467
|
+
@property
|
|
468
|
+
def bars_consumed(self) -> int:
|
|
469
|
+
return self._bars_consumed
|
|
470
|
+
|
|
471
|
+
def __getattr__(self, name):
|
|
472
|
+
# Delegate everything else to the inner streaming object
|
|
473
|
+
return getattr(self._inner, name)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def wrap_streaming(streaming_instance, name: str = None, warmup_bars_count: int = None):
|
|
477
|
+
"""Wrap a streaming indicator instance to get is_ready / bars_consumed tracking.
|
|
478
|
+
|
|
479
|
+
Pass ``name=`` for warmup-based readiness from metadata, or ``warmup_bars_count=``
|
|
480
|
+
for an explicit bar count (Rust ``TrackedNext`` parity).
|
|
481
|
+
"""
|
|
482
|
+
return StreamingWrapper(streaming_instance, name=name, warmup_bars_count=warmup_bars_count)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def track_streaming(streaming_instance, warmup_bars_count: int = None, name: str = None):
|
|
486
|
+
"""Alias for ``wrap_streaming`` — explicit readiness tracking API (h6xe)."""
|
|
487
|
+
return wrap_streaming(streaming_instance, name=name, warmup_bars_count=warmup_bars_count)
|
|
488
|
+
|
|
489
|
+
# (The old manual "class ta:" with 100+ hardcoded aliases has been replaced by the
|
|
490
|
+
# dynamic population block earlier in this file. It pulls from _quantwave + _metadata
|
|
491
|
+
# so it stays in sync automatically as new indicators / ML / PA objects are added in
|
|
492
|
+
# tha, gw7s, cu03 children, etc. The options_india legacy compat class remains below.
|
|
493
|
+
# It is built safely via getattr to avoid NameError on import if the preceding
|
|
494
|
+
# population was partial for any reason.)
|
|
495
|
+
|
|
496
|
+
class options_india:
|
|
497
|
+
"""Legacy top-level access for Options India helpers.
|
|
498
|
+
|
|
499
|
+
Prefer: from quantwave import options (or from quantwave.options import ...)
|
|
500
|
+
This will be deprecated in a future release.
|
|
501
|
+
"""
|
|
502
|
+
pass
|
|
503
|
+
|
|
504
|
+
# Build safely (no bare-name RHS that can NameError if a symbol wasn't populated).
|
|
505
|
+
_option_legacy_names = [
|
|
506
|
+
"bs_call_price", "bs_put_price", "bs_delta", "bs_gamma", "bs_theta",
|
|
507
|
+
"bs_vega", "bs_rho", "implied_vol", "max_pain", "strike_pcr",
|
|
508
|
+
"chain_pcr", "oi_zones", "gex_per_strike", "gex_flip_strike",
|
|
509
|
+
"atm_straddle", "synthetic_futures", "moneyness", "nse_lot_size",
|
|
510
|
+
"nse_risk_free_rate",
|
|
511
|
+
]
|
|
512
|
+
for _name in _option_legacy_names:
|
|
513
|
+
_val = getattr(options, _name, None)
|
|
514
|
+
if _val is not None:
|
|
515
|
+
setattr(options_india, _name, _val)
|
|
516
|
+
|
|
517
|
+
# (Removed the giant legacy __all__ that referenced many PascalCase / old names no longer
|
|
518
|
+
# explicitly assigned here; the dynamic population + final clean __all__ below provide the
|
|
519
|
+
# supported public surface. Top-level indicator names like sma/rsi/SuperTrend are still
|
|
520
|
+
# available via the globals() population for backward compat.)
|
|
521
|
+
|
|
522
|
+
# =============================================================================
|
|
523
|
+
# Final clean public surface for 0.5.2 / P0 DX (quantwave-p3z9 children)
|
|
524
|
+
# =============================================================================
|
|
525
|
+
|
|
526
|
+
# ML feature matrix (rdpk) — imported after ``ta`` is populated to avoid circular import.
|
|
527
|
+
from .features import (
|
|
528
|
+
FeatureSpec,
|
|
529
|
+
RECOMMENDED_PRESET,
|
|
530
|
+
build_feature_matrix,
|
|
531
|
+
feature_column_names,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def __getattr__(name: str):
|
|
536
|
+
"""Deprecated top-level access for options helpers (quantwave-05q7)."""
|
|
537
|
+
if name in _OPTIONS_SYMBOLS:
|
|
538
|
+
warnings.warn(
|
|
539
|
+
f"quantwave.{name} is deprecated; use quantwave.options.{name} instead",
|
|
540
|
+
DeprecationWarning,
|
|
541
|
+
stacklevel=2,
|
|
542
|
+
)
|
|
543
|
+
return getattr(options, name)
|
|
544
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
__all__ = [
|
|
548
|
+
"__version__",
|
|
549
|
+
"indicators",
|
|
550
|
+
"is_indicator",
|
|
551
|
+
"metadata",
|
|
552
|
+
"list_metadata",
|
|
553
|
+
"warmup_bars",
|
|
554
|
+
"get_indicator_signature",
|
|
555
|
+
"build_feature_matrix",
|
|
556
|
+
"feature_column_names",
|
|
557
|
+
"FeatureSpec",
|
|
558
|
+
"RECOMMENDED_PRESET",
|
|
559
|
+
"boundary_info",
|
|
560
|
+
"categories",
|
|
561
|
+
"indicators_by_category",
|
|
562
|
+
"category",
|
|
563
|
+
"IndicatorMeta",
|
|
564
|
+
"BoundaryInfo",
|
|
565
|
+
"Param",
|
|
566
|
+
"Series",
|
|
567
|
+
"assert_parity",
|
|
568
|
+
"streaming_class",
|
|
569
|
+
"wrap_streaming",
|
|
570
|
+
"track_streaming",
|
|
571
|
+
"StreamingWrapper",
|
|
572
|
+
"QuantwaveError",
|
|
573
|
+
"IndicatorNotFoundError",
|
|
574
|
+
"InvalidParameterError",
|
|
575
|
+
"ParityError",
|
|
576
|
+
"StreamingError",
|
|
577
|
+
"InternalError",
|
|
578
|
+
"ta",
|
|
579
|
+
"polars",
|
|
580
|
+
"backtest",
|
|
581
|
+
"results",
|
|
582
|
+
"options",
|
|
583
|
+
"talib",
|
|
584
|
+
"options_india", # legacy compat (will warn in future)
|
|
585
|
+
"PerformanceMetrics",
|
|
586
|
+
"BacktestStats",
|
|
587
|
+
]
|
quantwave/_backtest.pyd
ADDED
|
Binary file
|
quantwave/_errors.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public exception hierarchy for quantwave (quantwave-1x2z).
|
|
3
|
+
|
|
4
|
+
Exception contract
|
|
5
|
+
------------------
|
|
6
|
+
* ``QuantwaveError`` — base for all library-raised errors you should catch.
|
|
7
|
+
* ``IndicatorNotFoundError`` — unknown indicator name in discovery/parity APIs.
|
|
8
|
+
* ``InvalidParameterError`` — bad params (negative period, missing required key).
|
|
9
|
+
* ``ParityError`` — batch vs streaming mismatch (subclass of AssertionError for
|
|
10
|
+
pytest compatibility, but also QuantwaveError for unified handling).
|
|
11
|
+
* ``StreamingError`` — streaming path failed to run.
|
|
12
|
+
* ``InternalError`` — native FFI/uniffi failure (re-exported from ``__init__``).
|
|
13
|
+
|
|
14
|
+
Typical usage::
|
|
15
|
+
|
|
16
|
+
import quantwave as qw
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
qw.assert_parity("rsi", {"period": 14}, closes)
|
|
20
|
+
except qw.ParityError as e:
|
|
21
|
+
... # batch/stream diverged
|
|
22
|
+
except qw.IndicatorNotFoundError:
|
|
23
|
+
... # bad indicator name
|
|
24
|
+
except qw.QuantwaveError:
|
|
25
|
+
... # any other quantwave-specific error
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class QuantwaveError(Exception):
|
|
30
|
+
"""Base exception for quantwave errors."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class IndicatorNotFoundError(QuantwaveError, ValueError):
|
|
34
|
+
"""Raised when an indicator name is not registered or has no streaming class."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InvalidParameterError(QuantwaveError, ValueError):
|
|
38
|
+
"""Raised when indicator parameters are invalid or incomplete."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class StreamingError(QuantwaveError, RuntimeError):
|
|
42
|
+
"""Raised when the streaming execution path fails."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ParityError(QuantwaveError, AssertionError):
|
|
46
|
+
"""Raised when batch and streaming outputs disagree beyond tolerance."""
|