boruta-quant 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.
- boruta_quant-0.1.0/.gitignore +58 -0
- boruta_quant-0.1.0/.pre-commit-config.yaml +27 -0
- boruta_quant-0.1.0/.python-version +1 -0
- boruta_quant-0.1.0/CHANGELOG.md +27 -0
- boruta_quant-0.1.0/LICENSE +21 -0
- boruta_quant-0.1.0/PKG-INFO +203 -0
- boruta_quant-0.1.0/README.md +151 -0
- boruta_quant-0.1.0/domains/PORTFOLIO.md +169 -0
- boruta_quant-0.1.0/domains/PREDICTION.md +119 -0
- boruta_quant-0.1.0/domains/TRADING.md +80 -0
- boruta_quant-0.1.0/pyproject.toml +142 -0
- boruta_quant-0.1.0/src/boruta_quant/__init__.py +29 -0
- boruta_quant-0.1.0/src/boruta_quant/_version.py +3 -0
- boruta_quant-0.1.0/src/boruta_quant/metrics/__init__.py +31 -0
- boruta_quant-0.1.0/src/boruta_quant/metrics/auc.py +83 -0
- boruta_quant-0.1.0/src/boruta_quant/metrics/directional_accuracy.py +67 -0
- boruta_quant-0.1.0/src/boruta_quant/metrics/rank_ic.py +72 -0
- boruta_quant-0.1.0/src/boruta_quant/oracle/__init__.py +32 -0
- boruta_quant-0.1.0/src/boruta_quant/oracle/base.py +124 -0
- boruta_quant-0.1.0/src/boruta_quant/oracle/block_permutation.py +165 -0
- boruta_quant-0.1.0/src/boruta_quant/oracle/drop_column.py +99 -0
- boruta_quant-0.1.0/src/boruta_quant/oracle/permutation.py +85 -0
- boruta_quant-0.1.0/src/boruta_quant/profiling/__init__.py +18 -0
- boruta_quant-0.1.0/src/boruta_quant/profiling/helpers.py +40 -0
- boruta_quant-0.1.0/src/boruta_quant/profiling/results.py +37 -0
- boruta_quant-0.1.0/src/boruta_quant/profiling/session.py +128 -0
- boruta_quant-0.1.0/src/boruta_quant/profiling/timer.py +58 -0
- boruta_quant-0.1.0/src/boruta_quant/py.typed +0 -0
- boruta_quant-0.1.0/src/boruta_quant/selector/__init__.py +47 -0
- boruta_quant-0.1.0/src/boruta_quant/selector/config.py +56 -0
- boruta_quant-0.1.0/src/boruta_quant/selector/hypothesis.py +132 -0
- boruta_quant-0.1.0/src/boruta_quant/selector/results.py +44 -0
- boruta_quant-0.1.0/src/boruta_quant/selector/selector.py +353 -0
- boruta_quant-0.1.0/src/boruta_quant/selector/shadow.py +100 -0
- boruta_quant-0.1.0/src/boruta_quant/selector/shuffle.py +53 -0
- boruta_quant-0.1.0/src/boruta_quant/temporal/__init__.py +40 -0
- boruta_quant-0.1.0/src/boruta_quant/temporal/config.py +37 -0
- boruta_quant-0.1.0/src/boruta_quant/temporal/cv.py +74 -0
- boruta_quant-0.1.0/src/boruta_quant/temporal/purged_cv.py +147 -0
- boruta_quant-0.1.0/src/boruta_quant/temporal/split.py +28 -0
- boruta_quant-0.1.0/tests/__init__.py +1 -0
- boruta_quant-0.1.0/tests/fixtures/__init__.py +0 -0
- boruta_quant-0.1.0/tests/test_metrics/__init__.py +1 -0
- boruta_quant-0.1.0/tests/test_metrics/test_auc.py +104 -0
- boruta_quant-0.1.0/tests/test_metrics/test_directional_accuracy.py +128 -0
- boruta_quant-0.1.0/tests/test_metrics/test_rank_ic.py +89 -0
- boruta_quant-0.1.0/tests/test_oracle/__init__.py +1 -0
- boruta_quant-0.1.0/tests/test_oracle/conftest.py +92 -0
- boruta_quant-0.1.0/tests/test_oracle/test_block_permutation.py +404 -0
- boruta_quant-0.1.0/tests/test_oracle/test_drop_column.py +224 -0
- boruta_quant-0.1.0/tests/test_oracle/test_permutation.py +267 -0
- boruta_quant-0.1.0/tests/test_profiling/__init__.py +1 -0
- boruta_quant-0.1.0/tests/test_profiling/test_helpers.py +63 -0
- boruta_quant-0.1.0/tests/test_profiling/test_session.py +166 -0
- boruta_quant-0.1.0/tests/test_profiling/test_timer.py +80 -0
- boruta_quant-0.1.0/tests/test_selector/__init__.py +1 -0
- boruta_quant-0.1.0/tests/test_selector/conftest.py +120 -0
- boruta_quant-0.1.0/tests/test_selector/test_config.py +307 -0
- boruta_quant-0.1.0/tests/test_selector/test_hypothesis.py +265 -0
- boruta_quant-0.1.0/tests/test_selector/test_selector.py +573 -0
- boruta_quant-0.1.0/tests/test_selector/test_shadow.py +331 -0
- boruta_quant-0.1.0/tests/test_selector/test_shuffle.py +223 -0
- boruta_quant-0.1.0/tests/test_temporal/__init__.py +1 -0
- boruta_quant-0.1.0/tests/test_temporal/test_purged_cv.py +466 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# IDE
|
|
13
|
+
.idea/
|
|
14
|
+
.vscode/
|
|
15
|
+
*.swp
|
|
16
|
+
*.swo
|
|
17
|
+
*~
|
|
18
|
+
|
|
19
|
+
# Testing
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.coverage
|
|
22
|
+
.coverage.*
|
|
23
|
+
htmlcov/
|
|
24
|
+
.tox/
|
|
25
|
+
.nox/
|
|
26
|
+
|
|
27
|
+
# Type checking
|
|
28
|
+
.mypy_cache/
|
|
29
|
+
.pyright/
|
|
30
|
+
|
|
31
|
+
# Distribution
|
|
32
|
+
*.whl
|
|
33
|
+
*.tar.gz
|
|
34
|
+
|
|
35
|
+
# Jupyter
|
|
36
|
+
.ipynb_checkpoints/
|
|
37
|
+
|
|
38
|
+
# OS
|
|
39
|
+
.DS_Store
|
|
40
|
+
Thumbs.db
|
|
41
|
+
|
|
42
|
+
# UV
|
|
43
|
+
uv.lock
|
|
44
|
+
|
|
45
|
+
# Claude Code (imported from .ai_workspace_template, not tracked)
|
|
46
|
+
.claude/
|
|
47
|
+
.ai_workspace/
|
|
48
|
+
CLAUDE.md
|
|
49
|
+
CLAUDE_TEMPLATE.md
|
|
50
|
+
CLAUDE_DOMAIN.md
|
|
51
|
+
CLAUDE_REPO.md
|
|
52
|
+
.claude_config.yaml
|
|
53
|
+
|
|
54
|
+
# Internal development infrastructure (not for public)
|
|
55
|
+
hooks/
|
|
56
|
+
templates/
|
|
57
|
+
STANDARDS.md
|
|
58
|
+
docs/research/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Pre-commit hooks configuration for boruta-quant
|
|
2
|
+
# Install: pre-commit install
|
|
3
|
+
# Run manually: pre-commit run --all-files
|
|
4
|
+
|
|
5
|
+
repos:
|
|
6
|
+
# Linting, import sorting, and formatting with Ruff (fast Rust-based tool)
|
|
7
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
8
|
+
rev: v0.12.9
|
|
9
|
+
hooks:
|
|
10
|
+
- id: ruff
|
|
11
|
+
name: Ruff linter (includes import sorting)
|
|
12
|
+
args: [--fix, --exit-non-zero-on-fix]
|
|
13
|
+
- id: ruff-format
|
|
14
|
+
name: Ruff formatter
|
|
15
|
+
|
|
16
|
+
# Type checking with Pyright (3-10x faster than mypy)
|
|
17
|
+
- repo: local
|
|
18
|
+
hooks:
|
|
19
|
+
- id: pyright
|
|
20
|
+
name: Pyright type checker
|
|
21
|
+
entry: pyright
|
|
22
|
+
language: node
|
|
23
|
+
types: [python]
|
|
24
|
+
pass_filenames: false
|
|
25
|
+
additional_dependencies: ['pyright@1.1.380']
|
|
26
|
+
files: '\.py$'
|
|
27
|
+
exclude: ^tests/fixtures/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-02-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **BorutaSelector**: Main feature selection class with configurable trials, alpha, percentile, and two-step tentative resolution
|
|
12
|
+
- **Importance Oracles**: Pluggable `ImportanceOracle` protocol with three implementations:
|
|
13
|
+
- `PermutationOracle` (default) — OOS-safe permutation importance
|
|
14
|
+
- `DropColumnOracle` — refit-based importance via column removal
|
|
15
|
+
- `BlockPermutationOracle` — block-preserving permutation for autocorrelated data
|
|
16
|
+
- **Purged Temporal CV**: `PurgedTemporalCV` with configurable purge window, embargo window, and min train size to prevent lookahead bias
|
|
17
|
+
- **Shadow Features**: Shadow feature generation with three shuffle modes:
|
|
18
|
+
- `RANDOM` — standard i.i.d. permutation (default)
|
|
19
|
+
- `BLOCK` — block-preserving shuffle for serial correlation
|
|
20
|
+
- `ERA` — within-era-only shuffle for regime-aware selection
|
|
21
|
+
- **Era Support**: `boundaries_to_eras()` utility for converting date boundaries to era labels
|
|
22
|
+
- **Statistical Testing**: Binomial hypothesis testing with Bonferroni correction and optional two-step rough fix for tentative features
|
|
23
|
+
- **Metrics**: `rank_ic` (Spearman correlation) and `auc_scorer` for alpha research evaluation
|
|
24
|
+
- **Profiling**: Built-in `ProfilingSession` integration for timing and memory tracking during `fit()`
|
|
25
|
+
- **Type Safety**: Full type hints with beartype runtime enforcement and Pyright strict mode
|
|
26
|
+
- **Pydantic Configs**: `BorutaSelectorConfig` and `PurgedCVConfig` with fail-fast validation
|
|
27
|
+
- 229 tests with 92% statement coverage
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BlackArbsCEO
|
|
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.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: boruta-quant
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Temporal-aware Boruta feature selection for quantitative finance. OOS-only importance with purged cross-validation.
|
|
5
|
+
Project-URL: Homepage, https://github.com/BlackArbsCEO/boruta-quant
|
|
6
|
+
Project-URL: Documentation, https://github.com/BlackArbsCEO/boruta-quant#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/BlackArbsCEO/boruta-quant
|
|
8
|
+
Project-URL: Issues, https://github.com/BlackArbsCEO/boruta-quant/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/BlackArbsCEO/boruta-quant/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: BlackArbsCEO <bcr@blackarbs.com>
|
|
11
|
+
Maintainer-email: BlackArbsCEO <bcr@blackarbs.com>
|
|
12
|
+
License: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: boruta,cross-validation,feature-selection,machine-learning,out-of-sample,permutation-importance,quantitative-finance,temporal-cv
|
|
15
|
+
Classifier: Development Status :: 3 - Alpha
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.12
|
|
29
|
+
Requires-Dist: beartype>=0.18.0
|
|
30
|
+
Requires-Dist: numpy>=2.0.0
|
|
31
|
+
Requires-Dist: pandas>=2.0.0
|
|
32
|
+
Requires-Dist: psutil>=5.9.0
|
|
33
|
+
Requires-Dist: pydantic>=2.0.0
|
|
34
|
+
Requires-Dist: scikit-learn>=1.3.0
|
|
35
|
+
Requires-Dist: scipy>=1.11.0
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Requires-Dist: lightgbm>=4.0.0; extra == 'all'
|
|
38
|
+
Requires-Dist: matplotlib>=3.7.0; extra == 'all'
|
|
39
|
+
Requires-Dist: seaborn>=0.13.0; extra == 'all'
|
|
40
|
+
Requires-Dist: shap>=0.48.0; extra == 'all'
|
|
41
|
+
Requires-Dist: xgboost>=2.0.0; extra == 'all'
|
|
42
|
+
Provides-Extra: lightgbm
|
|
43
|
+
Requires-Dist: lightgbm>=4.0.0; extra == 'lightgbm'
|
|
44
|
+
Provides-Extra: shap
|
|
45
|
+
Requires-Dist: shap>=0.48.0; extra == 'shap'
|
|
46
|
+
Provides-Extra: viz
|
|
47
|
+
Requires-Dist: matplotlib>=3.7.0; extra == 'viz'
|
|
48
|
+
Requires-Dist: seaborn>=0.13.0; extra == 'viz'
|
|
49
|
+
Provides-Extra: xgboost
|
|
50
|
+
Requires-Dist: xgboost>=2.0.0; extra == 'xgboost'
|
|
51
|
+
Description-Content-Type: text/markdown
|
|
52
|
+
|
|
53
|
+
# boruta-quant
|
|
54
|
+
|
|
55
|
+
[](https://www.python.org/downloads/)
|
|
56
|
+
[](https://opensource.org/licenses/MIT)
|
|
57
|
+
[](https://github.com/astral-sh/ruff)
|
|
58
|
+
|
|
59
|
+
**Temporal-aware Boruta feature selection for quantitative finance.**
|
|
60
|
+
|
|
61
|
+
`boruta-quant` computes feature importance on validation data only, using purged cross-validation to prevent lookahead bias. Built for financial time series where temporal integrity matters.
|
|
62
|
+
|
|
63
|
+
## Why boruta-quant?
|
|
64
|
+
|
|
65
|
+
Standard feature selection (SHAP, sklearn permutation importance) computes importance on training data. In financial time series, this leaks future information into feature rankings. `boruta-quant` fixes this:
|
|
66
|
+
|
|
67
|
+
- **OOS-Only Importance**: Importance computed exclusively on validation folds
|
|
68
|
+
- **Purged Cross-Validation**: Train/test gap with purge and embargo windows
|
|
69
|
+
- **Shadow Features**: Boruta's all-relevant selection via shadow comparison
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Basic (permutation importance only)
|
|
75
|
+
pip install boruta-quant
|
|
76
|
+
|
|
77
|
+
# With LightGBM
|
|
78
|
+
pip install boruta-quant[lightgbm]
|
|
79
|
+
|
|
80
|
+
# With SHAP support
|
|
81
|
+
pip install boruta-quant[shap]
|
|
82
|
+
|
|
83
|
+
# Everything
|
|
84
|
+
pip install boruta-quant[all]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Development
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://github.com/BlackArbsCEO/boruta-quant.git
|
|
91
|
+
cd boruta-quant
|
|
92
|
+
uv sync --all-extras --dev
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Quick Start
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from boruta_quant import BorutaSelector, BorutaSelectorConfig
|
|
99
|
+
from boruta_quant.oracle import PermutationImportanceOracle
|
|
100
|
+
from boruta_quant.temporal import PurgedTemporalCV, PurgedCVConfig
|
|
101
|
+
from boruta_quant.metrics import rank_ic_scorer
|
|
102
|
+
from lightgbm import LGBMRegressor
|
|
103
|
+
|
|
104
|
+
# 1. Configure purged temporal CV
|
|
105
|
+
cv = PurgedTemporalCV(PurgedCVConfig(
|
|
106
|
+
n_splits=5,
|
|
107
|
+
purge_window_days=5, # gap before validation fold
|
|
108
|
+
embargo_window_days=5, # gap after validation fold
|
|
109
|
+
min_train_size=100,
|
|
110
|
+
test_size_ratio=0.2,
|
|
111
|
+
))
|
|
112
|
+
|
|
113
|
+
# 2. Configure importance oracle (OOS-only)
|
|
114
|
+
oracle = PermutationImportanceOracle(
|
|
115
|
+
scoring=rank_ic_scorer, # Spearman rank correlation
|
|
116
|
+
n_repeats=10,
|
|
117
|
+
random_state=42,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# 3. Configure Boruta selector
|
|
121
|
+
selector = BorutaSelector(
|
|
122
|
+
config=BorutaSelectorConfig(
|
|
123
|
+
n_trials=20, # Boruta iterations
|
|
124
|
+
percentile=100, # shadow threshold percentile
|
|
125
|
+
alpha=0.05, # significance level
|
|
126
|
+
two_step=True, # resolve tentative features
|
|
127
|
+
random_state=42,
|
|
128
|
+
),
|
|
129
|
+
oracle=oracle,
|
|
130
|
+
cv=cv,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# 4. Fit — model goes here, not in the constructor
|
|
134
|
+
result = selector.fit(
|
|
135
|
+
X, y,
|
|
136
|
+
timestamps=timestamps, # must be timezone-aware
|
|
137
|
+
model=LGBMRegressor(n_estimators=100, random_state=42),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# 5. Results
|
|
141
|
+
print(result.accepted_features) # confirmed important
|
|
142
|
+
print(result.rejected_features) # confirmed unimportant
|
|
143
|
+
print(result.tentative_features) # borderline (resolved if two_step=True)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Importance Oracles
|
|
147
|
+
|
|
148
|
+
All oracles fit the model on training data but measure importance on validation data only.
|
|
149
|
+
|
|
150
|
+
| Oracle | How it works | When to use |
|
|
151
|
+
|--------|-------------|-------------|
|
|
152
|
+
| `PermutationImportanceOracle` | Shuffles one feature in validation set, measures prediction drop | Default — reliable, no refit needed |
|
|
153
|
+
| `DropColumnImportanceOracle` | Removes feature, refits model, measures prediction drop | When refit cost is acceptable |
|
|
154
|
+
| `BlockPermutationImportanceOracle` | Block-shuffles feature (preserves autocorrelation structure) | Autocorrelated time series |
|
|
155
|
+
|
|
156
|
+
## Temporal Cross-Validation
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
Training Purge Validation Embargo
|
|
160
|
+
|--------------| |-------| |----------| |-------|
|
|
161
|
+
^ ^
|
|
162
|
+
train_start embargo_end
|
|
163
|
+
|
|
164
|
+
- Purge: removes observations that could leak into validation
|
|
165
|
+
- Embargo: prevents information from validation bleeding forward
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Shadow Shuffle Modes
|
|
169
|
+
|
|
170
|
+
Shadow features are shuffled copies of real features. The shuffle mode controls how temporal structure is handled:
|
|
171
|
+
|
|
172
|
+
| Mode | Description | Use case |
|
|
173
|
+
|------|-------------|----------|
|
|
174
|
+
| `ShuffleMode.RANDOM` | Standard i.i.d. permutation | Default — i.i.d. data |
|
|
175
|
+
| `ShuffleMode.BLOCK` | Block-preserving shuffle | Autocorrelated features |
|
|
176
|
+
| `ShuffleMode.ERA` | Shuffle within eras only | Regime-aware selection |
|
|
177
|
+
|
|
178
|
+
## Metrics
|
|
179
|
+
|
|
180
|
+
| Function | Description |
|
|
181
|
+
|----------|-------------|
|
|
182
|
+
| `rank_ic` | Spearman correlation between predictions and actuals |
|
|
183
|
+
| `rank_ic_scorer` | sklearn-compatible scorer wrapping `rank_ic` |
|
|
184
|
+
| `directional_accuracy` | Fraction of correct sign predictions (up vs down) |
|
|
185
|
+
| `directional_accuracy_scorer` | sklearn-compatible scorer wrapping `directional_accuracy` |
|
|
186
|
+
| `auc_score` | Area under ROC curve |
|
|
187
|
+
| `auc_scorer` | sklearn-compatible scorer wrapping `auc_score` |
|
|
188
|
+
|
|
189
|
+
## Design Principles
|
|
190
|
+
|
|
191
|
+
1. **OOS-Only**: Importance never computed on training data
|
|
192
|
+
2. **Fail-Fast**: Invalid temporal data (naive timestamps, unsorted) raises immediately
|
|
193
|
+
3. **Type-Safe**: Runtime enforcement with beartype, strict Pyright
|
|
194
|
+
4. **Explicit**: All config parameters required — no hidden defaults
|
|
195
|
+
|
|
196
|
+
## References
|
|
197
|
+
|
|
198
|
+
- [Boruta Algorithm](https://www.jstatsoft.org/article/view/v036i11) — Kursa & Rudnicki (2010)
|
|
199
|
+
- [Advances in Financial Machine Learning](https://www.wiley.com/en-us/Advances+in+Financial+Machine+Learning-p-9781119482086) — Lopez de Prado (2018), Ch. 7 (purged CV)
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# boruta-quant
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/astral-sh/ruff)
|
|
6
|
+
|
|
7
|
+
**Temporal-aware Boruta feature selection for quantitative finance.**
|
|
8
|
+
|
|
9
|
+
`boruta-quant` computes feature importance on validation data only, using purged cross-validation to prevent lookahead bias. Built for financial time series where temporal integrity matters.
|
|
10
|
+
|
|
11
|
+
## Why boruta-quant?
|
|
12
|
+
|
|
13
|
+
Standard feature selection (SHAP, sklearn permutation importance) computes importance on training data. In financial time series, this leaks future information into feature rankings. `boruta-quant` fixes this:
|
|
14
|
+
|
|
15
|
+
- **OOS-Only Importance**: Importance computed exclusively on validation folds
|
|
16
|
+
- **Purged Cross-Validation**: Train/test gap with purge and embargo windows
|
|
17
|
+
- **Shadow Features**: Boruta's all-relevant selection via shadow comparison
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Basic (permutation importance only)
|
|
23
|
+
pip install boruta-quant
|
|
24
|
+
|
|
25
|
+
# With LightGBM
|
|
26
|
+
pip install boruta-quant[lightgbm]
|
|
27
|
+
|
|
28
|
+
# With SHAP support
|
|
29
|
+
pip install boruta-quant[shap]
|
|
30
|
+
|
|
31
|
+
# Everything
|
|
32
|
+
pip install boruta-quant[all]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Development
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/BlackArbsCEO/boruta-quant.git
|
|
39
|
+
cd boruta-quant
|
|
40
|
+
uv sync --all-extras --dev
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from boruta_quant import BorutaSelector, BorutaSelectorConfig
|
|
47
|
+
from boruta_quant.oracle import PermutationImportanceOracle
|
|
48
|
+
from boruta_quant.temporal import PurgedTemporalCV, PurgedCVConfig
|
|
49
|
+
from boruta_quant.metrics import rank_ic_scorer
|
|
50
|
+
from lightgbm import LGBMRegressor
|
|
51
|
+
|
|
52
|
+
# 1. Configure purged temporal CV
|
|
53
|
+
cv = PurgedTemporalCV(PurgedCVConfig(
|
|
54
|
+
n_splits=5,
|
|
55
|
+
purge_window_days=5, # gap before validation fold
|
|
56
|
+
embargo_window_days=5, # gap after validation fold
|
|
57
|
+
min_train_size=100,
|
|
58
|
+
test_size_ratio=0.2,
|
|
59
|
+
))
|
|
60
|
+
|
|
61
|
+
# 2. Configure importance oracle (OOS-only)
|
|
62
|
+
oracle = PermutationImportanceOracle(
|
|
63
|
+
scoring=rank_ic_scorer, # Spearman rank correlation
|
|
64
|
+
n_repeats=10,
|
|
65
|
+
random_state=42,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# 3. Configure Boruta selector
|
|
69
|
+
selector = BorutaSelector(
|
|
70
|
+
config=BorutaSelectorConfig(
|
|
71
|
+
n_trials=20, # Boruta iterations
|
|
72
|
+
percentile=100, # shadow threshold percentile
|
|
73
|
+
alpha=0.05, # significance level
|
|
74
|
+
two_step=True, # resolve tentative features
|
|
75
|
+
random_state=42,
|
|
76
|
+
),
|
|
77
|
+
oracle=oracle,
|
|
78
|
+
cv=cv,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# 4. Fit — model goes here, not in the constructor
|
|
82
|
+
result = selector.fit(
|
|
83
|
+
X, y,
|
|
84
|
+
timestamps=timestamps, # must be timezone-aware
|
|
85
|
+
model=LGBMRegressor(n_estimators=100, random_state=42),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# 5. Results
|
|
89
|
+
print(result.accepted_features) # confirmed important
|
|
90
|
+
print(result.rejected_features) # confirmed unimportant
|
|
91
|
+
print(result.tentative_features) # borderline (resolved if two_step=True)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Importance Oracles
|
|
95
|
+
|
|
96
|
+
All oracles fit the model on training data but measure importance on validation data only.
|
|
97
|
+
|
|
98
|
+
| Oracle | How it works | When to use |
|
|
99
|
+
|--------|-------------|-------------|
|
|
100
|
+
| `PermutationImportanceOracle` | Shuffles one feature in validation set, measures prediction drop | Default — reliable, no refit needed |
|
|
101
|
+
| `DropColumnImportanceOracle` | Removes feature, refits model, measures prediction drop | When refit cost is acceptable |
|
|
102
|
+
| `BlockPermutationImportanceOracle` | Block-shuffles feature (preserves autocorrelation structure) | Autocorrelated time series |
|
|
103
|
+
|
|
104
|
+
## Temporal Cross-Validation
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
Training Purge Validation Embargo
|
|
108
|
+
|--------------| |-------| |----------| |-------|
|
|
109
|
+
^ ^
|
|
110
|
+
train_start embargo_end
|
|
111
|
+
|
|
112
|
+
- Purge: removes observations that could leak into validation
|
|
113
|
+
- Embargo: prevents information from validation bleeding forward
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Shadow Shuffle Modes
|
|
117
|
+
|
|
118
|
+
Shadow features are shuffled copies of real features. The shuffle mode controls how temporal structure is handled:
|
|
119
|
+
|
|
120
|
+
| Mode | Description | Use case |
|
|
121
|
+
|------|-------------|----------|
|
|
122
|
+
| `ShuffleMode.RANDOM` | Standard i.i.d. permutation | Default — i.i.d. data |
|
|
123
|
+
| `ShuffleMode.BLOCK` | Block-preserving shuffle | Autocorrelated features |
|
|
124
|
+
| `ShuffleMode.ERA` | Shuffle within eras only | Regime-aware selection |
|
|
125
|
+
|
|
126
|
+
## Metrics
|
|
127
|
+
|
|
128
|
+
| Function | Description |
|
|
129
|
+
|----------|-------------|
|
|
130
|
+
| `rank_ic` | Spearman correlation between predictions and actuals |
|
|
131
|
+
| `rank_ic_scorer` | sklearn-compatible scorer wrapping `rank_ic` |
|
|
132
|
+
| `directional_accuracy` | Fraction of correct sign predictions (up vs down) |
|
|
133
|
+
| `directional_accuracy_scorer` | sklearn-compatible scorer wrapping `directional_accuracy` |
|
|
134
|
+
| `auc_score` | Area under ROC curve |
|
|
135
|
+
| `auc_scorer` | sklearn-compatible scorer wrapping `auc_score` |
|
|
136
|
+
|
|
137
|
+
## Design Principles
|
|
138
|
+
|
|
139
|
+
1. **OOS-Only**: Importance never computed on training data
|
|
140
|
+
2. **Fail-Fast**: Invalid temporal data (naive timestamps, unsorted) raises immediately
|
|
141
|
+
3. **Type-Safe**: Runtime enforcement with beartype, strict Pyright
|
|
142
|
+
4. **Explicit**: All config parameters required — no hidden defaults
|
|
143
|
+
|
|
144
|
+
## References
|
|
145
|
+
|
|
146
|
+
- [Boruta Algorithm](https://www.jstatsoft.org/article/view/v036i11) — Kursa & Rudnicki (2010)
|
|
147
|
+
- [Advances in Financial Machine Learning](https://www.wiley.com/en-us/Advances+in+Financial+Machine+Learning-p-9781119482086) — Lopez de Prado (2018), Ch. 7 (purged CV)
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# PORTFOLIO DOMAIN RULES
|
|
2
|
+
|
|
3
|
+
## 💼 Portfolio Management Requirements
|
|
4
|
+
|
|
5
|
+
### Core Portfolio Rules
|
|
6
|
+
1. **NEVER exceed position limits** - Risk management is paramount
|
|
7
|
+
2. **ALWAYS maintain audit trail** - Every rebalance must be traceable
|
|
8
|
+
3. **ENFORCE factor attribution** - Know what drives returns
|
|
9
|
+
4. **REQUIRE transaction cost modeling** - Real costs in optimization
|
|
10
|
+
|
|
11
|
+
### Portfolio Construction Requirements
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
# ✅ REQUIRED - Position limits enforcement
|
|
15
|
+
@beartype
|
|
16
|
+
def validate_weights(weights: np.ndarray, limits: PositionLimits):
|
|
17
|
+
assert abs(weights.sum() - 1.0) < 1e-6, "Weights must sum to 1"
|
|
18
|
+
assert (weights >= limits.min_weight).all(), f"Below minimum: {weights.min()}"
|
|
19
|
+
assert (weights <= limits.max_weight).all(), f"Above maximum: {weights.max()}"
|
|
20
|
+
assert weights[weights != 0].shape[0] <= limits.max_positions, "Too many positions"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Risk Management Rules
|
|
24
|
+
|
|
25
|
+
| Metric | Required Check | Failure Action |
|
|
26
|
+
|--------|---------------|----------------|
|
|
27
|
+
| Portfolio VaR | < Risk budget | Scale positions down |
|
|
28
|
+
| Concentration | < 25% per position | Rebalance |
|
|
29
|
+
| Correlation | < 0.8 between positions | Diversify |
|
|
30
|
+
| Leverage | < Maximum allowed | Reduce exposure |
|
|
31
|
+
|
|
32
|
+
### Rebalancing Requirements
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
# Event-driven rebalancing triggers
|
|
36
|
+
RebalanceTriggers = {
|
|
37
|
+
"drift": 5.0, # % drift from target
|
|
38
|
+
"vol_spike": 2.0, # Volatility multiplier
|
|
39
|
+
"correlation": 0.85, # Correlation threshold
|
|
40
|
+
"calendar": 30, # Days since last rebalance
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# REQUIRED: Rebalancing audit
|
|
44
|
+
RebalanceAudit = {
|
|
45
|
+
"timestamp": datetime.utcnow(),
|
|
46
|
+
"trigger": trigger_reason,
|
|
47
|
+
"weights_before": current_weights.copy(),
|
|
48
|
+
"weights_after": new_weights.copy(),
|
|
49
|
+
"turnover": calculate_turnover(current, new),
|
|
50
|
+
"expected_cost": transaction_costs,
|
|
51
|
+
"risk_metrics": calculate_risk_metrics(new)
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Factor Attribution Requirements
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# MANDATORY - Track factor exposures
|
|
59
|
+
@dataclass
|
|
60
|
+
class FactorExposure:
|
|
61
|
+
market_beta: float
|
|
62
|
+
size: float
|
|
63
|
+
value: float
|
|
64
|
+
momentum: float
|
|
65
|
+
quality: float
|
|
66
|
+
volatility: float
|
|
67
|
+
|
|
68
|
+
def validate(self):
|
|
69
|
+
assert -3 <= self.market_beta <= 3, "Unrealistic beta"
|
|
70
|
+
total_exposure = abs(self.size) + abs(self.value) + abs(self.momentum)
|
|
71
|
+
assert total_exposure > 0, "No factor exposure"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Optimization Requirements
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# REQUIRED: Robust optimization
|
|
78
|
+
from scipy.optimize import minimize
|
|
79
|
+
|
|
80
|
+
def optimize_portfolio(
|
|
81
|
+
expected_returns: np.ndarray,
|
|
82
|
+
covariance: np.ndarray,
|
|
83
|
+
constraints: list
|
|
84
|
+
):
|
|
85
|
+
# MANDATORY: Regularization for numerical stability
|
|
86
|
+
cov_regularized = covariance + np.eye(len(covariance)) * 1e-8
|
|
87
|
+
|
|
88
|
+
# REQUIRED: Multiple objectives
|
|
89
|
+
objectives = {
|
|
90
|
+
"return": expected_returns,
|
|
91
|
+
"risk": cov_regularized,
|
|
92
|
+
"transaction_costs": cost_model
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# FORBIDDEN: Unconstrained optimization
|
|
96
|
+
assert len(constraints) > 0, "Must have constraints"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Execution Requirements
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# Saga pattern for multi-step execution
|
|
103
|
+
@dataclass
|
|
104
|
+
class RebalanceSaga:
|
|
105
|
+
saga_id: str
|
|
106
|
+
steps: list[RebalanceStep]
|
|
107
|
+
state: SagaState
|
|
108
|
+
|
|
109
|
+
def execute(self):
|
|
110
|
+
for step in self.steps:
|
|
111
|
+
try:
|
|
112
|
+
step.execute()
|
|
113
|
+
self.checkpoint(step)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self.rollback(step)
|
|
116
|
+
raise SagaFailure(f"Failed at {step}: {e}")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Performance Attribution
|
|
120
|
+
|
|
121
|
+
| Component | Calculation | Required |
|
|
122
|
+
|-----------|------------|----------|
|
|
123
|
+
| Asset allocation | Benchmark vs actual weights | Yes |
|
|
124
|
+
| Security selection | Within-sector returns | Yes |
|
|
125
|
+
| Factor contribution | Factor return × exposure | Yes |
|
|
126
|
+
| Transaction costs | Explicit tracking | Yes |
|
|
127
|
+
| Timing | Entry/exit vs benchmark | Optional |
|
|
128
|
+
|
|
129
|
+
### Data Management
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# Zero-copy optimization for large portfolios
|
|
133
|
+
import pyarrow as pa
|
|
134
|
+
|
|
135
|
+
# REQUIRED: Efficient data structures
|
|
136
|
+
prices_table = pa.Table.from_pandas(prices_df)
|
|
137
|
+
returns_array = pa.compute.diff(prices_table['close'])
|
|
138
|
+
|
|
139
|
+
# FORBIDDEN: Repeated DataFrame copies
|
|
140
|
+
# ❌ df2 = df.copy(); df3 = df2.copy()
|
|
141
|
+
# ✅ Use views or Arrow tables
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Forbidden Patterns
|
|
145
|
+
|
|
146
|
+
| ❌ Forbidden | ✅ Required | Why |
|
|
147
|
+
|--------------|-------------|-----|
|
|
148
|
+
| Optimize without constraints | Constrained optimization | Prevents unrealistic portfolios |
|
|
149
|
+
| Ignore transaction costs | Explicit cost modeling | Real-world performance |
|
|
150
|
+
| Single-period optimization | Multi-period planning | Path dependency |
|
|
151
|
+
| Static risk limits | Dynamic risk budgeting | Market regime changes |
|
|
152
|
+
| Rebalance without audit | Complete audit trail | Regulatory compliance |
|
|
153
|
+
|
|
154
|
+
### Production Safety
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# MANDATORY: Pre-trade compliance checks
|
|
158
|
+
def pre_trade_compliance(orders: list[Order]) -> list[Order]:
|
|
159
|
+
validated = []
|
|
160
|
+
for order in orders:
|
|
161
|
+
# Position limits
|
|
162
|
+
assert check_position_limits(order), f"Position limit breach: {order}"
|
|
163
|
+
# Risk limits
|
|
164
|
+
assert check_risk_limits(order), f"Risk limit breach: {order}"
|
|
165
|
+
# Regulatory
|
|
166
|
+
assert check_regulatory(order), f"Regulatory violation: {order}"
|
|
167
|
+
validated.append(order)
|
|
168
|
+
return validated
|
|
169
|
+
```
|