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.
- {quantlite-0.6.0/src/quantlite.egg-info → quantlite-0.8.0}/PKG-INFO +67 -1
- {quantlite-0.6.0 → quantlite-0.8.0}/README.md +66 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/pyproject.toml +4 -1
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/__init__.py +3 -0
- quantlite-0.8.0/src/quantlite/crypto/__init__.py +43 -0
- quantlite-0.8.0/src/quantlite/crypto/exchange.py +390 -0
- quantlite-0.8.0/src/quantlite/crypto/onchain.py +384 -0
- quantlite-0.8.0/src/quantlite/crypto/stablecoin.py +380 -0
- quantlite-0.8.0/src/quantlite/factors/__init__.py +43 -0
- quantlite-0.8.0/src/quantlite/factors/classical.py +288 -0
- quantlite-0.8.0/src/quantlite/factors/custom.py +340 -0
- quantlite-0.8.0/src/quantlite/factors/tail_risk.py +296 -0
- {quantlite-0.6.0 → quantlite-0.8.0/src/quantlite.egg-info}/PKG-INFO +67 -1
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite.egg-info/SOURCES.txt +14 -0
- quantlite-0.8.0/tests/test_crypto_exchange.py +167 -0
- quantlite-0.8.0/tests/test_crypto_onchain.py +161 -0
- quantlite-0.8.0/tests/test_crypto_stablecoin.py +186 -0
- quantlite-0.8.0/tests/test_factors_classical.py +208 -0
- quantlite-0.8.0/tests/test_factors_custom.py +205 -0
- quantlite-0.8.0/tests/test_factors_tail_risk.py +224 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/LICENSE +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/setup.cfg +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/antifragile/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/backtesting/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/backtesting/analysis.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/backtesting/engine.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/backtesting/legacy.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/backtesting/signals.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/contagion/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/core/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/core/types.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/base.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/cache.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/crypto.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/fred.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/local.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/registry.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data/yahoo.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/data_generation.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/dependency/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/dependency/clustering.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/dependency/copulas.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/dependency/correlation.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/distributions/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/distributions/fat_tails.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/diversification/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/ergodicity/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/forensics/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/instruments/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/instruments/bond_pricing.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/instruments/exotic_options.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/instruments/option_pricing.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/metrics.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/monte_carlo.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/network/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/overfit/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/portfolio/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/portfolio/optimisation.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/portfolio/rebalancing.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/regimes/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/regimes/changepoint.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/regimes/conditional.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/regimes/hmm.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/report/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/report/html_renderer.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/report/pdf_renderer.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/report/sections.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/report/tearsheet.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/resample/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/risk/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/risk/evt.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/risk/metrics.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/scenarios/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/visualisation.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/dependency.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/__init__.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/dependency.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/portfolio.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/regimes.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/risk.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/theme.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/portfolio.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/regimes.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/risk.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite/viz/theme.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite.egg-info/dependency_links.txt +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite.egg-info/requires.txt +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/src/quantlite.egg-info/top_level.txt +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_analysis.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_antifragile.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_backtesting.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_changepoint.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_clustering.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_conditional.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_contagion.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_copulas.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_correlation.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_data_connectors.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_data_generation.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_diversification.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_engine.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_ergodicity.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_evt.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_fat_tails.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_forensics.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_hmm.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_instruments.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_metrics.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_monte_carlo.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_network.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_optimisation.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_overfit.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_plotly_viz.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_rebalancing.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_report.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_resample.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_risk_metrics.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_scenarios.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_signals.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_visualisation.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_viz.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_viz_dependency.py +0 -0
- {quantlite-0.6.0 → quantlite-0.8.0}/tests/test_viz_portfolio.py +0 -0
- {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.
|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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.
|
|
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
|
+
}
|