quantlite 0.7.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {quantlite-0.7.0/src/quantlite.egg-info → quantlite-0.8.0}/PKG-INFO +64 -1
  2. {quantlite-0.7.0 → quantlite-0.8.0}/README.md +63 -0
  3. {quantlite-0.7.0 → quantlite-0.8.0}/pyproject.toml +4 -1
  4. quantlite-0.8.0/src/quantlite/factors/__init__.py +43 -0
  5. quantlite-0.8.0/src/quantlite/factors/classical.py +288 -0
  6. quantlite-0.8.0/src/quantlite/factors/custom.py +340 -0
  7. quantlite-0.8.0/src/quantlite/factors/tail_risk.py +296 -0
  8. {quantlite-0.7.0 → quantlite-0.8.0/src/quantlite.egg-info}/PKG-INFO +64 -1
  9. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite.egg-info/SOURCES.txt +7 -0
  10. quantlite-0.8.0/tests/test_factors_classical.py +208 -0
  11. quantlite-0.8.0/tests/test_factors_custom.py +205 -0
  12. quantlite-0.8.0/tests/test_factors_tail_risk.py +224 -0
  13. {quantlite-0.7.0 → quantlite-0.8.0}/LICENSE +0 -0
  14. {quantlite-0.7.0 → quantlite-0.8.0}/setup.cfg +0 -0
  15. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/__init__.py +0 -0
  16. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/antifragile/__init__.py +0 -0
  17. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/backtesting/__init__.py +0 -0
  18. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/backtesting/analysis.py +0 -0
  19. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/backtesting/engine.py +0 -0
  20. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/backtesting/legacy.py +0 -0
  21. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/backtesting/signals.py +0 -0
  22. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/contagion/__init__.py +0 -0
  23. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/core/__init__.py +0 -0
  24. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/core/types.py +0 -0
  25. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/crypto/__init__.py +0 -0
  26. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/crypto/exchange.py +0 -0
  27. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/crypto/onchain.py +0 -0
  28. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/crypto/stablecoin.py +0 -0
  29. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/data/__init__.py +0 -0
  30. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/data/base.py +0 -0
  31. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/data/cache.py +0 -0
  32. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/data/crypto.py +0 -0
  33. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/data/fred.py +0 -0
  34. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/data/local.py +0 -0
  35. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/data/registry.py +0 -0
  36. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/data/yahoo.py +0 -0
  37. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/data_generation.py +0 -0
  38. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/dependency/__init__.py +0 -0
  39. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/dependency/clustering.py +0 -0
  40. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/dependency/copulas.py +0 -0
  41. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/dependency/correlation.py +0 -0
  42. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/distributions/__init__.py +0 -0
  43. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/distributions/fat_tails.py +0 -0
  44. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/diversification/__init__.py +0 -0
  45. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/ergodicity/__init__.py +0 -0
  46. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/forensics/__init__.py +0 -0
  47. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/instruments/__init__.py +0 -0
  48. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/instruments/bond_pricing.py +0 -0
  49. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/instruments/exotic_options.py +0 -0
  50. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/instruments/option_pricing.py +0 -0
  51. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/metrics.py +0 -0
  52. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/monte_carlo.py +0 -0
  53. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/network/__init__.py +0 -0
  54. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/overfit/__init__.py +0 -0
  55. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/portfolio/__init__.py +0 -0
  56. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/portfolio/optimisation.py +0 -0
  57. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/portfolio/rebalancing.py +0 -0
  58. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/regimes/__init__.py +0 -0
  59. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/regimes/changepoint.py +0 -0
  60. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/regimes/conditional.py +0 -0
  61. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/regimes/hmm.py +0 -0
  62. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/report/__init__.py +0 -0
  63. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/report/html_renderer.py +0 -0
  64. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/report/pdf_renderer.py +0 -0
  65. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/report/sections.py +0 -0
  66. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/report/tearsheet.py +0 -0
  67. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/resample/__init__.py +0 -0
  68. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/risk/__init__.py +0 -0
  69. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/risk/evt.py +0 -0
  70. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/risk/metrics.py +0 -0
  71. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/scenarios/__init__.py +0 -0
  72. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/visualisation.py +0 -0
  73. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/__init__.py +0 -0
  74. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/dependency.py +0 -0
  75. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/__init__.py +0 -0
  76. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/dependency.py +0 -0
  77. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/portfolio.py +0 -0
  78. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/regimes.py +0 -0
  79. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/risk.py +0 -0
  80. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/plotly_backend/theme.py +0 -0
  81. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/portfolio.py +0 -0
  82. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/regimes.py +0 -0
  83. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/risk.py +0 -0
  84. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite/viz/theme.py +0 -0
  85. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite.egg-info/dependency_links.txt +0 -0
  86. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite.egg-info/requires.txt +0 -0
  87. {quantlite-0.7.0 → quantlite-0.8.0}/src/quantlite.egg-info/top_level.txt +0 -0
  88. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_analysis.py +0 -0
  89. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_antifragile.py +0 -0
  90. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_backtesting.py +0 -0
  91. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_changepoint.py +0 -0
  92. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_clustering.py +0 -0
  93. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_conditional.py +0 -0
  94. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_contagion.py +0 -0
  95. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_copulas.py +0 -0
  96. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_correlation.py +0 -0
  97. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_crypto_exchange.py +0 -0
  98. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_crypto_onchain.py +0 -0
  99. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_crypto_stablecoin.py +0 -0
  100. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_data_connectors.py +0 -0
  101. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_data_generation.py +0 -0
  102. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_diversification.py +0 -0
  103. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_engine.py +0 -0
  104. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_ergodicity.py +0 -0
  105. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_evt.py +0 -0
  106. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_fat_tails.py +0 -0
  107. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_forensics.py +0 -0
  108. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_hmm.py +0 -0
  109. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_instruments.py +0 -0
  110. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_metrics.py +0 -0
  111. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_monte_carlo.py +0 -0
  112. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_network.py +0 -0
  113. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_optimisation.py +0 -0
  114. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_overfit.py +0 -0
  115. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_plotly_viz.py +0 -0
  116. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_rebalancing.py +0 -0
  117. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_report.py +0 -0
  118. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_resample.py +0 -0
  119. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_risk_metrics.py +0 -0
  120. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_scenarios.py +0 -0
  121. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_signals.py +0 -0
  122. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_visualisation.py +0 -0
  123. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_viz.py +0 -0
  124. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_viz_dependency.py +0 -0
  125. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_viz_portfolio.py +0 -0
  126. {quantlite-0.7.0 → quantlite-0.8.0}/tests/test_viz_regimes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantlite
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: A fat-tail-native quantitative finance toolkit: EVT, risk metrics, and honest modelling for markets that bite.
5
5
  Author-email: Prasant Sudhakaran <code@prasant.net>
