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 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
+ ]
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."""