quantlite 0.8.0__tar.gz → 1.0.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.8.0/src/quantlite.egg-info → quantlite-1.0.0}/PKG-INFO +155 -1
- {quantlite-0.8.0 → quantlite-1.0.0}/README.md +154 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/pyproject.toml +1 -1
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/__init__.py +20 -1
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/antifragile/__init__.py +42 -22
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/ergodicity/__init__.py +30 -16
- quantlite-1.0.0/src/quantlite/pipeline.py +338 -0
- quantlite-1.0.0/src/quantlite/regime_integration/__init__.py +31 -0
- quantlite-1.0.0/src/quantlite/regime_integration/portfolio.py +267 -0
- quantlite-1.0.0/src/quantlite/regime_integration/reporting.py +204 -0
- quantlite-1.0.0/src/quantlite/regime_integration/risk.py +212 -0
- quantlite-1.0.0/src/quantlite/simulation/__init__.py +45 -0
- quantlite-1.0.0/src/quantlite/simulation/copula_mc.py +238 -0
- quantlite-1.0.0/src/quantlite/simulation/evt_simulation.py +304 -0
- quantlite-1.0.0/src/quantlite/simulation/regime_mc.py +328 -0
- {quantlite-0.8.0 → quantlite-1.0.0/src/quantlite.egg-info}/PKG-INFO +155 -1
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite.egg-info/SOURCES.txt +14 -0
- quantlite-1.0.0/tests/test_pipeline.py +165 -0
- quantlite-1.0.0/tests/test_regime_integration.py +287 -0
- quantlite-1.0.0/tests/test_sim_copula.py +159 -0
- quantlite-1.0.0/tests/test_sim_evt.py +164 -0
- quantlite-1.0.0/tests/test_sim_regime.py +211 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/LICENSE +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/setup.cfg +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/backtesting/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/backtesting/analysis.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/backtesting/engine.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/backtesting/legacy.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/backtesting/signals.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/contagion/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/core/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/core/types.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/crypto/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/crypto/exchange.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/crypto/onchain.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/crypto/stablecoin.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/data/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/data/base.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/data/cache.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/data/crypto.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/data/fred.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/data/local.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/data/registry.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/data/yahoo.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/data_generation.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/dependency/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/dependency/clustering.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/dependency/copulas.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/dependency/correlation.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/distributions/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/distributions/fat_tails.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/diversification/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/factors/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/factors/classical.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/factors/custom.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/factors/tail_risk.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/forensics/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/instruments/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/instruments/bond_pricing.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/instruments/exotic_options.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/instruments/option_pricing.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/metrics.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/monte_carlo.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/network/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/overfit/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/portfolio/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/portfolio/optimisation.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/portfolio/rebalancing.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/regimes/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/regimes/changepoint.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/regimes/conditional.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/regimes/hmm.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/report/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/report/html_renderer.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/report/pdf_renderer.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/report/sections.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/report/tearsheet.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/resample/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/risk/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/risk/evt.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/risk/metrics.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/scenarios/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/visualisation.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/dependency.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/plotly_backend/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/plotly_backend/dependency.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/plotly_backend/portfolio.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/plotly_backend/regimes.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/plotly_backend/risk.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/plotly_backend/theme.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/portfolio.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/regimes.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/risk.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite/viz/theme.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite.egg-info/dependency_links.txt +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite.egg-info/requires.txt +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/src/quantlite.egg-info/top_level.txt +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_analysis.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_antifragile.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_backtesting.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_changepoint.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_clustering.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_conditional.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_contagion.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_copulas.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_correlation.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_crypto_exchange.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_crypto_onchain.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_crypto_stablecoin.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_data_connectors.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_data_generation.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_diversification.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_engine.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_ergodicity.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_evt.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_factors_classical.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_factors_custom.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_factors_tail_risk.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_fat_tails.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_forensics.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_hmm.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_instruments.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_metrics.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_monte_carlo.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_network.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_optimisation.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_overfit.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_plotly_viz.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_rebalancing.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_report.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_resample.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_risk_metrics.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_scenarios.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_signals.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_visualisation.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_viz.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_viz_dependency.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.0}/tests/test_viz_portfolio.py +0 -0
- {quantlite-0.8.0 → quantlite-1.0.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: 1.0.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
|
|
@@ -716,6 +716,104 @@ Same Stephen Few theme, same muted palette, but with hover info, zoom, and nativ
|
|
|
716
716
|
| `quantlite.factors.classical` | Fama-French three/five-factor, Carhart four-factor, factor attribution, factor summary |
|
|
717
717
|
| `quantlite.factors.custom` | CustomFactor, significance testing, correlation matrix, factor portfolios, decay analysis |
|
|
718
718
|
| `quantlite.factors.tail_risk` | CVaR decomposition, regime factor exposure, crowding score, tail factor beta |
|
|
719
|
+
| `quantlite.simulation.evt_simulation` | EVT tail simulation, parametric tail simulation, historical bootstrap EVT, scenario fan |
|
|
720
|
+
| `quantlite.simulation.copula_mc` | Gaussian copula MC, t-copula MC, stress correlation MC, joint tail probability |
|
|
721
|
+
| `quantlite.simulation.regime_mc` | Regime-switching simulation, stress test scenarios, reverse stress test, simulation summary |
|
|
722
|
+
|
|
723
|
+
## v0.9: Fat-Tail Monte Carlo
|
|
724
|
+
|
|
725
|
+
Three simulation families that go beyond naive Monte Carlo. Standard MC assumes returns are Gaussian and independent. QuantLite's fat-tail MC uses EVT for realistic tails, copulas for joint dependence, and regime switching for structural breaks.
|
|
726
|
+
|
|
727
|
+
### EVT Tail Simulation
|
|
728
|
+
|
|
729
|
+
Generate scenarios with GPD-fitted tails that respect the true shape of financial returns.
|
|
730
|
+
|
|
731
|
+
```python
|
|
732
|
+
import numpy as np
|
|
733
|
+
from quantlite.simulation import evt_tail_simulation, scenario_fan
|
|
734
|
+
|
|
735
|
+
rng = np.random.default_rng(42)
|
|
736
|
+
returns = np.concatenate([
|
|
737
|
+
rng.normal(0.0003, 0.01, 900),
|
|
738
|
+
rng.standard_t(3, 100) * 0.03,
|
|
739
|
+
])
|
|
740
|
+
|
|
741
|
+
# EVT-based scenario generation
|
|
742
|
+
simulated = evt_tail_simulation(returns, n_scenarios=20000, seed=42)
|
|
743
|
+
print(f"Historical 1st pctl: {np.percentile(returns, 1):.4f}")
|
|
744
|
+
print(f"Simulated 1st pctl: {np.percentile(simulated, 1):.4f}")
|
|
745
|
+
|
|
746
|
+
# Fan chart across multiple horizons
|
|
747
|
+
fan = scenario_fan(returns, horizons=[1, 5, 21, 63, 252])
|
|
748
|
+
for h in fan["horizons"]:
|
|
749
|
+
p5, p95 = fan["fans"][h]["5"], fan["fans"][h]["95"]
|
|
750
|
+
print(f" {h:>3}d: [{p5:+.2%}, {p95:+.2%}]")
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+

|
|
754
|
+
|
|
755
|
+

|
|
756
|
+
|
|
757
|
+
### Copula Monte Carlo
|
|
758
|
+
|
|
759
|
+
Multivariate simulation that preserves fat-tailed marginals and captures tail dependence.
|
|
760
|
+
|
|
761
|
+
```python
|
|
762
|
+
import numpy as np
|
|
763
|
+
from quantlite.simulation import t_copula_mc, joint_tail_probability
|
|
764
|
+
|
|
765
|
+
rng = np.random.default_rng(42)
|
|
766
|
+
fund_a = np.concatenate([rng.normal(0.0005, 0.012, 900), rng.standard_t(3, 100) * 0.03])
|
|
767
|
+
fund_b = np.concatenate([rng.normal(0.0003, 0.015, 900), rng.standard_t(4, 100) * 0.025])
|
|
768
|
+
corr = np.array([[1.0, 0.6], [0.6, 1.0]])
|
|
769
|
+
|
|
770
|
+
# t-copula captures tail dependence that Gaussian copula misses
|
|
771
|
+
simulated = t_copula_mc([fund_a, fund_b], corr, df=4, n_scenarios=50000)
|
|
772
|
+
|
|
773
|
+
result = joint_tail_probability(simulated, thresholds=[-0.03, -0.03])
|
|
774
|
+
print(f"Joint crash probability: {result['joint_probability']:.4f}")
|
|
775
|
+
print(f"Marginal probabilities: {result['marginal_probabilities']}")
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+

|
|
779
|
+
|
|
780
|
+

|
|
781
|
+
|
|
782
|
+
### Regime-Switching Simulation
|
|
783
|
+
|
|
784
|
+
Paths that switch between calm, volatile, and crisis regimes via a Markov chain.
|
|
785
|
+
|
|
786
|
+
```python
|
|
787
|
+
import numpy as np
|
|
788
|
+
from quantlite.simulation import (
|
|
789
|
+
regime_switching_simulation,
|
|
790
|
+
reverse_stress_test,
|
|
791
|
+
simulation_summary,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
regimes = [
|
|
795
|
+
{"mu": 0.0004, "sigma": 0.008}, # Calm
|
|
796
|
+
{"mu": 0.0001, "sigma": 0.020}, # Volatile
|
|
797
|
+
{"mu": -0.002, "sigma": 0.035}, # Crisis
|
|
798
|
+
]
|
|
799
|
+
transition = np.array([
|
|
800
|
+
[0.95, 0.04, 0.01],
|
|
801
|
+
[0.10, 0.80, 0.10],
|
|
802
|
+
[0.05, 0.15, 0.80],
|
|
803
|
+
])
|
|
804
|
+
|
|
805
|
+
sim = regime_switching_simulation(regimes, transition, n_steps=252, n_scenarios=5000)
|
|
806
|
+
stats = simulation_summary(sim["returns"])
|
|
807
|
+
print(f"VaR 95%: {stats['var']['95%']:.2%}")
|
|
808
|
+
print(f"CVaR 95%: {stats['cvar']['95%']:.2%}")
|
|
809
|
+
print(f"P(loss > 20%): {stats['probability_of_ruin']['20%']:.2%}")
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+

|
|
813
|
+
|
|
814
|
+

|
|
815
|
+
|
|
816
|
+
See [docs/simulation_evt.md](docs/simulation_evt.md), [docs/simulation_copula.md](docs/simulation_copula.md), and [docs/simulation_regime.md](docs/simulation_regime.md) for the full API reference.
|
|
719
817
|
|
|
720
818
|
## v0.8: Factor Models
|
|
721
819
|
|
|
@@ -998,6 +1096,62 @@ print(f"Tail diversification: {td['tail_diversification']:.3f}")
|
|
|
998
1096
|
| `quantlite.network` | Correlation networks, eigenvector centrality, cascade simulation, community detection |
|
|
999
1097
|
| `quantlite.diversification` | Effective Number of Bets, entropy diversification, tail diversification, diversification ratio, Herfindahl index |
|
|
1000
1098
|
|
|
1099
|
+
## v1.0: The Dream API
|
|
1100
|
+
|
|
1101
|
+
QuantLite v1.0 introduces the **Dream API**, a five-function pipeline that chains the entire quant workflow:
|
|
1102
|
+
|
|
1103
|
+
```python
|
|
1104
|
+
import quantlite as ql
|
|
1105
|
+
|
|
1106
|
+
data = ql.fetch(["AAPL", "BTC-USD", "GLD", "TLT"], period="5y")
|
|
1107
|
+
regimes = ql.detect_regimes(data, n_regimes=3)
|
|
1108
|
+
weights = ql.construct_portfolio(data, regime_aware=True, regimes=regimes)
|
|
1109
|
+
result = ql.backtest(data, weights)
|
|
1110
|
+
ql.tearsheet(result, regimes=regimes, save="portfolio.txt")
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
### Regime-Aware Portfolio Construction
|
|
1114
|
+
|
|
1115
|
+
Weights automatically tilt defensive during crisis regimes, increasing allocations to bonds and gold while reducing equity exposure:
|
|
1116
|
+
|
|
1117
|
+

|
|
1118
|
+
|
|
1119
|
+
### Regime Risk Analysis
|
|
1120
|
+
|
|
1121
|
+
VaR, CVaR, volatility, skewness, and kurtosis computed separately for each market regime:
|
|
1122
|
+
|
|
1123
|
+

|
|
1124
|
+
|
|
1125
|
+
### Regime-Filtered Backtesting
|
|
1126
|
+
|
|
1127
|
+
Different weight sets applied per regime, with full performance attribution:
|
|
1128
|
+
|
|
1129
|
+

|
|
1130
|
+
|
|
1131
|
+
### v1.0 Module Reference
|
|
1132
|
+
|
|
1133
|
+
| Module | Description |
|
|
1134
|
+
|--------|-------------|
|
|
1135
|
+
| `quantlite.pipeline` | Dream API: `fetch`, `detect_regimes`, `construct_portfolio`, `backtest`, `tearsheet` |
|
|
1136
|
+
| `quantlite.regime_integration` | Regime-conditional risk, defensive portfolio tilting, filtered backtesting, tearsheets |
|
|
1137
|
+
| `quantlite.regimes` | HMM regime detection, Bayesian changepoint detection, conditional metrics |
|
|
1138
|
+
| `quantlite.portfolio` | Markowitz, CVaR, risk parity, HRP, Black-Litterman, Kelly optimisation |
|
|
1139
|
+
| `quantlite.backtesting` | Multi-asset engine with circuit breakers, slippage, regime-aware signals |
|
|
1140
|
+
| `quantlite.risk` | VaR, CVaR, Sortino, Calmar, omega ratio, tail ratio, drawdown analysis |
|
|
1141
|
+
| `quantlite.data` | Unified fetching: Yahoo Finance, CCXT, FRED, local files |
|
|
1142
|
+
| `quantlite.distributions` | Student-t, stable, GPD, GEV fitting and simulation |
|
|
1143
|
+
| `quantlite.simulation` | Fat-tail Monte Carlo: EVT-based, copula-based engines |
|
|
1144
|
+
| `quantlite.viz` | Stephen Few-inspired charts: regimes, portfolios, risk dashboards |
|
|
1145
|
+
| `quantlite.factors` | Classical factors, custom factors, tail risk factors |
|
|
1146
|
+
| `quantlite.ergodicity` | Time-average vs ensemble-average growth, Kelly sizing |
|
|
1147
|
+
| `quantlite.antifragile` | Barbell metrics, convexity scores, stress testing |
|
|
1148
|
+
| `quantlite.scenarios` | Historical, hypothetical, and Monte Carlo scenario analysis |
|
|
1149
|
+
| `quantlite.forensics` | Overfitting detection, data snooping tests, walk-forward analysis |
|
|
1150
|
+
| `quantlite.contagion` | CoVaR, Delta CoVaR, MES, Granger causality |
|
|
1151
|
+
| `quantlite.network` | Correlation networks, centrality, cascade simulation |
|
|
1152
|
+
| `quantlite.diversification` | Effective Number of Bets, entropy, tail diversification |
|
|
1153
|
+
| `quantlite.crypto` | On-chain risk, stablecoin depeg, exchange risk scoring |
|
|
1154
|
+
|
|
1001
1155
|
## Design Philosophy
|
|
1002
1156
|
|
|
1003
1157
|
1. **Fat tails are the default.** Gaussian assumptions are explicitly opt-in, never implicit.
|
|
@@ -635,6 +635,104 @@ Same Stephen Few theme, same muted palette, but with hover info, zoom, and nativ
|
|
|
635
635
|
| `quantlite.factors.classical` | Fama-French three/five-factor, Carhart four-factor, factor attribution, factor summary |
|
|
636
636
|
| `quantlite.factors.custom` | CustomFactor, significance testing, correlation matrix, factor portfolios, decay analysis |
|
|
637
637
|
| `quantlite.factors.tail_risk` | CVaR decomposition, regime factor exposure, crowding score, tail factor beta |
|
|
638
|
+
| `quantlite.simulation.evt_simulation` | EVT tail simulation, parametric tail simulation, historical bootstrap EVT, scenario fan |
|
|
639
|
+
| `quantlite.simulation.copula_mc` | Gaussian copula MC, t-copula MC, stress correlation MC, joint tail probability |
|
|
640
|
+
| `quantlite.simulation.regime_mc` | Regime-switching simulation, stress test scenarios, reverse stress test, simulation summary |
|
|
641
|
+
|
|
642
|
+
## v0.9: Fat-Tail Monte Carlo
|
|
643
|
+
|
|
644
|
+
Three simulation families that go beyond naive Monte Carlo. Standard MC assumes returns are Gaussian and independent. QuantLite's fat-tail MC uses EVT for realistic tails, copulas for joint dependence, and regime switching for structural breaks.
|
|
645
|
+
|
|
646
|
+
### EVT Tail Simulation
|
|
647
|
+
|
|
648
|
+
Generate scenarios with GPD-fitted tails that respect the true shape of financial returns.
|
|
649
|
+
|
|
650
|
+
```python
|
|
651
|
+
import numpy as np
|
|
652
|
+
from quantlite.simulation import evt_tail_simulation, scenario_fan
|
|
653
|
+
|
|
654
|
+
rng = np.random.default_rng(42)
|
|
655
|
+
returns = np.concatenate([
|
|
656
|
+
rng.normal(0.0003, 0.01, 900),
|
|
657
|
+
rng.standard_t(3, 100) * 0.03,
|
|
658
|
+
])
|
|
659
|
+
|
|
660
|
+
# EVT-based scenario generation
|
|
661
|
+
simulated = evt_tail_simulation(returns, n_scenarios=20000, seed=42)
|
|
662
|
+
print(f"Historical 1st pctl: {np.percentile(returns, 1):.4f}")
|
|
663
|
+
print(f"Simulated 1st pctl: {np.percentile(simulated, 1):.4f}")
|
|
664
|
+
|
|
665
|
+
# Fan chart across multiple horizons
|
|
666
|
+
fan = scenario_fan(returns, horizons=[1, 5, 21, 63, 252])
|
|
667
|
+
for h in fan["horizons"]:
|
|
668
|
+
p5, p95 = fan["fans"][h]["5"], fan["fans"][h]["95"]
|
|
669
|
+
print(f" {h:>3}d: [{p5:+.2%}, {p95:+.2%}]")
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+

|
|
673
|
+
|
|
674
|
+

|
|
675
|
+
|
|
676
|
+
### Copula Monte Carlo
|
|
677
|
+
|
|
678
|
+
Multivariate simulation that preserves fat-tailed marginals and captures tail dependence.
|
|
679
|
+
|
|
680
|
+
```python
|
|
681
|
+
import numpy as np
|
|
682
|
+
from quantlite.simulation import t_copula_mc, joint_tail_probability
|
|
683
|
+
|
|
684
|
+
rng = np.random.default_rng(42)
|
|
685
|
+
fund_a = np.concatenate([rng.normal(0.0005, 0.012, 900), rng.standard_t(3, 100) * 0.03])
|
|
686
|
+
fund_b = np.concatenate([rng.normal(0.0003, 0.015, 900), rng.standard_t(4, 100) * 0.025])
|
|
687
|
+
corr = np.array([[1.0, 0.6], [0.6, 1.0]])
|
|
688
|
+
|
|
689
|
+
# t-copula captures tail dependence that Gaussian copula misses
|
|
690
|
+
simulated = t_copula_mc([fund_a, fund_b], corr, df=4, n_scenarios=50000)
|
|
691
|
+
|
|
692
|
+
result = joint_tail_probability(simulated, thresholds=[-0.03, -0.03])
|
|
693
|
+
print(f"Joint crash probability: {result['joint_probability']:.4f}")
|
|
694
|
+
print(f"Marginal probabilities: {result['marginal_probabilities']}")
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+

|
|
698
|
+
|
|
699
|
+

|
|
700
|
+
|
|
701
|
+
### Regime-Switching Simulation
|
|
702
|
+
|
|
703
|
+
Paths that switch between calm, volatile, and crisis regimes via a Markov chain.
|
|
704
|
+
|
|
705
|
+
```python
|
|
706
|
+
import numpy as np
|
|
707
|
+
from quantlite.simulation import (
|
|
708
|
+
regime_switching_simulation,
|
|
709
|
+
reverse_stress_test,
|
|
710
|
+
simulation_summary,
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
regimes = [
|
|
714
|
+
{"mu": 0.0004, "sigma": 0.008}, # Calm
|
|
715
|
+
{"mu": 0.0001, "sigma": 0.020}, # Volatile
|
|
716
|
+
{"mu": -0.002, "sigma": 0.035}, # Crisis
|
|
717
|
+
]
|
|
718
|
+
transition = np.array([
|
|
719
|
+
[0.95, 0.04, 0.01],
|
|
720
|
+
[0.10, 0.80, 0.10],
|
|
721
|
+
[0.05, 0.15, 0.80],
|
|
722
|
+
])
|
|
723
|
+
|
|
724
|
+
sim = regime_switching_simulation(regimes, transition, n_steps=252, n_scenarios=5000)
|
|
725
|
+
stats = simulation_summary(sim["returns"])
|
|
726
|
+
print(f"VaR 95%: {stats['var']['95%']:.2%}")
|
|
727
|
+
print(f"CVaR 95%: {stats['cvar']['95%']:.2%}")
|
|
728
|
+
print(f"P(loss > 20%): {stats['probability_of_ruin']['20%']:.2%}")
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+

|
|
732
|
+
|
|
733
|
+

|
|
734
|
+
|
|
735
|
+
See [docs/simulation_evt.md](docs/simulation_evt.md), [docs/simulation_copula.md](docs/simulation_copula.md), and [docs/simulation_regime.md](docs/simulation_regime.md) for the full API reference.
|
|
638
736
|
|
|
639
737
|
## v0.8: Factor Models
|
|
640
738
|
|
|
@@ -917,6 +1015,62 @@ print(f"Tail diversification: {td['tail_diversification']:.3f}")
|
|
|
917
1015
|
| `quantlite.network` | Correlation networks, eigenvector centrality, cascade simulation, community detection |
|
|
918
1016
|
| `quantlite.diversification` | Effective Number of Bets, entropy diversification, tail diversification, diversification ratio, Herfindahl index |
|
|
919
1017
|
|
|
1018
|
+
## v1.0: The Dream API
|
|
1019
|
+
|
|
1020
|
+
QuantLite v1.0 introduces the **Dream API**, a five-function pipeline that chains the entire quant workflow:
|
|
1021
|
+
|
|
1022
|
+
```python
|
|
1023
|
+
import quantlite as ql
|
|
1024
|
+
|
|
1025
|
+
data = ql.fetch(["AAPL", "BTC-USD", "GLD", "TLT"], period="5y")
|
|
1026
|
+
regimes = ql.detect_regimes(data, n_regimes=3)
|
|
1027
|
+
weights = ql.construct_portfolio(data, regime_aware=True, regimes=regimes)
|
|
1028
|
+
result = ql.backtest(data, weights)
|
|
1029
|
+
ql.tearsheet(result, regimes=regimes, save="portfolio.txt")
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### Regime-Aware Portfolio Construction
|
|
1033
|
+
|
|
1034
|
+
Weights automatically tilt defensive during crisis regimes, increasing allocations to bonds and gold while reducing equity exposure:
|
|
1035
|
+
|
|
1036
|
+

|
|
1037
|
+
|
|
1038
|
+
### Regime Risk Analysis
|
|
1039
|
+
|
|
1040
|
+
VaR, CVaR, volatility, skewness, and kurtosis computed separately for each market regime:
|
|
1041
|
+
|
|
1042
|
+

|
|
1043
|
+
|
|
1044
|
+
### Regime-Filtered Backtesting
|
|
1045
|
+
|
|
1046
|
+
Different weight sets applied per regime, with full performance attribution:
|
|
1047
|
+
|
|
1048
|
+

|
|
1049
|
+
|
|
1050
|
+
### v1.0 Module Reference
|
|
1051
|
+
|
|
1052
|
+
| Module | Description |
|
|
1053
|
+
|--------|-------------|
|
|
1054
|
+
| `quantlite.pipeline` | Dream API: `fetch`, `detect_regimes`, `construct_portfolio`, `backtest`, `tearsheet` |
|
|
1055
|
+
| `quantlite.regime_integration` | Regime-conditional risk, defensive portfolio tilting, filtered backtesting, tearsheets |
|
|
1056
|
+
| `quantlite.regimes` | HMM regime detection, Bayesian changepoint detection, conditional metrics |
|
|
1057
|
+
| `quantlite.portfolio` | Markowitz, CVaR, risk parity, HRP, Black-Litterman, Kelly optimisation |
|
|
1058
|
+
| `quantlite.backtesting` | Multi-asset engine with circuit breakers, slippage, regime-aware signals |
|
|
1059
|
+
| `quantlite.risk` | VaR, CVaR, Sortino, Calmar, omega ratio, tail ratio, drawdown analysis |
|
|
1060
|
+
| `quantlite.data` | Unified fetching: Yahoo Finance, CCXT, FRED, local files |
|
|
1061
|
+
| `quantlite.distributions` | Student-t, stable, GPD, GEV fitting and simulation |
|
|
1062
|
+
| `quantlite.simulation` | Fat-tail Monte Carlo: EVT-based, copula-based engines |
|
|
1063
|
+
| `quantlite.viz` | Stephen Few-inspired charts: regimes, portfolios, risk dashboards |
|
|
1064
|
+
| `quantlite.factors` | Classical factors, custom factors, tail risk factors |
|
|
1065
|
+
| `quantlite.ergodicity` | Time-average vs ensemble-average growth, Kelly sizing |
|
|
1066
|
+
| `quantlite.antifragile` | Barbell metrics, convexity scores, stress testing |
|
|
1067
|
+
| `quantlite.scenarios` | Historical, hypothetical, and Monte Carlo scenario analysis |
|
|
1068
|
+
| `quantlite.forensics` | Overfitting detection, data snooping tests, walk-forward analysis |
|
|
1069
|
+
| `quantlite.contagion` | CoVaR, Delta CoVaR, MES, Granger causality |
|
|
1070
|
+
| `quantlite.network` | Correlation networks, centrality, cascade simulation |
|
|
1071
|
+
| `quantlite.diversification` | Effective Number of Bets, entropy, tail diversification |
|
|
1072
|
+
| `quantlite.crypto` | On-chain risk, stablecoin depeg, exchange risk scoring |
|
|
1073
|
+
|
|
920
1074
|
## Design Philosophy
|
|
921
1075
|
|
|
922
1076
|
1. **Fat tails are the default.** Gaussian assumptions are explicitly opt-in, never implicit.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quantlite"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.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" }
|
|
@@ -6,7 +6,7 @@ portfolio optimisation, multi-asset backtesting, and
|
|
|
6
6
|
Stephen Few-inspired visualisation.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "0.
|
|
9
|
+
__version__ = "1.0.0"
|
|
10
10
|
|
|
11
11
|
from .backtesting import (
|
|
12
12
|
BacktestConfig,
|
|
@@ -77,6 +77,16 @@ __all__ = [
|
|
|
77
77
|
"diversification",
|
|
78
78
|
# Crypto-native risk
|
|
79
79
|
"crypto",
|
|
80
|
+
# Fat-tail Monte Carlo simulation
|
|
81
|
+
"simulation",
|
|
82
|
+
# Regime-aware integration
|
|
83
|
+
"regime_integration",
|
|
84
|
+
# Dream API (pipeline)
|
|
85
|
+
"fetch",
|
|
86
|
+
"detect_regimes",
|
|
87
|
+
"construct_portfolio",
|
|
88
|
+
"backtest",
|
|
89
|
+
"tearsheet",
|
|
80
90
|
]
|
|
81
91
|
|
|
82
92
|
from . import ( # noqa: E402
|
|
@@ -88,6 +98,15 @@ from . import ( # noqa: E402
|
|
|
88
98
|
forensics,
|
|
89
99
|
network,
|
|
90
100
|
overfit,
|
|
101
|
+
regime_integration,
|
|
91
102
|
resample,
|
|
92
103
|
scenarios,
|
|
104
|
+
simulation,
|
|
93
105
|
)
|
|
106
|
+
from .pipeline import ( # noqa: E402
|
|
107
|
+
backtest,
|
|
108
|
+
construct_portfolio,
|
|
109
|
+
detect_regimes,
|
|
110
|
+
tearsheet,
|
|
111
|
+
)
|
|
112
|
+
from .pipeline import fetch as fetch # noqa: E402
|
|
@@ -218,44 +218,40 @@ def barbell_allocation(
|
|
|
218
218
|
def lindy_estimate(age: float, confidence: float = 0.95) -> dict[str, float]:
|
|
219
219
|
"""Estimate remaining life expectancy using the Lindy effect.
|
|
220
220
|
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
Models non-perishable entities (ideas, technologies, institutions)
|
|
222
|
+
with a Pareto survival distribution (alpha = 1). Under this model:
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
P(T > age + t | T > age) = age / (age + t)
|
|
225
|
+
|
|
226
|
+
Key properties:
|
|
227
|
+
|
|
228
|
+
* **Expected remaining life** = age (the longer it has survived,
|
|
229
|
+
the longer we expect it to last).
|
|
230
|
+
* **Lower bound at confidence level c**: the additional time *t*
|
|
231
|
+
such that we are *c*-confident the entity survives at least *t*
|
|
232
|
+
more units. Solve ``age / (age + t) = 1 - c`` to get
|
|
233
|
+
``t = age * c / (1 - c)``.
|
|
226
234
|
|
|
227
235
|
Parameters
|
|
228
236
|
----------
|
|
229
237
|
age : float
|
|
230
238
|
Current age of the entity (in any consistent unit).
|
|
231
239
|
confidence : float
|
|
232
|
-
Confidence level for the survival bound (default 0.95).
|
|
240
|
+
Confidence level for the survival lower bound (default 0.95).
|
|
233
241
|
|
|
234
242
|
Returns
|
|
235
243
|
-------
|
|
236
244
|
dict
|
|
237
|
-
Keys: 'age', 'expected_remaining' (
|
|
238
|
-
|
|
245
|
+
Keys: 'age', 'expected_remaining', 'lower_bound' (at the
|
|
246
|
+
given confidence), 'total_expected'.
|
|
239
247
|
"""
|
|
240
248
|
if age <= 0:
|
|
241
249
|
raise ValueError("age must be positive")
|
|
242
250
|
if not 0 < confidence < 1:
|
|
243
251
|
raise ValueError("confidence must be between 0 and 1")
|
|
244
252
|
|
|
245
|
-
# Under Lindy (Pareto with alpha=1), expected remaining life = age
|
|
246
253
|
expected_remaining = age
|
|
247
|
-
|
|
248
|
-
# Lower bound: at confidence level, survival beyond this point
|
|
249
|
-
# P(survive t more) = age / (age + t), so t = age * (1/p - 1)
|
|
250
|
-
lower_bound = age * (1.0 / (1.0 - confidence) - 1.0) * (1.0 - confidence)
|
|
251
|
-
# Simplifies to: age * confidence / (1 - confidence) * (1 - confidence) = age * confidence
|
|
252
|
-
# Actually: P(T > age + t | T > age) = age / (age + t) for Pareto
|
|
253
|
-
# Set this = 1 - confidence: age/(age+t) = 1 - confidence
|
|
254
|
-
# t = age * confidence / (1 - confidence)
|
|
255
|
-
lower_bound = age * (1.0 - confidence) / confidence
|
|
256
|
-
# That's the point we're confident we'll reach (small value)
|
|
257
|
-
# More useful: expected remaining at median
|
|
258
|
-
# P(T > age + t | T > age) = 0.5 => t = age (median remaining = age)
|
|
254
|
+
lower_bound = age * confidence / (1.0 - confidence)
|
|
259
255
|
|
|
260
256
|
return {
|
|
261
257
|
"age": age,
|
|
@@ -268,11 +264,25 @@ def lindy_estimate(age: float, confidence: float = 0.95) -> dict[str, float]:
|
|
|
268
264
|
def skin_in_game_score(
|
|
269
265
|
manager_returns: ArrayLike,
|
|
270
266
|
fund_returns: ArrayLike,
|
|
267
|
+
alignment_weight: float = 0.4,
|
|
268
|
+
downside_weight: float = 0.4,
|
|
269
|
+
asymmetry_weight: float = 0.2,
|
|
271
270
|
) -> dict[str, float]:
|
|
272
271
|
"""Measure principal-agent alignment via payoff asymmetry.
|
|
273
272
|
|
|
274
273
|
Compares the manager's exposure to downside vs upside relative
|
|
275
|
-
to the fund.
|
|
274
|
+
to the fund. A good score means the manager shares the pain.
|
|
275
|
+
|
|
276
|
+
The composite score weights three components:
|
|
277
|
+
|
|
278
|
+
* **Alignment** (default 0.4): correlation between manager and fund
|
|
279
|
+
returns. Are incentives actually correlated?
|
|
280
|
+
* **Downside sharing** (default 0.4): when the fund loses, does the
|
|
281
|
+
manager bleed proportionally? This matters as much as alignment —
|
|
282
|
+
asymmetric downside is the hallmark of agency problems.
|
|
283
|
+
* **Upside asymmetry** (default 0.2): does the manager capture
|
|
284
|
+
disproportionate upside? A secondary check — some asymmetry is
|
|
285
|
+
expected (performance fees), but extreme values signal misalignment.
|
|
276
286
|
|
|
277
287
|
Parameters
|
|
278
288
|
----------
|
|
@@ -280,6 +290,12 @@ def skin_in_game_score(
|
|
|
280
290
|
Returns experienced by the manager (compensation-adjusted).
|
|
281
291
|
fund_returns : array-like
|
|
282
292
|
Returns experienced by the fund investors.
|
|
293
|
+
alignment_weight : float
|
|
294
|
+
Weight for the alignment (correlation) component (default 0.4).
|
|
295
|
+
downside_weight : float
|
|
296
|
+
Weight for the downside-sharing component (default 0.4).
|
|
297
|
+
asymmetry_weight : float
|
|
298
|
+
Weight for the upside-asymmetry component (default 0.2).
|
|
283
299
|
|
|
284
300
|
Returns
|
|
285
301
|
-------
|
|
@@ -317,7 +333,11 @@ def skin_in_game_score(
|
|
|
317
333
|
|
|
318
334
|
# Composite score: high alignment + high downside sharing + low upside asymmetry = good
|
|
319
335
|
# Normalise to [0, 1] approximately
|
|
320
|
-
score = (
|
|
336
|
+
score = (
|
|
337
|
+
alignment * alignment_weight
|
|
338
|
+
+ min(downside_sharing, 1.0) * downside_weight
|
|
339
|
+
+ max(0, 1.0 - abs(upside_asymmetry - 1.0)) * asymmetry_weight
|
|
340
|
+
)
|
|
321
341
|
|
|
322
342
|
return {
|
|
323
343
|
"alignment": alignment,
|
|
@@ -91,11 +91,13 @@ def ergodicity_gap(returns: ArrayLike) -> float:
|
|
|
91
91
|
def kelly_fraction(returns: ArrayLike, risk_free: float = 0.0) -> float:
|
|
92
92
|
"""Compute the optimal Kelly fraction for geometric growth.
|
|
93
93
|
|
|
94
|
-
The Kelly criterion maximises the expected logarithmic growth rate
|
|
95
|
-
For a simple binary-style approximation from empirical returns,
|
|
96
|
-
we optimise f to maximise E[log(1 + f * (r - risk_free))].
|
|
94
|
+
The Kelly criterion maximises the expected logarithmic growth rate:
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
f* = argmax_f E[log(1 + f * (r - r_f))]
|
|
97
|
+
|
|
98
|
+
Uses ``scipy.optimize.minimize_scalar`` with bounded search on
|
|
99
|
+
[-0.5, 3.0]. Falls back to Brent-bounded optimisation if the
|
|
100
|
+
primary solve fails.
|
|
99
101
|
|
|
100
102
|
Parameters
|
|
101
103
|
----------
|
|
@@ -110,24 +112,36 @@ def kelly_fraction(returns: ArrayLike, risk_free: float = 0.0) -> float:
|
|
|
110
112
|
Optimal fraction of capital to deploy. Can be < 0 (short)
|
|
111
113
|
or > 1 (leveraged).
|
|
112
114
|
"""
|
|
115
|
+
from scipy.optimize import minimize_scalar
|
|
116
|
+
|
|
113
117
|
r = _to_array(returns)
|
|
114
118
|
excess = r - risk_free
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
fractions = np.linspace(-0.5, 3.0, 3500)
|
|
118
|
-
best_f = 0.0
|
|
119
|
-
best_g = -np.inf
|
|
120
|
-
|
|
121
|
-
for f in fractions:
|
|
120
|
+
def neg_expected_log_growth(f: float) -> float:
|
|
122
121
|
portfolio = 1.0 + f * excess
|
|
123
122
|
if np.any(portfolio <= 0):
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
return 1e10 # infeasible
|
|
124
|
+
return -float(np.mean(np.log(portfolio)))
|
|
125
|
+
|
|
126
|
+
bounds = (-0.5, 3.0)
|
|
127
|
+
|
|
128
|
+
result = minimize_scalar(
|
|
129
|
+
neg_expected_log_growth, bounds=bounds, method="bounded",
|
|
130
|
+
options={"xatol": 1e-8, "maxiter": 500},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if result.success:
|
|
134
|
+
return float(round(result.x, 4))
|
|
135
|
+
|
|
136
|
+
# Fallback: try again with Brent in the same interval
|
|
137
|
+
result2 = minimize_scalar(
|
|
138
|
+
neg_expected_log_growth, bracket=(-0.5, 0.5, 3.0), method="brent",
|
|
139
|
+
)
|
|
140
|
+
if result2.success and bounds[0] <= result2.x <= bounds[1]:
|
|
141
|
+
return float(round(result2.x, 4))
|
|
129
142
|
|
|
130
|
-
return
|
|
143
|
+
# Last resort: return 0 (no bet)
|
|
144
|
+
return 0.0
|
|
131
145
|
|
|
132
146
|
|
|
133
147
|
def leverage_effect(
|