6
6
  License: MIT License
@@ -713,6 +713,69 @@ Same Stephen Few theme, same muted palette, but with hover info, zoom, and nativ
713
713
  | `quantlite.crypto.stablecoin` | Depeg probability, peg deviation tracking, recovery time, reserve risk scoring |
714
714
  | `quantlite.crypto.exchange` | Exchange concentration (HHI), wallet risk, proof of reserves, liquidity risk, slippage |
715
715
  | `quantlite.crypto.onchain` | Wallet exposure, TVL tracking, DeFi dependency graphs, smart contract risk scoring |
716
+ | `quantlite.factors.classical` | Fama-French three/five-factor, Carhart four-factor, factor attribution, factor summary |
717
+ | `quantlite.factors.custom` | CustomFactor, significance testing, correlation matrix, factor portfolios, decay analysis |
718
+ | `quantlite.factors.tail_risk` | CVaR decomposition, regime factor exposure, crowding score, tail factor beta |
719
+
720
+ ## v0.8: Factor Models
721
+
722
+ Three modules for comprehensive factor analysis: classical academic models, custom factor tools, and tail-risk-aware factor decomposition.
723
+
724
+ ### Classical Factor Models
725
+
726
+ Decompose returns into systematic factor exposures and genuine alpha.
727
+
728
+ ```python
729
+ from quantlite.factors import fama_french_three, factor_summary
730
+
731
+ # Fama-French three-factor regression
732
+ result = fama_french_three(fund_returns, market, smb, hml)
733
+ print(f"Alpha: {result['alpha']:.5f} (t={result['t_stats']['alpha']:.2f})")
734
+ print(f"Market beta: {result['betas']['market']:.3f}")
735
+ print(f"R-squared: {result['r_squared']:.3f}")
736
+ ```
737
+
738
+ ![Factor Betas](docs/images/factor_betas.png)
739
+
740
+ ### Custom Factor Tools
741
+
742
+ Build, test, and analyse proprietary factors.
743
+
744
+ ```python
745
+ from quantlite.factors import CustomFactor, factor_portfolio, factor_decay
746
+
747
+ # Test factor decay
748
+ decay = factor_decay(returns, momentum_signal, max_lag=20)
749
+ print(f"Half-life: {decay['half_life']:.1f} periods")
750
+
751
+ # Build long-short portfolios
752
+ result = factor_portfolio(stock_returns, factor_values, n_quantiles=5)
753
+ print(f"Long-short spread: {result['spread'] * 252:.2%} annualised")
754
+ ```
755
+
756
+ ![Factor Quintiles](docs/images/factor_quintiles.png)
757
+
758
+ ### Tail Risk Factor Analysis
759
+
760
+ Understand how factor exposures behave in the tails and across regimes.
761
+
762
+ ```python
763
+ from quantlite.factors import tail_factor_beta, factor_crowding_score
764
+
765
+ # Tail betas: how exposures amplify in crises
766
+ result = tail_factor_beta(returns, [market, value], ["Market", "Value"])
767
+ for name in ["Market", "Value"]:
768
+ print(f"{name}: full={result['full_betas'][name]:.2f}, "
769
+ f"tail={result['tail_betas'][name]:.2f}")
770
+
771
+ # Factor crowding detection
772
+ crowd = factor_crowding_score([value_rets, momentum_rets])
773
+ print(f"Crowding score: {crowd['current_score']:.3f}")
774
+ ```
775
+
776
+ ![Factor Crowding](docs/images/factor_crowding.png)
777
+
778
+ See [docs/factors_classical.md](docs/factors_classical.md), [docs/factors_custom.md](docs/factors_custom.md), and [docs/factors_tail_risk.md](docs/factors_tail_risk.md) for the full API reference.
716
779
 
