quantlite 1.0.2__tar.gz → 1.7.0__tar.gz
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.
- {quantlite-1.0.2/src/quantlite.egg-info → quantlite-1.7.0}/PKG-INFO +29 -1
- {quantlite-1.0.2 → quantlite-1.7.0}/README.md +18 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/pyproject.toml +12 -1
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/__init__.py +26 -1
- quantlite-1.7.0/src/quantlite/alerts/__init__.py +415 -0
- quantlite-1.7.0/src/quantlite/benchmark/__init__.py +21 -0
- quantlite-1.7.0/src/quantlite/benchmark/compare.py +483 -0
- quantlite-1.7.0/src/quantlite/benchmark/runner.py +207 -0
- quantlite-1.7.0/src/quantlite/benchmark/speed.py +211 -0
- quantlite-1.7.0/src/quantlite/benchmark/tail_events.py +314 -0
- quantlite-1.7.0/src/quantlite/data/stream.py +410 -0
- quantlite-1.7.0/src/quantlite/explain/__init__.py +1 -0
- quantlite-1.7.0/src/quantlite/explain/attribution.py +368 -0
- quantlite-1.7.0/src/quantlite/explain/audit.py +325 -0
- quantlite-1.7.0/src/quantlite/explain/narratives.py +334 -0
- quantlite-1.7.0/src/quantlite/explain/whatif.py +308 -0
- quantlite-1.7.0/src/quantlite/portfolio/__init__.py +97 -0
- quantlite-1.7.0/src/quantlite/portfolio/dynamic_kelly.py +270 -0
- quantlite-1.7.0/src/quantlite/portfolio/ensemble.py +215 -0
- quantlite-1.7.0/src/quantlite/portfolio/regime_bl.py +278 -0
- quantlite-1.7.0/src/quantlite/portfolio/tail_risk_parity.py +336 -0
- quantlite-1.7.0/src/quantlite/portfolio/walkforward.py +288 -0
- quantlite-1.7.0/src/quantlite/regimes/online.py +273 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/risk/metrics.py +53 -1
- quantlite-1.7.0/src/quantlite/score/__init__.py +76 -0
- quantlite-1.7.0/src/quantlite/score/artifact.py +212 -0
- quantlite-1.7.0/src/quantlite/score/engine.py +496 -0
- quantlite-1.7.0/src/quantlite/score/integrity.py +240 -0
- quantlite-1.7.0/src/quantlite/score/monitoring.py +666 -0
- quantlite-1.7.0/src/quantlite/score/provenance.py +414 -0
- quantlite-1.7.0/src/quantlite/viz/alerts.py +173 -0
- quantlite-1.7.0/src/quantlite/viz/allocation.py +642 -0
- quantlite-1.7.0/src/quantlite/viz/benchmark.py +615 -0
- quantlite-1.7.0/src/quantlite/viz/explain.py +648 -0
- quantlite-1.7.0/src/quantlite/viz/online_regimes.py +211 -0
- quantlite-1.7.0/src/quantlite/viz/streaming.py +173 -0
- {quantlite-1.0.2 → quantlite-1.7.0/src/quantlite.egg-info}/PKG-INFO +29 -1
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite.egg-info/SOURCES.txt +39 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite.egg-info/requires.txt +12 -0
- quantlite-1.7.0/tests/test_alerts.py +160 -0
- quantlite-1.7.0/tests/test_allocation_engine.py +358 -0
- quantlite-1.7.0/tests/test_benchmarks.py +150 -0
- quantlite-1.7.0/tests/test_explainability.py +219 -0
- quantlite-1.7.0/tests/test_monitoring.py +179 -0
- quantlite-1.7.0/tests/test_online_regime.py +137 -0
- quantlite-1.7.0/tests/test_provenance.py +172 -0
- quantlite-1.7.0/tests/test_score.py +175 -0
- quantlite-1.7.0/tests/test_stream.py +159 -0
- quantlite-1.0.2/src/quantlite/portfolio/__init__.py +0 -35
- {quantlite-1.0.2 → quantlite-1.7.0}/LICENSE +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/setup.cfg +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/antifragile/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/backtesting/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/backtesting/analysis.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/backtesting/engine.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/backtesting/legacy.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/backtesting/signals.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/contagion/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/core/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/core/types.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/crypto/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/crypto/exchange.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/crypto/onchain.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/crypto/stablecoin.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/base.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/cache.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/crypto.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/fred.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/local.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/registry.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/yahoo.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data_generation.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/dependency/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/dependency/clustering.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/dependency/copulas.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/dependency/correlation.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/distributions/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/distributions/fat_tails.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/diversification/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/ergodicity/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/factors/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/factors/classical.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/factors/custom.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/factors/tail_risk.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/forensics/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/instruments/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/instruments/bond_pricing.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/instruments/exotic_options.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/instruments/option_pricing.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/metrics.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/monte_carlo.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/network/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/overfit/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/pipeline.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/portfolio/optimisation.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/portfolio/rebalancing.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regime_integration/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regime_integration/portfolio.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regime_integration/reporting.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regime_integration/risk.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regimes/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regimes/changepoint.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regimes/conditional.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regimes/hmm.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/report/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/report/html_renderer.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/report/pdf_renderer.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/report/sections.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/report/tearsheet.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/resample/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/risk/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/risk/evt.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/scenarios/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/simulation/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/simulation/copula_mc.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/simulation/evt_simulation.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/simulation/regime_mc.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/visualisation.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/dependency.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/__init__.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/dependency.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/portfolio.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/regimes.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/risk.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/theme.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/portfolio.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/regimes.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/risk.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/theme.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite.egg-info/dependency_links.txt +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite.egg-info/top_level.txt +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_analysis.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_antifragile.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_backtesting.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_changepoint.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_clustering.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_conditional.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_contagion.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_copulas.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_correlation.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_crypto_exchange.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_crypto_onchain.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_crypto_stablecoin.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_data_connectors.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_data_generation.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_diversification.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_engine.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_ergodicity.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_evt.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_factors_classical.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_factors_custom.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_factors_tail_risk.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_fat_tails.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_forensics.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_hmm.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_instruments.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_metrics.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_monte_carlo.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_network.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_optimisation.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_overfit.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_pipeline.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_plotly_viz.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_rebalancing.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_regime_integration.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_report.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_resample.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_risk_metrics.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_scenarios.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_signals.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_sim_copula.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_sim_evt.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_sim_regime.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_visualisation.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_viz.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_viz_dependency.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_viz_portfolio.py +0 -0
- {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_viz_regimes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quantlite
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: A fat-tail-native quantitative finance toolkit: EVT, risk metrics, and honest modelling for markets that bite.
|
|
5
5
|
Author-email: Prasant Sudhakaran <code@prasant.net>
|
|
6
6
|
License: MIT License
|
|
@@ -54,6 +54,8 @@ Provides-Extra: yahoo
|
|
|
54
54
|
Requires-Dist: yfinance>=0.2; extra == "yahoo"
|
|
55
55
|
Provides-Extra: crypto
|
|
56
56
|
Requires-Dist: ccxt>=4.0; extra == "crypto"
|
|
57
|
+
Provides-Extra: stream
|
|
58
|
+
Requires-Dist: ccxt>=4.0; extra == "stream"
|
|
57
59
|
Provides-Extra: fred
|
|
58
60
|
Requires-Dist: fredapi>=0.5; extra == "fred"
|
|
59
61
|
Provides-Extra: plotly
|
|
@@ -72,11 +74,19 @@ Requires-Dist: plotly>=5.0; extra == "all"
|
|
|
72
74
|
Requires-Dist: kaleido>=0.2; extra == "all"
|
|
73
75
|
Requires-Dist: weasyprint>=60; extra == "all"
|
|
74
76
|
Requires-Dist: hmmlearn>=0.3; extra == "all"
|
|
77
|
+
Provides-Extra: docs
|
|
78
|
+
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
79
|
+
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
|
|
80
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
|
|
75
81
|
Provides-Extra: dev
|
|
76
82
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
83
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
77
84
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
78
85
|
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
79
86
|
Requires-Dist: pandas-stubs; extra == "dev"
|
|
87
|
+
Requires-Dist: mkdocs>=1.5; extra == "dev"
|
|
88
|
+
Requires-Dist: mkdocs-material>=9.5; extra == "dev"
|
|
89
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == "dev"
|
|
80
90
|
Dynamic: license-file
|
|
81
91
|
|
|
82
92
|
# QuantLite
|
|
@@ -114,6 +124,24 @@ Five lines. Fetch data, detect market regimes, build a regime-aware portfolio, b
|
|
|
114
124
|
|
|
115
125
|
---
|
|
116
126
|
|
|
127
|
+
## The QuantLite Score
|
|
128
|
+
|
|
129
|
+
An open, versioned, verifiable rating for trading track records. Raw Sharpe ratios are trivially gamed: test fifty variants and publish the winner, start the chart at the bottom of a drawdown, sell tail risk and post a 92% win rate. The QuantLite Score rates a track record on the statistics that are hard to game — deflated Sharpe, bootstrap robustness, tail risk, consistency — and flags the classic manipulation patterns.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from quantlite.score import compute_score, verify_artifact
|
|
133
|
+
|
|
134
|
+
result = compute_score(returns, n_trials=20)
|
|
135
|
+
print(result.score, result.grade) # e.g. 72.4 B
|
|
136
|
+
|
|
137
|
+
payload = result.artifact.to_json() # publish or store this
|
|
138
|
+
assert verify_artifact(payload, returns) # anyone can reproduce it bit for bit
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Every score ships as a portable artifact with a SHA-256 content hash. Same returns, same parameters, same library version: same score, bit for bit. The methodology (QLS-1.0) is open and frozen per version — see [the specification](docs/score.md).
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
117
145
|
## Visual Showcase
|
|
118
146
|
|
|
119
147
|
### Fat Tails vs Gaussian
|
|
@@ -33,6 +33,24 @@ Five lines. Fetch data, detect market regimes, build a regime-aware portfolio, b
|
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
+
## The QuantLite Score
|
|
37
|
+
|
|
38
|
+
An open, versioned, verifiable rating for trading track records. Raw Sharpe ratios are trivially gamed: test fifty variants and publish the winner, start the chart at the bottom of a drawdown, sell tail risk and post a 92% win rate. The QuantLite Score rates a track record on the statistics that are hard to game — deflated Sharpe, bootstrap robustness, tail risk, consistency — and flags the classic manipulation patterns.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from quantlite.score import compute_score, verify_artifact
|
|
42
|
+
|
|
43
|
+
result = compute_score(returns, n_trials=20)
|
|
44
|
+
print(result.score, result.grade) # e.g. 72.4 B
|
|
45
|
+
|
|
46
|
+
payload = result.artifact.to_json() # publish or store this
|
|
47
|
+
assert verify_artifact(payload, returns) # anyone can reproduce it bit for bit
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Every score ships as a portable artifact with a SHA-256 content hash. Same returns, same parameters, same library version: same score, bit for bit. The methodology (QLS-1.0) is open and frozen per version — see [the specification](docs/score.md).
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
36
54
|
## Visual Showcase
|
|
37
55
|
|
|
38
56
|
### Fat Tails vs Gaussian
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quantlite"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.7.0"
|
|
8
8
|
description = "A fat-tail-native quantitative finance toolkit: EVT, risk metrics, and honest modelling for markets that bite."
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -48,16 +48,26 @@ dependencies = [
|
|
|
48
48
|
[project.optional-dependencies]
|
|
49
49
|
yahoo = ["yfinance>=0.2"]
|
|
50
50
|
crypto = ["ccxt>=4.0"]
|
|
51
|
+
stream = ["ccxt>=4.0"]
|
|
51
52
|
fred = ["fredapi>=0.5"]
|
|
52
53
|
plotly = ["plotly>=5.0", "kaleido>=0.2"]
|
|
53
54
|
report = ["plotly>=5.0", "kaleido>=0.2"]
|
|
54
55
|
pdf = ["weasyprint>=60"]
|
|
55
56
|
all = ["yfinance>=0.2", "ccxt>=4.0", "fredapi>=0.5", "plotly>=5.0", "kaleido>=0.2", "weasyprint>=60", "hmmlearn>=0.3"]
|
|
57
|
+
docs = [
|
|
58
|
+
"mkdocs>=1.5",
|
|
59
|
+
"mkdocs-material>=9.5",
|
|
60
|
+
"mkdocstrings[python]>=0.24",
|
|
61
|
+
]
|
|
56
62
|
dev = [
|
|
57
63
|
"pytest>=7.0",
|
|
64
|
+
"pytest-asyncio>=0.23",
|
|
58
65
|
"ruff>=0.4",
|
|
59
66
|
"mypy>=1.8",
|
|
60
67
|
"pandas-stubs",
|
|
68
|
+
"mkdocs>=1.5",
|
|
69
|
+
"mkdocs-material>=9.5",
|
|
70
|
+
"mkdocstrings[python]>=0.24",
|
|
61
71
|
]
|
|
62
72
|
|
|
63
73
|
[project.urls]
|
|
@@ -76,3 +86,4 @@ disallow_untyped_defs = false
|
|
|
76
86
|
|
|
77
87
|
[tool.pytest.ini_options]
|
|
78
88
|
testpaths = ["tests"]
|
|
89
|
+
asyncio_mode = "auto"
|
|
@@ -6,7 +6,7 @@ portfolio optimisation, multi-asset backtesting, and
|
|
|
6
6
|
Stephen Few-inspired visualisation.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "1.0
|
|
9
|
+
__version__ = "1.6.0"
|
|
10
10
|
|
|
11
11
|
from .backtesting import (
|
|
12
12
|
BacktestConfig,
|
|
@@ -87,6 +87,19 @@ __all__ = [
|
|
|
87
87
|
"construct_portfolio",
|
|
88
88
|
"backtest",
|
|
89
89
|
"tearsheet",
|
|
90
|
+
# Streaming & alerts (v1.1)
|
|
91
|
+
"AlertManager",
|
|
92
|
+
"AlertRule",
|
|
93
|
+
"Alert",
|
|
94
|
+
"PriceStream",
|
|
95
|
+
"PriceTick",
|
|
96
|
+
"create_stream",
|
|
97
|
+
"OnlineRegimeDetector",
|
|
98
|
+
"RegimeUpdate",
|
|
99
|
+
# Benchmarking (v1.5)
|
|
100
|
+
"benchmark",
|
|
101
|
+
# QuantLite Score (v1.6)
|
|
102
|
+
"score",
|
|
90
103
|
]
|
|
91
104
|
|
|
92
105
|
from . import ( # noqa: E402
|
|
@@ -103,6 +116,10 @@ from . import ( # noqa: E402
|
|
|
103
116
|
scenarios,
|
|
104
117
|
simulation,
|
|
105
118
|
)
|
|
119
|
+
|
|
120
|
+
# v1.1: Streaming, online regimes, alerts
|
|
121
|
+
from .alerts import Alert, AlertManager, AlertRule # noqa: E402
|
|
122
|
+
from .data.stream import PriceStream, PriceTick, create_stream # noqa: E402
|
|
106
123
|
from .pipeline import ( # noqa: E402
|
|
107
124
|
backtest,
|
|
108
125
|
construct_portfolio,
|
|
@@ -110,3 +127,11 @@ from .pipeline import ( # noqa: E402
|
|
|
110
127
|
tearsheet,
|
|
111
128
|
)
|
|
112
129
|
from .pipeline import fetch as fetch # noqa: E402
|
|
130
|
+
from .regimes.online import OnlineRegimeDetector, RegimeUpdate # noqa: E402
|
|
131
|
+
|
|
132
|
+
# Convenience alias
|
|
133
|
+
stream = create_stream
|
|
134
|
+
|
|
135
|
+
# v1.5: Benchmarking suite
|
|
136
|
+
# v1.6: QuantLite Score (open scoring spec for track records)
|
|
137
|
+
from . import benchmark, score # noqa: E402
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Alert system for QuantLite.
|
|
2
|
+
|
|
3
|
+
Provides rule-based and threshold alerts with callback/webhook
|
|
4
|
+
mechanisms for real-time monitoring of market conditions,
|
|
5
|
+
regime changes, and portfolio metrics.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
import quantlite as ql
|
|
10
|
+
|
|
11
|
+
def notify(alert):
|
|
12
|
+
print(f"ALERT: {alert}")
|
|
13
|
+
|
|
14
|
+
manager = ql.AlertManager()
|
|
15
|
+
manager.add_rule("BTC-USD", condition="regime_change", callback=notify)
|
|
16
|
+
manager.add_threshold("portfolio_var", threshold=0.05, direction="above")
|
|
17
|
+
|
|
18
|
+
# Check alerts against new data
|
|
19
|
+
manager.check(metric="BTC-USD", value=1, regime=2, previous_regime=1)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import time
|
|
26
|
+
from collections.abc import Callable, Sequence
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from enum import Enum
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Alert",
|
|
33
|
+
"AlertManager",
|
|
34
|
+
"AlertRule",
|
|
35
|
+
"AlertStatus",
|
|
36
|
+
"ThresholdDirection",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AlertStatus(Enum):
|
|
43
|
+
"""Status of a fired alert."""
|
|
44
|
+
|
|
45
|
+
TRIGGERED = "triggered"
|
|
46
|
+
ACKNOWLEDGED = "acknowledged"
|
|
47
|
+
RESOLVED = "resolved"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ThresholdDirection(Enum):
|
|
51
|
+
"""Direction for threshold-based alerts."""
|
|
52
|
+
|
|
53
|
+
ABOVE = "above"
|
|
54
|
+
BELOW = "below"
|
|
55
|
+
CROSS = "cross"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Alert:
|
|
60
|
+
"""A fired alert record.
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
rule_name: Name of the rule that triggered.
|
|
64
|
+
metric: The metric or symbol being monitored.
|
|
65
|
+
message: Human-readable alert message.
|
|
66
|
+
value: The value that triggered the alert.
|
|
67
|
+
timestamp: Unix timestamp when the alert fired.
|
|
68
|
+
status: Current alert status.
|
|
69
|
+
metadata: Additional context about the alert.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
rule_name: str
|
|
73
|
+
metric: str
|
|
74
|
+
message: str
|
|
75
|
+
value: float | None = None
|
|
76
|
+
timestamp: float = field(default_factory=time.time)
|
|
77
|
+
status: AlertStatus = AlertStatus.TRIGGERED
|
|
78
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
AlertCallback = Callable[[Alert], Any]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class AlertRule:
|
|
86
|
+
"""A configured alert rule.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
name: Unique name for this rule.
|
|
90
|
+
metric: The metric or symbol to monitor.
|
|
91
|
+
condition: Condition type (e.g. ``"regime_change"``,
|
|
92
|
+
``"threshold"``).
|
|
93
|
+
callback: Function to call when the alert fires.
|
|
94
|
+
threshold: Threshold value (for threshold alerts).
|
|
95
|
+
direction: Direction for threshold comparison.
|
|
96
|
+
cooldown_s: Minimum seconds between repeated firings
|
|
97
|
+
of the same rule. Defaults to 60.
|
|
98
|
+
enabled: Whether this rule is active.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
name: str
|
|
102
|
+
metric: str
|
|
103
|
+
condition: str
|
|
104
|
+
callback: AlertCallback | None = None
|
|
105
|
+
threshold: float | None = None
|
|
106
|
+
direction: ThresholdDirection = ThresholdDirection.ABOVE
|
|
107
|
+
cooldown_s: float = 60.0
|
|
108
|
+
enabled: bool = True
|
|
109
|
+
_last_fired: float = field(default=0.0, repr=False)
|
|
110
|
+
_last_value: float | None = field(default=None, repr=False)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AlertManager:
|
|
114
|
+
"""Manages alert rules, checks conditions, and maintains history.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
max_history: Maximum number of alerts to keep in the
|
|
118
|
+
history log. Defaults to 1000.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, max_history: int = 1000) -> None:
|
|
122
|
+
self._rules: dict[str, AlertRule] = {}
|
|
123
|
+
self._history: list[Alert] = []
|
|
124
|
+
self._max_history = max_history
|
|
125
|
+
self._global_callbacks: list[AlertCallback] = []
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def rules(self) -> dict[str, AlertRule]:
|
|
129
|
+
"""All configured alert rules, keyed by name."""
|
|
130
|
+
return dict(self._rules)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def history(self) -> list[Alert]:
|
|
134
|
+
"""List of all fired alerts (most recent last)."""
|
|
135
|
+
return list(self._history)
|
|
136
|
+
|
|
137
|
+
def on_alert(self, callback: AlertCallback) -> None:
|
|
138
|
+
"""Register a global callback for all alerts.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
callback: Function called whenever any alert fires.
|
|
142
|
+
"""
|
|
143
|
+
self._global_callbacks.append(callback)
|
|
144
|
+
|
|
145
|
+
def add_rule(
|
|
146
|
+
self,
|
|
147
|
+
metric: str,
|
|
148
|
+
condition: str = "regime_change",
|
|
149
|
+
callback: AlertCallback | None = None,
|
|
150
|
+
name: str | None = None,
|
|
151
|
+
cooldown_s: float = 60.0,
|
|
152
|
+
) -> AlertRule:
|
|
153
|
+
"""Add a rule-based alert.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
metric: The metric or symbol to monitor.
|
|
157
|
+
condition: Condition type. Currently supported:
|
|
158
|
+
``"regime_change"``.
|
|
159
|
+
callback: Optional callback when the alert fires.
|
|
160
|
+
name: Unique name. Auto-generated if not provided.
|
|
161
|
+
cooldown_s: Cooldown between repeated firings.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The created ``AlertRule``.
|
|
165
|
+
"""
|
|
166
|
+
if name is None:
|
|
167
|
+
name = f"{metric}_{condition}_{len(self._rules)}"
|
|
168
|
+
|
|
169
|
+
rule = AlertRule(
|
|
170
|
+
name=name,
|
|
171
|
+
metric=metric,
|
|
172
|
+
condition=condition,
|
|
173
|
+
callback=callback,
|
|
174
|
+
cooldown_s=cooldown_s,
|
|
175
|
+
)
|
|
176
|
+
self._rules[name] = rule
|
|
177
|
+
return rule
|
|
178
|
+
|
|
179
|
+
def add_threshold(
|
|
180
|
+
self,
|
|
181
|
+
metric: str,
|
|
182
|
+
threshold: float,
|
|
183
|
+
direction: str = "above",
|
|
184
|
+
callback: AlertCallback | None = None,
|
|
185
|
+
name: str | None = None,
|
|
186
|
+
cooldown_s: float = 60.0,
|
|
187
|
+
) -> AlertRule:
|
|
188
|
+
"""Add a threshold-based alert.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
metric: The metric or symbol to monitor.
|
|
192
|
+
threshold: The threshold value.
|
|
193
|
+
direction: One of ``"above"``, ``"below"``, or
|
|
194
|
+
``"cross"``.
|
|
195
|
+
callback: Optional callback when the alert fires.
|
|
196
|
+
name: Unique name. Auto-generated if not provided.
|
|
197
|
+
cooldown_s: Cooldown between repeated firings.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
The created ``AlertRule``.
|
|
201
|
+
"""
|
|
202
|
+
if name is None:
|
|
203
|
+
name = f"{metric}_threshold_{len(self._rules)}"
|
|
204
|
+
|
|
205
|
+
dir_enum = ThresholdDirection(direction.lower())
|
|
206
|
+
|
|
207
|
+
rule = AlertRule(
|
|
208
|
+
name=name,
|
|
209
|
+
metric=metric,
|
|
210
|
+
condition="threshold",
|
|
211
|
+
callback=callback,
|
|
212
|
+
threshold=threshold,
|
|
213
|
+
direction=dir_enum,
|
|
214
|
+
cooldown_s=cooldown_s,
|
|
215
|
+
)
|
|
216
|
+
self._rules[name] = rule
|
|
217
|
+
return rule
|
|
218
|
+
|
|
219
|
+
def remove_rule(self, name: str) -> None:
|
|
220
|
+
"""Remove an alert rule by name.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
name: The rule name to remove.
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
KeyError: If no rule with that name exists.
|
|
227
|
+
"""
|
|
228
|
+
if name not in self._rules:
|
|
229
|
+
raise KeyError(f"No alert rule named '{name}'")
|
|
230
|
+
del self._rules[name]
|
|
231
|
+
|
|
232
|
+
def enable_rule(self, name: str) -> None:
|
|
233
|
+
"""Enable a disabled alert rule.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
name: The rule name to enable.
|
|
237
|
+
"""
|
|
238
|
+
self._rules[name].enabled = True
|
|
239
|
+
|
|
240
|
+
def disable_rule(self, name: str) -> None:
|
|
241
|
+
"""Disable an alert rule without removing it.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
name: The rule name to disable.
|
|
245
|
+
"""
|
|
246
|
+
self._rules[name].enabled = False
|
|
247
|
+
|
|
248
|
+
def check(
|
|
249
|
+
self,
|
|
250
|
+
metric: str,
|
|
251
|
+
value: float | None = None,
|
|
252
|
+
regime: int | None = None,
|
|
253
|
+
previous_regime: int | None = None,
|
|
254
|
+
**metadata: Any,
|
|
255
|
+
) -> list[Alert]:
|
|
256
|
+
"""Check all rules for the given metric and fire matching alerts.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
metric: The metric or symbol being reported.
|
|
260
|
+
value: Current value of the metric.
|
|
261
|
+
regime: Current regime index (for regime change alerts).
|
|
262
|
+
previous_regime: Previous regime index.
|
|
263
|
+
**metadata: Additional context passed to the alert.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
List of alerts that were fired.
|
|
267
|
+
"""
|
|
268
|
+
now = time.time()
|
|
269
|
+
fired: list[Alert] = []
|
|
270
|
+
|
|
271
|
+
for rule in self._rules.values():
|
|
272
|
+
if not rule.enabled:
|
|
273
|
+
continue
|
|
274
|
+
if rule.metric != metric:
|
|
275
|
+
continue
|
|
276
|
+
if now - rule._last_fired < rule.cooldown_s:
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
alert = self._evaluate_rule(
|
|
280
|
+
rule, value, regime, previous_regime, metadata
|
|
281
|
+
)
|
|
282
|
+
if alert is not None:
|
|
283
|
+
rule._last_fired = now
|
|
284
|
+
rule._last_value = value
|
|
285
|
+
self._record_alert(alert)
|
|
286
|
+
fired.append(alert)
|
|
287
|
+
|
|
288
|
+
return fired
|
|
289
|
+
|
|
290
|
+
def check_many(
|
|
291
|
+
self,
|
|
292
|
+
updates: Sequence[dict[str, Any]],
|
|
293
|
+
) -> list[Alert]:
|
|
294
|
+
"""Check multiple metric updates at once.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
updates: Sequence of dicts, each with keys matching
|
|
298
|
+
the ``check()`` parameters.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
List of all alerts fired across all updates.
|
|
302
|
+
"""
|
|
303
|
+
all_fired: list[Alert] = []
|
|
304
|
+
for update in updates:
|
|
305
|
+
all_fired.extend(self.check(**update))
|
|
306
|
+
return all_fired
|
|
307
|
+
|
|
308
|
+
def clear_history(self) -> None:
|
|
309
|
+
"""Clear the alert history log."""
|
|
310
|
+
self._history.clear()
|
|
311
|
+
|
|
312
|
+
def _evaluate_rule(
|
|
313
|
+
self,
|
|
314
|
+
rule: AlertRule,
|
|
315
|
+
value: float | None,
|
|
316
|
+
regime: int | None,
|
|
317
|
+
previous_regime: int | None,
|
|
318
|
+
metadata: dict[str, Any],
|
|
319
|
+
) -> Alert | None:
|
|
320
|
+
"""Evaluate a single rule and return an Alert if triggered.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
rule: The rule to evaluate.
|
|
324
|
+
value: Current metric value.
|
|
325
|
+
regime: Current regime.
|
|
326
|
+
previous_regime: Previous regime.
|
|
327
|
+
metadata: Extra context.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
An ``Alert`` if the rule fires, otherwise None.
|
|
331
|
+
"""
|
|
332
|
+
if rule.condition == "regime_change":
|
|
333
|
+
if (
|
|
334
|
+
regime is not None
|
|
335
|
+
and previous_regime is not None
|
|
336
|
+
and regime != previous_regime
|
|
337
|
+
):
|
|
338
|
+
return Alert(
|
|
339
|
+
rule_name=rule.name,
|
|
340
|
+
metric=rule.metric,
|
|
341
|
+
message=(
|
|
342
|
+
f"Regime change on {rule.metric}: "
|
|
343
|
+
f"{previous_regime} -> {regime}"
|
|
344
|
+
),
|
|
345
|
+
value=float(regime),
|
|
346
|
+
metadata={
|
|
347
|
+
"previous_regime": previous_regime,
|
|
348
|
+
"new_regime": regime,
|
|
349
|
+
**metadata,
|
|
350
|
+
},
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
elif (
|
|
354
|
+
rule.condition == "threshold"
|
|
355
|
+
and value is not None
|
|
356
|
+
and rule.threshold is not None
|
|
357
|
+
):
|
|
358
|
+
triggered = False
|
|
359
|
+
if rule.direction == ThresholdDirection.ABOVE:
|
|
360
|
+
triggered = value > rule.threshold
|
|
361
|
+
elif rule.direction == ThresholdDirection.BELOW:
|
|
362
|
+
triggered = value < rule.threshold
|
|
363
|
+
elif rule.direction == ThresholdDirection.CROSS:
|
|
364
|
+
if rule._last_value is not None:
|
|
365
|
+
crossed_up = (
|
|
366
|
+
rule._last_value <= rule.threshold
|
|
367
|
+
and value > rule.threshold
|
|
368
|
+
)
|
|
369
|
+
crossed_down = (
|
|
370
|
+
rule._last_value >= rule.threshold
|
|
371
|
+
and value < rule.threshold
|
|
372
|
+
)
|
|
373
|
+
triggered = crossed_up or crossed_down
|
|
374
|
+
rule._last_value = value
|
|
375
|
+
|
|
376
|
+
if triggered:
|
|
377
|
+
return Alert(
|
|
378
|
+
rule_name=rule.name,
|
|
379
|
+
metric=rule.metric,
|
|
380
|
+
message=(
|
|
381
|
+
f"{rule.metric} is {rule.direction.value} "
|
|
382
|
+
f"threshold {rule.threshold}: {value}"
|
|
383
|
+
),
|
|
384
|
+
value=value,
|
|
385
|
+
metadata={"threshold": rule.threshold, **metadata},
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
def _record_alert(self, alert: Alert) -> None:
|
|
391
|
+
"""Record an alert and invoke callbacks.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
alert: The alert to record.
|
|
395
|
+
"""
|
|
396
|
+
self._history.append(alert)
|
|
397
|
+
if len(self._history) > self._max_history:
|
|
398
|
+
self._history = self._history[-self._max_history:]
|
|
399
|
+
|
|
400
|
+
# Fire rule-specific callback
|
|
401
|
+
rule = self._rules.get(alert.rule_name)
|
|
402
|
+
if rule is not None and rule.callback is not None:
|
|
403
|
+
try:
|
|
404
|
+
rule.callback(alert)
|
|
405
|
+
except Exception:
|
|
406
|
+
logger.exception(
|
|
407
|
+
"Error in alert callback for rule %s", rule.name
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Fire global callbacks
|
|
411
|
+
for cb in self._global_callbacks:
|
|
412
|
+
try:
|
|
413
|
+
cb(alert)
|
|
414
|
+
except Exception:
|
|
415
|
+
logger.exception("Error in global alert callback")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Benchmarking suite for QuantLite.
|
|
2
|
+
|
|
3
|
+
Provides head-to-head comparisons against baseline methods,
|
|
4
|
+
tail event backtesting, speed benchmarks, and a unified runner.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .compare import ComparisonResult, run_comparison
|
|
8
|
+
from .runner import BenchmarkReport, run_benchmarks
|
|
9
|
+
from .speed import SpeedResult, run_speed_benchmarks
|
|
10
|
+
from .tail_events import CrisisResult, run_tail_event_analysis
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"run_comparison",
|
|
14
|
+
"ComparisonResult",
|
|
15
|
+
"run_tail_event_analysis",
|
|
16
|
+
"CrisisResult",
|
|
17
|
+
"run_speed_benchmarks",
|
|
18
|
+
"SpeedResult",
|
|
19
|
+
"run_benchmarks",
|
|
20
|
+
"BenchmarkReport",
|
|
21
|
+
]
|