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

|
|
739
|
+
|
|
740
|
+
### Custom Factor Tools
|
|
741
|
+
|
|
742
|
+
Build, test, and analyse proprietary factors.
|
|
743
|
+
|
|
744
|
+
```python
|
|
745
|
+
from quantlite.factors import CustomFactor, factor_portfolio, factor_decay
|
|
746
|
+
|
|
747
|
+
# Test factor decay
|
|
748
|
+
decay = factor_decay(returns, momentum_signal, max_lag=20)
|
|
749
|
+
print(f"Half-life: {decay['half_life']:.1f} periods")
|
|
750
|
+
|
|
751
|
+
# Build long-short portfolios
|
|
752
|
+
result = factor_portfolio(stock_returns, factor_values, n_quantiles=5)
|
|
753
|
+
print(f"Long-short spread: {result['spread'] * 252:.2%} annualised")
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+

|
|
757
|
+
|
|
758
|
+
### Tail Risk Factor Analysis
|
|
759
|
+
|
|
760
|
+
Understand how factor exposures behave in the tails and across regimes.
|
|
761
|
+
|
|
762
|
+
```python
|
|
763
|
+
from quantlite.factors import tail_factor_beta, factor_crowding_score
|
|
764
|
+
|
|
765
|
+
# Tail betas: how exposures amplify in crises
|
|
766
|
+
result = tail_factor_beta(returns, [market, value], ["Market", "Value"])
|
|
767
|
+
for name in ["Market", "Value"]:
|
|
768
|
+
print(f"{name}: full={result['full_betas'][name]:.2f}, "
|
|
769
|
+
f"tail={result['tail_betas'][name]:.2f}")
|
|
770
|
+
|
|
771
|
+
# Factor crowding detection
|
|
772
|
+
crowd = factor_crowding_score([value_rets, momentum_rets])
|
|
773
|
+
print(f"Crowding score: {crowd['current_score']:.3f}")
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+

|
|
777
|
+
|
|
778
|
+
See [docs/factors_classical.md](docs/factors_classical.md), [docs/factors_custom.md](docs/factors_custom.md), and [docs/factors_tail_risk.md](docs/factors_tail_risk.md) for the full API reference.
|
|
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
|
+

|
|
658
|
+
|
|
659
|
+
### Custom Factor Tools
|
|
660
|
+
|
|
661
|
+
Build, test, and analyse proprietary factors.
|
|
662
|
+
|
|
663
|
+
```python
|
|
664
|
+
from quantlite.factors import CustomFactor, factor_portfolio, factor_decay
|
|
665
|
+
|
|
666
|
+
# Test factor decay
|
|
667
|
+
decay = factor_decay(returns, momentum_signal, max_lag=20)
|
|
668
|
+
print(f"Half-life: {decay['half_life']:.1f} periods")
|
|
669
|
+
|
|
670
|
+
# Build long-short portfolios
|
|
671
|
+
result = factor_portfolio(stock_returns, factor_values, n_quantiles=5)
|
|
672
|
+
print(f"Long-short spread: {result['spread'] * 252:.2%} annualised")
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+

|
|
676
|
+
|
|
677
|
+
### Tail Risk Factor Analysis
|
|
678
|
+
|
|
679
|
+
Understand how factor exposures behave in the tails and across regimes.
|
|
680
|
+
|
|
681
|
+
```python
|
|
682
|
+
from quantlite.factors import tail_factor_beta, factor_crowding_score
|
|
683
|
+
|
|
684
|
+
# Tail betas: how exposures amplify in crises
|
|
685
|
+
result = tail_factor_beta(returns, [market, value], ["Market", "Value"])
|
|
686
|
+
for name in ["Market", "Value"]:
|
|
687
|
+
print(f"{name}: full={result['full_betas'][name]:.2f}, "
|
|
688
|
+
f"tail={result['tail_betas'][name]:.2f}")
|
|
689
|
+
|
|
690
|
+
# Factor crowding detection
|
|
691
|
+
crowd = factor_crowding_score([value_rets, momentum_rets])
|
|
692
|
+
print(f"Crowding score: {crowd['current_score']:.3f}")
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+

|
|
696
|
+
|
|
697
|
+
See [docs/factors_classical.md](docs/factors_classical.md), [docs/factors_custom.md](docs/factors_custom.md), and [docs/factors_tail_risk.md](docs/factors_tail_risk.md) for the full API reference.
|
|
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
|
+
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
|
+
}
|