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.
Files changed (133) hide show
  1. {quantlite-0.8.0/src/quantlite.egg-info → quantlite-0.9.0}/PKG-INFO +99 -1
  2. {quantlite-0.8.0 → quantlite-0.9.0}/README.md +98 -0
  3. {quantlite-0.8.0 → quantlite-0.9.0}/pyproject.toml +1 -1
  4. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/__init__.py +3 -0
  5. quantlite-0.9.0/src/quantlite/simulation/__init__.py +45 -0
  6. quantlite-0.9.0/src/quantlite/simulation/copula_mc.py +238 -0
  7. quantlite-0.9.0/src/quantlite/simulation/evt_simulation.py +304 -0
  8. quantlite-0.9.0/src/quantlite/simulation/regime_mc.py +328 -0
  9. {quantlite-0.8.0 → quantlite-0.9.0/src/quantlite.egg-info}/PKG-INFO +99 -1
  10. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite.egg-info/SOURCES.txt +7 -0
  11. quantlite-0.9.0/tests/test_sim_copula.py +159 -0
  12. quantlite-0.9.0/tests/test_sim_evt.py +164 -0
  13. quantlite-0.9.0/tests/test_sim_regime.py +211 -0
  14. {quantlite-0.8.0 → quantlite-0.9.0}/LICENSE +0 -0
  15. {quantlite-0.8.0 → quantlite-0.9.0}/setup.cfg +0 -0
  16. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/antifragile/__init__.py +0 -0
  17. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/backtesting/__init__.py +0 -0
  18. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/backtesting/analysis.py +0 -0
  19. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/backtesting/engine.py +0 -0
  20. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/backtesting/legacy.py +0 -0
  21. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/backtesting/signals.py +0 -0
  22. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/contagion/__init__.py +0 -0
  23. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/core/__init__.py +0 -0
  24. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/core/types.py +0 -0
  25. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/crypto/__init__.py +0 -0
  26. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/crypto/exchange.py +0 -0
  27. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/crypto/onchain.py +0 -0
  28. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/crypto/stablecoin.py +0 -0
  29. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/__init__.py +0 -0
  30. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/base.py +0 -0
  31. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/cache.py +0 -0
  32. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/crypto.py +0 -0
  33. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/fred.py +0 -0
  34. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/local.py +0 -0
  35. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/registry.py +0 -0
  36. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data/yahoo.py +0 -0
  37. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/data_generation.py +0 -0
  38. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/dependency/__init__.py +0 -0
  39. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/dependency/clustering.py +0 -0
  40. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/dependency/copulas.py +0 -0
  41. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/dependency/correlation.py +0 -0
  42. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/distributions/__init__.py +0 -0
  43. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/distributions/fat_tails.py +0 -0
  44. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/diversification/__init__.py +0 -0
  45. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/ergodicity/__init__.py +0 -0
  46. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/factors/__init__.py +0 -0
  47. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/factors/classical.py +0 -0
  48. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/factors/custom.py +0 -0
  49. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/factors/tail_risk.py +0 -0
  50. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/forensics/__init__.py +0 -0
  51. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/instruments/__init__.py +0 -0
  52. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/instruments/bond_pricing.py +0 -0
  53. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/instruments/exotic_options.py +0 -0
  54. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/instruments/option_pricing.py +0 -0
  55. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/metrics.py +0 -0
  56. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/monte_carlo.py +0 -0
  57. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/network/__init__.py +0 -0
  58. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/overfit/__init__.py +0 -0
  59. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/portfolio/__init__.py +0 -0
  60. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/portfolio/optimisation.py +0 -0
  61. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/portfolio/rebalancing.py +0 -0
  62. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/regimes/__init__.py +0 -0
  63. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/regimes/changepoint.py +0 -0
  64. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/regimes/conditional.py +0 -0
  65. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/regimes/hmm.py +0 -0
  66. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/report/__init__.py +0 -0
  67. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/report/html_renderer.py +0 -0
  68. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/report/pdf_renderer.py +0 -0
  69. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/report/sections.py +0 -0
  70. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/report/tearsheet.py +0 -0
  71. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/resample/__init__.py +0 -0
  72. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/risk/__init__.py +0 -0
  73. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/risk/evt.py +0 -0
  74. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/risk/metrics.py +0 -0
  75. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/scenarios/__init__.py +0 -0
  76. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/visualisation.py +0 -0
  77. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/__init__.py +0 -0
  78. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/dependency.py +0 -0
  79. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/__init__.py +0 -0
  80. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/dependency.py +0 -0
  81. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/portfolio.py +0 -0
  82. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/regimes.py +0 -0
  83. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/risk.py +0 -0
  84. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/plotly_backend/theme.py +0 -0
  85. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/portfolio.py +0 -0
  86. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/regimes.py +0 -0
  87. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/risk.py +0 -0
  88. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite/viz/theme.py +0 -0
  89. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite.egg-info/dependency_links.txt +0 -0
  90. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite.egg-info/requires.txt +0 -0
  91. {quantlite-0.8.0 → quantlite-0.9.0}/src/quantlite.egg-info/top_level.txt +0 -0
  92. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_analysis.py +0 -0
  93. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_antifragile.py +0 -0
  94. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_backtesting.py +0 -0
  95. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_changepoint.py +0 -0
  96. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_clustering.py +0 -0
  97. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_conditional.py +0 -0
  98. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_contagion.py +0 -0
  99. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_copulas.py +0 -0
  100. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_correlation.py +0 -0
  101. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_crypto_exchange.py +0 -0
  102. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_crypto_onchain.py +0 -0
  103. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_crypto_stablecoin.py +0 -0
  104. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_data_connectors.py +0 -0
  105. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_data_generation.py +0 -0
  106. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_diversification.py +0 -0
  107. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_engine.py +0 -0
  108. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_ergodicity.py +0 -0
  109. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_evt.py +0 -0
  110. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_factors_classical.py +0 -0
  111. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_factors_custom.py +0 -0
  112. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_factors_tail_risk.py +0 -0
  113. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_fat_tails.py +0 -0
  114. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_forensics.py +0 -0
  115. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_hmm.py +0 -0
  116. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_instruments.py +0 -0
  117. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_metrics.py +0 -0
  118. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_monte_carlo.py +0 -0
  119. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_network.py +0 -0
  120. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_optimisation.py +0 -0
  121. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_overfit.py +0 -0
  122. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_plotly_viz.py +0 -0
  123. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_rebalancing.py +0 -0
  124. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_report.py +0 -0
  125. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_resample.py +0 -0
  126. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_risk_metrics.py +0 -0
  127. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_scenarios.py +0 -0
  128. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_signals.py +0 -0
  129. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_visualisation.py +0 -0
  130. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_viz.py +0 -0
  131. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_viz_dependency.py +0 -0
  132. {quantlite-0.8.0 → quantlite-0.9.0}/tests/test_viz_portfolio.py +0 -0
  133. {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.8.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
+ ![EVT Tail Simulation](docs/images/evt_tail_simulation.png)
754
+
755
+ ![Scenario Fan](docs/images/scenario_fan.png)
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
+ ![Copula Comparison](docs/images/copula_comparison.png)
779
+
780
+ ![Stressed Correlations](docs/images/stressed_correlations.png)
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
+ ![Regime Simulation](docs/images/regime_simulation.png)
813
+
814
+ ![Reverse Stress Test](docs/images/reverse_stress_test.png)
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
+ ![EVT Tail Simulation](docs/images/evt_tail_simulation.png)
673
+
674
+ ![Scenario Fan](docs/images/scenario_fan.png)
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
+ ![Copula Comparison](docs/images/copula_comparison.png)
698
+
699
+ ![Stressed Correlations](docs/images/stressed_correlations.png)
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
+ ![Regime Simulation](docs/images/regime_simulation.png)
732
+
733
+ ![Reverse Stress Test](docs/images/reverse_stress_test.png)
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.8.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
+ }