quant-pml 0.1.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.
- quant_pml-0.1.0/LICENSE +21 -0
- quant_pml-0.1.0/PKG-INFO +172 -0
- quant_pml-0.1.0/README.md +146 -0
- quant_pml-0.1.0/pyproject.toml +179 -0
- quant_pml-0.1.0/setup.cfg +7 -0
- quant_pml-0.1.0/setup.py +6 -0
- quant_pml-0.1.0/src/quant_pml/__init__.py +1 -0
- quant_pml-0.1.0/src/quant_pml/ap/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/ap/base_asset_pricer.py +58 -0
- quant_pml-0.1.0/src/quant_pml/ap/ipca_factor_model.py +104 -0
- quant_pml-0.1.0/src/quant_pml/ap/kns_factor_model.py +90 -0
- quant_pml-0.1.0/src/quant_pml/ap/uncond_factor_model.py +72 -0
- quant_pml-0.1.0/src/quant_pml/backtest/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/backtest/assessor.py +214 -0
- quant_pml-0.1.0/src/quant_pml/backtest/backtester.py +647 -0
- quant_pml-0.1.0/src/quant_pml/backtest/plot.py +140 -0
- quant_pml-0.1.0/src/quant_pml/backtest/research_us_backtest.py +54 -0
- quant_pml-0.1.0/src/quant_pml/backtest/russell3000_backtest.py +54 -0
- quant_pml-0.1.0/src/quant_pml/backtest/transaction_costs_charger.py +84 -0
- quant_pml-0.1.0/src/quant_pml/base/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/base/currencies.py +29 -0
- quant_pml-0.1.0/src/quant_pml/base/prices.py +31 -0
- quant_pml-0.1.0/src/quant_pml/base/returns.py +40 -0
- quant_pml-0.1.0/src/quant_pml/config/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/config/base_experiment_config.py +219 -0
- quant_pml-0.1.0/src/quant_pml/config/base_experiment_config_v2.py +178 -0
- quant_pml-0.1.0/src/quant_pml/config/experiment_config.py +96 -0
- quant_pml-0.1.0/src/quant_pml/config/jkp_experiment_config.py +15 -0
- quant_pml-0.1.0/src/quant_pml/config/research_us_experiment_config.py +15 -0
- quant_pml-0.1.0/src/quant_pml/config/research_us_trading_config.py +17 -0
- quant_pml-0.1.0/src/quant_pml/config/russell_3000_experiment_config.py +15 -0
- quant_pml-0.1.0/src/quant_pml/config/russia_experiment_config.py +112 -0
- quant_pml-0.1.0/src/quant_pml/config/spx_experiment_config.py +15 -0
- quant_pml-0.1.0/src/quant_pml/config/topn_experiment_config.py +19 -0
- quant_pml-0.1.0/src/quant_pml/config/trading_config.py +25 -0
- quant_pml-0.1.0/src/quant_pml/config/us_experiment_config.py +104 -0
- quant_pml-0.1.0/src/quant_pml/data_handlers/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/data_handlers/additional_data.py +54 -0
- quant_pml-0.1.0/src/quant_pml/data_handlers/create_compustat_dataset.py +129 -0
- quant_pml-0.1.0/src/quant_pml/data_handlers/create_crsp_dataset.py +142 -0
- quant_pml-0.1.0/src/quant_pml/data_handlers/create_spx_dataset.py +35 -0
- quant_pml-0.1.0/src/quant_pml/data_handlers/dataset.py +18 -0
- quant_pml-0.1.0/src/quant_pml/data_handlers/dataset_builder_functions.py +194 -0
- quant_pml-0.1.0/src/quant_pml/data_handlers/prepare_data.py +94 -0
- quant_pml-0.1.0/src/quant_pml/data_handlers/universe_builder_functions.py +73 -0
- quant_pml-0.1.0/src/quant_pml/dataset/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/dataset/dataset_builder.py +428 -0
- quant_pml-0.1.0/src/quant_pml/dataset/dnk_features_targets.py +0 -0
- quant_pml-0.1.0/src/quant_pml/date/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/date/pd_date.py +17 -0
- quant_pml-0.1.0/src/quant_pml/estimation/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/base_estimator.py +45 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/base_cov_estimator.py +10 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/factor/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/factor/factor_cov_estimator.py +78 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/heuristic/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/heuristic/hist_cov_estimator.py +23 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/ml/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/ml/glasso_estimator.py +52 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/ml/glasso_tscv_estimator.py +39 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/ml/predictors/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/ml/predictors/sklearn_ml_predictor.py +68 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/base_rl_estimator.py +199 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/ar_estimator.py +38 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/average_estimator.py +22 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/bound_estimator.py +21 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/dl/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/dl/dl_model.py +139 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/dl/models/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/dl/models/mlp.py +26 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/dl_estimator.py +36 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/dnk_estimator.py +61 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/gp_estimator.py +46 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/gpr_estimator.py +48 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/lasso_estimator.py +50 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/last_optimal_estimator.py +30 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/ma_estimator.py +30 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/ols_estimator.py +45 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/pretrained_estimator.py +63 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/rf_estimator.py +43 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/rf_xiu_estimator.py +50 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/ridge_estimator.py +49 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/ridge_prior_estimator.py +57 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/uncert_ensemble_estimator.py +79 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/behavioral_cloning/xgb_estimator.py +68 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/rl/env.py +174 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/sample_cov_estimator.py +23 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/shrinkage/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/shrinkage/base_cross_val_cov_estimator.py +105 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/shrinkage/de_nard_cov_estimator.py +48 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/shrinkage/diag_hist_cov_estimator.py +24 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/shrinkage/identity_based_cov_estimator.py +36 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/shrinkage/lw_cv_cov_estimator.py +92 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/shrinkage/lw_linear_estimator.py +36 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/shrinkage/pca_cov_estimator.py +51 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/shrinkage/qis.py +169 -0
- quant_pml-0.1.0/src/quant_pml/estimation/covariance/shrinkage/rp_cov_estimator.py +42 -0
- quant_pml-0.1.0/src/quant_pml/estimation/mean/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/estimation/mean/base_mu_estimator.py +10 -0
- quant_pml-0.1.0/src/quant_pml/estimation/mean/sample_mu_estimator.py +23 -0
- quant_pml-0.1.0/src/quant_pml/feature_processor.py +24 -0
- quant_pml-0.1.0/src/quant_pml/features/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/features/base_preprocessor.py +24 -0
- quant_pml-0.1.0/src/quant_pml/features/ols_betas.py +102 -0
- quant_pml-0.1.0/src/quant_pml/features/online_ols_betas.py +194 -0
- quant_pml-0.1.0/src/quant_pml/features/preprocessor.py +33 -0
- quant_pml-0.1.0/src/quant_pml/hedge/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/hedge/base_hedger.py +71 -0
- quant_pml-0.1.0/src/quant_pml/hedge/constant_hedge.py +31 -0
- quant_pml-0.1.0/src/quant_pml/hedge/hedger.py +30 -0
- quant_pml-0.1.0/src/quant_pml/hedge/market_futures_hedge.py +53 -0
- quant_pml-0.1.0/src/quant_pml/market_data/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/market_data/market_data.py +175 -0
- quant_pml-0.1.0/src/quant_pml/market_data/risk_free_conventions.py +11 -0
- quant_pml-0.1.0/src/quant_pml/market_data/ticker.py +10 -0
- quant_pml-0.1.0/src/quant_pml/market_data/tickers.py +57 -0
- quant_pml-0.1.0/src/quant_pml/meta_portfolio/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/optimization/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/optimization/constraints.py +201 -0
- quant_pml-0.1.0/src/quant_pml/optimization/helper_functions.py +57 -0
- quant_pml-0.1.0/src/quant_pml/optimization/optimization.py +254 -0
- quant_pml-0.1.0/src/quant_pml/optimization/quadratic_program.py +271 -0
- quant_pml-0.1.0/src/quant_pml/py.typed +0 -0
- quant_pml-0.1.0/src/quant_pml/runner.py +417 -0
- quant_pml-0.1.0/src/quant_pml/stats/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/stats/helpers/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/stats/helpers/linalg.py +14 -0
- quant_pml-0.1.0/src/quant_pml/stats/helpers/max_drawdown.py +11 -0
- quant_pml-0.1.0/src/quant_pml/stats/stats_pack.py +9 -0
- quant_pml-0.1.0/src/quant_pml/strategies/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/strategies/base_strategy.py +142 -0
- quant_pml-0.1.0/src/quant_pml/strategies/factors/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/strategies/factors/high_beta.py +33 -0
- quant_pml-0.1.0/src/quant_pml/strategies/factors/low_beta.py +64 -0
- quant_pml-0.1.0/src/quant_pml/strategies/factors/momentum.py +50 -0
- quant_pml-0.1.0/src/quant_pml/strategies/factors/momentum_sign.py +46 -0
- quant_pml-0.1.0/src/quant_pml/strategies/factors/size.py +53 -0
- quant_pml-0.1.0/src/quant_pml/strategies/factors/sorting_strategy.py +92 -0
- quant_pml-0.1.0/src/quant_pml/strategies/heuristics/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/strategies/heuristics/capitalization_weighted.py +29 -0
- quant_pml-0.1.0/src/quant_pml/strategies/heuristics/equally_weighted.py +25 -0
- quant_pml-0.1.0/src/quant_pml/strategies/heuristics/target_strategy.py +26 -0
- quant_pml-0.1.0/src/quant_pml/strategies/ml/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/strategies/ml/ret_clf/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/strategies/ml/ret_clf/fractional_momentum_sample_mean.py +69 -0
- quant_pml-0.1.0/src/quant_pml/strategies/ml/ret_clf/fractional_momentum_sklearn.py +74 -0
- quant_pml-0.1.0/src/quant_pml/strategies/ml/scoring/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/strategies/ml/scoring/base_ml_scoring_strategy.py +66 -0
- quant_pml-0.1.0/src/quant_pml/strategies/optimization_data.py +83 -0
- quant_pml-0.1.0/src/quant_pml/strategies/optimized/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/strategies/optimized/base_estimated_strategy.py +66 -0
- quant_pml-0.1.0/src/quant_pml/strategies/optimized/long_short_min_var.py +47 -0
- quant_pml-0.1.0/src/quant_pml/strategies/optimized/max_decorr.py +62 -0
- quant_pml-0.1.0/src/quant_pml/strategies/optimized/mean_var.py +62 -0
- quant_pml-0.1.0/src/quant_pml/strategies/optimized/min_var.py +67 -0
- quant_pml-0.1.0/src/quant_pml/strategies/optimized/resid_min_var.py +62 -0
- quant_pml-0.1.0/src/quant_pml/strategies/optimized/risk_parity.py +40 -0
- quant_pml-0.1.0/src/quant_pml/strategies/optimized/timed_risk_parity.py +56 -0
- quant_pml-0.1.0/src/quant_pml/strategies/optimized/unconditional_mean_var.py +56 -0
- quant_pml-0.1.0/src/quant_pml/strategies/scaling/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/strategies/scaling/volatility_targeting.py +74 -0
- quant_pml-0.1.0/src/quant_pml/strategies/timing/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/strategies/timing/vol_timing.py +66 -0
- quant_pml-0.1.0/src/quant_pml/strategies/timing/vol_timing_vs_bonds.py +37 -0
- quant_pml-0.1.0/src/quant_pml/strategies/weighting/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/strategies/weighting/weighting_mixin.py +82 -0
- quant_pml-0.1.0/src/quant_pml/utils/__init__.py +0 -0
- quant_pml-0.1.0/src/quant_pml/utils/data.py +76 -0
- quant_pml-0.1.0/src/quant_pml/utils/linalg.py +41 -0
- quant_pml-0.1.0/src/quant_pml/utils/pd_date.py +31 -0
- quant_pml-0.1.0/src/quant_pml.egg-info/PKG-INFO +172 -0
- quant_pml-0.1.0/src/quant_pml.egg-info/SOURCES.txt +177 -0
- quant_pml-0.1.0/src/quant_pml.egg-info/dependency_links.txt +1 -0
- quant_pml-0.1.0/src/quant_pml.egg-info/requires.txt +15 -0
- quant_pml-0.1.0/src/quant_pml.egg-info/top_level.txt +1 -0
quant_pml-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Viacheslav Buchkov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
quant_pml-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quant-pml
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Author: Viacheslav Buchkov
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: quant,quantitative finance,machine learning
|
|
7
|
+
Requires-Python: ==3.12.5
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: statsmodels>=0.14.4
|
|
11
|
+
Requires-Dist: scikit-learn>=1.5.2
|
|
12
|
+
Requires-Dist: numpy>=2.2.4
|
|
13
|
+
Requires-Dist: pandas>=2.3.1
|
|
14
|
+
Requires-Dist: matplotlib>=3.10.1
|
|
15
|
+
Requires-Dist: qpsolvers>=4.5.0
|
|
16
|
+
Requires-Dist: torch==2.8.0
|
|
17
|
+
Requires-Dist: pyarrow==20.0.0
|
|
18
|
+
Requires-Dist: fastparquet==2024.11.0
|
|
19
|
+
Requires-Dist: tqdm>=4.67.1
|
|
20
|
+
Requires-Dist: scipy>=1.16.1
|
|
21
|
+
Requires-Dist: optuna==4.4.0
|
|
22
|
+
Requires-Dist: plotly>=6.2.0
|
|
23
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
24
|
+
Requires-Dist: polars==1.34.0
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Quantitative Portfolio Machine Learning Library
|
|
28
|
+
|
|
29
|
+
_Viacheslav Buchkov_ \
|
|
30
|
+
ETH Zürich
|
|
31
|
+
|
|
32
|
+
**Keywords**: quant; quantitative finance; portfolio management; machine learning; reinforcement learning.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install quant-pml
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## How To Run A Backtest
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
from quant_pml.config.trading_config import TradingConfig
|
|
45
|
+
from quant_pml.config.russell_3000_experiment_config import Russell3000ExperimentConfig
|
|
46
|
+
from quant_pml.hedge.market_futures_hedge import MarketFuturesHedge
|
|
47
|
+
from quant_pml.runner import build_backtest
|
|
48
|
+
from quant_pml.strategies.factors.momentum import Momentum
|
|
49
|
+
|
|
50
|
+
# Specify the model hyperparameters
|
|
51
|
+
REBAL_FREQ = "D" # Rebalance frequency (Month End)
|
|
52
|
+
QUANTILE = 0.1 # Sorted portfolio quantile
|
|
53
|
+
MODE = "long_short" # Trading mode
|
|
54
|
+
|
|
55
|
+
# Specify the trading configurations
|
|
56
|
+
trading_config = TradingConfig(
|
|
57
|
+
broker_fee=0, # Broker commission in decimals
|
|
58
|
+
bid_ask_spread=0, # Bid-ask spread in decimals
|
|
59
|
+
total_exposure=1, # Budget constraint
|
|
60
|
+
max_exposure=None, # Maximum weight constraint
|
|
61
|
+
min_exposure=None, # Minimum weight constraint
|
|
62
|
+
trading_lag_days=1, # Trading Lag
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Create a strategy with logic for weights selection
|
|
66
|
+
strategy = Momentum(
|
|
67
|
+
mode=MODE,
|
|
68
|
+
quantile=QUANTILE,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Initialize the hedger (can be set to `None` if no hedging is required)
|
|
72
|
+
hedger = MarketFuturesHedge(market_name="spx")
|
|
73
|
+
|
|
74
|
+
# Build the backtest pipeline
|
|
75
|
+
preprocessor, runner = build_backtest(
|
|
76
|
+
experiment_config=Russell3000ExperimentConfig(),
|
|
77
|
+
trading_config=trading_config,
|
|
78
|
+
rebal_freq=REBAL_FREQ,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Run the backtest
|
|
82
|
+
res = runner(
|
|
83
|
+
feature_processor=preprocessor,
|
|
84
|
+
strategy=strategy,
|
|
85
|
+
hedger=hedger,
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## How To Create A New Strategy
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
import pandas as pd
|
|
93
|
+
from quant_pml.strategies.base_strategy import BaseStrategy
|
|
94
|
+
from quant_pml.strategies.optimization_data import TrainingData, PredictionData
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Create a blueprint for your strategy
|
|
98
|
+
class NewStrategy(BaseStrategy):
|
|
99
|
+
def __init__(self) -> None:
|
|
100
|
+
super().__init__()
|
|
101
|
+
|
|
102
|
+
# Store the data across time, if needed
|
|
103
|
+
|
|
104
|
+
def _fit(self, training_data: TrainingData) -> None:
|
|
105
|
+
# Specify here your fitting logic
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def _get_weights(self, prediction_data: PredictionData, weights_: pd.DataFrame) -> pd.DataFrame:
|
|
109
|
+
# Specify here your prediction logic
|
|
110
|
+
|
|
111
|
+
# Update the weights DataFrame by modifying the respective columns of stock_ids
|
|
112
|
+
return weights_
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## How To Create A New Covariance Estimator
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
from __future__ import annotations
|
|
119
|
+
|
|
120
|
+
from typing import TYPE_CHECKING
|
|
121
|
+
|
|
122
|
+
if TYPE_CHECKING:
|
|
123
|
+
import pandas as pd
|
|
124
|
+
|
|
125
|
+
from quant_pml.strategies.optimization_data import PredictionData, TrainingData
|
|
126
|
+
from quant_pml.cov_estimators.base_cov_estimator import BaseCovEstimator
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class NewCovEstimator(BaseCovEstimator):
|
|
130
|
+
def __init__(self) -> None:
|
|
131
|
+
super().__init__()
|
|
132
|
+
|
|
133
|
+
self._fitted_cov = None
|
|
134
|
+
|
|
135
|
+
def _fit(self, training_data: TrainingData) -> None:
|
|
136
|
+
# Specify here your fitting logic (e.g., store a historical covmat)
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
def _predict(self, prediction_data: PredictionData) -> pd.DataFrame:
|
|
140
|
+
# Specify here your prediction logic (e.g., get a stored estimate)
|
|
141
|
+
...
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## How To Create A New Data-Driven Covariance Estimator
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
from __future__ import annotations
|
|
148
|
+
|
|
149
|
+
from typing import TYPE_CHECKING
|
|
150
|
+
|
|
151
|
+
if TYPE_CHECKING:
|
|
152
|
+
import pandas as pd
|
|
153
|
+
|
|
154
|
+
from quant_pml.cov_estimators.rl.base_rl_estimator import BaseRLCovEstimator
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class NewRLCovEstimator(BaseRLCovEstimator):
|
|
158
|
+
def __init__(self, shrinkage_type: str, window_size: int | None = None) -> None:
|
|
159
|
+
super().__init__(shrinkage_type=shrinkage_type, window_size=window_size)
|
|
160
|
+
|
|
161
|
+
def _fit_shrinkage(
|
|
162
|
+
self, features: pd.DataFrame, shrinkage_target: pd.Series
|
|
163
|
+
) -> None:
|
|
164
|
+
# Specify here your model (might be classical ML / RL / etc.)
|
|
165
|
+
self.model = ...
|
|
166
|
+
self.model.fit(X=features, y=shrinkage_target)
|
|
167
|
+
|
|
168
|
+
def _predict_shrinkage(self, features: pd.DataFrame) -> float:
|
|
169
|
+
pred = self.model.predict(features).item()
|
|
170
|
+
|
|
171
|
+
return pred
|
|
172
|
+
```
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Quantitative Portfolio Machine Learning Library
|
|
2
|
+
|
|
3
|
+
_Viacheslav Buchkov_ \
|
|
4
|
+
ETH Zürich
|
|
5
|
+
|
|
6
|
+
**Keywords**: quant; quantitative finance; portfolio management; machine learning; reinforcement learning.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
pip install quant-pml
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## How To Run A Backtest
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
from quant_pml.config.trading_config import TradingConfig
|
|
19
|
+
from quant_pml.config.russell_3000_experiment_config import Russell3000ExperimentConfig
|
|
20
|
+
from quant_pml.hedge.market_futures_hedge import MarketFuturesHedge
|
|
21
|
+
from quant_pml.runner import build_backtest
|
|
22
|
+
from quant_pml.strategies.factors.momentum import Momentum
|
|
23
|
+
|
|
24
|
+
# Specify the model hyperparameters
|
|
25
|
+
REBAL_FREQ = "D" # Rebalance frequency (Month End)
|
|
26
|
+
QUANTILE = 0.1 # Sorted portfolio quantile
|
|
27
|
+
MODE = "long_short" # Trading mode
|
|
28
|
+
|
|
29
|
+
# Specify the trading configurations
|
|
30
|
+
trading_config = TradingConfig(
|
|
31
|
+
broker_fee=0, # Broker commission in decimals
|
|
32
|
+
bid_ask_spread=0, # Bid-ask spread in decimals
|
|
33
|
+
total_exposure=1, # Budget constraint
|
|
34
|
+
max_exposure=None, # Maximum weight constraint
|
|
35
|
+
min_exposure=None, # Minimum weight constraint
|
|
36
|
+
trading_lag_days=1, # Trading Lag
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Create a strategy with logic for weights selection
|
|
40
|
+
strategy = Momentum(
|
|
41
|
+
mode=MODE,
|
|
42
|
+
quantile=QUANTILE,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Initialize the hedger (can be set to `None` if no hedging is required)
|
|
46
|
+
hedger = MarketFuturesHedge(market_name="spx")
|
|
47
|
+
|
|
48
|
+
# Build the backtest pipeline
|
|
49
|
+
preprocessor, runner = build_backtest(
|
|
50
|
+
experiment_config=Russell3000ExperimentConfig(),
|
|
51
|
+
trading_config=trading_config,
|
|
52
|
+
rebal_freq=REBAL_FREQ,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Run the backtest
|
|
56
|
+
res = runner(
|
|
57
|
+
feature_processor=preprocessor,
|
|
58
|
+
strategy=strategy,
|
|
59
|
+
hedger=hedger,
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## How To Create A New Strategy
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
import pandas as pd
|
|
67
|
+
from quant_pml.strategies.base_strategy import BaseStrategy
|
|
68
|
+
from quant_pml.strategies.optimization_data import TrainingData, PredictionData
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Create a blueprint for your strategy
|
|
72
|
+
class NewStrategy(BaseStrategy):
|
|
73
|
+
def __init__(self) -> None:
|
|
74
|
+
super().__init__()
|
|
75
|
+
|
|
76
|
+
# Store the data across time, if needed
|
|
77
|
+
|
|
78
|
+
def _fit(self, training_data: TrainingData) -> None:
|
|
79
|
+
# Specify here your fitting logic
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def _get_weights(self, prediction_data: PredictionData, weights_: pd.DataFrame) -> pd.DataFrame:
|
|
83
|
+
# Specify here your prediction logic
|
|
84
|
+
|
|
85
|
+
# Update the weights DataFrame by modifying the respective columns of stock_ids
|
|
86
|
+
return weights_
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## How To Create A New Covariance Estimator
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
from __future__ import annotations
|
|
93
|
+
|
|
94
|
+
from typing import TYPE_CHECKING
|
|
95
|
+
|
|
96
|
+
if TYPE_CHECKING:
|
|
97
|
+
import pandas as pd
|
|
98
|
+
|
|
99
|
+
from quant_pml.strategies.optimization_data import PredictionData, TrainingData
|
|
100
|
+
from quant_pml.cov_estimators.base_cov_estimator import BaseCovEstimator
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class NewCovEstimator(BaseCovEstimator):
|
|
104
|
+
def __init__(self) -> None:
|
|
105
|
+
super().__init__()
|
|
106
|
+
|
|
107
|
+
self._fitted_cov = None
|
|
108
|
+
|
|
109
|
+
def _fit(self, training_data: TrainingData) -> None:
|
|
110
|
+
# Specify here your fitting logic (e.g., store a historical covmat)
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
def _predict(self, prediction_data: PredictionData) -> pd.DataFrame:
|
|
114
|
+
# Specify here your prediction logic (e.g., get a stored estimate)
|
|
115
|
+
...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## How To Create A New Data-Driven Covariance Estimator
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
from __future__ import annotations
|
|
122
|
+
|
|
123
|
+
from typing import TYPE_CHECKING
|
|
124
|
+
|
|
125
|
+
if TYPE_CHECKING:
|
|
126
|
+
import pandas as pd
|
|
127
|
+
|
|
128
|
+
from quant_pml.cov_estimators.rl.base_rl_estimator import BaseRLCovEstimator
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class NewRLCovEstimator(BaseRLCovEstimator):
|
|
132
|
+
def __init__(self, shrinkage_type: str, window_size: int | None = None) -> None:
|
|
133
|
+
super().__init__(shrinkage_type=shrinkage_type, window_size=window_size)
|
|
134
|
+
|
|
135
|
+
def _fit_shrinkage(
|
|
136
|
+
self, features: pd.DataFrame, shrinkage_target: pd.Series
|
|
137
|
+
) -> None:
|
|
138
|
+
# Specify here your model (might be classical ML / RL / etc.)
|
|
139
|
+
self.model = ...
|
|
140
|
+
self.model.fit(X=features, y=shrinkage_target)
|
|
141
|
+
|
|
142
|
+
def _predict_shrinkage(self, features: pd.DataFrame) -> float:
|
|
143
|
+
pred = self.model.predict(features).item()
|
|
144
|
+
|
|
145
|
+
return pred
|
|
146
|
+
```
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "quant-pml"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
requires-python = "==3.12.5"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"statsmodels>=0.14.4",
|
|
9
|
+
"scikit-learn>=1.5.2",
|
|
10
|
+
"numpy>=2.2.4",
|
|
11
|
+
"pandas>=2.3.1",
|
|
12
|
+
"matplotlib>=3.10.1",
|
|
13
|
+
"qpsolvers>=4.5.0",
|
|
14
|
+
"torch==2.8.0",
|
|
15
|
+
"pyarrow==20.0.0",
|
|
16
|
+
"fastparquet==2024.11.0",
|
|
17
|
+
"tqdm>=4.67.1",
|
|
18
|
+
"scipy>=1.16.1",
|
|
19
|
+
"optuna==4.4.0",
|
|
20
|
+
"plotly>=6.2.0",
|
|
21
|
+
"openpyxl>=3.1.5",
|
|
22
|
+
"polars==1.34.0",
|
|
23
|
+
]
|
|
24
|
+
authors = [{name = "Viacheslav Buchkov"}]
|
|
25
|
+
keywords = ["quant", "quantitative finance", "machine learning"]
|
|
26
|
+
license = "MIT"
|
|
27
|
+
|
|
28
|
+
[tool.uv.workspace]
|
|
29
|
+
members = [
|
|
30
|
+
"src/",
|
|
31
|
+
"backtesting_checks/dummy",
|
|
32
|
+
"backtesting_checks/timing_strategy",
|
|
33
|
+
"backtesting_checks/low_beta",
|
|
34
|
+
"backtesting_checks/spx_reconstr",
|
|
35
|
+
"backtests/enhanced_momentum",
|
|
36
|
+
"backtests/fractional_momentum",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"basedpyright>=1.31.6",
|
|
42
|
+
"deptry>=0.23.1",
|
|
43
|
+
"ipykernel>=6.30.1",
|
|
44
|
+
"ruff>=0.13.3",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 130
|
|
50
|
+
fix = true
|
|
51
|
+
|
|
52
|
+
exclude = [
|
|
53
|
+
".bzr",
|
|
54
|
+
".direnv",
|
|
55
|
+
".eggs",
|
|
56
|
+
".git",
|
|
57
|
+
".git-rewrite",
|
|
58
|
+
".hg",
|
|
59
|
+
".ipynb_checkpoints",
|
|
60
|
+
".mypy_cache",
|
|
61
|
+
".nox",
|
|
62
|
+
".pants.d",
|
|
63
|
+
".pyenv",
|
|
64
|
+
".pytest_cache",
|
|
65
|
+
".pytype",
|
|
66
|
+
".ruff_cache",
|
|
67
|
+
".svn",
|
|
68
|
+
".tox",
|
|
69
|
+
".venv",
|
|
70
|
+
".vscode",
|
|
71
|
+
"__pypackages__",
|
|
72
|
+
"_build",
|
|
73
|
+
"buck-out",
|
|
74
|
+
"build",
|
|
75
|
+
"dist",
|
|
76
|
+
"node_modules",
|
|
77
|
+
"site-packages",
|
|
78
|
+
"venv",
|
|
79
|
+
"__init__.py",
|
|
80
|
+
"libraries/pmplib/pmplib/optimization", # V added this but (?)
|
|
81
|
+
"check_dependencies.py" # scripts, maybe should move to tools later
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
target-version = "py312"
|
|
85
|
+
|
|
86
|
+
extend-exclude = [".venv"]
|
|
87
|
+
|
|
88
|
+
# See: https://github.com/charliermarsh/ruff#supported-rules
|
|
89
|
+
[tool.ruff.lint]
|
|
90
|
+
select = [
|
|
91
|
+
"F", # Pyflakes
|
|
92
|
+
"E", # pycodestyle Error
|
|
93
|
+
"W", # pycodestyle Warning
|
|
94
|
+
"C90", # mccabe
|
|
95
|
+
"I", # isort
|
|
96
|
+
"D", # pydocstyle
|
|
97
|
+
"UP", # pyupgrade
|
|
98
|
+
"N", # pep8-naming
|
|
99
|
+
"YTT", # flake8-2020
|
|
100
|
+
"ASYNC", # flake8-async
|
|
101
|
+
"ANN", # flake8-annotations
|
|
102
|
+
"S", # flake8-bandit
|
|
103
|
+
"BLE", # flake8-blind-except
|
|
104
|
+
"FBT", # flake8-boolean-trap
|
|
105
|
+
"B", # flake8-bugbear
|
|
106
|
+
"A", # flake8-builtins
|
|
107
|
+
"C4", # flake8-comprehensions
|
|
108
|
+
"T10", # flake8-debugger
|
|
109
|
+
"DJ", # flake8-django
|
|
110
|
+
"EM", # flake8-errmsg
|
|
111
|
+
"ISC", # flake8-implicit-str-concat
|
|
112
|
+
"ICN", # flake8-import-conventions
|
|
113
|
+
"T20", # flake8-print
|
|
114
|
+
"PYI", # flake8-pyi
|
|
115
|
+
"R", # refactor
|
|
116
|
+
"Q", # flake8-quotes
|
|
117
|
+
"RET", # flake8-return
|
|
118
|
+
"SLF", # flake8-self
|
|
119
|
+
"PT", # flake8-pytest-style
|
|
120
|
+
"SIM", # flake8-simplify
|
|
121
|
+
"TID", # flake8-tidy-imports
|
|
122
|
+
"ARG", # flake8-unused-arguments
|
|
123
|
+
"DTZ", # flake8-datetimez
|
|
124
|
+
"ERA", # eradicate
|
|
125
|
+
"PD", # pandas-vet
|
|
126
|
+
"PGH", # pygrep-hooks
|
|
127
|
+
"PLC", # Pylint Convention
|
|
128
|
+
"PLE", # Pylint Error
|
|
129
|
+
"PLR", # Pylint Refactor
|
|
130
|
+
"PLW", # Pylint Warning
|
|
131
|
+
"PIE", # flake8-pie
|
|
132
|
+
"COM", # flake8-commas
|
|
133
|
+
"INP", # flake8-no-pep420
|
|
134
|
+
"EXE", # flake8-executable
|
|
135
|
+
"TCH", # flake8-type-checking
|
|
136
|
+
"ARG", # flake8-unused-arguments
|
|
137
|
+
"TRY", # tryceratops
|
|
138
|
+
"PTH", # flake8-use-pathlib
|
|
139
|
+
"RUF", # Ruff-specific rules
|
|
140
|
+
"RSE", # flake8-raise
|
|
141
|
+
"SLOT", # flake8-slots
|
|
142
|
+
"TD", # flake8-todos
|
|
143
|
+
"FLY", # flynt
|
|
144
|
+
"NPY", # NumPy-specific rules
|
|
145
|
+
"PERF", # Perflint
|
|
146
|
+
"G", # flake8-logging-format
|
|
147
|
+
"PL", # Pylint
|
|
148
|
+
"FURB", # refurb
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
ignore = [
|
|
152
|
+
"D100", # Missing docstring in public module
|
|
153
|
+
"D101", # Missing docstring in public class
|
|
154
|
+
"D102", # Missing docstring in public method
|
|
155
|
+
"D103", # Missing docstring in public function
|
|
156
|
+
"D104", # Missing docstring in public package
|
|
157
|
+
"D105", # Missing docstring in magic method
|
|
158
|
+
"D106", # Missing docstring in public nested class
|
|
159
|
+
"D107", # Missing docstring in __init__
|
|
160
|
+
"TD003", # Missing issue link on the line following this TODORuffTD003
|
|
161
|
+
"D211", # No blank lines allowed before class docstring
|
|
162
|
+
"D213", # Multi-line docstring summary should start at the second line
|
|
163
|
+
"G004", # Logging statement uses string formatting
|
|
164
|
+
"D203", # 1 blank line required before class docstring
|
|
165
|
+
"COM812", # Trailing comma missing (conflicts with formatter)
|
|
166
|
+
"DTZ011", # Use `datetime.datetime.now(tz=...).date()` instead
|
|
167
|
+
"DTZ005", # datetime.datetime.now()` called without a `tz` argument
|
|
168
|
+
"S101", #Use of `assert` detected
|
|
169
|
+
"PD010", #`.pivot_table` is preferred to `.pivot` or `.unstack`; provides same functionality
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
unfixable = ["F401"]
|
|
173
|
+
|
|
174
|
+
[tool.ruff.lint.per-file-ignores]
|
|
175
|
+
"**/*.ipynb" = [
|
|
176
|
+
"E501", # ignore line-length in all notebooks
|
|
177
|
+
"F404", # ignore `from __future__` imports must occur at the beginning of the file
|
|
178
|
+
"T201" # ignore print statements
|
|
179
|
+
]
|
quant_pml-0.1.0/setup.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseAssetPricer(ABC):
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
super().__init__()
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def fit(self, test_assets_xs_r: pd.DataFrame, factors: pd.DataFrame) -> pd.DataFrame:
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def predict(self, factors: pd.DataFrame) -> pd.DataFrame:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
def _get_deviations(self, test_assets_xs_r: pd.DataFrame, factors: pd.DataFrame) -> tuple[pd.Series, pd.Series]:
|
|
25
|
+
pred_xs_r = self.predict(factors)
|
|
26
|
+
|
|
27
|
+
ts_average = test_assets_xs_r.mean(axis=0)
|
|
28
|
+
|
|
29
|
+
model_deviation = ts_average - pred_xs_r.mean(axis=1)
|
|
30
|
+
baseline_deviation = ts_average - ts_average.mean() * np.ones(len(ts_average))
|
|
31
|
+
|
|
32
|
+
return model_deviation, baseline_deviation
|
|
33
|
+
|
|
34
|
+
def r2_score(self, test_assets_xs_r: pd.DataFrame, factors: pd.DataFrame) -> float:
|
|
35
|
+
model_deviation, baseline_deviation = self._get_deviations(test_assets_xs_r, factors)
|
|
36
|
+
|
|
37
|
+
mse_model = model_deviation.T @ model_deviation
|
|
38
|
+
mse_baseline = baseline_deviation.T @ baseline_deviation
|
|
39
|
+
|
|
40
|
+
return 1 - mse_model / mse_baseline
|
|
41
|
+
|
|
42
|
+
def r2_gls_score(self, test_assets_xs_r: pd.DataFrame, factors: pd.DataFrame) -> float:
|
|
43
|
+
model_deviation, baseline_deviation = self._get_deviations(test_assets_xs_r, factors)
|
|
44
|
+
|
|
45
|
+
var_r_inv = np.linalg.inv(test_assets_xs_r.cov())
|
|
46
|
+
|
|
47
|
+
mse_model = model_deviation.T @ var_r_inv @ model_deviation
|
|
48
|
+
mse_baseline = baseline_deviation.T @ var_r_inv @ baseline_deviation
|
|
49
|
+
|
|
50
|
+
return 1 - mse_model / mse_baseline
|
|
51
|
+
|
|
52
|
+
def implied_sharpe_ratio(self, test_assets_xs_r: pd.DataFrame, factors: pd.DataFrame) -> float:
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
|
|
55
|
+
def rmse_score(self, test_assets_xs_r: pd.DataFrame, factors: pd.DataFrame) -> float:
|
|
56
|
+
model_deviation, _baseline_deviation = self._get_deviations(test_assets_xs_r, factors)
|
|
57
|
+
|
|
58
|
+
return np.sqrt(model_deviation.T @ model_deviation / len(model_deviation))
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from ipca import InstrumentedPCA
|
|
8
|
+
|
|
9
|
+
from quant_pml.ap.base_asset_pricer import BaseAssetPricer
|
|
10
|
+
|
|
11
|
+
warnings.filterwarnings("ignore")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IPCAFactorModel(BaseAssetPricer):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
n_factors: int = 4,
|
|
18
|
+
*,
|
|
19
|
+
fit_alpha: bool = True,
|
|
20
|
+
) -> None:
|
|
21
|
+
super().__init__()
|
|
22
|
+
|
|
23
|
+
self.n_factors = n_factors
|
|
24
|
+
self.fit_alpha = fit_alpha
|
|
25
|
+
|
|
26
|
+
self.ipca = None
|
|
27
|
+
self.gamma = None
|
|
28
|
+
self.factors = None
|
|
29
|
+
|
|
30
|
+
def fit(self, test_assets_xs_r: pd.Series, ranks: pd.DataFrame) -> None:
|
|
31
|
+
self.ipca = InstrumentedPCA(
|
|
32
|
+
n_factors=self.n_factors,
|
|
33
|
+
intercept=self.fit_alpha,
|
|
34
|
+
max_iter=10_000,
|
|
35
|
+
)
|
|
36
|
+
self.ipca = self.ipca.fit(X=ranks, y=test_assets_xs_r)
|
|
37
|
+
self.gamma, self.factors = self.ipca.get_factors(label_ind=True)
|
|
38
|
+
|
|
39
|
+
def predict(self, ranks: pd.DataFrame) -> pd.DataFrame:
|
|
40
|
+
pred = self.ipca.predict(X=ranks)
|
|
41
|
+
pred = pd.DataFrame(pred, index=ranks.index, columns=["pred"])
|
|
42
|
+
return pd.pivot_table(pred, index="date", columns="portfolio", values="pred")
|
|
43
|
+
|
|
44
|
+
def _get_deviations(self, test_assets_xs_r: pd.DataFrame, ranks: pd.DataFrame) -> tuple[pd.Series, pd.Series]:
|
|
45
|
+
pred_xs_r = self.predict(ranks)
|
|
46
|
+
|
|
47
|
+
ts_average = test_assets_xs_r.mean(axis=0)
|
|
48
|
+
|
|
49
|
+
model_deviation = ts_average - pred_xs_r.mean(axis=0)
|
|
50
|
+
baseline_deviation = ts_average - ts_average.mean() * np.ones(len(ts_average))
|
|
51
|
+
|
|
52
|
+
return model_deviation, baseline_deviation
|
|
53
|
+
|
|
54
|
+
def r2_score(self, test_assets_xs_r: pd.DataFrame, ranks: pd.DataFrame) -> float:
|
|
55
|
+
model_deviation, baseline_deviation = self._get_deviations(test_assets_xs_r, ranks)
|
|
56
|
+
|
|
57
|
+
mse_model = model_deviation.T @ model_deviation
|
|
58
|
+
mse_baseline = baseline_deviation.T @ baseline_deviation
|
|
59
|
+
|
|
60
|
+
return 1 - mse_model / mse_baseline
|
|
61
|
+
|
|
62
|
+
def r2_gls_score(self, test_assets_xs_r: pd.DataFrame, ranks: pd.DataFrame) -> float:
|
|
63
|
+
model_deviation, baseline_deviation = self._get_deviations(test_assets_xs_r, ranks)
|
|
64
|
+
|
|
65
|
+
var_r_inv = np.linalg.inv(test_assets_xs_r.cov())
|
|
66
|
+
|
|
67
|
+
mse_model = model_deviation.T @ var_r_inv @ model_deviation
|
|
68
|
+
mse_baseline = baseline_deviation.T @ var_r_inv @ baseline_deviation
|
|
69
|
+
|
|
70
|
+
return 1 - mse_model / mse_baseline
|
|
71
|
+
|
|
72
|
+
def get_mv_weights(self, test_assets_xs_r: pd.DataFrame, ranks: pd.DataFrame) -> float:
|
|
73
|
+
z_t = ranks
|
|
74
|
+
gamma_b = self.gamma.iloc[:, :-1] if self.fit_alpha else self.gamma
|
|
75
|
+
var = gamma_b.T @ z_t.T @ z_t @ gamma_b
|
|
76
|
+
var_inv = np.linalg.inv(var)
|
|
77
|
+
|
|
78
|
+
weights = (var_inv @ gamma_b.T @ z_t.T).T
|
|
79
|
+
|
|
80
|
+
factors_mu = self.factors.mean(axis=1)
|
|
81
|
+
factors_cov = self.factors.T.cov()
|
|
82
|
+
|
|
83
|
+
mv_weights = np.linalg.inv(factors_cov) @ factors_mu
|
|
84
|
+
|
|
85
|
+
factor_rets = pd.DataFrame(
|
|
86
|
+
index=test_assets_xs_r.index,
|
|
87
|
+
columns=[f"Factor{k}" for k in range(1, self.n_factors + 1)],
|
|
88
|
+
)
|
|
89
|
+
for factor in range(self.n_factors):
|
|
90
|
+
factor_weights = pd.pivot_table(
|
|
91
|
+
weights.iloc[:, factor].to_frame("weights"),
|
|
92
|
+
index="date",
|
|
93
|
+
columns="portfolio",
|
|
94
|
+
values="weights",
|
|
95
|
+
)
|
|
96
|
+
factor_ret = (factor_weights.to_numpy() * test_assets_xs_r.to_numpy()).sum(axis=1)
|
|
97
|
+
factor_rets[f"Factor{factor + 1}"] = factor_ret
|
|
98
|
+
|
|
99
|
+
return factor_rets @ mv_weights
|
|
100
|
+
|
|
101
|
+
def rmse_score(self, test_assets_xs_r: pd.DataFrame, ranks: pd.DataFrame) -> float:
|
|
102
|
+
model_deviation, _baseline_deviation = self._get_deviations(test_assets_xs_r, ranks)
|
|
103
|
+
|
|
104
|
+
return np.sqrt(model_deviation.T @ model_deviation / len(model_deviation))
|