717
780
  ## v0.4: The Taleb Stack
718
781
 
@@ -632,6 +632,69 @@ Same Stephen Few theme, same muted palette, but with hover info, zoom, and nativ
632
632
  | `quantlite.crypto.stablecoin` | Depeg probability, peg deviation tracking, recovery time, reserve risk scoring |
633
633
  | `quantlite.crypto.exchange` | Exchange concentration (HHI), wallet risk, proof of reserves, liquidity risk, slippage |
634
634
  | `quantlite.crypto.onchain` | Wallet exposure, TVL tracking, DeFi dependency graphs, smart contract risk scoring |
635
+ | `quantlite.factors.classical` | Fama-French three/five-factor, Carhart four-factor, factor attribution, factor summary |
636
+ | `quantlite.factors.custom` | CustomFactor, significance testing, correlation matrix, factor portfolios, decay analysis |
637
+ | `quantlite.factors.tail_risk` | CVaR decomposition, regime factor exposure, crowding score, tail factor beta |
638
+
639
+ ## v0.8: Factor Models
640
+
641
+ Three modules for comprehensive factor analysis: classical academic models, custom factor tools, and tail-risk-aware factor decomposition.
642
+
643
+ ### Classical Factor Models
644
+
645
+ Decompose returns into systematic factor exposures and genuine alpha.
646
+
647
+ ```python
648
+ from quantlite.factors import fama_french_three, factor_summary
649
+
650
+ # Fama-French three-factor regression
651
+ result = fama_french_three(fund_returns, market, smb, hml)
652
+ print(f"Alpha: {result['alpha']:.5f} (t={result['t_stats']['alpha']:.2f})")
653
+ print(f"Market beta: {result['betas']['market']:.3f}")
654
+ print(f"R-squared: {result['r_squared']:.3f}")
655
+ ```
656
+
657
+ ![Factor Betas](docs/images/factor_betas.png)
658
+
659
+ ### Custom Factor Tools
660
+
661
+ Build, test, and analyse proprietary factors.
662
+
663
+ ```python
664
+ from quantlite.factors import CustomFactor, factor_portfolio, factor_decay
665
+
666
+ # Test factor decay
667
+ decay = factor_decay(returns, momentum_signal, max_lag=20)
668
+ print(f"Half-life: {decay['half_life']:.1f} periods")
669
+
670
+ # Build long-short portfolios
671
+ result = factor_portfolio(stock_returns, factor_values, n_quantiles=5)
672
+ print(f"Long-short spread: {result['spread'] * 252:.2%} annualised")
673
+ ```
674
+
675
+ ![Factor Quintiles](docs/images/factor_quintiles.png)
676
+
677
+ ### Tail Risk Factor Analysis
678
+
679
+ Understand how factor exposures behave in the tails and across regimes.
680
+
681
+ ```python
682
+ from quantlite.factors import tail_factor_beta, factor_crowding_score
683
+
684
+ # Tail betas: how exposures amplify in crises
685
+ result = tail_factor_beta(returns, [market, value], ["Market", "Value"])
686
+ for name in ["Market", "Value"]:
687
+ print(f"{name}: full={result['full_betas'][name]:.2f}, "
688
+ f"tail={result['tail_betas'][name]:.2f}")
689
+
690
+ # Factor crowding detection
691
+ crowd = factor_crowding_score([value_rets, momentum_rets])
692
+ print(f"Crowding score: {crowd['current_score']:.3f}")
693
+ ```
694
+
695
+ ![Factor Crowding](docs/images/factor_crowding.png)
696
+
697
+ See [docs/factors_classical.md](docs/factors_classical.md), [docs/factors_custom.md](docs/factors_custom.md), and [docs/factors_tail_risk.md](docs/factors_tail_risk.md) for the full API reference.
635
698
 
