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.
Files changed (180) hide show
  1. {quantlite-1.0.2/src/quantlite.egg-info → quantlite-1.7.0}/PKG-INFO +29 -1
  2. {quantlite-1.0.2 → quantlite-1.7.0}/README.md +18 -0
  3. {quantlite-1.0.2 → quantlite-1.7.0}/pyproject.toml +12 -1
  4. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/__init__.py +26 -1
  5. quantlite-1.7.0/src/quantlite/alerts/__init__.py +415 -0
  6. quantlite-1.7.0/src/quantlite/benchmark/__init__.py +21 -0
  7. quantlite-1.7.0/src/quantlite/benchmark/compare.py +483 -0
  8. quantlite-1.7.0/src/quantlite/benchmark/runner.py +207 -0
  9. quantlite-1.7.0/src/quantlite/benchmark/speed.py +211 -0
  10. quantlite-1.7.0/src/quantlite/benchmark/tail_events.py +314 -0
  11. quantlite-1.7.0/src/quantlite/data/stream.py +410 -0
  12. quantlite-1.7.0/src/quantlite/explain/__init__.py +1 -0
  13. quantlite-1.7.0/src/quantlite/explain/attribution.py +368 -0
  14. quantlite-1.7.0/src/quantlite/explain/audit.py +325 -0
  15. quantlite-1.7.0/src/quantlite/explain/narratives.py +334 -0
  16. quantlite-1.7.0/src/quantlite/explain/whatif.py +308 -0
  17. quantlite-1.7.0/src/quantlite/portfolio/__init__.py +97 -0
  18. quantlite-1.7.0/src/quantlite/portfolio/dynamic_kelly.py +270 -0
  19. quantlite-1.7.0/src/quantlite/portfolio/ensemble.py +215 -0
  20. quantlite-1.7.0/src/quantlite/portfolio/regime_bl.py +278 -0
  21. quantlite-1.7.0/src/quantlite/portfolio/tail_risk_parity.py +336 -0
  22. quantlite-1.7.0/src/quantlite/portfolio/walkforward.py +288 -0
  23. quantlite-1.7.0/src/quantlite/regimes/online.py +273 -0
  24. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/risk/metrics.py +53 -1
  25. quantlite-1.7.0/src/quantlite/score/__init__.py +76 -0
  26. quantlite-1.7.0/src/quantlite/score/artifact.py +212 -0
  27. quantlite-1.7.0/src/quantlite/score/engine.py +496 -0
  28. quantlite-1.7.0/src/quantlite/score/integrity.py +240 -0
  29. quantlite-1.7.0/src/quantlite/score/monitoring.py +666 -0
  30. quantlite-1.7.0/src/quantlite/score/provenance.py +414 -0
  31. quantlite-1.7.0/src/quantlite/viz/alerts.py +173 -0
  32. quantlite-1.7.0/src/quantlite/viz/allocation.py +642 -0
  33. quantlite-1.7.0/src/quantlite/viz/benchmark.py +615 -0
  34. quantlite-1.7.0/src/quantlite/viz/explain.py +648 -0
  35. quantlite-1.7.0/src/quantlite/viz/online_regimes.py +211 -0
  36. quantlite-1.7.0/src/quantlite/viz/streaming.py +173 -0
  37. {quantlite-1.0.2 → quantlite-1.7.0/src/quantlite.egg-info}/PKG-INFO +29 -1
  38. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite.egg-info/SOURCES.txt +39 -0
  39. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite.egg-info/requires.txt +12 -0
  40. quantlite-1.7.0/tests/test_alerts.py +160 -0
  41. quantlite-1.7.0/tests/test_allocation_engine.py +358 -0
  42. quantlite-1.7.0/tests/test_benchmarks.py +150 -0
  43. quantlite-1.7.0/tests/test_explainability.py +219 -0
  44. quantlite-1.7.0/tests/test_monitoring.py +179 -0
  45. quantlite-1.7.0/tests/test_online_regime.py +137 -0
  46. quantlite-1.7.0/tests/test_provenance.py +172 -0
  47. quantlite-1.7.0/tests/test_score.py +175 -0
  48. quantlite-1.7.0/tests/test_stream.py +159 -0
  49. quantlite-1.0.2/src/quantlite/portfolio/__init__.py +0 -35
  50. {quantlite-1.0.2 → quantlite-1.7.0}/LICENSE +0 -0
  51. {quantlite-1.0.2 → quantlite-1.7.0}/setup.cfg +0 -0
  52. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/antifragile/__init__.py +0 -0
  53. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/backtesting/__init__.py +0 -0
  54. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/backtesting/analysis.py +0 -0
  55. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/backtesting/engine.py +0 -0
  56. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/backtesting/legacy.py +0 -0
  57. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/backtesting/signals.py +0 -0
  58. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/contagion/__init__.py +0 -0
  59. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/core/__init__.py +0 -0
  60. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/core/types.py +0 -0
  61. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/crypto/__init__.py +0 -0
  62. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/crypto/exchange.py +0 -0
  63. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/crypto/onchain.py +0 -0
  64. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/crypto/stablecoin.py +0 -0
  65. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/__init__.py +0 -0
  66. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/base.py +0 -0
  67. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/cache.py +0 -0
  68. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/crypto.py +0 -0
  69. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/fred.py +0 -0
  70. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/local.py +0 -0
  71. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/registry.py +0 -0
  72. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data/yahoo.py +0 -0
  73. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/data_generation.py +0 -0
  74. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/dependency/__init__.py +0 -0
  75. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/dependency/clustering.py +0 -0
  76. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/dependency/copulas.py +0 -0
  77. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/dependency/correlation.py +0 -0
  78. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/distributions/__init__.py +0 -0
  79. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/distributions/fat_tails.py +0 -0
  80. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/diversification/__init__.py +0 -0
  81. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/ergodicity/__init__.py +0 -0
  82. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/factors/__init__.py +0 -0
  83. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/factors/classical.py +0 -0
  84. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/factors/custom.py +0 -0
  85. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/factors/tail_risk.py +0 -0
  86. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/forensics/__init__.py +0 -0
  87. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/instruments/__init__.py +0 -0
  88. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/instruments/bond_pricing.py +0 -0
  89. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/instruments/exotic_options.py +0 -0
  90. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/instruments/option_pricing.py +0 -0
  91. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/metrics.py +0 -0
  92. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/monte_carlo.py +0 -0
  93. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/network/__init__.py +0 -0
  94. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/overfit/__init__.py +0 -0
  95. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/pipeline.py +0 -0
  96. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/portfolio/optimisation.py +0 -0
  97. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/portfolio/rebalancing.py +0 -0
  98. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regime_integration/__init__.py +0 -0
  99. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regime_integration/portfolio.py +0 -0
  100. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regime_integration/reporting.py +0 -0
  101. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regime_integration/risk.py +0 -0
  102. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regimes/__init__.py +0 -0
  103. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regimes/changepoint.py +0 -0
  104. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regimes/conditional.py +0 -0
  105. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/regimes/hmm.py +0 -0
  106. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/report/__init__.py +0 -0
  107. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/report/html_renderer.py +0 -0
  108. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/report/pdf_renderer.py +0 -0
  109. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/report/sections.py +0 -0
  110. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/report/tearsheet.py +0 -0
  111. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/resample/__init__.py +0 -0
  112. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/risk/__init__.py +0 -0
  113. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/risk/evt.py +0 -0
  114. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/scenarios/__init__.py +0 -0
  115. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/simulation/__init__.py +0 -0
  116. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/simulation/copula_mc.py +0 -0
  117. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/simulation/evt_simulation.py +0 -0
  118. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/simulation/regime_mc.py +0 -0
  119. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/visualisation.py +0 -0
  120. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/__init__.py +0 -0
  121. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/dependency.py +0 -0
  122. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/__init__.py +0 -0
  123. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/dependency.py +0 -0
  124. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/portfolio.py +0 -0
  125. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/regimes.py +0 -0
  126. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/risk.py +0 -0
  127. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/plotly_backend/theme.py +0 -0
  128. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/portfolio.py +0 -0
  129. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/regimes.py +0 -0
  130. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/risk.py +0 -0
  131. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite/viz/theme.py +0 -0
  132. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite.egg-info/dependency_links.txt +0 -0
  133. {quantlite-1.0.2 → quantlite-1.7.0}/src/quantlite.egg-info/top_level.txt +0 -0
  134. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_analysis.py +0 -0
  135. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_antifragile.py +0 -0
  136. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_backtesting.py +0 -0
  137. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_changepoint.py +0 -0
  138. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_clustering.py +0 -0
  139. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_conditional.py +0 -0
  140. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_contagion.py +0 -0
  141. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_copulas.py +0 -0
  142. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_correlation.py +0 -0
  143. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_crypto_exchange.py +0 -0
  144. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_crypto_onchain.py +0 -0
  145. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_crypto_stablecoin.py +0 -0
  146. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_data_connectors.py +0 -0
  147. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_data_generation.py +0 -0
  148. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_diversification.py +0 -0
  149. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_engine.py +0 -0
  150. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_ergodicity.py +0 -0
  151. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_evt.py +0 -0
  152. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_factors_classical.py +0 -0
  153. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_factors_custom.py +0 -0
  154. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_factors_tail_risk.py +0 -0
  155. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_fat_tails.py +0 -0
  156. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_forensics.py +0 -0
  157. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_hmm.py +0 -0
  158. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_instruments.py +0 -0
  159. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_metrics.py +0 -0
  160. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_monte_carlo.py +0 -0
  161. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_network.py +0 -0
  162. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_optimisation.py +0 -0
  163. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_overfit.py +0 -0
  164. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_pipeline.py +0 -0
  165. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_plotly_viz.py +0 -0
  166. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_rebalancing.py +0 -0
  167. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_regime_integration.py +0 -0
  168. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_report.py +0 -0
  169. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_resample.py +0 -0
  170. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_risk_metrics.py +0 -0
  171. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_scenarios.py +0 -0
  172. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_signals.py +0 -0
  173. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_sim_copula.py +0 -0
  174. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_sim_evt.py +0 -0
  175. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_sim_regime.py +0 -0
  176. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_visualisation.py +0 -0
  177. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_viz.py +0 -0
  178. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_viz_dependency.py +0 -0
  179. {quantlite-1.0.2 → quantlite-1.7.0}/tests/test_viz_portfolio.py +0 -0
  180. {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.2
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.2"
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.2"
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
+ ]