quantmine 0.2.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.
- quantmine-0.2.0/.airflowignore +10 -0
- quantmine-0.2.0/.gitignore +40 -0
- quantmine-0.2.0/.python-version +1 -0
- quantmine-0.2.0/LICENSE +21 -0
- quantmine-0.2.0/PKG-INFO +222 -0
- quantmine-0.2.0/README.md +200 -0
- quantmine-0.2.0/airflow.cfg.example +18 -0
- quantmine-0.2.0/config.example.yaml +33 -0
- quantmine-0.2.0/pipelines/DAG_pipeline.py +76 -0
- quantmine-0.2.0/pipelines/task_1.py +70 -0
- quantmine-0.2.0/pipelines/task_2.py +45 -0
- quantmine-0.2.0/pipelines/task_3.py +59 -0
- quantmine-0.2.0/pipelines/task_retry.py +43 -0
- quantmine-0.2.0/pyproject.toml +33 -0
- quantmine-0.2.0/quantmine/__init__.py +97 -0
- quantmine-0.2.0/quantmine/back_testing.py +381 -0
- quantmine-0.2.0/quantmine/config.py +115 -0
- quantmine-0.2.0/quantmine/data_acquisition.py +326 -0
- quantmine-0.2.0/quantmine/datareader.py +119 -0
- quantmine-0.2.0/quantmine/factor_attribution.py +70 -0
- quantmine-0.2.0/quantmine/factor_mining.py +192 -0
- quantmine-0.2.0/quantmine/factor_register.py +73 -0
- quantmine-0.2.0/quantmine/ic_calculator.py +543 -0
- quantmine-0.2.0/quantmine/load_config.py +16 -0
- quantmine-0.2.0/task_2.py +45 -0
- quantmine-0.2.0/test/README.md +51 -0
- quantmine-0.2.0/test/conftest.py +89 -0
- quantmine-0.2.0/test/test_calculate_all_factors.py +107 -0
- quantmine-0.2.0/test/test_config.py +80 -0
- quantmine-0.2.0/test/test_constituents_source.py +65 -0
- quantmine-0.2.0/test/test_cs_ic_golden.py +82 -0
- quantmine-0.2.0/test/test_factor_registry.py +102 -0
- quantmine-0.2.0/test/test_factors_golden.py +99 -0
- quantmine-0.2.0/test/test_forward_returns_golden.py +67 -0
- quantmine-0.2.0/test/test_market_data.py +31 -0
- quantmine-0.2.0/test/test_quantile_backtest.py +119 -0
- quantmine-0.2.0/test/test_turnover_cost.py +81 -0
- quantmine-0.2.0/tests/.gitkeep +0 -0
- quantmine-0.2.0/uv.lock +5000 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# --- market data & run artifacts (never committed: size + data licensing) ---
|
|
2
|
+
data/
|
|
3
|
+
tmp/
|
|
4
|
+
analysis/
|
|
5
|
+
output.txt
|
|
6
|
+
run.log
|
|
7
|
+
*.parquet
|
|
8
|
+
*.pkl
|
|
9
|
+
*.xlsx
|
|
10
|
+
|
|
11
|
+
# --- airflow runtime (airflow.cfg contains secret keys; db/logs are local state) ---
|
|
12
|
+
airflow/
|
|
13
|
+
*.db
|
|
14
|
+
*.db-shm
|
|
15
|
+
*.db-wal
|
|
16
|
+
|
|
17
|
+
# --- python ---
|
|
18
|
+
__pycache__/
|
|
19
|
+
*.pyc
|
|
20
|
+
.venv/
|
|
21
|
+
.venv-win/
|
|
22
|
+
.ipynb_checkpoints/
|
|
23
|
+
|
|
24
|
+
# --- build & test artifacts ---
|
|
25
|
+
dist/
|
|
26
|
+
build/
|
|
27
|
+
*.egg-info/
|
|
28
|
+
.pytest_cache/
|
|
29
|
+
.coverage
|
|
30
|
+
htmlcov/
|
|
31
|
+
|
|
32
|
+
# --- local scratch / superseded scripts ---
|
|
33
|
+
scripts/
|
|
34
|
+
|
|
35
|
+
# --- generated dashboards (frontend being rebuilt) ---
|
|
36
|
+
factor_analysis_dashboard.html
|
|
37
|
+
|
|
38
|
+
# --- editor/os noise ---
|
|
39
|
+
.DS_Store
|
|
40
|
+
Thumbs.db
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
quantmine-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 b00831205-web
|
|
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.
|
quantmine-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quantmine
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Equity factor research library with honest statistics: Newey-West IC tests, multiple-testing control, point-in-time universe, turnover-based costs, Carhart attribution
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Requires-Dist: numpy>=2.4
|
|
9
|
+
Requires-Dist: pandas>=3.0
|
|
10
|
+
Requires-Dist: pyarrow>=24.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0
|
|
12
|
+
Requires-Dist: scipy>=1.17
|
|
13
|
+
Requires-Dist: statsmodels>=0.14
|
|
14
|
+
Provides-Extra: data
|
|
15
|
+
Requires-Dist: beautifulsoup4>=4.14; extra == 'data'
|
|
16
|
+
Requires-Dist: curl-cffi>=0.15; extra == 'data'
|
|
17
|
+
Requires-Dist: pandas-datareader>=0.11; extra == 'data'
|
|
18
|
+
Requires-Dist: yfinance>=1.4; extra == 'data'
|
|
19
|
+
Provides-Extra: viz
|
|
20
|
+
Requires-Dist: plotly>=6.8; extra == 'viz'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# quantmine
|
|
24
|
+
|
|
25
|
+
An equity factor research library built around one principle: **statistical
|
|
26
|
+
honesty**. Every step that commonly inflates backtest results — survivorship
|
|
27
|
+
bias, overlapping-return autocorrelation, multiple testing, look-ahead in
|
|
28
|
+
orthogonalization, unrealistic transaction costs — is explicitly addressed,
|
|
29
|
+
and conclusions are reported with their uncertainty, not just their point
|
|
30
|
+
estimates.
|
|
31
|
+
|
|
32
|
+
The repo doubles as a full S&P 500 research case study: the library
|
|
33
|
+
(`quantmine/`), the daily Airflow pipeline (`pipelines/`), and the findings
|
|
34
|
+
below were produced by the same code you can `pip install`.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install quantmine # library only
|
|
40
|
+
pip install "quantmine[data]" # + yfinance download stack
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For development (repo checkout, Python ≥ 3.13, [uv](https://docs.astral.sh/uv/)):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv sync
|
|
47
|
+
python -m pytest test
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
Bring your own price data — any wide DataFrame (index = trading days,
|
|
53
|
+
columns = tickers) works; nothing is hard-wired to yfinance or the S&P 500:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import pandas as pd
|
|
57
|
+
import quantmine as qf
|
|
58
|
+
|
|
59
|
+
# 1) Wrap your data (or use qf.ParquetSource / qf.YFinanceSource)
|
|
60
|
+
data = qf.MarketData(close=close_df, volume=volume_df)
|
|
61
|
+
|
|
62
|
+
# 2) Register a custom factor — built-in factors register automatically
|
|
63
|
+
@qf.factor_register("my_reversal")
|
|
64
|
+
def my_reversal(close: pd.DataFrame, tickers: list) -> pd.DataFrame:
|
|
65
|
+
return -close[tickers].pct_change(5)
|
|
66
|
+
|
|
67
|
+
# 3) Compute all registered factors (dependencies resolve automatically)
|
|
68
|
+
pool = qf.build_param_pool(data, day=5, halflife=10, period=20)
|
|
69
|
+
failed, factors = qf.calculate_all_factors(pool)
|
|
70
|
+
|
|
71
|
+
# 4) Cross-sectional IC with Newey-West t-stats and multiple-testing control
|
|
72
|
+
fwd = qf.forward_return(data.close, periods=[1, 5, 20])
|
|
73
|
+
cs_ic = qf.CS_Information_Correlation(factors, fwd, output_path="cs_ic.parquet")
|
|
74
|
+
report = qf.multiple_testing(qf.newey_west_summary(cs_ic))
|
|
75
|
+
|
|
76
|
+
# 5) Quantile backtest on a point-in-time universe, turnover-based costs
|
|
77
|
+
universe = qf.MembershipTableSource(membership_df) # or qf.StaticUniverse([...])
|
|
78
|
+
results, history = qf.quantile_backtest(universe, factors, ["my_reversal"], fwd)
|
|
79
|
+
daily = qf.expand_all_to_daily_returns(history, data.close)
|
|
80
|
+
|
|
81
|
+
# 6) Carhart four-factor attribution of the long-short returns (daily, HAC)
|
|
82
|
+
french = qf.load_french_factors("ff3_daily.csv", "momentum_daily.csv")
|
|
83
|
+
model = qf.carhart_attribution(daily[("my_reversal", 20)]["long_short"], french)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Extension points
|
|
87
|
+
|
|
88
|
+
| Protocol / hook | Purpose | Ships with |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `DataSource.load(...) -> MarketData` | plug in any price/volume source | `ParquetSource`, `CSVSource`, `ExcelSource`, `YFinanceSource` |
|
|
91
|
+
| `ConstituentsSource.get_constituents(date) -> set` | point-in-time universe from any provider | `MembershipTableSource` (interval table), `StaticUniverse` |
|
|
92
|
+
| `@factor_register(name)` | add factors; params injected by name, factor-on-factor dependencies resolved | 8 built-in factors |
|
|
93
|
+
| `config.example.yaml` | every pipeline parameter as validated dataclasses via `qf.load_configs` | defaults documented in-file |
|
|
94
|
+
|
|
95
|
+
## Headline result (S&P 500 case study)
|
|
96
|
+
|
|
97
|
+
The 20-day average volume factor (`TwentyDayAvgVol`) is the only candidate
|
|
98
|
+
that survives the full testing gauntlet:
|
|
99
|
+
|
|
100
|
+
| Stage | Result |
|
|
101
|
+
|---|---|
|
|
102
|
+
| Train IC (2015–2023, Newey-West) | t = 3.78 (20d holding), passes Bonferroni & Benjamini-Hochberg across all 18 factor × horizon tests |
|
|
103
|
+
| Out-of-sample quintiles (2024–2026) | Monotonic (Spearman 0.9); long-short gross ~14 %/yr (Sharpe ~1.5), ~1.4 Sharpe net of turnover-based costs |
|
|
104
|
+
| Carhart 4-factor attribution (daily, HAC, net of costs) | Market beta 0.24 (significant), large-cap tilt (SMB −0.13); momentum & value loadings insignificant |
|
|
105
|
+
| Net alpha | ~10 %/yr net of costs (t = 1.78, p ≈ 0.07, n = 603 daily obs) — economically meaningful, **marginally short of the 5 % significance bar** on 2.4 years of out-of-sample data |
|
|
106
|
+
|
|
107
|
+
The honest conclusion: the factor's IC is robustly significant in-sample under
|
|
108
|
+
conservative testing; its out-of-sample net alpha is economically meaningful
|
|
109
|
+
but does not clear conventional significance. Live verification over a longer
|
|
110
|
+
window is required — and that is exactly what a research report should say.
|
|
111
|
+
(A low-power period-level regression, n ≈ 30, says nothing either way —
|
|
112
|
+
p ≈ 0.38 with a confidence interval wide enough to hold any conclusion. Test
|
|
113
|
+
power and test bookkeeping move the verdict as much as the signal does.)
|
|
114
|
+
|
|
115
|
+
The remaining seven candidate factors (momentum, short-term reversal,
|
|
116
|
+
volatility, downside volatility, volume-price correlation, …) fail the
|
|
117
|
+
corrected significance tests. Documenting *why* they fail is part of the
|
|
118
|
+
point.
|
|
119
|
+
|
|
120
|
+
## Methodology highlights
|
|
121
|
+
|
|
122
|
+
- **Survivorship-bias correction** — the universe is rebuilt from historical
|
|
123
|
+
S&P 500 membership (764 tickers over 2015–2026); 569 were recoverable via
|
|
124
|
+
yfinance, and the residual gap is disclosed rather than hidden.
|
|
125
|
+
- **Point-in-time universe** — each backtest cross-section only contains
|
|
126
|
+
stocks that were actually index members on that date.
|
|
127
|
+
- **Newey-West IC tests** — daily ICs on overlapping k-day forward returns are
|
|
128
|
+
autocorrelated; plain `t = IR·√n` overstates significance several-fold. NW
|
|
129
|
+
(Bartlett kernel, lag = 2(k−1)) uses all daily observations while correcting
|
|
130
|
+
the standard error. A down-sampled IID test is kept as a robustness control.
|
|
131
|
+
- **Multiple-testing control** — Bonferroni and Benjamini-Hochberg across all
|
|
132
|
+
factor × holding-period combinations.
|
|
133
|
+
- **Train/test split with embargo** — factors are selected and the
|
|
134
|
+
orthogonalization is fit on 2015–2023 only; a gap of one month before the
|
|
135
|
+
test window prevents overlapping forward returns from leaking across the
|
|
136
|
+
split.
|
|
137
|
+
- **Expanding-window orthogonalization** — correlated factors are residualized
|
|
138
|
+
with betas estimated on data available up to each date (no full-sample
|
|
139
|
+
look-ahead).
|
|
140
|
+
- **Turnover-based transaction costs** — costs are charged on actual
|
|
141
|
+
membership turnover per rebalance, not on a flat 100 %-turnover assumption.
|
|
142
|
+
- **Sanity checks** — factor displacement and cross-sectional shuffling tests
|
|
143
|
+
confirm the backtest machinery itself is not the source of the returns.
|
|
144
|
+
- **Tested** — the research chain is covered by a unit + golden-value test
|
|
145
|
+
suite (`test/`), including hand-computed backtest fixtures, determinism
|
|
146
|
+
checks, and point-in-time universe edge cases.
|
|
147
|
+
|
|
148
|
+
## Pipeline
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
yfinance (batch download, retry, blacklist, checkpoints)
|
|
152
|
+
│ quantmine/data_acquisition.py · pipelines/task_1.py
|
|
153
|
+
▼
|
|
154
|
+
cleaning & merge (ffill, dedup) Airflow DAG: pipelines/DAG_pipeline.py
|
|
155
|
+
│ pipelines/task_2.py
|
|
156
|
+
▼
|
|
157
|
+
factor computation (registry-driven, vectorized pandas)
|
|
158
|
+
│ quantmine/factor_mining.py · pipelines/task_3.py
|
|
159
|
+
▼
|
|
160
|
+
IC testing: cross-sectional & time-series IC, NW t, BH/Bonferroni,
|
|
161
|
+
train/test split, orthogonalization quantmine/ic_calculator.py
|
|
162
|
+
▼
|
|
163
|
+
quintile backtest: PIT universe, monotonicity, turnover costs,
|
|
164
|
+
displacement/shuffle sanity tests quantmine/back_testing.py
|
|
165
|
+
▼
|
|
166
|
+
Carhart 4-factor attribution (daily, HAC) quantmine/factor_attribution.py
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Repository layout:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
quantmine/ research library (importable package)
|
|
173
|
+
pipelines/ Airflow DAG + daily CLI tasks
|
|
174
|
+
test/ pytest suite (unit + golden-value)
|
|
175
|
+
config.example.yaml all pipeline parameters, documented defaults
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Reproducing the case study
|
|
179
|
+
|
|
180
|
+
**Market data is not included** (Yahoo Finance terms of service do not permit
|
|
181
|
+
redistribution). To reproduce:
|
|
182
|
+
|
|
183
|
+
1. Historical S&P 500 membership: provide a CSV with `ticker`, `start_date`,
|
|
184
|
+
`end_date` columns and point `SP500_MEMBERSHIP_CSV` at it.
|
|
185
|
+
2. Prices/volumes: `python pipelines/task_1.py --date <ds> --batch manual`
|
|
186
|
+
downloads in batches with checkpointing, then `pipelines/task_2.py` cleans
|
|
187
|
+
and merges.
|
|
188
|
+
3. Fama-French factors: download the daily FF3 and momentum CSVs from the
|
|
189
|
+
[Ken French Data Library](https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html)
|
|
190
|
+
into `tmp/ff3/`.
|
|
191
|
+
4. Run the research chain: `python pipelines/task_3.py ...` for factors, then
|
|
192
|
+
the IC → backtest → attribution steps as in the quick start (or
|
|
193
|
+
`python -m quantmine.ic_calculator` for the packaged train/test workflow).
|
|
194
|
+
|
|
195
|
+
For the Airflow DAG, set `QUANT_PROJECT_ROOT` and `QUANT_PYTHON_BIN` (see
|
|
196
|
+
`DAG_pipeline.py` docstring) and copy `airflow.cfg.example` keys into your own
|
|
197
|
+
config — never commit a real `airflow.cfg`.
|
|
198
|
+
|
|
199
|
+
## Known limitations
|
|
200
|
+
|
|
201
|
+
- ~195 of 764 historical members could not be recovered from yfinance (mostly
|
|
202
|
+
true delistings/acquisitions), so a residual survivorship bias remains and
|
|
203
|
+
likely flatters the results slightly.
|
|
204
|
+
- The out-of-sample window (2024–2026) covers a single market regime.
|
|
205
|
+
- The long-short portfolio carries a significant 0.24 market beta; a
|
|
206
|
+
beta-hedged variant is on the roadmap.
|
|
207
|
+
- Transaction cost model is a flat per-turnover rate; no market-impact or
|
|
208
|
+
borrow-cost modeling.
|
|
209
|
+
|
|
210
|
+
## Roadmap
|
|
211
|
+
|
|
212
|
+
- [ ] Migrate storage from parquet files to PostgreSQL/DuckDB
|
|
213
|
+
- [ ] REST API + MCP server exposing the research chain as agent-callable tools
|
|
214
|
+
- [ ] RAG-based automated research reports (ChromaDB + LLM)
|
|
215
|
+
- [ ] Extend the Airflow DAG to cover IC testing → backtest → reporting
|
|
216
|
+
- [ ] Rebuild the analytics dashboard (frontend rewrite in progress)
|
|
217
|
+
- [ ] Scale the data layer (full US market) with PySpark
|
|
218
|
+
- [ ] Beta-hedged long-short variant; GARCH volatility targeting
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# quantmine
|
|
2
|
+
|
|
3
|
+
An equity factor research library built around one principle: **statistical
|
|
4
|
+
honesty**. Every step that commonly inflates backtest results — survivorship
|
|
5
|
+
bias, overlapping-return autocorrelation, multiple testing, look-ahead in
|
|
6
|
+
orthogonalization, unrealistic transaction costs — is explicitly addressed,
|
|
7
|
+
and conclusions are reported with their uncertainty, not just their point
|
|
8
|
+
estimates.
|
|
9
|
+
|
|
10
|
+
The repo doubles as a full S&P 500 research case study: the library
|
|
11
|
+
(`quantmine/`), the daily Airflow pipeline (`pipelines/`), and the findings
|
|
12
|
+
below were produced by the same code you can `pip install`.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install quantmine # library only
|
|
18
|
+
pip install "quantmine[data]" # + yfinance download stack
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For development (repo checkout, Python ≥ 3.13, [uv](https://docs.astral.sh/uv/)):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv sync
|
|
25
|
+
python -m pytest test
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
Bring your own price data — any wide DataFrame (index = trading days,
|
|
31
|
+
columns = tickers) works; nothing is hard-wired to yfinance or the S&P 500:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import pandas as pd
|
|
35
|
+
import quantmine as qf
|
|
36
|
+
|
|
37
|
+
# 1) Wrap your data (or use qf.ParquetSource / qf.YFinanceSource)
|
|
38
|
+
data = qf.MarketData(close=close_df, volume=volume_df)
|
|
39
|
+
|
|
40
|
+
# 2) Register a custom factor — built-in factors register automatically
|
|
41
|
+
@qf.factor_register("my_reversal")
|
|
42
|
+
def my_reversal(close: pd.DataFrame, tickers: list) -> pd.DataFrame:
|
|
43
|
+
return -close[tickers].pct_change(5)
|
|
44
|
+
|
|
45
|
+
# 3) Compute all registered factors (dependencies resolve automatically)
|
|
46
|
+
pool = qf.build_param_pool(data, day=5, halflife=10, period=20)
|
|
47
|
+
failed, factors = qf.calculate_all_factors(pool)
|
|
48
|
+
|
|
49
|
+
# 4) Cross-sectional IC with Newey-West t-stats and multiple-testing control
|
|
50
|
+
fwd = qf.forward_return(data.close, periods=[1, 5, 20])
|
|
51
|
+
cs_ic = qf.CS_Information_Correlation(factors, fwd, output_path="cs_ic.parquet")
|
|
52
|
+
report = qf.multiple_testing(qf.newey_west_summary(cs_ic))
|
|
53
|
+
|
|
54
|
+
# 5) Quantile backtest on a point-in-time universe, turnover-based costs
|
|
55
|
+
universe = qf.MembershipTableSource(membership_df) # or qf.StaticUniverse([...])
|
|
56
|
+
results, history = qf.quantile_backtest(universe, factors, ["my_reversal"], fwd)
|
|
57
|
+
daily = qf.expand_all_to_daily_returns(history, data.close)
|
|
58
|
+
|
|
59
|
+
# 6) Carhart four-factor attribution of the long-short returns (daily, HAC)
|
|
60
|
+
french = qf.load_french_factors("ff3_daily.csv", "momentum_daily.csv")
|
|
61
|
+
model = qf.carhart_attribution(daily[("my_reversal", 20)]["long_short"], french)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Extension points
|
|
65
|
+
|
|
66
|
+
| Protocol / hook | Purpose | Ships with |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `DataSource.load(...) -> MarketData` | plug in any price/volume source | `ParquetSource`, `CSVSource`, `ExcelSource`, `YFinanceSource` |
|
|
69
|
+
| `ConstituentsSource.get_constituents(date) -> set` | point-in-time universe from any provider | `MembershipTableSource` (interval table), `StaticUniverse` |
|
|
70
|
+
| `@factor_register(name)` | add factors; params injected by name, factor-on-factor dependencies resolved | 8 built-in factors |
|
|
71
|
+
| `config.example.yaml` | every pipeline parameter as validated dataclasses via `qf.load_configs` | defaults documented in-file |
|
|
72
|
+
|
|
73
|
+
## Headline result (S&P 500 case study)
|
|
74
|
+
|
|
75
|
+
The 20-day average volume factor (`TwentyDayAvgVol`) is the only candidate
|
|
76
|
+
that survives the full testing gauntlet:
|
|
77
|
+
|
|
78
|
+
| Stage | Result |
|
|
79
|
+
|---|---|
|
|
80
|
+
| Train IC (2015–2023, Newey-West) | t = 3.78 (20d holding), passes Bonferroni & Benjamini-Hochberg across all 18 factor × horizon tests |
|
|
81
|
+
| Out-of-sample quintiles (2024–2026) | Monotonic (Spearman 0.9); long-short gross ~14 %/yr (Sharpe ~1.5), ~1.4 Sharpe net of turnover-based costs |
|
|
82
|
+
| Carhart 4-factor attribution (daily, HAC, net of costs) | Market beta 0.24 (significant), large-cap tilt (SMB −0.13); momentum & value loadings insignificant |
|
|
83
|
+
| Net alpha | ~10 %/yr net of costs (t = 1.78, p ≈ 0.07, n = 603 daily obs) — economically meaningful, **marginally short of the 5 % significance bar** on 2.4 years of out-of-sample data |
|
|
84
|
+
|
|
85
|
+
The honest conclusion: the factor's IC is robustly significant in-sample under
|
|
86
|
+
conservative testing; its out-of-sample net alpha is economically meaningful
|
|
87
|
+
but does not clear conventional significance. Live verification over a longer
|
|
88
|
+
window is required — and that is exactly what a research report should say.
|
|
89
|
+
(A low-power period-level regression, n ≈ 30, says nothing either way —
|
|
90
|
+
p ≈ 0.38 with a confidence interval wide enough to hold any conclusion. Test
|
|
91
|
+
power and test bookkeeping move the verdict as much as the signal does.)
|
|
92
|
+
|
|
93
|
+
The remaining seven candidate factors (momentum, short-term reversal,
|
|
94
|
+
volatility, downside volatility, volume-price correlation, …) fail the
|
|
95
|
+
corrected significance tests. Documenting *why* they fail is part of the
|
|
96
|
+
point.
|
|
97
|
+
|
|
98
|
+
## Methodology highlights
|
|
99
|
+
|
|
100
|
+
- **Survivorship-bias correction** — the universe is rebuilt from historical
|
|
101
|
+
S&P 500 membership (764 tickers over 2015–2026); 569 were recoverable via
|
|
102
|
+
yfinance, and the residual gap is disclosed rather than hidden.
|
|
103
|
+
- **Point-in-time universe** — each backtest cross-section only contains
|
|
104
|
+
stocks that were actually index members on that date.
|
|
105
|
+
- **Newey-West IC tests** — daily ICs on overlapping k-day forward returns are
|
|
106
|
+
autocorrelated; plain `t = IR·√n` overstates significance several-fold. NW
|
|
107
|
+
(Bartlett kernel, lag = 2(k−1)) uses all daily observations while correcting
|
|
108
|
+
the standard error. A down-sampled IID test is kept as a robustness control.
|
|
109
|
+
- **Multiple-testing control** — Bonferroni and Benjamini-Hochberg across all
|
|
110
|
+
factor × holding-period combinations.
|
|
111
|
+
- **Train/test split with embargo** — factors are selected and the
|
|
112
|
+
orthogonalization is fit on 2015–2023 only; a gap of one month before the
|
|
113
|
+
test window prevents overlapping forward returns from leaking across the
|
|
114
|
+
split.
|
|
115
|
+
- **Expanding-window orthogonalization** — correlated factors are residualized
|
|
116
|
+
with betas estimated on data available up to each date (no full-sample
|
|
117
|
+
look-ahead).
|
|
118
|
+
- **Turnover-based transaction costs** — costs are charged on actual
|
|
119
|
+
membership turnover per rebalance, not on a flat 100 %-turnover assumption.
|
|
120
|
+
- **Sanity checks** — factor displacement and cross-sectional shuffling tests
|
|
121
|
+
confirm the backtest machinery itself is not the source of the returns.
|
|
122
|
+
- **Tested** — the research chain is covered by a unit + golden-value test
|
|
123
|
+
suite (`test/`), including hand-computed backtest fixtures, determinism
|
|
124
|
+
checks, and point-in-time universe edge cases.
|
|
125
|
+
|
|
126
|
+
## Pipeline
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
yfinance (batch download, retry, blacklist, checkpoints)
|
|
130
|
+
│ quantmine/data_acquisition.py · pipelines/task_1.py
|
|
131
|
+
▼
|
|
132
|
+
cleaning & merge (ffill, dedup) Airflow DAG: pipelines/DAG_pipeline.py
|
|
133
|
+
│ pipelines/task_2.py
|
|
134
|
+
▼
|
|
135
|
+
factor computation (registry-driven, vectorized pandas)
|
|
136
|
+
│ quantmine/factor_mining.py · pipelines/task_3.py
|
|
137
|
+
▼
|
|
138
|
+
IC testing: cross-sectional & time-series IC, NW t, BH/Bonferroni,
|
|
139
|
+
train/test split, orthogonalization quantmine/ic_calculator.py
|
|
140
|
+
▼
|
|
141
|
+
quintile backtest: PIT universe, monotonicity, turnover costs,
|
|
142
|
+
displacement/shuffle sanity tests quantmine/back_testing.py
|
|
143
|
+
▼
|
|
144
|
+
Carhart 4-factor attribution (daily, HAC) quantmine/factor_attribution.py
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Repository layout:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
quantmine/ research library (importable package)
|
|
151
|
+
pipelines/ Airflow DAG + daily CLI tasks
|
|
152
|
+
test/ pytest suite (unit + golden-value)
|
|
153
|
+
config.example.yaml all pipeline parameters, documented defaults
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Reproducing the case study
|
|
157
|
+
|
|
158
|
+
**Market data is not included** (Yahoo Finance terms of service do not permit
|
|
159
|
+
redistribution). To reproduce:
|
|
160
|
+
|
|
161
|
+
1. Historical S&P 500 membership: provide a CSV with `ticker`, `start_date`,
|
|
162
|
+
`end_date` columns and point `SP500_MEMBERSHIP_CSV` at it.
|
|
163
|
+
2. Prices/volumes: `python pipelines/task_1.py --date <ds> --batch manual`
|
|
164
|
+
downloads in batches with checkpointing, then `pipelines/task_2.py` cleans
|
|
165
|
+
and merges.
|
|
166
|
+
3. Fama-French factors: download the daily FF3 and momentum CSVs from the
|
|
167
|
+
[Ken French Data Library](https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html)
|
|
168
|
+
into `tmp/ff3/`.
|
|
169
|
+
4. Run the research chain: `python pipelines/task_3.py ...` for factors, then
|
|
170
|
+
the IC → backtest → attribution steps as in the quick start (or
|
|
171
|
+
`python -m quantmine.ic_calculator` for the packaged train/test workflow).
|
|
172
|
+
|
|
173
|
+
For the Airflow DAG, set `QUANT_PROJECT_ROOT` and `QUANT_PYTHON_BIN` (see
|
|
174
|
+
`DAG_pipeline.py` docstring) and copy `airflow.cfg.example` keys into your own
|
|
175
|
+
config — never commit a real `airflow.cfg`.
|
|
176
|
+
|
|
177
|
+
## Known limitations
|
|
178
|
+
|
|
179
|
+
- ~195 of 764 historical members could not be recovered from yfinance (mostly
|
|
180
|
+
true delistings/acquisitions), so a residual survivorship bias remains and
|
|
181
|
+
likely flatters the results slightly.
|
|
182
|
+
- The out-of-sample window (2024–2026) covers a single market regime.
|
|
183
|
+
- The long-short portfolio carries a significant 0.24 market beta; a
|
|
184
|
+
beta-hedged variant is on the roadmap.
|
|
185
|
+
- Transaction cost model is a flat per-turnover rate; no market-impact or
|
|
186
|
+
borrow-cost modeling.
|
|
187
|
+
|
|
188
|
+
## Roadmap
|
|
189
|
+
|
|
190
|
+
- [ ] Migrate storage from parquet files to PostgreSQL/DuckDB
|
|
191
|
+
- [ ] REST API + MCP server exposing the research chain as agent-callable tools
|
|
192
|
+
- [ ] RAG-based automated research reports (ChromaDB + LLM)
|
|
193
|
+
- [ ] Extend the Airflow DAG to cover IC testing → backtest → reporting
|
|
194
|
+
- [ ] Rebuild the analytics dashboard (frontend rewrite in progress)
|
|
195
|
+
- [ ] Scale the data layer (full US market) with PySpark
|
|
196
|
+
- [ ] Beta-hedged long-short variant; GARCH volatility targeting
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Minimal Airflow settings for this project.
|
|
2
|
+
# Copy the relevant keys into your own $AIRFLOW_HOME/airflow.cfg.
|
|
3
|
+
# NEVER commit the real airflow.cfg: it contains fernet_key / secret_key.
|
|
4
|
+
|
|
5
|
+
[core]
|
|
6
|
+
# Point Airflow at this repo so it picks up DAG_pipeline.py
|
|
7
|
+
dags_folder = /path/to/quant-factor-mining
|
|
8
|
+
|
|
9
|
+
# Generate your own key:
|
|
10
|
+
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
|
11
|
+
fernet_key = REPLACE_ME
|
|
12
|
+
|
|
13
|
+
[database]
|
|
14
|
+
sql_alchemy_conn = sqlite:////path/to/airflow/airflow.db
|
|
15
|
+
|
|
16
|
+
[api]
|
|
17
|
+
# Generate your own random secret, e.g.: openssl rand -base64 16
|
|
18
|
+
secret_key = REPLACE_ME
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# quantmine 配置示例 —— 复制为 config.yaml 后按需修改。
|
|
2
|
+
# 所有 key 和字段都可省略, 省略即使用默认值(下方标注的即默认值)。
|
|
3
|
+
|
|
4
|
+
data_acquisition:
|
|
5
|
+
checkpoint_dir: tmp/checkpoint
|
|
6
|
+
max_retries: 3
|
|
7
|
+
wait: 60 # 批次下载失败后的重试等待秒数
|
|
8
|
+
|
|
9
|
+
momentum:
|
|
10
|
+
day: 5 # 动量回看天数(5 = 5日动量)
|
|
11
|
+
|
|
12
|
+
forward_return:
|
|
13
|
+
periods: [1, 5, 20] # 持有期(交易日)
|
|
14
|
+
|
|
15
|
+
newey_west:
|
|
16
|
+
lag_multiplier: 2 # NW修正的lag = lag_multiplier * (持有期-1)
|
|
17
|
+
|
|
18
|
+
orthogonalize:
|
|
19
|
+
threshold: 0.03 # 平均|IR|低于该值的因子被剔除
|
|
20
|
+
min_period: 60 # expanding beta 的最小估计窗口
|
|
21
|
+
|
|
22
|
+
time_series_stationary_test:
|
|
23
|
+
rolling_period: 126 # 滚动IC窗口(约半年)
|
|
24
|
+
periods: [1, 5, 20] # ACF的滞后阶
|
|
25
|
+
|
|
26
|
+
backtest:
|
|
27
|
+
part: 5 # 分位组数(5 = 五分组)
|
|
28
|
+
|
|
29
|
+
transaction_cost:
|
|
30
|
+
cost_per_trade: 0.001 # 单边费率(按实际换手计费)
|
|
31
|
+
|
|
32
|
+
carhart_attribution:
|
|
33
|
+
maxlags: 20 # HAC标准误的最大滞后
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Airflow DAG for the daily data-download / clean / factor-calculation loop.
|
|
2
|
+
|
|
3
|
+
Configuration comes from environment variables so the DAG file itself is
|
|
4
|
+
portable across machines:
|
|
5
|
+
|
|
6
|
+
QUANT_PROJECT_ROOT Absolute path of this project. Defaults to the parent
|
|
7
|
+
of the directory containing this file (repo root).
|
|
8
|
+
QUANT_PYTHON_BIN Python interpreter used to run the task scripts.
|
|
9
|
+
Defaults to ``python``. On a WSL-hosted Airflow that
|
|
10
|
+
drives a Windows venv, point it at
|
|
11
|
+
``.venv-win/Scripts/python.exe`` (WSL interop launches
|
|
12
|
+
Windows executables directly, no PowerShell wrapper
|
|
13
|
+
needed).
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
from datetime import datetime, timedelta
|
|
17
|
+
from airflow.providers.standard.operators.bash import BashOperator
|
|
18
|
+
from airflow.utils.trigger_rule import TriggerRule
|
|
19
|
+
from airflow.sdk import DAG
|
|
20
|
+
|
|
21
|
+
PROJECT_ROOT = os.environ.get("QUANT_PROJECT_ROOT",
|
|
22
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
23
|
+
PYTHON_BIN = os.environ.get("QUANT_PYTHON_BIN", "python")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def task_command(script: str) -> str:
|
|
27
|
+
"""Build the bash command that runs one pipeline script.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
script: Script filename inside the ``pipelines/`` directory.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A bash command string with the Airflow ``ds``/``run_id`` templates.
|
|
34
|
+
|
|
35
|
+
Notes:
|
|
36
|
+
The command runs from the repo root so that relative data paths
|
|
37
|
+
(``tmp/``, ``data/``) resolve correctly.
|
|
38
|
+
"""
|
|
39
|
+
return (f'cd "{PROJECT_ROOT}" && "{PYTHON_BIN}" pipelines/{script} '
|
|
40
|
+
'--date {{ ds }} --batch {{ run_id }}')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
with DAG("quant_factor_mining",
|
|
44
|
+
default_args={
|
|
45
|
+
"retries": 1,
|
|
46
|
+
"retry_delay": timedelta(minutes=5)
|
|
47
|
+
},
|
|
48
|
+
description="quant_factor_mining pipeline version 0.1, using S&P 500",
|
|
49
|
+
schedule=timedelta(days=1),
|
|
50
|
+
start_date=datetime(2020, 1, 1),
|
|
51
|
+
catchup=False,
|
|
52
|
+
tags=['quant_factor_mining'],
|
|
53
|
+
) as dag:
|
|
54
|
+
t1 = BashOperator(
|
|
55
|
+
task_id="data_downloading",
|
|
56
|
+
bash_command=task_command("task_1.py"),
|
|
57
|
+
)
|
|
58
|
+
t2 = BashOperator(
|
|
59
|
+
task_id="data_cleaning",
|
|
60
|
+
depends_on_past=True,
|
|
61
|
+
bash_command=task_command("task_2.py"),
|
|
62
|
+
trigger_rule=TriggerRule.ALL_DONE
|
|
63
|
+
)
|
|
64
|
+
t3 = BashOperator(
|
|
65
|
+
task_id="factor_calculation",
|
|
66
|
+
bash_command=task_command("task_3.py"),
|
|
67
|
+
)
|
|
68
|
+
task_retry = BashOperator(
|
|
69
|
+
task_id="retry_downloading",
|
|
70
|
+
bash_command=task_command("task_retry.py"),
|
|
71
|
+
trigger_rule=TriggerRule.ALL_FAILED
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
t1 >> t2 >> t3
|
|
76
|
+
t1 >> task_retry
|