636
699
  ## v0.4: The Taleb Stack
637
700
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quantlite"
7
- version = "0.7.0"
7
+ version = "0.8.0"
8
8
  description = "A fat-tail-native quantitative finance toolkit: EVT, risk metrics, and honest modelling for markets that bite."
9
9
  requires-python = ">=3.9"
10
10
  license = { file = "LICENSE" }
@@ -73,3 +73,6 @@ python_version = "3.10"
73
73
  warn_return_any = true
74
74
  warn_unused_configs = true
75
75
  disallow_untyped_defs = false
76
+
77
+ [tool.pytest.ini_options]
78
+ testpaths = ["tests"]
@@ -0,0 +1,43 @@
1
+ """Factor models: classical, custom, and tail risk factor analysis.
2
+
3
+ Provides tools for multi-factor attribution, custom factor construction,
4
+ factor significance testing, and tail-risk-aware factor decomposition.
5
+ """
6
+
7
+ from quantlite.factors.classical import (
8
+ carhart_four,
9
+ factor_attribution,
10
+ factor_summary,
11
+ fama_french_five,
12
+ fama_french_three,
13
+ )
14
+ from quantlite.factors.custom import (
15
+ CustomFactor,
16
+ factor_correlation_matrix,
17
+ factor_decay,
18
+ factor_portfolio,
19
+ test_factor_significance,
20
+ )
21
+ from quantlite.factors.tail_risk import (
22
+ factor_crowding_score,
23
+ factor_cvar_decomposition,
24
+ regime_factor_exposure,
25
+ tail_factor_beta,
26
+ )
27
+
28
+ __all__ = [
29
+ "fama_french_three",
30
+ "fama_french_five",
31
+ "carhart_four",
32
+ "factor_attribution",
33
+ "factor_summary",
34
+ "CustomFactor",
35
+ "test_factor_significance",
36
+ "factor_correlation_matrix",
37
+ "factor_portfolio",
38
+ "factor_decay",
39
+ "factor_cvar_decomposition",
40
+ "regime_factor_exposure",
41
+ "factor_crowding_score",
42
+ "tail_factor_beta",
43
+ ]
@@ -0,0 +1,288 @@
1
+ """Classical factor models: Fama-French, Carhart, and generic multi-factor attribution.
2
+
3
+ Implements the standard academic factor models used in performance attribution
4
+ and risk analysis, plus a flexible generic factor regression framework.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+ from scipy import stats as sp_stats
11
+
12
+
13
+ def _ols_regression(y, X):
14
+ """Run OLS regression with intercept already included in X.
15
+
16
+ Returns dict with coefficients, t-stats, p-values, R-squared,
17
+ adjusted R-squared, and residuals.
18
+ """
19
+ y = np.asarray(y, dtype=float)
20
+ X = np.asarray(X, dtype=float)
21
+ n, k = X.shape
22
+
23
+ # OLS: beta = (X'X)^-1 X'y
24
+ XtX = X.T @ X
25
+ Xty = X.T @ y
26
+ try:
27
+ beta = np.linalg.solve(XtX, Xty)
28
+ except np.linalg.LinAlgError:
29
+ beta = np.linalg.lstsq(X, y, rcond=None)[0]
30
+
31
+ residuals = y - X @ beta
32
+ ss_res = float(np.sum(residuals ** 2))
33
+ ss_tot = float(np.sum((y - np.mean(y)) ** 2))
34
+
35
+ r_squared = 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0
36
+ adj_r_squared = 1.0 - (1.0 - r_squared) * (n - 1) / (n - k) if n > k else 0.0
37
+
38
+ # Standard errors
39
+ dof = max(n - k, 1)
40
+ mse = ss_res / dof
41
+ try:
42
+ cov_matrix = mse * np.linalg.inv(XtX)
43
+ except np.linalg.LinAlgError:
44
+ cov_matrix = mse * np.linalg.pinv(XtX)
45
+ se = np.sqrt(np.maximum(np.diag(cov_matrix), 0.0))
46
+
47
+ t_stats = np.where(se > 0, beta / se, 0.0)
48
+ p_values = np.array([
49
+ 2.0 * (1.0 - sp_stats.t.cdf(abs(t), df=dof)) for t in t_stats
50
+ ])
51
+
52
+ return {
53
+ "coefficients": beta,
54
+ "t_stats": t_stats,
55
+ "p_values": p_values,
56
+ "r_squared": r_squared,
57
+ "adj_r_squared": adj_r_squared,
58
+ "residuals": residuals,
59
+ "se": se,
60
+ }
61
+
62
+
63
+ def _run_factor_model(returns, factor_arrays, factor_names):
64
+ """Common logic for running a factor regression.
65
+
66
+ Parameters
67
+ ----------
68
+ returns : array-like
69
+ Asset return series.
70
+ factor_arrays : list of array-like
71
+ Factor return series (each same length as returns).
72
+ factor_names : list of str
73
+ Names for each factor.
74
+
75
+ Returns
76
+ -------
77
+ dict with alpha, betas, r_squared, t_stats, p_values, residuals,
78
+ adj_r_squared, and named beta entries.
79
+ """
80
+ y = np.asarray(returns, dtype=float)
81
+ factors = [np.asarray(f, dtype=float) for f in factor_arrays]
82
+
83
+ n = len(y)
84
+
85
+ # Build design matrix with intercept
86
+ X = np.column_stack([np.ones(n)] + factors)
87
+
88
+ reg = _ols_regression(y, X)
89
+
90
+ alpha = float(reg["coefficients"][0])
91
+ betas = {name: float(reg["coefficients"][i + 1]) for i, name in enumerate(factor_names)}
92
+
93
+ result = {
94
+ "alpha": alpha,
95
+ "betas": betas,
96
+ "r_squared": reg["r_squared"],
97
+ "adj_r_squared": reg["adj_r_squared"],
98
+ "t_stats": {
99
+ "alpha": float(reg["t_stats"][0]),
100
+ },
101
+ "p_values": {
102
+ "alpha": float(reg["p_values"][0]),
103
+ },
104
+ "residuals": reg["residuals"],
105
+ }
106
+
107
+ for i, name in enumerate(factor_names):
108
+ result["t_stats"][name] = float(reg["t_stats"][i + 1])
109
+ result["p_values"][name] = float(reg["p_values"][i + 1])
110
+
111
+ return result
112
+
113
+
114
+ def fama_french_three(returns, market_returns, smb, hml):
115
+ """Fama-French three-factor model regression.
116
+
117
+ Regresses asset returns against market excess returns, SMB (Small Minus
118
+ Big), and HML (High Minus Low) factors.
119
+
120
+ Parameters
121
+ ----------
122
+ returns : array-like
123
+ Asset excess return series.
124
+ market_returns : array-like
125
+ Market excess return series.
126
+ smb : array-like
127
+ Size factor (Small Minus Big).
128
+ hml : array-like
129
+ Value factor (High Minus Low).
130
+
131
+ Returns
132
+ -------
133
+ dict
134
+ Keys: alpha, betas (dict with 'market', 'smb', 'hml'),
135
+ r_squared, adj_r_squared, t_stats, p_values, residuals.
136
+ """
137
+ return _run_factor_model(
138
+ returns,
139
+ [market_returns, smb, hml],
140
+ ["market", "smb", "hml"],
141
+ )
142
+
143
+
144
+ def fama_french_five(returns, market_returns, smb, hml, rmw, cma):
145
+ """Fama-French five-factor model regression.
146
+
147
+ Extends the three-factor model with profitability (RMW) and
148
+ investment (CMA) factors.
149
+
150
+ Parameters
151
+ ----------
152
+ returns : array-like
153
+ Asset excess return series.
154
+ market_returns : array-like
155
+ Market excess return series.
156
+ smb : array-like
157
+ Size factor (Small Minus Big).
158
+ hml : array-like
159
+ Value factor (High Minus Low).
160
+ rmw : array-like
161
+ Profitability factor (Robust Minus Weak).
162
+ cma : array-like
163
+ Investment factor (Conservative Minus Aggressive).
164
+
165
+ Returns
166
+ -------
167
+ dict
168
+ Keys: alpha, betas (dict with 'market', 'smb', 'hml', 'rmw', 'cma'),
169
+ r_squared, adj_r_squared, t_stats, p_values, residuals.
170
+ """
171
+ return _run_factor_model(
172
+ returns,
173
+ [market_returns, smb, hml, rmw, cma],
174
+ ["market", "smb", "hml", "rmw", "cma"],
175
+ )
176
+
177
+
178
+ def carhart_four(returns, market_returns, smb, hml, mom):
179
+ """Carhart four-factor model regression.
180
+
181
+ Extends the Fama-French three-factor model with a momentum factor.
182
+
183
+ Parameters
184
+ ----------
185
+ returns : array-like
186
+ Asset excess return series.
187
+ market_returns : array-like
188
+ Market excess return series.
189
+ smb : array-like
190
+ Size factor (Small Minus Big).
191
+ hml : array-like
192
+ Value factor (High Minus Low).
193
+ mom : array-like
194
+ Momentum factor (Winners Minus Losers).
195
+
196
+ Returns
197
+ -------
198
+ dict
199
+ Keys: alpha, betas (dict with 'market', 'smb', 'hml', 'mom'),
200
+ r_squared, adj_r_squared, t_stats, p_values, residuals.
201
+ """
202
+ return _run_factor_model(
203
+ returns,
204
+ [market_returns, smb, hml, mom],
205
+ ["market", "smb", "hml", "mom"],
206
+ )
207
+
208
+
209
+ def factor_attribution(returns, factor_returns, factor_names):
210
+ """Generic multi-factor attribution.
211
+
212
+ Decomposes total returns into factor contributions and unexplained
213
+ (alpha) component.
214
+
215
+ Parameters
216
+ ----------
217
+ returns : array-like
218
+ Asset return series.
219
+ factor_returns : list of array-like
220
+ Factor return series.
221
+ factor_names : list of str
222
+ Names for each factor.
223
+
224
+ Returns
225
+ -------
226
+ dict
227
+ Keys: alpha, factor_contributions (dict mapping factor name to
228
+ mean contribution), unexplained (mean residual return),
229
+ r_squared, total_return (annualised mean).
230
+ """
231
+ result = _run_factor_model(returns, factor_returns, factor_names)
232
+
233
+ y = np.asarray(returns, dtype=float)
234
+ factors = [np.asarray(f, dtype=float) for f in factor_returns]
235
+
236
+ contributions = {}
237
+ for i, name in enumerate(factor_names):
238
+ beta = result["betas"][name]
239
+ mean_factor = float(np.mean(factors[i]))
240
+ contributions[name] = beta * mean_factor
241
+
242
+ total_mean = float(np.mean(y))
243
+ explained = sum(contributions.values())
244
+ unexplained = total_mean - explained
245
+
246
+ return {
247
+ "alpha": result["alpha"],
248
+ "factor_contributions": contributions,
249
+ "unexplained": unexplained,
250
+ "r_squared": result["r_squared"],
251
+ "total_return": total_mean,
252
+ }
253
+
254
+
255
+ def factor_summary(returns, factor_returns, factor_names):
256
+ """One-call summary table for multi-factor regression.
257
+
258
+ Returns a comprehensive summary including alpha, each factor beta,
259
+ t-statistics, p-values, R-squared, and adjusted R-squared.
260
+
261
+ Parameters
262
+ ----------
263
+ returns : array-like
264
+ Asset return series.
265
+ factor_returns : list of array-like
266
+ Factor return series.
267
+ factor_names : list of str
268
+ Names for each factor.
269
+
270
+ Returns
271
+ -------
272
+ dict
273
+ Keys: alpha, alpha_t, alpha_p, betas (dict), t_stats (dict),
274
+ p_values (dict), r_squared, adj_r_squared, n_obs.
275
+ """
276
+ result = _run_factor_model(returns, factor_returns, factor_names)
277
+
278
+ return {
279
+ "alpha": result["alpha"],
280
+ "alpha_t": result["t_stats"]["alpha"],
281
+ "alpha_p": result["p_values"]["alpha"],
282
+ "betas": result["betas"],
283
+ "t_stats": {name: result["t_stats"][name] for name in factor_names},
284
+ "p_values": {name: result["p_values"][name] for name in factor_names},
285
+ "r_squared": result["r_squared"],
286
+ "adj_r_squared": result["adj_r_squared"],
287
+ "n_obs": len(np.asarray(returns)),
288
+ }