quantlite 0.6.0__tar.gz → 0.8.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 (126) hide show
  1. {quantlite-0.6.0/src/quantlite.egg-info → quantlite-0.8.0}/PKG-INFO +67 -1
  2. {quantlite-0.6.0 → quantlite-0.8.0}/README.md +66 -0
  3. {quantlite-0.6.0 → quantlite-0.8.0}/pyproject.toml +4 -1
  4. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/__init__.py +3 -0
  5. quantlite-0.8.0/src/quantlite/crypto/__init__.py +43 -0
  6. quantlite-0.8.0/src/quantlite/crypto/exchange.py +390 -0
  7. quantlite-0.8.0/src/quantlite/crypto/onchain.py +384 -0
  8. quantlite-0.8.0/src/quantlite/crypto/stablecoin.py +380 -0
  9. quantlite-0.8.0/src/quantlite/factors/__init__.py +43 -0
  10. quantlite-0.8.0/src/quantlite/factors/classical.py +288 -0
  11. quantlite-0.8.0/src/quantlite/factors/custom.py +340 -0
  12. quantlite-0.8.0/src/quantlite/factors/tail_risk.py +296 -0
  13. {quantlite-0.6.0 → quantlite-0.8.0/src/quantlite.egg-info}/PKG-INFO +67 -1
  14. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite.egg-info/SOURCES.txt +14 -0
  15. quantlite-0.8.0/tests/test_crypto_exchange.py +167 -0
  16. quantlite-0.8.0/tests/test_crypto_onchain.py +161 -0
  17. quantlite-0.8.0/tests/test_crypto_stablecoin.py +186 -0
  18. quantlite-0.8.0/tests/test_factors_classical.py +208 -0
  19. quantlite-0.8.0/tests/test_factors_custom.py +205 -0
  20. quantlite-0.8.0/tests/test_factors_tail_risk.py +224 -0
  21. {quantlite-0.6.0 → quantlite-0.8.0}/LICENSE +0 -0
  22. {quantlite-0.6.0 → quantlite-0.8.0}/setup.cfg +0 -0
  23. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/antifragile/__init__.py +0 -0
  24. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/backtesting/__init__.py +0 -0
  25. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/backtesting/analysis.py +0 -0
  26. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/backtesting/engine.py +0 -0
  27. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/backtesting/legacy.py +0 -0
  28. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/backtesting/signals.py +0 -0
  29. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/contagion/__init__.py +0 -0
  30. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/core/__init__.py +0 -0
  31. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/core/types.py +0 -0
  32. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/__init__.py +0 -0
  33. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/base.py +0 -0
  34. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/cache.py +0 -0
  35. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/crypto.py +0 -0
  36. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/fred.py +0 -0
  37. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/local.py +0 -0
  38. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/registry.py +0 -0
  39. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/yahoo.py +0 -0
  40. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data_generation.py +0 -0
  41. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/dependency/__init__.py +0 -0
  42. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/dependency/clustering.py +0 -0
  43. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/dependency/copulas.py +0 -0
  44. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/dependency/correlation.py +0 -0
  45. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/distributions/__init__.py +0 -0
  46. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/distributions/fat_tails.py +0 -0
  47. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/diversification/__init__.py +0 -0
  48. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/ergodicity/__init__.py +0 -0
  49. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/forensics/__init__.py +0 -0
  50. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/instruments/__init__.py +0 -0
  51. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/instruments/bond_pricing.py +0 -0
  52. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/instruments/exotic_options.py +0 -0
  53. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/instruments/option_pricing.py +0 -0
  54. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/metrics.py +0 -0
  55. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/monte_carlo.py +0 -0
  56. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/network/__init__.py +0 -0
  57. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/overfit/__init__.py +0 -0
  58. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/portfolio/__init__.py +0 -0
  59. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/portfolio/optimisation.py +0 -0
  60. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/portfolio/rebalancing.py +0 -0
  61. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/regimes/__init__.py +0 -0
  62. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/regimes/changepoint.py +0 -0
  63. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/regimes/conditional.py +0 -0
  64. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/regimes/hmm.py +0 -0
  65. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/report/__init__.py +0 -0
  66. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/report/html_renderer.py +0 -0
  67. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/report/pdf_renderer.py +0 -0
  68. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/report/sections.py +0 -0
  69. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/report/tearsheet.py +0 -0
  70. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/resample/__init__.py +0 -0
  71. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/risk/__init__.py +0 -0
  72. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/risk/evt.py +0 -0
  73. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/risk/metrics.py +0 -0
  74. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/scenarios/__init__.py +0 -0
  75. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/visualisation.py +0 -0
  76. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/__init__.py +0 -0
  77. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/dependency.py +0 -0
  78. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/__init__.py +0 -0
  79. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/dependency.py +0 -0
  80. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/portfolio.py +0 -0
  81. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/regimes.py +0 -0
  82. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/risk.py +0 -0
  83. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/theme.py +0 -0
  84. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/portfolio.py +0 -0
  85. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/regimes.py +0 -0
  86. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/risk.py +0 -0
  87. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/theme.py +0 -0
  88. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite.egg-info/dependency_links.txt +0 -0
  89. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite.egg-info/requires.txt +0 -0
  90. {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite.egg-info/top_level.txt +0 -0
  91. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_analysis.py +0 -0
  92. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_antifragile.py +0 -0
  93. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_backtesting.py +0 -0
  94. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_changepoint.py +0 -0
  95. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_clustering.py +0 -0
  96. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_conditional.py +0 -0
  97. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_contagion.py +0 -0
  98. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_copulas.py +0 -0
  99. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_correlation.py +0 -0
  100. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_data_connectors.py +0 -0
  101. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_data_generation.py +0 -0
  102. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_diversification.py +0 -0
  103. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_engine.py +0 -0
  104. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_ergodicity.py +0 -0
  105. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_evt.py +0 -0
  106. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_fat_tails.py +0 -0
  107. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_forensics.py +0 -0
  108. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_hmm.py +0 -0
  109. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_instruments.py +0 -0
  110. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_metrics.py +0 -0
  111. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_monte_carlo.py +0 -0
  112. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_network.py +0 -0
  113. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_optimisation.py +0 -0
  114. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_overfit.py +0 -0
  115. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_plotly_viz.py +0 -0
  116. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_rebalancing.py +0 -0
  117. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_report.py +0 -0
  118. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_resample.py +0 -0
  119. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_risk_metrics.py +0 -0
  120. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_scenarios.py +0 -0
  121. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_signals.py +0 -0
  122. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_visualisation.py +0 -0
  123. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_viz.py +0 -0
  124. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_viz_dependency.py +0 -0
  125. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_viz_portfolio.py +0 -0
  126. {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_viz_regimes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantlite
3
- Version: 0.6.0
3
+ Version: 0.8.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
@@ -710,6 +710,72 @@ Same Stephen Few theme, same muted palette, but with hover info, zoom, and nativ
710
710
  | `quantlite.contagion` | CoVaR, Delta CoVaR, MES, Granger causality, causal networks |
711
711
  | `quantlite.network` | Correlation networks, eigenvector centrality, cascade simulation, community detection |
712
712
  | `quantlite.diversification` | ENB, entropy diversification, tail diversification, diversification ratio, Herfindahl index |
713
+ | `quantlite.crypto.stablecoin` | Depeg probability, peg deviation tracking, recovery time, reserve risk scoring |
714
+ | `quantlite.crypto.exchange` | Exchange concentration (HHI), wallet risk, proof of reserves, liquidity risk, slippage |
715
+ | `quantlite.crypto.onchain` | Wallet exposure, TVL tracking, DeFi dependency graphs, smart contract risk scoring |
716
+ | `quantlite.factors.classical` | Fama-French three/five-factor, Carhart four-factor, factor attribution, factor summary |
717
+ | `quantlite.factors.custom` | CustomFactor, significance testing, correlation matrix, factor portfolios, decay analysis |
718
+ | `quantlite.factors.tail_risk` | CVaR decomposition, regime factor exposure, crowding score, tail factor beta |
719
+
720
+ ## v0.8: Factor Models
721
+
722
+ Three modules for comprehensive factor analysis: classical academic models, custom factor tools, and tail-risk-aware factor decomposition.
723
+
724
+ ### Classical Factor Models
725
+
726
+ Decompose returns into systematic factor exposures and genuine alpha.
727
+
728
+ ```python
729
+ from quantlite.factors import fama_french_three, factor_summary
730
+
731
+ # Fama-French three-factor regression
732
+ result = fama_french_three(fund_returns, market, smb, hml)
733
+ print(f"Alpha: {result['alpha']:.5f} (t={result['t_stats']['alpha']:.2f})")
734
+ print(f"Market beta: {result['betas']['market']:.3f}")
735
+ print(f"R-squared: {result['r_squared']:.3f}")
736
+ ```
737
+
738
+ ![Factor Betas](docs/images/factor_betas.png)
739
+
740
+ ### Custom Factor Tools
741
+
742
+ Build, test, and analyse proprietary factors.
743
+
744
+ ```python
745
+ from quantlite.factors import CustomFactor, factor_portfolio, factor_decay
746
+
747
+ # Test factor decay
748
+ decay = factor_decay(returns, momentum_signal, max_lag=20)
749
+ print(f"Half-life: {decay['half_life']:.1f} periods")
750
+
751
+ # Build long-short portfolios
752
+ result = factor_portfolio(stock_returns, factor_values, n_quantiles=5)
753
+ print(f"Long-short spread: {result['spread'] * 252:.2%} annualised")
754
+ ```
755
+
756
+ ![Factor Quintiles](docs/images/factor_quintiles.png)
757
+
758
+ ### Tail Risk Factor Analysis
759
+
760
+ Understand how factor exposures behave in the tails and across regimes.
761
+
762
+ ```python
763
+ from quantlite.factors import tail_factor_beta, factor_crowding_score
764
+
765
+ # Tail betas: how exposures amplify in crises
766
+ result = tail_factor_beta(returns, [market, value], ["Market", "Value"])
767
+ for name in ["Market", "Value"]:
768
+ print(f"{name}: full={result['full_betas'][name]:.2f}, "
769
+ f"tail={result['tail_betas'][name]:.2f}")
770
+
771
+ # Factor crowding detection
772
+ crowd = factor_crowding_score([value_rets, momentum_rets])
773
+ print(f"Crowding score: {crowd['current_score']:.3f}")
774
+ ```
775
+
776
+ ![Factor Crowding](docs/images/factor_crowding.png)
777
+
778
+ See [docs/factors_classical.md](docs/factors_classical.md), [docs/factors_custom.md](docs/factors_custom.md), and [docs/factors_tail_risk.md](docs/factors_tail_risk.md) for the full API reference.
713
779
 
714
780
  ## v0.4: The Taleb Stack
715
781
 
@@ -629,6 +629,72 @@ Same Stephen Few theme, same muted palette, but with hover info, zoom, and nativ
629
629
  | `quantlite.contagion` | CoVaR, Delta CoVaR, MES, Granger causality, causal networks |
630
630
  | `quantlite.network` | Correlation networks, eigenvector centrality, cascade simulation, community detection |
631
631
  | `quantlite.diversification` | ENB, entropy diversification, tail diversification, diversification ratio, Herfindahl index |
632
+ | `quantlite.crypto.stablecoin` | Depeg probability, peg deviation tracking, recovery time, reserve risk scoring |
633
+ | `quantlite.crypto.exchange` | Exchange concentration (HHI), wallet risk, proof of reserves, liquidity risk, slippage |
634
+ | `quantlite.crypto.onchain` | Wallet exposure, TVL tracking, DeFi dependency graphs, smart contract risk scoring |
635
+ | `quantlite.factors.classical` | Fama-French three/five-factor, Carhart four-factor, factor attribution, factor summary |
636
+ | `quantlite.factors.custom` | CustomFactor, significance testing, correlation matrix, factor portfolios, decay analysis |
637
+ | `quantlite.factors.tail_risk` | CVaR decomposition, regime factor exposure, crowding score, tail factor beta |
638
+
639
+ ## v0.8: Factor Models
640
+
641
+ Three modules for comprehensive factor analysis: classical academic models, custom factor tools, and tail-risk-aware factor decomposition.
642
+
643
+ ### Classical Factor Models
644
+
645
+ Decompose returns into systematic factor exposures and genuine alpha.
646
+
647
+ ```python
648
+ from quantlite.factors import fama_french_three, factor_summary
649
+
650
+ # Fama-French three-factor regression
651
+ result = fama_french_three(fund_returns, market, smb, hml)
652
+ print(f"Alpha: {result['alpha']:.5f} (t={result['t_stats']['alpha']:.2f})")
653
+ print(f"Market beta: {result['betas']['market']:.3f}")
654
+ print(f"R-squared: {result['r_squared']:.3f}")
655
+ ```
656
+
657
+ ![Factor Betas](docs/images/factor_betas.png)
658
+
659
+ ### Custom Factor Tools
660
+
661
+ Build, test, and analyse proprietary factors.
662
+
663
+ ```python
664
+ from quantlite.factors import CustomFactor, factor_portfolio, factor_decay
665
+
666
+ # Test factor decay
667
+ decay = factor_decay(returns, momentum_signal, max_lag=20)
668
+ print(f"Half-life: {decay['half_life']:.1f} periods")
669
+
670
+ # Build long-short portfolios
671
+ result = factor_portfolio(stock_returns, factor_values, n_quantiles=5)
672
+ print(f"Long-short spread: {result['spread'] * 252:.2%} annualised")
673
+ ```
674
+
675
+ ![Factor Quintiles](docs/images/factor_quintiles.png)
676
+
677
+ ### Tail Risk Factor Analysis
678
+
679
+ Understand how factor exposures behave in the tails and across regimes.
680
+
681
+ ```python
682
+ from quantlite.factors import tail_factor_beta, factor_crowding_score
683
+
684
+ # Tail betas: how exposures amplify in crises
685
+ result = tail_factor_beta(returns, [market, value], ["Market", "Value"])
686
+ for name in ["Market", "Value"]:
687
+ print(f"{name}: full={result['full_betas'][name]:.2f}, "
688
+ f"tail={result['tail_betas'][name]:.2f}")
689
+
690
+ # Factor crowding detection
691
+ crowd = factor_crowding_score([value_rets, momentum_rets])
692
+ print(f"Crowding score: {crowd['current_score']:.3f}")
693
+ ```
694
+
695
+ ![Factor Crowding](docs/images/factor_crowding.png)
696
+
697
+ See [docs/factors_classical.md](docs/factors_classical.md), [docs/factors_custom.md](docs/factors_custom.md), and [docs/factors_tail_risk.md](docs/factors_tail_risk.md) for the full API reference.
632
698
 
633
699
  ## v0.4: The Taleb Stack
634
700
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quantlite"
7
- version = "0.6.0"
7
+ version = "0.8.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" }
@@ -73,3 +73,6 @@ python_version = "3.10"
73
73
  warn_return_any = true
74
74
  warn_unused_configs = true
75
75
  disallow_untyped_defs = false
76
+
77
+ [tool.pytest.ini_options]
78
+ testpaths = ["tests"]
@@ -75,11 +75,14 @@ __all__ = [
75
75
  "network",
76
76
  # Diversification analysis
77
77
  "diversification",
78
+ # Crypto-native risk
79
+ "crypto",
78
80
  ]
79
81
 
80
82
  from . import ( # noqa: E402
81
83
  antifragile,
82
84
  contagion,
85
+ crypto,
83
86
  diversification,
84
87
  ergodicity,
85
88
  forensics,
@@ -0,0 +1,43 @@
1
+ """Crypto-native risk analysis tools.
2
+
3
+ Provides stablecoin depeg analysis, exchange risk assessment,
4
+ and on-chain risk metrics for crypto-native portfolios.
5
+ """
6
+
7
+ from .exchange import (
8
+ concentration_score,
9
+ liquidity_risk,
10
+ proof_of_reserves_check,
11
+ slippage_estimate,
12
+ wallet_risk_assessment,
13
+ )
14
+ from .onchain import (
15
+ defi_dependency_graph,
16
+ smart_contract_risk_score,
17
+ tvl_tracker,
18
+ wallet_exposure,
19
+ )
20
+ from .stablecoin import (
21
+ HISTORICAL_DEPEGS,
22
+ depeg_probability,
23
+ depeg_recovery_time,
24
+ peg_deviation_tracker,
25
+ reserve_risk_score,
26
+ )
27
+
28
+ __all__ = [
29
+ "HISTORICAL_DEPEGS",
30
+ "concentration_score",
31
+ "defi_dependency_graph",
32
+ "depeg_probability",
33
+ "depeg_recovery_time",
34
+ "liquidity_risk",
35
+ "peg_deviation_tracker",
36
+ "proof_of_reserves_check",
37
+ "reserve_risk_score",
38
+ "slippage_estimate",
39
+ "smart_contract_risk_score",
40
+ "tvl_tracker",
41
+ "wallet_exposure",
42
+ "wallet_risk_assessment",
43
+ ]
@@ -0,0 +1,390 @@
1
+ """Exchange risk analysis: concentration, wallet risk, reserves, and liquidity.
2
+
3
+ Tools for assessing counterparty risk across crypto exchanges,
4
+ including balance concentration, hot/cold wallet ratios, proof of
5
+ reserves verification, and order book liquidity analysis.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import numpy as np
11
+
12
+ __all__ = [
13
+ "concentration_score",
14
+ "liquidity_risk",
15
+ "proof_of_reserves_check",
16
+ "slippage_estimate",
17
+ "wallet_risk_assessment",
18
+ ]
19
+
20
+
21
+ def concentration_score(balances_by_exchange):
22
+ """Compute HHI-based concentration score across exchanges.
23
+
24
+ The Herfindahl-Hirschman Index (HHI) measures how concentrated
25
+ holdings are across exchanges. An HHI near 1.0 means all funds
26
+ sit on a single exchange (maximum counterparty risk).
27
+
28
+ Parameters
29
+ ----------
30
+ balances_by_exchange : dict
31
+ Mapping of exchange name to balance value.
32
+
33
+ Returns
34
+ -------
35
+ dict
36
+ Dictionary with keys:
37
+ - hhi: float in [0, 1], Herfindahl-Hirschman Index
38
+ - normalised_hhi: HHI normalised to [0, 1] accounting for number of exchanges
39
+ - risk_rating: qualitative rating
40
+ - shares: dict of exchange to percentage share
41
+ - dominant_exchange: name of exchange with largest share
42
+ - n_exchanges: number of exchanges with non-zero balance
43
+ """
44
+ total = sum(balances_by_exchange.values())
45
+ if total <= 0:
46
+ return {
47
+ "hhi": 1.0,
48
+ "normalised_hhi": 1.0,
49
+ "risk_rating": "critical",
50
+ "shares": {},
51
+ "dominant_exchange": None,
52
+ "n_exchanges": 0,
53
+ }
54
+
55
+ shares = {}
56
+ for name, balance in balances_by_exchange.items():
57
+ shares[name] = balance / total
58
+
59
+ hhi = sum(s ** 2 for s in shares.values())
60
+ n = len(shares)
61
+ normalised_hhi = (hhi - 1.0 / n) / (1.0 - 1.0 / n) if n > 1 else 1.0
62
+
63
+ normalised_hhi = float(np.clip(normalised_hhi, 0.0, 1.0))
64
+
65
+ dominant = max(shares, key=shares.get)
66
+
67
+ if normalised_hhi > 0.75:
68
+ risk_rating = "critical"
69
+ elif normalised_hhi > 0.50:
70
+ risk_rating = "high"
71
+ elif normalised_hhi > 0.25:
72
+ risk_rating = "medium"
73
+ else:
74
+ risk_rating = "low"
75
+
76
+ return {
77
+ "hhi": float(hhi),
78
+ "normalised_hhi": normalised_hhi,
79
+ "risk_rating": risk_rating,
80
+ "shares": {k: float(v) for k, v in shares.items()},
81
+ "dominant_exchange": dominant,
82
+ "n_exchanges": n,
83
+ }
84
+
85
+
86
+ def wallet_risk_assessment(hot_pct, cold_pct, total_value):
87
+ """Score hot/cold wallet ratio risk.
88
+
89
+ Evaluates the risk of a wallet allocation strategy based on
90
+ the proportion of assets in hot wallets (online, higher risk)
91
+ versus cold wallets (offline, lower risk).
92
+
93
+ Parameters
94
+ ----------
95
+ hot_pct : float
96
+ Percentage of total value in hot wallets (0-100).
97
+ cold_pct : float
98
+ Percentage of total value in cold wallets (0-100).
99
+ total_value : float
100
+ Total value across all wallets.
101
+
102
+ Returns
103
+ -------
104
+ dict
105
+ Dictionary with keys:
106
+ - risk_score: float in [0, 1], higher = riskier
107
+ - risk_rating: qualitative rating
108
+ - hot_value: absolute value in hot wallets
109
+ - cold_value: absolute value in cold wallets
110
+ - recommendations: list of risk mitigation suggestions
111
+ """
112
+ hot_frac = hot_pct / 100.0
113
+ cold_frac = cold_pct / 100.0
114
+
115
+ hot_value = total_value * hot_frac
116
+ cold_value = total_value * cold_frac
117
+
118
+ # Risk score: heavily penalise high hot wallet ratios
119
+ # Industry best practice: < 5% hot
120
+ if hot_pct <= 2:
121
+ risk_score = 0.05
122
+ elif hot_pct <= 5:
123
+ risk_score = 0.15
124
+ elif hot_pct <= 10:
125
+ risk_score = 0.30
126
+ elif hot_pct <= 20:
127
+ risk_score = 0.50
128
+ elif hot_pct <= 50:
129
+ risk_score = 0.75
130
+ else:
131
+ risk_score = 0.95
132
+
133
+ # Scale by total value (larger holdings are higher risk)
134
+ if total_value > 1_000_000_000:
135
+ risk_score = min(1.0, risk_score * 1.15)
136
+ elif total_value > 100_000_000:
137
+ risk_score = min(1.0, risk_score * 1.05)
138
+
139
+ recommendations = []
140
+ if hot_pct > 10:
141
+ recommendations.append(
142
+ f"Reduce hot wallet exposure from {hot_pct:.1f}% to below 10%"
143
+ )
144
+ if hot_pct > 5:
145
+ recommendations.append("Implement multi-signature controls on hot wallets")
146
+ if hot_value > 50_000_000:
147
+ recommendations.append(
148
+ f"Hot wallet value ({hot_value / 1e6:.0f}M) exceeds prudent limits; "
149
+ "consider tiered withdrawal architecture"
150
+ )
151
+ if cold_pct < 80:
152
+ recommendations.append("Increase cold storage allocation to at least 80%")
153
+ if total_value > 500_000_000 and hot_pct > 3:
154
+ recommendations.append(
155
+ "For holdings above $500M, target hot wallet ratio below 3%"
156
+ )
157
+
158
+ if risk_score >= 0.75:
159
+ risk_rating = "critical"
160
+ elif risk_score >= 0.50:
161
+ risk_rating = "high"
162
+ elif risk_score >= 0.25:
163
+ risk_rating = "medium"
164
+ else:
165
+ risk_rating = "low"
166
+
167
+ return {
168
+ "risk_score": float(risk_score),
169
+ "risk_rating": risk_rating,
170
+ "hot_value": float(hot_value),
171
+ "cold_value": float(cold_value),
172
+ "recommendations": recommendations,
173
+ }
174
+
175
+
176
+ def proof_of_reserves_check(claimed_reserves, on_chain_verified):
177
+ """Compare claimed reserves against on-chain verified amounts.
178
+
179
+ Assesses the credibility of an exchange's proof-of-reserves
180
+ disclosure by comparing claimed totals with independently
181
+ verifiable on-chain data.
182
+
183
+ Parameters
184
+ ----------
185
+ claimed_reserves : dict
186
+ Mapping of asset name to claimed reserve amount.
187
+ on_chain_verified : dict
188
+ Mapping of asset name to on-chain verified amount.
189
+
190
+ Returns
191
+ -------
192
+ dict
193
+ Dictionary with keys:
194
+ - overall_ratio: total verified / total claimed
195
+ - per_asset: dict of asset to verification details
196
+ - fully_verified: bool, True if all assets >= 100% verified
197
+ - risk_rating: qualitative rating
198
+ - warnings: list of specific concerns
199
+ """
200
+ per_asset = {}
201
+ total_claimed = 0.0
202
+ total_verified = 0.0
203
+ warnings = []
204
+
205
+ all_assets = set(list(claimed_reserves.keys()) + list(on_chain_verified.keys()))
206
+
207
+ for asset in sorted(all_assets):
208
+ claimed = claimed_reserves.get(asset, 0.0)
209
+ verified = on_chain_verified.get(asset, 0.0)
210
+
211
+ total_claimed += claimed
212
+ total_verified += verified
213
+
214
+ ratio = verified / claimed if claimed > 0 else 0.0
215
+
216
+ per_asset[asset] = {
217
+ "claimed": float(claimed),
218
+ "verified": float(verified),
219
+ "ratio": float(ratio),
220
+ "shortfall": float(max(0, claimed - verified)),
221
+ }
222
+
223
+ if claimed > 0 and verified == 0:
224
+ warnings.append(f"{asset}: claimed {claimed:.2f} but nothing verified on-chain")
225
+ elif ratio < 0.95:
226
+ warnings.append(
227
+ f"{asset}: only {ratio:.1%} verified (shortfall: {claimed - verified:.2f})"
228
+ )
229
+
230
+ if asset in on_chain_verified and asset not in claimed_reserves:
231
+ warnings.append(f"{asset}: found on-chain but not in claimed reserves")
232
+
233
+ overall_ratio = total_verified / total_claimed if total_claimed > 0 else 0.0
234
+ fully_verified = all(
235
+ per_asset[a]["ratio"] >= 1.0 for a in per_asset if per_asset[a]["claimed"] > 0
236
+ )
237
+
238
+ if overall_ratio >= 1.0 and fully_verified:
239
+ risk_rating = "low"
240
+ elif overall_ratio >= 0.95:
241
+ risk_rating = "medium"
242
+ elif overall_ratio >= 0.80:
243
+ risk_rating = "high"
244
+ else:
245
+ risk_rating = "critical"
246
+
247
+ return {
248
+ "overall_ratio": float(overall_ratio),
249
+ "per_asset": per_asset,
250
+ "fully_verified": fully_verified,
251
+ "risk_rating": risk_rating,
252
+ "warnings": warnings,
253
+ }
254
+
255
+
256
+ def liquidity_risk(order_book_depth, position_size):
257
+ """Estimate unwind time and risk given order book depth.
258
+
259
+ Calculates how many periods it would take to unwind a position
260
+ given the available order book depth, and assesses the associated
261
+ liquidity risk.
262
+
263
+ Parameters
264
+ ----------
265
+ order_book_depth : float
266
+ Average available depth per period (e.g. daily volume at
267
+ acceptable slippage).
268
+ position_size : float
269
+ Total position size to unwind.
270
+
271
+ Returns
272
+ -------
273
+ dict
274
+ Dictionary with keys:
275
+ - periods_to_unwind: estimated periods to fully unwind
276
+ - position_to_depth_ratio: position size relative to depth
277
+ - risk_rating: qualitative rating
278
+ - daily_participation_rate: suggested daily rate (max 10% of depth)
279
+ - recommended_unwind_periods: periods at conservative participation
280
+ """
281
+ if order_book_depth <= 0:
282
+ return {
283
+ "periods_to_unwind": float("inf"),
284
+ "position_to_depth_ratio": float("inf"),
285
+ "risk_rating": "critical",
286
+ "daily_participation_rate": 0.0,
287
+ "recommended_unwind_periods": float("inf"),
288
+ }
289
+
290
+ ratio = position_size / order_book_depth
291
+ periods = ratio # 100% participation
292
+
293
+ # Conservative: participate at most 10% of depth per period
294
+ conservative_rate = order_book_depth * 0.10
295
+ recommended_periods = position_size / conservative_rate if conservative_rate > 0 else float("inf")
296
+
297
+ if ratio <= 0.5:
298
+ risk_rating = "low"
299
+ elif ratio <= 2.0:
300
+ risk_rating = "medium"
301
+ elif ratio <= 10.0:
302
+ risk_rating = "high"
303
+ else:
304
+ risk_rating = "critical"
305
+
306
+ return {
307
+ "periods_to_unwind": float(periods),
308
+ "position_to_depth_ratio": float(ratio),
309
+ "risk_rating": risk_rating,
310
+ "daily_participation_rate": float(conservative_rate),
311
+ "recommended_unwind_periods": float(recommended_periods),
312
+ }
313
+
314
+
315
+ def slippage_estimate(order_book, trade_size):
316
+ """Estimate price impact for a given trade size.
317
+
318
+ Walks through an order book (list of price/quantity levels)
319
+ to estimate the volume-weighted average price and resulting
320
+ slippage for a given trade size.
321
+
322
+ Parameters
323
+ ----------
324
+ order_book : list of tuples
325
+ List of (price, quantity) tuples, sorted by price
326
+ (ascending for asks, descending for bids).
327
+ trade_size : float
328
+ Total quantity to trade.
329
+
330
+ Returns
331
+ -------
332
+ dict
333
+ Dictionary with keys:
334
+ - vwap: volume-weighted average execution price
335
+ - best_price: best available price (first level)
336
+ - worst_price: worst price touched
337
+ - slippage_pct: percentage slippage from best price
338
+ - levels_consumed: number of order book levels consumed
339
+ - unfilled: quantity that could not be filled
340
+ - risk_rating: qualitative rating
341
+ """
342
+ if not order_book or trade_size <= 0:
343
+ return {
344
+ "vwap": 0.0,
345
+ "best_price": 0.0,
346
+ "worst_price": 0.0,
347
+ "slippage_pct": 0.0,
348
+ "levels_consumed": 0,
349
+ "unfilled": trade_size if trade_size > 0 else 0.0,
350
+ "risk_rating": "critical" if trade_size > 0 else "low",
351
+ }
352
+
353
+ remaining = trade_size
354
+ total_cost = 0.0
355
+ total_filled = 0.0
356
+ best_price = order_book[0][0]
357
+ worst_price = best_price
358
+ levels_consumed = 0
359
+
360
+ for price, qty in order_book:
361
+ if remaining <= 0:
362
+ break
363
+ fill = min(remaining, qty)
364
+ total_cost += fill * price
365
+ total_filled += fill
366
+ remaining -= fill
367
+ worst_price = price
368
+ levels_consumed += 1
369
+
370
+ vwap = total_cost / total_filled if total_filled > 0 else 0.0
371
+ slippage_pct = abs(vwap - best_price) / best_price * 100 if best_price > 0 else 0.0
372
+
373
+ if slippage_pct <= 0.1:
374
+ risk_rating = "low"
375
+ elif slippage_pct <= 0.5:
376
+ risk_rating = "medium"
377
+ elif slippage_pct <= 2.0:
378
+ risk_rating = "high"
379
+ else:
380
+ risk_rating = "critical"
381
+
382
+ return {
383
+ "vwap": float(vwap),
384
+ "best_price": float(best_price),
385
+ "worst_price": float(worst_price),
386
+ "slippage_pct": float(slippage_pct),
387
+ "levels_consumed": levels_consumed,
388
+ "unfilled": float(remaining),
389
+ "risk_rating": risk_rating,
390
+ }