mlquantx 0.2.5__py3-none-any.whl
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.
- mlquant/__init__.py +18 -0
- mlquant/backtest/__init__.py +14 -0
- mlquant/backtest/engine.py +118 -0
- mlquant/backtest/metrics.py +95 -0
- mlquant/cli/__init__.py +1 -0
- mlquant/cli/main.py +292 -0
- mlquant/configs/__init__.py +1 -0
- mlquant/configs/small.yaml +41 -0
- mlquant/data/__init__.py +88 -0
- mlquant/data/akshare_loader.py +168 -0
- mlquant/data/baostock_loader.py +137 -0
- mlquant/data/loaders.py +125 -0
- mlquant/data/panel.py +206 -0
- mlquant/data/synthetic.py +138 -0
- mlquant/data/yfinance_loader.py +101 -0
- mlquant/features/__init__.py +42 -0
- mlquant/features/_factors_add.py +288 -0
- mlquant/features/_factors_alpha101.py +98 -0
- mlquant/features/_factors_best.py +177 -0
- mlquant/features/_factors_better.py +385 -0
- mlquant/features/_factors_change.py +53 -0
- mlquant/features/_factors_extra.py +147 -0
- mlquant/features/_factors_market.py +61 -0
- mlquant/features/_factors_old.py +563 -0
- mlquant/features/_factors_original.py +215 -0
- mlquant/features/_factors_stock.py +270 -0
- mlquant/features/alpha101.py +84 -0
- mlquant/features/bias.py +81 -0
- mlquant/features/legacy_factors.py +126 -0
- mlquant/features/market_breadth.py +190 -0
- mlquant/features/neutralize.py +75 -0
- mlquant/features/target.py +62 -0
- mlquant/features/tensor_factors.py +301 -0
- mlquant/features.py +613 -0
- mlquant/models/__init__.py +8 -0
- mlquant/models/losses.py +88 -0
- mlquant/models/nets.py +62 -0
- mlquant/portfolio/__init__.py +5 -0
- mlquant/portfolio/frontier.py +49 -0
- mlquant/portfolio/markowitz.py +144 -0
- mlquant/training/__init__.py +6 -0
- mlquant/training/augment.py +89 -0
- mlquant/training/dataset.py +50 -0
- mlquant/training/trainer.py +108 -0
- mlquant/utils/__init__.py +5 -0
- mlquant/utils/config.py +60 -0
- mlquant/utils/seed.py +35 -0
- mlquantx-0.2.5.dist-info/METADATA +108 -0
- mlquantx-0.2.5.dist-info/RECORD +54 -0
- mlquantx-0.2.5.dist-info/WHEEL +5 -0
- mlquantx-0.2.5.dist-info/entry_points.txt +2 -0
- mlquantx-0.2.5.dist-info/licenses/LICENSE +21 -0
- mlquantx-0.2.5.dist-info/licenses/NOTICE.md +5 -0
- mlquantx-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""original_* family: direct close/volume statistics (28 factors)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
import torch
|
|
5
|
+
|
|
6
|
+
from ..data.panel import Panel
|
|
7
|
+
from .tensor_factors import (
|
|
8
|
+
cs_rank, cs_zscore, delay, delta, ts_corr, ts_max, ts_mean, ts_min, ts_rank, ts_std,
|
|
9
|
+
)
|
|
10
|
+
from .legacy_factors import register_legacy_factor
|
|
11
|
+
|
|
12
|
+
Tensor = torch.Tensor
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register_legacy_factor("original_001")
|
|
16
|
+
def original_001(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
17
|
+
"""20-day close volatility, cross-sectionally z-scored."""
|
|
18
|
+
ret, rm = delta(panel.close, panel.mask, 1)
|
|
19
|
+
ret = ret / panel.close.clamp_min(1e-9)
|
|
20
|
+
sd, sm = ts_std(ret, rm, 20)
|
|
21
|
+
return cs_zscore(sd, sm)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@register_legacy_factor("original_002")
|
|
25
|
+
def original_002(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
26
|
+
"""Close to 20-day high distance, ranked."""
|
|
27
|
+
hi, hm = ts_max(panel.close, panel.mask, 20)
|
|
28
|
+
dist = panel.close / hi.clamp_min(1e-9) - 1.0
|
|
29
|
+
return cs_rank(dist * hm.float(), hm)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@register_legacy_factor("original_003")
|
|
33
|
+
def original_003(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
34
|
+
"""Close to 20-day low distance, ranked."""
|
|
35
|
+
lo, lm = ts_min(panel.close, panel.mask, 20)
|
|
36
|
+
dist = panel.close / lo.clamp_min(1e-9) - 1.0
|
|
37
|
+
return cs_rank(dist * lm.float(), lm)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@register_legacy_factor("original_004")
|
|
41
|
+
def original_004(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
42
|
+
"""ts_rank of close over 60 days."""
|
|
43
|
+
return ts_rank(panel.close, panel.mask, 60)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@register_legacy_factor("original_005")
|
|
47
|
+
def original_005(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
48
|
+
"""ts_rank of volume over 60 days."""
|
|
49
|
+
return ts_rank(panel.volume, panel.mask, 60)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@register_legacy_factor("original_006")
|
|
53
|
+
def original_006(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
54
|
+
"""20-day correlation of close and volume."""
|
|
55
|
+
return ts_corr(panel.close, panel.volume, panel.mask, 20)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@register_legacy_factor("original_007")
|
|
59
|
+
def original_007(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
60
|
+
"""ts_rank of close over 20 days."""
|
|
61
|
+
return ts_rank(panel.close, panel.mask, 20)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@register_legacy_factor("original_008")
|
|
65
|
+
def original_008(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
66
|
+
"""ts_rank of volume over 20 days."""
|
|
67
|
+
return ts_rank(panel.volume, panel.mask, 20)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@register_legacy_factor("original_009")
|
|
71
|
+
def original_009(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
72
|
+
"""10-day correlation of close and volume."""
|
|
73
|
+
return ts_corr(panel.close, panel.volume, panel.mask, 10)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@register_legacy_factor("original_010")
|
|
77
|
+
def original_010(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
78
|
+
"""5-day correlation of close and volume."""
|
|
79
|
+
return ts_corr(panel.close, panel.volume, panel.mask, 5)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@register_legacy_factor("original_011")
|
|
83
|
+
def original_011(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
84
|
+
"""ts_rank of close over 10 days."""
|
|
85
|
+
return ts_rank(panel.close, panel.mask, 10)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@register_legacy_factor("original_012")
|
|
89
|
+
def original_012(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
90
|
+
"""ts_rank of volume over 10 days."""
|
|
91
|
+
return ts_rank(panel.volume, panel.mask, 10)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@register_legacy_factor("original_013")
|
|
95
|
+
def original_013(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
96
|
+
"""ts_rank of close over 5 days."""
|
|
97
|
+
return ts_rank(panel.close, panel.mask, 5)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@register_legacy_factor("original_014")
|
|
101
|
+
def original_014(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
102
|
+
"""ts_rank of volume over 5 days."""
|
|
103
|
+
return ts_rank(panel.volume, panel.mask, 5)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@register_legacy_factor("original_015")
|
|
107
|
+
def original_015(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
108
|
+
"""10-day close volatility."""
|
|
109
|
+
ret, rm = delta(panel.close, panel.mask, 1)
|
|
110
|
+
ret = ret / panel.close.clamp_min(1e-9)
|
|
111
|
+
return ts_std(ret, rm, 10)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@register_legacy_factor("original_016")
|
|
115
|
+
def original_016(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
116
|
+
"""5-day close volatility."""
|
|
117
|
+
ret, rm = delta(panel.close, panel.mask, 1)
|
|
118
|
+
ret = ret / panel.close.clamp_min(1e-9)
|
|
119
|
+
return ts_std(ret, rm, 5)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@register_legacy_factor("original_017")
|
|
123
|
+
def original_017(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
124
|
+
"""Close to 10-day high distance."""
|
|
125
|
+
hi, hm = ts_max(panel.close, panel.mask, 10)
|
|
126
|
+
dist = panel.close / hi.clamp_min(1e-9) - 1.0
|
|
127
|
+
return dist * hm.float(), hm
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@register_legacy_factor("original_018")
|
|
131
|
+
def original_018(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
132
|
+
"""Close to 10-day low distance."""
|
|
133
|
+
lo, lm = ts_min(panel.close, panel.mask, 10)
|
|
134
|
+
dist = panel.close / lo.clamp_min(1e-9) - 1.0
|
|
135
|
+
return dist * lm.float(), lm
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@register_legacy_factor("original_019")
|
|
139
|
+
def original_019(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
140
|
+
"""Close to 5-day high distance."""
|
|
141
|
+
hi, hm = ts_max(panel.close, panel.mask, 5)
|
|
142
|
+
dist = panel.close / hi.clamp_min(1e-9) - 1.0
|
|
143
|
+
return dist * hm.float(), hm
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@register_legacy_factor("original_020")
|
|
147
|
+
def original_020(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
148
|
+
"""Close to 5-day low distance."""
|
|
149
|
+
lo, lm = ts_min(panel.close, panel.mask, 5)
|
|
150
|
+
dist = panel.close / lo.clamp_min(1e-9) - 1.0
|
|
151
|
+
return dist * lm.float(), lm
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@register_legacy_factor("original_021")
|
|
155
|
+
def original_021(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
156
|
+
"""10-day momentum: close[t] / close[t-10] - 1."""
|
|
157
|
+
prev, pm = delay(panel.close, panel.mask, 10)
|
|
158
|
+
out = panel.close / prev.clamp_min(1e-9) - 1.0
|
|
159
|
+
return out * pm.float(), pm
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@register_legacy_factor("original_022")
|
|
163
|
+
def original_022(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
164
|
+
"""5-day momentum: close[t] / close[t-5] - 1."""
|
|
165
|
+
prev, pm = delay(panel.close, panel.mask, 5)
|
|
166
|
+
out = panel.close / prev.clamp_min(1e-9) - 1.0
|
|
167
|
+
return out * pm.float(), pm
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@register_legacy_factor("original_023")
|
|
171
|
+
def original_023(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
172
|
+
"""3-day momentum: close[t] / close[t-3] - 1."""
|
|
173
|
+
prev, pm = delay(panel.close, panel.mask, 3)
|
|
174
|
+
out = panel.close / prev.clamp_min(1e-9) - 1.0
|
|
175
|
+
return out * pm.float(), pm
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@register_legacy_factor("original_024")
|
|
179
|
+
def original_024(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
180
|
+
"""1-day momentum: close[t] / close[t-1] - 1."""
|
|
181
|
+
prev, pm = delay(panel.close, panel.mask, 1)
|
|
182
|
+
out = panel.close / prev.clamp_min(1e-9) - 1.0
|
|
183
|
+
return out * pm.float(), pm
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@register_legacy_factor("original_025")
|
|
187
|
+
def original_025(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
188
|
+
"""cs_rank of 10-day momentum."""
|
|
189
|
+
prev, pm = delay(panel.close, panel.mask, 10)
|
|
190
|
+
mom = panel.close / prev.clamp_min(1e-9) - 1.0
|
|
191
|
+
return cs_rank(mom * pm.float(), pm)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@register_legacy_factor("original_026")
|
|
195
|
+
def original_026(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
196
|
+
"""cs_rank of 5-day momentum."""
|
|
197
|
+
prev, pm = delay(panel.close, panel.mask, 5)
|
|
198
|
+
mom = panel.close / prev.clamp_min(1e-9) - 1.0
|
|
199
|
+
return cs_rank(mom * pm.float(), pm)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@register_legacy_factor("original_027")
|
|
203
|
+
def original_027(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
204
|
+
"""cs_rank of 1-day momentum."""
|
|
205
|
+
prev, pm = delay(panel.close, panel.mask, 1)
|
|
206
|
+
mom = panel.close / prev.clamp_min(1e-9) - 1.0
|
|
207
|
+
return cs_rank(mom * pm.float(), pm)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@register_legacy_factor("original_028")
|
|
211
|
+
def original_028(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
212
|
+
"""cs_rank of 20-day momentum."""
|
|
213
|
+
prev, pm = delay(panel.close, panel.mask, 20)
|
|
214
|
+
mom = panel.close / prev.clamp_min(1e-9) - 1.0
|
|
215
|
+
return cs_rank(mom * pm.float(), pm)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""stock_* family: per-stock derived series (22 factors).
|
|
2
|
+
|
|
3
|
+
Ported from legacy Feature.py stock001-stock022.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from ..data.panel import Panel
|
|
10
|
+
from .tensor_factors import (
|
|
11
|
+
cs_rank, delay, delta, ewma, ts_corr, ts_max, ts_mean, ts_min, ts_rank, ts_std, ts_sum,
|
|
12
|
+
)
|
|
13
|
+
from .legacy_factors import register_legacy_factor, _amount
|
|
14
|
+
|
|
15
|
+
Tensor = torch.Tensor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@register_legacy_factor("stock_001")
|
|
19
|
+
def stock_001(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
20
|
+
"""Corr(Δlog(volume), (close-open)/open, 4)."""
|
|
21
|
+
log_vol = torch.log(panel.volume + 1.0)
|
|
22
|
+
d_lv, dm = delta(log_vol, panel.mask, 1)
|
|
23
|
+
intra_ret = (panel.close - panel.open) / panel.open.clamp_min(1e-9)
|
|
24
|
+
m = dm & panel.mask
|
|
25
|
+
return ts_corr(d_lv, intra_ret, m, 4)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@register_legacy_factor("stock_002")
|
|
29
|
+
def stock_002(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
30
|
+
"""Corr(Δlog(volume), (close-open)/open, 6)."""
|
|
31
|
+
log_vol = torch.log(panel.volume + 1.0)
|
|
32
|
+
d_lv, dm = delta(log_vol, panel.mask, 1)
|
|
33
|
+
intra_ret = (panel.close - panel.open) / panel.open.clamp_min(1e-9)
|
|
34
|
+
m = dm & panel.mask
|
|
35
|
+
return ts_corr(d_lv, intra_ret, m, 6)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@register_legacy_factor("stock_003")
|
|
39
|
+
def stock_003(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
40
|
+
"""rank(close - ts_max(vwap, 15)) ^ delta(close, 5)."""
|
|
41
|
+
mx, mm = ts_max(panel.vwap, panel.mask, 15)
|
|
42
|
+
diff = (panel.close - mx) * mm.float()
|
|
43
|
+
r, rm = cs_rank(diff, mm)
|
|
44
|
+
dc, dcm = delta(panel.close, panel.mask, 5)
|
|
45
|
+
m = rm & dcm
|
|
46
|
+
# Use sign-preserving power: sign(r) * |r|^|dc/close|
|
|
47
|
+
dc_norm = dc / panel.close.clamp_min(1e-9)
|
|
48
|
+
out = torch.sign(r) * r.abs().clamp_min(1e-12).pow(dc_norm.abs().clamp(0, 2))
|
|
49
|
+
return out * m.float(), m
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@register_legacy_factor("stock_004")
|
|
53
|
+
def stock_004(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
54
|
+
"""EWMA((close - mean(close,6))/mean(close,6) - delay(..., 3), alpha=1/12)."""
|
|
55
|
+
avg, am = ts_mean(panel.close, panel.mask, 6)
|
|
56
|
+
dev = (panel.close - avg) / avg.clamp_min(1e-9)
|
|
57
|
+
dev_d, dm = delay(dev, am, 3)
|
|
58
|
+
diff = (dev - dev_d) * (am & dm).float()
|
|
59
|
+
return ewma(diff, am & dm, alpha=1.0 / 12.0)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@register_legacy_factor("stock_005")
|
|
63
|
+
def stock_005(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
64
|
+
"""(close - delay(close, 6)) / delay(close, 6) * (volume + 1)."""
|
|
65
|
+
prev, pm = delay(panel.close, panel.mask, 6)
|
|
66
|
+
out = (panel.close - prev) / prev.clamp_min(1e-9) * (panel.volume + 1.0)
|
|
67
|
+
return out * pm.float(), pm
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@register_legacy_factor("stock_006")
|
|
71
|
+
def stock_006(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
72
|
+
"""(close - mean(close, 12)) / mean(close, 12)."""
|
|
73
|
+
avg, am = ts_mean(panel.close, panel.mask, 12)
|
|
74
|
+
out = (panel.close - avg) / avg.clamp_min(1e-9)
|
|
75
|
+
return out * am.float(), am
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@register_legacy_factor("stock_007")
|
|
79
|
+
def stock_007(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
80
|
+
"""(delay(ts_min(low,5), 5) - ts_min(low,5)) * rank((sum(ret,60)-sum(ret,20))/40) * rank(volume)."""
|
|
81
|
+
mn, mm = ts_min(panel.low, panel.mask, 5)
|
|
82
|
+
mn_d, md = delay(mn, mm, 5)
|
|
83
|
+
ret, rm = delta(panel.close, panel.mask, 1)
|
|
84
|
+
ret = ret / panel.close.clamp_min(1e-9)
|
|
85
|
+
s60, s60m = ts_sum(ret, rm, 60)
|
|
86
|
+
s20, s20m = ts_sum(ret, rm, 20)
|
|
87
|
+
momentum = (s60 - s20) / 40.0
|
|
88
|
+
mom_r, momm = cs_rank(momentum, s60m & s20m)
|
|
89
|
+
vol_r, volm = cs_rank(panel.volume, panel.mask)
|
|
90
|
+
m = md & momm & volm
|
|
91
|
+
out = (mn_d - mn) * mom_r * vol_r
|
|
92
|
+
return out * m.float(), m
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@register_legacy_factor("stock_008")
|
|
96
|
+
def stock_008(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
97
|
+
"""-(sum(open,5)*sum(ret,5) - delay(sum(open,5)*sum(ret,5), 10)) ranked."""
|
|
98
|
+
ret, rm = delta(panel.close, panel.mask, 1)
|
|
99
|
+
ret = ret / panel.close.clamp_min(1e-9)
|
|
100
|
+
so, som = ts_sum(panel.open, panel.mask, 5)
|
|
101
|
+
sr, srm = ts_sum(ret, rm, 5)
|
|
102
|
+
prod = so * sr
|
|
103
|
+
m_prod = som & srm
|
|
104
|
+
prod_d, pdm = delay(prod, m_prod, 10)
|
|
105
|
+
diff = prod - prod_d
|
|
106
|
+
m = m_prod & pdm
|
|
107
|
+
r, rm2 = cs_rank(-diff * m.float(), m)
|
|
108
|
+
return r, rm2
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@register_legacy_factor("stock_009")
|
|
112
|
+
def stock_009(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
113
|
+
"""(mean(close,3)+mean(close,6)+mean(close,12)+mean(close,24)) / (4*close)."""
|
|
114
|
+
m3, mm3 = ts_mean(panel.close, panel.mask, 3)
|
|
115
|
+
m6, mm6 = ts_mean(panel.close, panel.mask, 6)
|
|
116
|
+
m12, mm12 = ts_mean(panel.close, panel.mask, 12)
|
|
117
|
+
m24, mm24 = ts_mean(panel.close, panel.mask, 24)
|
|
118
|
+
m = mm3 & mm6 & mm12 & mm24
|
|
119
|
+
out = (m3 + m6 + m12 + m24) * 0.25 / panel.close.clamp_min(1e-9)
|
|
120
|
+
return out * m.float(), m
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@register_legacy_factor("stock_010")
|
|
124
|
+
def stock_010(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
125
|
+
"""std(log(amount+1), 6)."""
|
|
126
|
+
amt = _amount(panel)
|
|
127
|
+
log_amt = torch.log(amt + 1.0)
|
|
128
|
+
return ts_std(log_amt, panel.mask, 6)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@register_legacy_factor("stock_011")
|
|
132
|
+
def stock_011(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
133
|
+
"""CCI-like: (typical - mean(typical,12)) / (0.015 * mean(|close - mean(typical,12)|, 12))."""
|
|
134
|
+
typical = (panel.high + panel.low + panel.close) / 3.0
|
|
135
|
+
avg_t, am = ts_mean(typical, panel.mask, 12)
|
|
136
|
+
dev = (panel.close - avg_t).abs()
|
|
137
|
+
avg_dev, adm = ts_mean(dev, am, 12)
|
|
138
|
+
m = am & adm
|
|
139
|
+
out = (typical - avg_t) / (0.015 * avg_dev.clamp_min(1e-9))
|
|
140
|
+
return out * m.float(), m
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@register_legacy_factor("stock_012")
|
|
144
|
+
def stock_012(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
145
|
+
"""RSI-like: ewma(max(Δvol, 0), 1/6) / ewma(|Δvol|, 1/6) * 100."""
|
|
146
|
+
dv, dm = delta(panel.volume, panel.mask, 1)
|
|
147
|
+
pos = torch.clamp(dv, min=0.0)
|
|
148
|
+
abs_dv = dv.abs()
|
|
149
|
+
ew_pos, m1 = ewma(pos, dm, alpha=1.0 / 6.0)
|
|
150
|
+
ew_abs, m2 = ewma(abs_dv, dm, alpha=1.0 / 6.0)
|
|
151
|
+
m = m1 & m2
|
|
152
|
+
out = ew_pos * 100.0 / ew_abs.clamp_min(1e-9)
|
|
153
|
+
return out * m.float(), m
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@register_legacy_factor("stock_013")
|
|
157
|
+
def stock_013(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
158
|
+
"""Δ(vwap - close) / Δ(vwap + close)."""
|
|
159
|
+
diff = panel.vwap - panel.close
|
|
160
|
+
summ = panel.vwap + panel.close
|
|
161
|
+
d_diff, dm1 = delta(diff, panel.mask, 1)
|
|
162
|
+
d_sum, dm2 = delta(summ, panel.mask, 1)
|
|
163
|
+
m = dm1 & dm2
|
|
164
|
+
out = d_diff / d_sum.abs().clamp_min(1e-9)
|
|
165
|
+
return out * m.float(), m
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@register_legacy_factor("stock_014")
|
|
169
|
+
def stock_014(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
170
|
+
"""(close / delay(close, 12) - 1) * volume."""
|
|
171
|
+
prev, pm = delay(panel.close, panel.mask, 12)
|
|
172
|
+
out = (panel.close / prev.clamp_min(1e-9) - 1.0) * panel.volume
|
|
173
|
+
return out * pm.float(), pm
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@register_legacy_factor("stock_015")
|
|
177
|
+
def stock_015(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
178
|
+
"""Conditional down-vol: sum(|ret|/log(amt+1) * (close<delay(close,1)), 20) / count(down, 20)."""
|
|
179
|
+
prev, pm = delay(panel.close, panel.mask, 1)
|
|
180
|
+
ret_abs = (panel.close / prev.clamp_min(1e-9) - 1.0).abs()
|
|
181
|
+
amt = _amount(panel)
|
|
182
|
+
log_amt = torch.log(amt + 1.0).clamp_min(1e-9)
|
|
183
|
+
down = (panel.close < prev).float() * pm.float()
|
|
184
|
+
signal = ret_abs / log_amt * down
|
|
185
|
+
s_sum, sm = ts_sum(signal, pm, 20)
|
|
186
|
+
d_count, cm = ts_sum(down, pm, 20)
|
|
187
|
+
m = sm & cm
|
|
188
|
+
out = s_sum / d_count.clamp_min(1.0)
|
|
189
|
+
return out * m.float(), m
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@register_legacy_factor("stock_016")
|
|
193
|
+
def stock_016(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
194
|
+
"""Williams %R multi-timeframe composite."""
|
|
195
|
+
prev, pm = delay(panel.close, panel.mask, 1)
|
|
196
|
+
true_low = torch.min(panel.low, prev)
|
|
197
|
+
true_high = torch.max(panel.high, prev)
|
|
198
|
+
# 6-day
|
|
199
|
+
sl6, slm6 = ts_sum(true_low, pm, 6)
|
|
200
|
+
sh6, shm6 = ts_sum(true_high - true_low, pm, 6)
|
|
201
|
+
p1 = (panel.close - sl6) / sh6.clamp_min(1e-9) * 12 * 24
|
|
202
|
+
# 12-day
|
|
203
|
+
sl12, slm12 = ts_sum(true_low, pm, 12)
|
|
204
|
+
sh12, shm12 = ts_sum(true_high - true_low, pm, 12)
|
|
205
|
+
p2 = (panel.close - sl12) / sh12.clamp_min(1e-9) * 6 * 24
|
|
206
|
+
# 24-day
|
|
207
|
+
sl24, slm24 = ts_sum(true_low, pm, 24)
|
|
208
|
+
sh24, shm24 = ts_sum(true_high - true_low, pm, 24)
|
|
209
|
+
p3 = (panel.close - sl24) / sh24.clamp_min(1e-9) * 6 * 12
|
|
210
|
+
m = slm6 & slm12 & slm24
|
|
211
|
+
out = (p1 + p2 + p3) * 100.0 / (6*12 + 6*24 + 12*24)
|
|
212
|
+
return out * m.float(), m
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@register_legacy_factor("stock_017")
|
|
216
|
+
def stock_017(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
217
|
+
"""-ret * mean(volume, 20) * vwap * (high - close)."""
|
|
218
|
+
ret, rm = delta(panel.close, panel.mask, 1)
|
|
219
|
+
ret = ret / panel.close.clamp_min(1e-9)
|
|
220
|
+
avg_v, am = ts_mean(panel.volume, panel.mask, 20)
|
|
221
|
+
m = rm & am
|
|
222
|
+
out = -ret * avg_v * panel.vwap * (panel.high - panel.close)
|
|
223
|
+
return out * m.float(), m
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@register_legacy_factor("stock_018")
|
|
227
|
+
def stock_018(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
228
|
+
"""5-day close return minus 20-day close return."""
|
|
229
|
+
c5, m5 = delay(panel.close, panel.mask, 5)
|
|
230
|
+
c20, m20 = delay(panel.close, panel.mask, 20)
|
|
231
|
+
r5 = (panel.close - c5) / c5.clamp_min(1e-9)
|
|
232
|
+
r20 = (panel.close - c20) / c20.clamp_min(1e-9)
|
|
233
|
+
m = m5 & m20
|
|
234
|
+
return (r5 - r20) * m.float(), m
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@register_legacy_factor("stock_019")
|
|
238
|
+
def stock_019(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
239
|
+
"""(low - close) * open^5 / ((close - high) * close^5)."""
|
|
240
|
+
num = -(panel.low - panel.close) * panel.open.pow(5)
|
|
241
|
+
den = (panel.close - panel.high) * panel.close.pow(5)
|
|
242
|
+
out = num / den.abs().clamp_min(1e-12).copysign(den + 1e-12)
|
|
243
|
+
return out * panel.mask.float(), panel.mask
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@register_legacy_factor("stock_020")
|
|
247
|
+
def stock_020(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
248
|
+
"""(close - delay(close,1)) / delay(close,1) * volume."""
|
|
249
|
+
prev, pm = delay(panel.close, panel.mask, 1)
|
|
250
|
+
out = (panel.close - prev) / prev.clamp_min(1e-9) * panel.volume
|
|
251
|
+
return out * pm.float(), pm
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@register_legacy_factor("stock_021")
|
|
255
|
+
def stock_021(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
256
|
+
"""((high-low) - ewma(high-low, 2/11)) / ewma(high-low, 2/11) * 100."""
|
|
257
|
+
rng = panel.high - panel.low
|
|
258
|
+
sma, sm = ewma(rng, panel.mask, alpha=2.0 / 11.0)
|
|
259
|
+
out = (rng - sma) / sma.clamp_min(1e-9) * 100.0
|
|
260
|
+
return out * sm.float(), sm
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@register_legacy_factor("stock_022")
|
|
264
|
+
def stock_022(panel: Panel) -> Tuple[Tensor, Tensor]:
|
|
265
|
+
"""corr(mean(volume,20), low, 5) + (high+low)/2 - close."""
|
|
266
|
+
avg_v, am = ts_mean(panel.volume, panel.mask, 20)
|
|
267
|
+
corr, cm = ts_corr(avg_v, panel.low, am, 5)
|
|
268
|
+
m = am & cm
|
|
269
|
+
out = corr + (panel.high + panel.low) / 2.0 - panel.close
|
|
270
|
+
return out * m.float(), m
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Backward-compatible Alpha101 interface.
|
|
2
|
+
|
|
3
|
+
The 9 curated Alpha101 factors are now registered in the unified
|
|
4
|
+
:data:`LEGACY_REGISTRY` (via ``_factors_alpha101.py``) alongside all
|
|
5
|
+
other factor families. This module provides the legacy
|
|
6
|
+
``compute_alpha_set`` / ``ALPHA_REGISTRY`` API for backward
|
|
7
|
+
compatibility.
|
|
8
|
+
|
|
9
|
+
The canonical way to compute all 213 factors is::
|
|
10
|
+
|
|
11
|
+
from mlquant.features import compute_legacy_set
|
|
12
|
+
factors, mask, names = compute_legacy_set(panel)
|
|
13
|
+
|
|
14
|
+
To compute only the Alpha101 subset::
|
|
15
|
+
|
|
16
|
+
from mlquant.features import compute_legacy_set
|
|
17
|
+
alpha_names = tuple(n for n in LEGACY_REGISTRY if n.startswith("alpha_"))
|
|
18
|
+
factors, mask, names = compute_legacy_set(panel, names=alpha_names)
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Dict, Tuple
|
|
23
|
+
|
|
24
|
+
import torch
|
|
25
|
+
|
|
26
|
+
from ..data.panel import Panel
|
|
27
|
+
from .tensor_factors import cs_zscore
|
|
28
|
+
from .legacy_factors import LEGACY_REGISTRY
|
|
29
|
+
|
|
30
|
+
Tensor = torch.Tensor
|
|
31
|
+
|
|
32
|
+
# Expose the alpha_* subset as ALPHA_REGISTRY for backward compat
|
|
33
|
+
ALPHA_REGISTRY: Dict[str, object] = {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _populate_alpha_registry() -> None:
|
|
37
|
+
"""Lazily populate ALPHA_REGISTRY from LEGACY_REGISTRY alpha_* entries."""
|
|
38
|
+
if ALPHA_REGISTRY:
|
|
39
|
+
return
|
|
40
|
+
for name, fn in LEGACY_REGISTRY.items():
|
|
41
|
+
if name.startswith("alpha_"):
|
|
42
|
+
# Map back to old naming convention (alpha_001 -> alpha001)
|
|
43
|
+
old_name = name.replace("alpha_", "alpha")
|
|
44
|
+
ALPHA_REGISTRY[old_name] = fn
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def compute_alpha_set(
|
|
48
|
+
panel: Panel,
|
|
49
|
+
*,
|
|
50
|
+
names: tuple[str, ...] | None = None,
|
|
51
|
+
neutralize: bool = True,
|
|
52
|
+
) -> Tuple[Tensor, Tensor, list[str]]:
|
|
53
|
+
"""Compute the Alpha101 subset → tensor of shape ``[T, N, F]``.
|
|
54
|
+
|
|
55
|
+
This is a backward-compatible wrapper. Prefer
|
|
56
|
+
:func:`~mlquant.features.legacy_factors.compute_legacy_set` for new
|
|
57
|
+
code.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
panel : Panel
|
|
62
|
+
names : tuple of str, optional
|
|
63
|
+
Subset of alpha names (old style: "alpha001", "alpha002", ...).
|
|
64
|
+
``None`` → all 9 registered alphas.
|
|
65
|
+
neutralize : bool
|
|
66
|
+
If True, each factor is cross-sectionally z-scored.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
factors : Tensor [T, N, F]
|
|
71
|
+
mask : Tensor [T, N]
|
|
72
|
+
names : list[str]
|
|
73
|
+
"""
|
|
74
|
+
_populate_alpha_registry()
|
|
75
|
+
names = names or tuple(ALPHA_REGISTRY)
|
|
76
|
+
cols, joint_mask = [], None
|
|
77
|
+
for n in names:
|
|
78
|
+
v, m = ALPHA_REGISTRY[n](panel)
|
|
79
|
+
if neutralize:
|
|
80
|
+
v, _ = cs_zscore(v, m)
|
|
81
|
+
cols.append(v)
|
|
82
|
+
joint_mask = m if joint_mask is None else joint_mask & m
|
|
83
|
+
factors = torch.stack(cols, dim=-1)
|
|
84
|
+
return factors, joint_mask, list(names)
|
mlquant/features/bias.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Bias-correction utilities for A-share microstructure quirks.
|
|
2
|
+
|
|
3
|
+
The two most common look-ahead biases in A-share alpha research are:
|
|
4
|
+
|
|
5
|
+
1. **Limit-up days** — when a stock closes at the +10 % cap, the close
|
|
6
|
+
you see *did not actually fill any unsatisfied buy orders*. Treating
|
|
7
|
+
such a day as tradable inflates returns.
|
|
8
|
+
2. **Limit-down days** — symmetric problem on the downside.
|
|
9
|
+
|
|
10
|
+
Both must be excluded from training labels and from the tradable
|
|
11
|
+
universe at portfolio construction time.
|
|
12
|
+
|
|
13
|
+
Two regimes
|
|
14
|
+
-----------
|
|
15
|
+
* **Real limits available** (``Panel.limit_up`` / ``Panel.limit_down`` set):
|
|
16
|
+
use the exchange-published prices — this is the regime real Wind
|
|
17
|
+
data lands in. A cell is masked if ``close >= limit_up - eps`` or
|
|
18
|
+
``close <= limit_down + eps``.
|
|
19
|
+
* **Proxy regime** (synthetic data, or any feed without explicit limits):
|
|
20
|
+
fall back to the ``|return| > limit_pct`` heuristic, which is the
|
|
21
|
+
unambiguous signature of a limit move on most A-shares.
|
|
22
|
+
|
|
23
|
+
Both code paths are exercised by the test suite via the synthetic
|
|
24
|
+
generator (which fills both real and derived fields) so this module
|
|
25
|
+
behaves identically on synthetic and real feeds.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import torch
|
|
30
|
+
|
|
31
|
+
from ..data.panel import Panel
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def limit_move_mask(
|
|
35
|
+
panel: Panel,
|
|
36
|
+
*,
|
|
37
|
+
limit_pct: float = 0.098,
|
|
38
|
+
eps: float = 1e-3,
|
|
39
|
+
) -> torch.Tensor:
|
|
40
|
+
"""Boolean mask, ``True`` where the day's price is **inside** the limit band.
|
|
41
|
+
|
|
42
|
+
When :attr:`Panel.has_real_limits` is True we use the exchange-published
|
|
43
|
+
``limit_up`` / ``limit_down`` prices and require ``limit_down + eps <
|
|
44
|
+
close < limit_up - eps``. Otherwise we fall back to the
|
|
45
|
+
``|return| <= limit_pct`` proxy.
|
|
46
|
+
|
|
47
|
+
Day 0 is always masked False because no previous close is available
|
|
48
|
+
to evaluate the return proxy. (Real-limit cells on day 0 are still
|
|
49
|
+
valid but we follow the legacy convention of masking the very first
|
|
50
|
+
row to keep the two regimes aligned.)
|
|
51
|
+
"""
|
|
52
|
+
if panel.has_real_limits:
|
|
53
|
+
up = panel.limit_up.clamp_min(0.0)
|
|
54
|
+
down = panel.limit_down.clamp_min(0.0)
|
|
55
|
+
# Treat zero/missing limit prices as "no limit" (fall through).
|
|
56
|
+
has_band = (up > 0) & (down > 0)
|
|
57
|
+
# Inside the band on every cell that has one, plus a tolerance
|
|
58
|
+
# so float round-off doesn't trip the comparison.
|
|
59
|
+
inside_band = (panel.close < up - eps) & (panel.close > down + eps)
|
|
60
|
+
# Where the band is missing, defer to the return-proxy rule.
|
|
61
|
+
ret_proxy = _return_inside(panel, limit_pct)
|
|
62
|
+
inside = torch.where(has_band, inside_band, ret_proxy)
|
|
63
|
+
else:
|
|
64
|
+
inside = _return_inside(panel, limit_pct)
|
|
65
|
+
|
|
66
|
+
inside[0] = False # day 0 has no previous close
|
|
67
|
+
return inside & panel.mask
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _return_inside(panel: Panel, limit_pct: float) -> torch.Tensor:
|
|
71
|
+
"""``|return| <= limit_pct`` proxy mask."""
|
|
72
|
+
if panel.last_close is not None:
|
|
73
|
+
prev = panel.last_close.clamp_min(1e-9)
|
|
74
|
+
else:
|
|
75
|
+
prev = torch.roll(panel.close, shifts=1, dims=0).clamp_min(1e-9)
|
|
76
|
+
prev[0] = panel.close[0]
|
|
77
|
+
move = (panel.close / prev) - 1.0
|
|
78
|
+
return move.abs() <= limit_pct
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["limit_move_mask"]
|