quantlite 0.8.0__tar.gz → 0.9.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-0.9.0}/PKG-INFO +99 -1
- {quantlite-0.8.0 → quantlite-0.9.0}/README.md +98 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/pyproject.toml +1 -1
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/__init__.py +3 -0
- quantlite-0.9.0/src/quantlite/simulation/__init__.py +45 -0
- quantlite-0.9.0/src/quantlite/simulation/copula_mc.py +238 -0
- quantlite-0.9.0/src/quantlite/simulation/evt_simulation.py +304 -0
- quantlite-0.9.0/src/quantlite/simulation/regime_mc.py +328 -0
- {quantlite-0.8.0 → quantlite-0.9.0/src/quantlite.egg-info}/PKG-INFO +99 -1
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite.egg-info/SOURCES.txt +7 -0
- quantlite-0.9.0/tests/test_sim_copula.py +159 -0
- quantlite-0.9.0/tests/test_sim_evt.py +164 -0
- quantlite-0.9.0/tests/test_sim_regime.py +211 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/LICENSE +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/setup.cfg +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/antifragile/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/backtesting/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/backtesting/analysis.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/backtesting/engine.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/backtesting/legacy.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/backtesting/signals.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/contagion/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/core/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/core/types.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/crypto/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/crypto/exchange.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/crypto/onchain.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/crypto/stablecoin.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/base.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/cache.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/crypto.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/fred.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/local.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/registry.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/yahoo.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data_generation.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/dependency/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/dependency/clustering.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/dependency/copulas.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/dependency/correlation.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/distributions/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/distributions/fat_tails.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/diversification/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/ergodicity/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/factors/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/factors/classical.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/factors/custom.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/factors/tail_risk.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/forensics/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/instruments/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/instruments/bond_pricing.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/instruments/exotic_options.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/instruments/option_pricing.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/metrics.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/monte_carlo.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/network/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/overfit/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/portfolio/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/portfolio/optimisation.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/portfolio/rebalancing.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/regimes/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/regimes/changepoint.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/regimes/conditional.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/regimes/hmm.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/report/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/report/html_renderer.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/report/pdf_renderer.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/report/sections.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/report/tearsheet.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/resample/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/risk/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/risk/evt.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/risk/metrics.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/scenarios/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/visualisation.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/dependency.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/__init__.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/dependency.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/portfolio.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/regimes.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/risk.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/theme.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/portfolio.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/regimes.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/risk.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/theme.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite.egg-info/dependency_links.txt +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite.egg-info/requires.txt +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite.egg-info/top_level.txt +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_analysis.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_antifragile.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_backtesting.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_changepoint.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_clustering.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_conditional.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_contagion.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_copulas.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_correlation.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_crypto_exchange.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_crypto_onchain.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_crypto_stablecoin.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_data_connectors.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_data_generation.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_diversification.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_engine.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_ergodicity.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_evt.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_factors_classical.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_factors_custom.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_factors_tail_risk.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_fat_tails.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_forensics.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_hmm.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_instruments.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_metrics.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_monte_carlo.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_network.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_optimisation.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_overfit.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_plotly_viz.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_rebalancing.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_report.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_resample.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_risk_metrics.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_scenarios.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_signals.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_visualisation.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_viz.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_viz_dependency.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_viz_portfolio.py +0 -0
- {quantlite-0.8.0 → quantlite-0.9.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.9.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
|
|
|
@@ -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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quantlite"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.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" }
|
|
@@ -77,6 +77,8 @@ __all__ = [
|
|
|
77
77
|
"diversification",
|
|
78
78
|
# Crypto-native risk
|
|
79
79
|
"crypto",
|
|
80
|
+
# Fat-tail Monte Carlo simulation
|
|
81
|
+
"simulation",
|
|
80
82
|
]
|
|
81
83
|
|
|
82
84
|
from . import ( # noqa: E402
|
|
@@ -90,4 +92,5 @@ from . import ( # noqa: E402
|
|
|
90
92
|
overfit,
|
|
91
93
|
resample,
|
|
92
94
|
scenarios,
|
|
95
|
+
simulation,
|
|
93
96
|
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Fat-tail Monte Carlo simulation: EVT scenarios, copula MC, and regime switching.
|
|
2
|
+
|
|
3
|
+
Provides three families of simulation:
|
|
4
|
+
|
|
5
|
+
1. **EVT simulation** -- GPD-based tail sampling with empirical body.
|
|
6
|
+
2. **Copula Monte Carlo** -- multivariate simulation with fat-tailed marginals.
|
|
7
|
+
3. **Regime Monte Carlo** -- regime-switching paths, stress tests, and reverse stress tests.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .copula_mc import (
|
|
11
|
+
gaussian_copula_mc,
|
|
12
|
+
joint_tail_probability,
|
|
13
|
+
stress_correlation_mc,
|
|
14
|
+
t_copula_mc,
|
|
15
|
+
)
|
|
16
|
+
from .evt_simulation import (
|
|
17
|
+
evt_tail_simulation,
|
|
18
|
+
historical_bootstrap_evt,
|
|
19
|
+
parametric_tail_simulation,
|
|
20
|
+
scenario_fan,
|
|
21
|
+
)
|
|
22
|
+
from .regime_mc import (
|
|
23
|
+
regime_switching_simulation,
|
|
24
|
+
reverse_stress_test,
|
|
25
|
+
simulation_summary,
|
|
26
|
+
stress_test_scenario,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# EVT simulation
|
|
31
|
+
"evt_tail_simulation",
|
|
32
|
+
"parametric_tail_simulation",
|
|
33
|
+
"historical_bootstrap_evt",
|
|
34
|
+
"scenario_fan",
|
|
35
|
+
# Copula MC
|
|
36
|
+
"gaussian_copula_mc",
|
|
37
|
+
"t_copula_mc",
|
|
38
|
+
"stress_correlation_mc",
|
|
39
|
+
"joint_tail_probability",
|
|
40
|
+
# Regime MC
|
|
41
|
+
"regime_switching_simulation",
|
|
42
|
+
"stress_test_scenario",
|
|
43
|
+
"reverse_stress_test",
|
|
44
|
+
"simulation_summary",
|
|
45
|
+
]
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Copula-based multivariate Monte Carlo simulation.
|
|
2
|
+
|
|
3
|
+
Generates joint return scenarios that preserve correlation structure
|
|
4
|
+
while allowing fat-tailed marginals. Supports Gaussian and Student-t
|
|
5
|
+
copulas, stressed correlations, and joint tail probability estimation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from scipy import stats
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"gaussian_copula_mc",
|
|
15
|
+
"t_copula_mc",
|
|
16
|
+
"stress_correlation_mc",
|
|
17
|
+
"joint_tail_probability",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _to_array(x: np.ndarray | list[float]) -> np.ndarray:
|
|
22
|
+
arr = np.asarray(x, dtype=float)
|
|
23
|
+
return arr[~np.isnan(arr)]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ensure_positive_definite(matrix: np.ndarray) -> np.ndarray:
|
|
27
|
+
"""Ensure a correlation matrix is positive semi-definite via eigenvalue clipping."""
|
|
28
|
+
eigenvalues, eigenvectors = np.linalg.eigh(matrix)
|
|
29
|
+
eigenvalues = np.maximum(eigenvalues, 1e-8)
|
|
30
|
+
fixed = eigenvectors @ np.diag(eigenvalues) @ eigenvectors.T
|
|
31
|
+
# Re-normalise to correlation matrix
|
|
32
|
+
d = np.sqrt(np.diag(fixed))
|
|
33
|
+
fixed = fixed / np.outer(d, d)
|
|
34
|
+
np.fill_diagonal(fixed, 1.0)
|
|
35
|
+
return fixed
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _empirical_cdf(data: np.ndarray) -> np.ndarray:
|
|
39
|
+
"""Compute empirical CDF values (rank-based, in (0,1))."""
|
|
40
|
+
from scipy.stats import rankdata
|
|
41
|
+
n = len(data)
|
|
42
|
+
return rankdata(data, method="ordinal") / (n + 1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _inverse_empirical(sorted_data: np.ndarray, u: np.ndarray) -> np.ndarray:
|
|
46
|
+
"""Map uniform samples back to data space via empirical quantile function."""
|
|
47
|
+
n = len(sorted_data)
|
|
48
|
+
indices = np.clip((u * n).astype(int), 0, n - 1)
|
|
49
|
+
return sorted_data[indices]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def gaussian_copula_mc(
|
|
53
|
+
marginals: list[np.ndarray] | np.ndarray,
|
|
54
|
+
correlation_matrix: np.ndarray,
|
|
55
|
+
n_scenarios: int = 10000,
|
|
56
|
+
seed: int = 42,
|
|
57
|
+
) -> np.ndarray:
|
|
58
|
+
"""Multivariate simulation using a Gaussian copula with empirical marginals.
|
|
59
|
+
|
|
60
|
+
Generates correlated uniform samples via a Gaussian copula, then
|
|
61
|
+
maps them back to the empirical marginal distributions. This
|
|
62
|
+
preserves the correlation structure while retaining the fat tails
|
|
63
|
+
of each individual asset's return distribution.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
marginals: List of 1-D arrays, one per asset, containing
|
|
67
|
+
historical returns. Each array defines the marginal
|
|
68
|
+
distribution for that asset.
|
|
69
|
+
correlation_matrix: Square correlation matrix of shape
|
|
70
|
+
``(n_assets, n_assets)``.
|
|
71
|
+
n_scenarios: Number of joint scenarios to generate.
|
|
72
|
+
seed: Random seed.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Array of shape ``(n_scenarios, n_assets)`` with simulated returns.
|
|
76
|
+
"""
|
|
77
|
+
rng = np.random.default_rng(seed)
|
|
78
|
+
n_assets = len(marginals)
|
|
79
|
+
corr = _ensure_positive_definite(np.asarray(correlation_matrix, dtype=float))
|
|
80
|
+
|
|
81
|
+
# Generate correlated normal samples
|
|
82
|
+
L = np.linalg.cholesky(corr)
|
|
83
|
+
z = rng.standard_normal((n_scenarios, n_assets))
|
|
84
|
+
correlated_z = z @ L.T
|
|
85
|
+
|
|
86
|
+
# Transform to uniform via normal CDF
|
|
87
|
+
u = stats.norm.cdf(correlated_z)
|
|
88
|
+
|
|
89
|
+
# Map to empirical marginals
|
|
90
|
+
result = np.empty((n_scenarios, n_assets))
|
|
91
|
+
for j in range(n_assets):
|
|
92
|
+
sorted_m = np.sort(_to_array(marginals[j]))
|
|
93
|
+
result[:, j] = _inverse_empirical(sorted_m, u[:, j])
|
|
94
|
+
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def t_copula_mc(
|
|
99
|
+
marginals: list[np.ndarray] | np.ndarray,
|
|
100
|
+
correlation_matrix: np.ndarray,
|
|
101
|
+
df: int = 4,
|
|
102
|
+
n_scenarios: int = 10000,
|
|
103
|
+
seed: int = 42,
|
|
104
|
+
) -> np.ndarray:
|
|
105
|
+
"""Multivariate simulation using a Student-t copula.
|
|
106
|
+
|
|
107
|
+
The t-copula generates tail dependence: extreme events across
|
|
108
|
+
assets are more likely to co-occur than under a Gaussian copula.
|
|
109
|
+
Lower degrees of freedom produce stronger tail dependence.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
marginals: List of 1-D arrays, one per asset.
|
|
113
|
+
correlation_matrix: Correlation matrix.
|
|
114
|
+
df: Degrees of freedom for the t-copula (default 4).
|
|
115
|
+
n_scenarios: Number of scenarios.
|
|
116
|
+
seed: Random seed.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Array of shape ``(n_scenarios, n_assets)``.
|
|
120
|
+
"""
|
|
121
|
+
rng = np.random.default_rng(seed)
|
|
122
|
+
n_assets = len(marginals)
|
|
123
|
+
corr = _ensure_positive_definite(np.asarray(correlation_matrix, dtype=float))
|
|
124
|
+
|
|
125
|
+
L = np.linalg.cholesky(corr)
|
|
126
|
+
z = rng.standard_normal((n_scenarios, n_assets))
|
|
127
|
+
correlated_z = z @ L.T
|
|
128
|
+
|
|
129
|
+
# Scale by chi-squared for t-distribution
|
|
130
|
+
chi2 = rng.chisquare(df, size=n_scenarios)
|
|
131
|
+
scaling = np.sqrt(df / chi2)
|
|
132
|
+
t_samples = correlated_z * scaling[:, np.newaxis]
|
|
133
|
+
|
|
134
|
+
# Transform to uniform via t CDF
|
|
135
|
+
u = stats.t.cdf(t_samples, df=df)
|
|
136
|
+
|
|
137
|
+
result = np.empty((n_scenarios, n_assets))
|
|
138
|
+
for j in range(n_assets):
|
|
139
|
+
sorted_m = np.sort(_to_array(marginals[j]))
|
|
140
|
+
result[:, j] = _inverse_empirical(sorted_m, u[:, j])
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def stress_correlation_mc(
|
|
146
|
+
marginals: list[np.ndarray] | np.ndarray,
|
|
147
|
+
correlation_matrix: np.ndarray,
|
|
148
|
+
stress_factor: float = 1.5,
|
|
149
|
+
n_scenarios: int = 10000,
|
|
150
|
+
seed: int = 42,
|
|
151
|
+
) -> np.ndarray:
|
|
152
|
+
"""Simulate under stressed correlations.
|
|
153
|
+
|
|
154
|
+
In crises, correlations tend to move towards 1.0. This function
|
|
155
|
+
scales off-diagonal correlations by ``stress_factor``, capping
|
|
156
|
+
at 1.0, then simulates using a Gaussian copula.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
marginals: List of 1-D arrays, one per asset.
|
|
160
|
+
correlation_matrix: Base correlation matrix.
|
|
161
|
+
stress_factor: Multiplier for off-diagonal correlations
|
|
162
|
+
(default 1.5). Values > 1 increase correlation.
|
|
163
|
+
n_scenarios: Number of scenarios.
|
|
164
|
+
seed: Random seed.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Array of shape ``(n_scenarios, n_assets)``.
|
|
168
|
+
"""
|
|
169
|
+
corr = np.asarray(correlation_matrix, dtype=float).copy()
|
|
170
|
+
n = corr.shape[0]
|
|
171
|
+
|
|
172
|
+
# Stress the off-diagonal elements
|
|
173
|
+
for i in range(n):
|
|
174
|
+
for j in range(n):
|
|
175
|
+
if i != j:
|
|
176
|
+
corr[i, j] = np.clip(corr[i, j] * stress_factor, -1.0, 1.0)
|
|
177
|
+
|
|
178
|
+
corr = _ensure_positive_definite(corr)
|
|
179
|
+
return gaussian_copula_mc(marginals, corr, n_scenarios=n_scenarios, seed=seed)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def joint_tail_probability(
|
|
183
|
+
simulated_returns: np.ndarray,
|
|
184
|
+
thresholds: list[float] | np.ndarray,
|
|
185
|
+
) -> dict[str, float]:
|
|
186
|
+
"""Compute joint tail probabilities from simulation output.
|
|
187
|
+
|
|
188
|
+
Estimates the probability of multiple assets simultaneously
|
|
189
|
+
breaching their respective thresholds (all falling below
|
|
190
|
+
their threshold in the same scenario).
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
simulated_returns: Array of shape ``(n_scenarios, n_assets)``
|
|
194
|
+
from a copula simulation.
|
|
195
|
+
thresholds: List of return thresholds, one per asset
|
|
196
|
+
(negative for losses, e.g. ``[-0.05, -0.03]``).
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Dictionary with:
|
|
200
|
+
|
|
201
|
+
- ``"joint_probability"``: probability all assets breach
|
|
202
|
+
simultaneously
|
|
203
|
+
- ``"marginal_probabilities"``: list of individual breach
|
|
204
|
+
probabilities
|
|
205
|
+
- ``"conditional_probabilities"``: probability of joint breach
|
|
206
|
+
given each asset breaches
|
|
207
|
+
- ``"n_joint_breaches"``: count of joint breach scenarios
|
|
208
|
+
- ``"n_scenarios"``: total scenarios
|
|
209
|
+
"""
|
|
210
|
+
thresholds_arr = np.asarray(thresholds, dtype=float)
|
|
211
|
+
n_scenarios = simulated_returns.shape[0]
|
|
212
|
+
n_assets = simulated_returns.shape[1]
|
|
213
|
+
|
|
214
|
+
# Individual breaches
|
|
215
|
+
breaches = simulated_returns < thresholds_arr[np.newaxis, :]
|
|
216
|
+
marginal_counts = breaches.sum(axis=0)
|
|
217
|
+
marginal_probs = [float(c / n_scenarios) for c in marginal_counts]
|
|
218
|
+
|
|
219
|
+
# Joint breach: all assets breach simultaneously
|
|
220
|
+
joint_breach = np.all(breaches, axis=1)
|
|
221
|
+
joint_count = int(joint_breach.sum())
|
|
222
|
+
joint_prob = joint_count / n_scenarios
|
|
223
|
+
|
|
224
|
+
# Conditional probabilities
|
|
225
|
+
conditional_probs = []
|
|
226
|
+
for j in range(n_assets):
|
|
227
|
+
if marginal_counts[j] > 0:
|
|
228
|
+
conditional_probs.append(float(joint_count / marginal_counts[j]))
|
|
229
|
+
else:
|
|
230
|
+
conditional_probs.append(0.0)
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
"joint_probability": joint_prob,
|
|
234
|
+
"marginal_probabilities": marginal_probs,
|
|
235
|
+
"conditional_probabilities": conditional_probs,
|
|
236
|
+
"n_joint_breaches": joint_count,
|
|
237
|
+
"n_scenarios": n_scenarios,
|
|
238
|
+
}
|