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.
Files changed (54) hide show
  1. mlquant/__init__.py +18 -0
  2. mlquant/backtest/__init__.py +14 -0
  3. mlquant/backtest/engine.py +118 -0
  4. mlquant/backtest/metrics.py +95 -0
  5. mlquant/cli/__init__.py +1 -0
  6. mlquant/cli/main.py +292 -0
  7. mlquant/configs/__init__.py +1 -0
  8. mlquant/configs/small.yaml +41 -0
  9. mlquant/data/__init__.py +88 -0
  10. mlquant/data/akshare_loader.py +168 -0
  11. mlquant/data/baostock_loader.py +137 -0
  12. mlquant/data/loaders.py +125 -0
  13. mlquant/data/panel.py +206 -0
  14. mlquant/data/synthetic.py +138 -0
  15. mlquant/data/yfinance_loader.py +101 -0
  16. mlquant/features/__init__.py +42 -0
  17. mlquant/features/_factors_add.py +288 -0
  18. mlquant/features/_factors_alpha101.py +98 -0
  19. mlquant/features/_factors_best.py +177 -0
  20. mlquant/features/_factors_better.py +385 -0
  21. mlquant/features/_factors_change.py +53 -0
  22. mlquant/features/_factors_extra.py +147 -0
  23. mlquant/features/_factors_market.py +61 -0
  24. mlquant/features/_factors_old.py +563 -0
  25. mlquant/features/_factors_original.py +215 -0
  26. mlquant/features/_factors_stock.py +270 -0
  27. mlquant/features/alpha101.py +84 -0
  28. mlquant/features/bias.py +81 -0
  29. mlquant/features/legacy_factors.py +126 -0
  30. mlquant/features/market_breadth.py +190 -0
  31. mlquant/features/neutralize.py +75 -0
  32. mlquant/features/target.py +62 -0
  33. mlquant/features/tensor_factors.py +301 -0
  34. mlquant/features.py +613 -0
  35. mlquant/models/__init__.py +8 -0
  36. mlquant/models/losses.py +88 -0
  37. mlquant/models/nets.py +62 -0
  38. mlquant/portfolio/__init__.py +5 -0
  39. mlquant/portfolio/frontier.py +49 -0
  40. mlquant/portfolio/markowitz.py +144 -0
  41. mlquant/training/__init__.py +6 -0
  42. mlquant/training/augment.py +89 -0
  43. mlquant/training/dataset.py +50 -0
  44. mlquant/training/trainer.py +108 -0
  45. mlquant/utils/__init__.py +5 -0
  46. mlquant/utils/config.py +60 -0
  47. mlquant/utils/seed.py +35 -0
  48. mlquantx-0.2.5.dist-info/METADATA +108 -0
  49. mlquantx-0.2.5.dist-info/RECORD +54 -0
  50. mlquantx-0.2.5.dist-info/WHEEL +5 -0
  51. mlquantx-0.2.5.dist-info/entry_points.txt +2 -0
  52. mlquantx-0.2.5.dist-info/licenses/LICENSE +21 -0
  53. mlquantx-0.2.5.dist-info/licenses/NOTICE.md +5 -0
  54. 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)
@@ -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"]