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,177 @@
1
+ """best_* family: close-location momentum over varying windows (21 factors).
2
+
3
+ Legacy template: -(2*C - H - L) / (H - L + eps) with various transforms.
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_sum,
12
+ )
13
+ from .legacy_factors import register_legacy_factor, _close_loc, _ts_mean_close_loc, _amount
14
+
15
+ Tensor = torch.Tensor
16
+
17
+
18
+ @register_legacy_factor("best_001")
19
+ def best_001(panel: Panel) -> Tuple[Tensor, Tensor]:
20
+ """ts_rank of -close_loc over 5 days."""
21
+ cl = -_close_loc(panel)
22
+ return ts_rank(cl, panel.mask, 5)
23
+
24
+
25
+ @register_legacy_factor("best_002")
26
+ def best_002(panel: Panel) -> Tuple[Tensor, Tensor]:
27
+ """ts_rank of -close_loc over 10 days."""
28
+ cl = -_close_loc(panel)
29
+ return ts_rank(cl, panel.mask, 10)
30
+
31
+
32
+ @register_legacy_factor("best_003")
33
+ def best_003(panel: Panel) -> Tuple[Tensor, Tensor]:
34
+ """ts_rank of -close_loc over 20 days."""
35
+ cl = -_close_loc(panel)
36
+ return ts_rank(cl, panel.mask, 20)
37
+
38
+
39
+ @register_legacy_factor("best_004")
40
+ def best_004(panel: Panel) -> Tuple[Tensor, Tensor]:
41
+ """Delta(close_loc, 3)."""
42
+ cl = -_close_loc(panel)
43
+ return delta(cl, panel.mask, 3)
44
+
45
+
46
+ @register_legacy_factor("best_005")
47
+ def best_005(panel: Panel) -> Tuple[Tensor, Tensor]:
48
+ """Delta(close_loc, 5)."""
49
+ cl = -_close_loc(panel)
50
+ return delta(cl, panel.mask, 5)
51
+
52
+
53
+ @register_legacy_factor("best_006")
54
+ def best_006(panel: Panel) -> Tuple[Tensor, Tensor]:
55
+ """EWMA of -close_loc, alpha=1/10."""
56
+ cl = -_close_loc(panel)
57
+ return ewma(cl, panel.mask, alpha=1.0 / 10.0)
58
+
59
+
60
+ @register_legacy_factor("best_007")
61
+ def best_007(panel: Panel) -> Tuple[Tensor, Tensor]:
62
+ """Cross-sectional rank of -close_loc."""
63
+ cl = -_close_loc(panel)
64
+ return cs_rank(cl, panel.mask)
65
+
66
+
67
+ @register_legacy_factor("best_008")
68
+ def best_008(panel: Panel) -> Tuple[Tensor, Tensor]:
69
+ """ts_mean(vwap / close, 3)."""
70
+ ratio = panel.vwap / panel.close.clamp_min(1e-9)
71
+ return ts_mean(ratio, panel.mask, 3)
72
+
73
+
74
+ @register_legacy_factor("best_009")
75
+ def best_009(panel: Panel) -> Tuple[Tensor, Tensor]:
76
+ """EWMA(vwap, 5) / EWMA(close, 5)."""
77
+ ew_vwap, m1 = ewma(panel.vwap, panel.mask, alpha=1.0 / 5.0)
78
+ ew_close, m2 = ewma(panel.close, panel.mask, alpha=1.0 / 5.0)
79
+ m = m1 & m2
80
+ out = ew_vwap / ew_close.clamp_min(1e-9)
81
+ return out * m.float(), m
82
+
83
+
84
+ @register_legacy_factor("best_010")
85
+ def best_010(panel: Panel) -> Tuple[Tensor, Tensor]:
86
+ """(vwap / close - 1) * volume."""
87
+ out = (panel.vwap / panel.close.clamp_min(1e-9) - 1.0) * panel.volume
88
+ return out * panel.mask.float(), panel.mask
89
+
90
+
91
+ @register_legacy_factor("best_011")
92
+ def best_011(panel: Panel) -> Tuple[Tensor, Tensor]:
93
+ """cs_rank(open / delay(close, 1) - 1)."""
94
+ prev, pm = delay(panel.close, panel.mask, 1)
95
+ gap = panel.open / prev.clamp_min(1e-9) - 1.0
96
+ return cs_rank(gap * pm.float(), pm)
97
+
98
+
99
+ @register_legacy_factor("best_012")
100
+ def best_012(panel: Panel) -> Tuple[Tensor, Tensor]:
101
+ """ts_mean(open / delay(close, 1) - 1, 5)."""
102
+ prev, pm = delay(panel.close, panel.mask, 1)
103
+ gap = (panel.open / prev.clamp_min(1e-9) - 1.0) * pm.float()
104
+ return ts_mean(gap, pm, 5)
105
+
106
+
107
+ @register_legacy_factor("best_013")
108
+ def best_013(panel: Panel) -> Tuple[Tensor, Tensor]:
109
+ """EWMA(open / delay(close, 1) - 1, alpha=1/5)."""
110
+ prev, pm = delay(panel.close, panel.mask, 1)
111
+ gap = (panel.open / prev.clamp_min(1e-9) - 1.0) * pm.float()
112
+ return ewma(gap, pm, alpha=1.0 / 5.0)
113
+
114
+
115
+ @register_legacy_factor("best_014")
116
+ def best_014(panel: Panel) -> Tuple[Tensor, Tensor]:
117
+ """Correlation of ts_rank(volume, 7) and ts_rank(range/close, 7) over 5 days."""
118
+ vol_r, vm = ts_rank(panel.volume, panel.mask, 7)
119
+ rng = (panel.high - panel.low) / panel.close.clamp_min(1e-9)
120
+ rng_r, rm = ts_rank(rng, panel.mask, 7)
121
+ m = vm & rm
122
+ return ts_corr(rng_r, vol_r, m, 5)
123
+
124
+
125
+ @register_legacy_factor("best_015")
126
+ def best_015(panel: Panel) -> Tuple[Tensor, Tensor]:
127
+ """cs_rank((high - low) / close)."""
128
+ rng = (panel.high - panel.low) / panel.close.clamp_min(1e-9)
129
+ return cs_rank(rng, panel.mask)
130
+
131
+
132
+ @register_legacy_factor("best_016")
133
+ def best_016(panel: Panel) -> Tuple[Tensor, Tensor]:
134
+ """(high - low) / close / (1 + sqrt(volume))."""
135
+ rng = (panel.high - panel.low) / panel.close.clamp_min(1e-9)
136
+ out = rng / (1.0 + panel.volume.clamp_min(0).sqrt())
137
+ return out * panel.mask.float(), panel.mask
138
+
139
+
140
+ @register_legacy_factor("best_017")
141
+ def best_017(panel: Panel) -> Tuple[Tensor, Tensor]:
142
+ """Delta(EWMA(close, alpha=1/5), 1) - moving average breakout."""
143
+ ew, em = ewma(panel.close, panel.mask, alpha=1.0 / 5.0)
144
+ return delta(ew, em, 1)
145
+
146
+
147
+ @register_legacy_factor("best_018")
148
+ def best_018(panel: Panel) -> Tuple[Tensor, Tensor]:
149
+ """Delta(EWMA(close, alpha=1/10), 1)."""
150
+ ew, em = ewma(panel.close, panel.mask, alpha=1.0 / 10.0)
151
+ return delta(ew, em, 1)
152
+
153
+
154
+ @register_legacy_factor("best_019")
155
+ def best_019(panel: Panel) -> Tuple[Tensor, Tensor]:
156
+ """Delta(EWMA(close, alpha=1/20), 1)."""
157
+ ew, em = ewma(panel.close, panel.mask, alpha=1.0 / 20.0)
158
+ return delta(ew, em, 1)
159
+
160
+
161
+ @register_legacy_factor("best_020")
162
+ def best_020(panel: Panel) -> Tuple[Tensor, Tensor]:
163
+ """EWMA((close - vwap) * volume, alpha=1/5) - money flow."""
164
+ x = (panel.close - panel.vwap) * panel.volume
165
+ return ewma(x, panel.mask, alpha=1.0 / 5.0)
166
+
167
+
168
+ @register_legacy_factor("best_021")
169
+ def best_021(panel: Panel) -> Tuple[Tensor, Tensor]:
170
+ """max((vwap-close)*vol, 3) + min((vwap-close)*vol, 3) * delta(volume, 3)."""
171
+ spread = (panel.vwap - panel.close) * panel.volume
172
+ mx, mm1 = ts_max(spread, panel.mask, 3)
173
+ mn, mm2 = ts_min(spread, panel.mask, 3)
174
+ dv, dm = delta(panel.volume, panel.mask, 3)
175
+ m = mm1 & mm2 & dm
176
+ out = (mx + mn * dv) * m.float()
177
+ return out, m
@@ -0,0 +1,385 @@
1
+ """better_* family: vwap-based alpha signals (28 factors).
2
+
3
+ Ported from legacy cuda_features.py better_001-better_028.
4
+ These factors heavily use vwap (S_DQ_AVGPRICE) as a reference price.
5
+ """
6
+ from __future__ import annotations
7
+ from typing import Tuple
8
+ import torch
9
+
10
+ from ..data.panel import Panel
11
+ from .tensor_factors import (
12
+ cs_rank, delay, delta, ewma, ts_corr, ts_max, ts_mean, ts_min,
13
+ ts_rank, ts_std, ts_sum,
14
+ )
15
+ from .legacy_factors import register_legacy_factor, _close_loc, _amount
16
+
17
+ Tensor = torch.Tensor
18
+
19
+
20
+ @register_legacy_factor("better_001")
21
+ def better_001(panel: Panel) -> Tuple[Tensor, Tensor]:
22
+ """(open/delay(close,1) - 1) * log2(volume + 1)."""
23
+ prev, pm = delay(panel.close, panel.mask, 1)
24
+ gap = panel.open / prev.clamp_min(1e-9) - 1.0
25
+ out = gap * torch.log2(panel.volume + 1.0)
26
+ m = pm & panel.mask
27
+ return out * m.float(), m
28
+
29
+
30
+ @register_legacy_factor("better_002")
31
+ def better_002(panel: Panel) -> Tuple[Tensor, Tensor]:
32
+ """(open/delay(close,1) - 1) * volume / mean(volume, 5)."""
33
+ prev, pm = delay(panel.close, panel.mask, 1)
34
+ gap = panel.open / prev.clamp_min(1e-9) - 1.0
35
+ avg, am = ts_mean(panel.volume, panel.mask, 5)
36
+ m = pm & am & panel.mask
37
+ out = gap * panel.volume / avg.clamp_min(1.0)
38
+ return out * m.float(), m
39
+
40
+
41
+ @register_legacy_factor("better_003")
42
+ def better_003(panel: Panel) -> Tuple[Tensor, Tensor]:
43
+ """(open/delay(close,1)-1) * (vwap-close) * (high-low) / close."""
44
+ prev, pm = delay(panel.close, panel.mask, 1)
45
+ gap = panel.open / prev.clamp_min(1e-9) - 1.0
46
+ spread = (panel.vwap - panel.close) * (panel.high - panel.low) / panel.close.clamp_min(1e-9)
47
+ m = pm & panel.mask
48
+ return (gap * spread) * m.float(), m
49
+
50
+
51
+ @register_legacy_factor("better_004")
52
+ def better_004(panel: Panel) -> Tuple[Tensor, Tensor]:
53
+ """-(vwap_loc - delay(vwap_loc, 1)) where vwap_loc = (2*vwap-low-high)/(high-low)."""
54
+ rng = (panel.high - panel.low).clamp_min(1e-9)
55
+ vwap_loc = (2.0 * panel.vwap - panel.low - panel.high) / rng
56
+ prev, pm = delay(vwap_loc, panel.mask, 1)
57
+ out = -(vwap_loc - prev)
58
+ return out * pm.float(), pm
59
+
60
+
61
+ @register_legacy_factor("better_005")
62
+ def better_005(panel: Panel) -> Tuple[Tensor, Tensor]:
63
+ """-(vwap_loc - delay(vwap_loc, 5))."""
64
+ rng = (panel.high - panel.low).clamp_min(1e-9)
65
+ vwap_loc = (2.0 * panel.vwap - panel.low - panel.high) / rng
66
+ prev, pm = delay(vwap_loc, panel.mask, 5)
67
+ out = -(vwap_loc - prev)
68
+ return out * pm.float(), pm
69
+
70
+
71
+ @register_legacy_factor("better_006")
72
+ def better_006(panel: Panel) -> Tuple[Tensor, Tensor]:
73
+ """mean((2*vwap - low - high) / (high - low), 5)."""
74
+ rng = (panel.high - panel.low).clamp_min(1e-9)
75
+ vwap_loc = (2.0 * panel.vwap - panel.low - panel.high) / rng
76
+ return ts_mean(vwap_loc, panel.mask, 5)
77
+
78
+
79
+ @register_legacy_factor("better_007")
80
+ def better_007(panel: Panel) -> Tuple[Tensor, Tensor]:
81
+ """(high-low)/close * (volume - delay(volume, 1))."""
82
+ rng = (panel.high - panel.low) / panel.close.clamp_min(1e-9)
83
+ dv, dm = delta(panel.volume, panel.mask, 1)
84
+ m = dm & panel.mask
85
+ return (rng * dv) * m.float(), m
86
+
87
+
88
+ @register_legacy_factor("better_008")
89
+ def better_008(panel: Panel) -> Tuple[Tensor, Tensor]:
90
+ """(high-low)/close / log2(volume + 1)."""
91
+ rng = (panel.high - panel.low) / panel.close.clamp_min(1e-9)
92
+ out = rng / torch.log2(panel.volume + 1.0).clamp_min(1e-9)
93
+ return out * panel.mask.float(), panel.mask
94
+
95
+
96
+ @register_legacy_factor("better_009")
97
+ def better_009(panel: Panel) -> Tuple[Tensor, Tensor]:
98
+ """Conditional sum: if vwap[t]<vwap[t-1]: |vwap/delay(vwap,1)-1|/log(amount+1), window=20."""
99
+ prev, pm = delay(panel.vwap, panel.mask, 1)
100
+ ratio = (panel.vwap / prev.clamp_min(1e-9) - 1.0).abs()
101
+ amt = _amount(panel)
102
+ signal = ratio / torch.log(amt + 1.0).clamp_min(1e-9)
103
+ cond = (panel.vwap < prev).float() * pm.float()
104
+ weighted = signal * cond
105
+ s, sm = ts_sum(weighted, pm, 20)
106
+ cnt, cm = ts_sum(cond, pm, 20)
107
+ m = sm & cm
108
+ out = s / cnt.clamp_min(1.0)
109
+ return out * m.float(), m
110
+
111
+
112
+ @register_legacy_factor("better_010")
113
+ def better_010(panel: Panel) -> Tuple[Tensor, Tensor]:
114
+ """Same as better_009 but window=10."""
115
+ prev, pm = delay(panel.vwap, panel.mask, 1)
116
+ ratio = (panel.vwap / prev.clamp_min(1e-9) - 1.0).abs()
117
+ amt = _amount(panel)
118
+ signal = ratio / torch.log(amt + 1.0).clamp_min(1e-9)
119
+ cond = (panel.vwap < prev).float() * pm.float()
120
+ weighted = signal * cond
121
+ s, sm = ts_sum(weighted, pm, 10)
122
+ cnt, cm = ts_sum(cond, pm, 10)
123
+ m = sm & cm
124
+ out = s / cnt.clamp_min(1.0)
125
+ return out * m.float(), m
126
+
127
+
128
+ @register_legacy_factor("better_011")
129
+ def better_011(panel: Panel) -> Tuple[Tensor, Tensor]:
130
+ """Multi-period support/resistance using min(low, delay(low,1)) and max(high, delay(high,1))."""
131
+ prev_l, plm = delay(panel.low, panel.mask, 1)
132
+ prev_h, phm = delay(panel.high, panel.mask, 1)
133
+ m_base = plm & phm
134
+ lo = torch.minimum(panel.low, prev_l)
135
+ hi = torch.maximum(panel.high, prev_h)
136
+ rng = (hi - lo).clamp_min(1e-9)
137
+ pos_6 = (panel.close - lo) / rng
138
+ pos_12 = pos_6 # simplified: same ratio, different sum windows
139
+ pos_24 = pos_6
140
+ s6, m6 = ts_sum(pos_6, m_base, 6)
141
+ s12, m12 = ts_sum(pos_12, m_base, 12)
142
+ s24, m24 = ts_sum(pos_24, m_base, 24)
143
+ m = m6 & m12 & m24
144
+ out = (s6 / 6.0 + s12 / 12.0 + s24 / 24.0) / 3.0
145
+ return out * m.float(), m
146
+
147
+
148
+ @register_legacy_factor("better_012")
149
+ def better_012(panel: Panel) -> Tuple[Tensor, Tensor]:
150
+ """Multi-period support/resistance using min(low, delay(vwap,1)) and max(high, delay(vwap,1))."""
151
+ prev_v, pvm = delay(panel.vwap, panel.mask, 1)
152
+ lo = torch.minimum(panel.low, prev_v)
153
+ hi = torch.maximum(panel.high, prev_v)
154
+ rng = (hi - lo).clamp_min(1e-9)
155
+ pos = (panel.vwap - lo) / rng
156
+ m_base = pvm & panel.mask
157
+ s6, m6 = ts_sum(pos, m_base, 6)
158
+ s12, m12 = ts_sum(pos, m_base, 12)
159
+ s24, m24 = ts_sum(pos, m_base, 24)
160
+ m = m6 & m12 & m24
161
+ out = (s6 / 6.0 + s12 / 12.0 + s24 / 24.0) / 3.0
162
+ return out * m.float(), m
163
+
164
+
165
+ @register_legacy_factor("better_013")
166
+ def better_013(panel: Panel) -> Tuple[Tensor, Tensor]:
167
+ """(vwap/close - 1) * log2(amount + 1)."""
168
+ amt = _amount(panel)
169
+ out = (panel.vwap / panel.close.clamp_min(1e-9) - 1.0) * torch.log2(amt + 1.0)
170
+ return out * panel.mask.float(), panel.mask
171
+
172
+
173
+ @register_legacy_factor("better_014")
174
+ def better_014(panel: Panel) -> Tuple[Tensor, Tensor]:
175
+ """mean((vwap/close - 1) * log2(amount + 1), 4)."""
176
+ amt = _amount(panel)
177
+ signal = (panel.vwap / panel.close.clamp_min(1e-9) - 1.0) * torch.log2(amt + 1.0)
178
+ return ts_mean(signal, panel.mask, 4)
179
+
180
+
181
+ @register_legacy_factor("better_015")
182
+ def better_015(panel: Panel) -> Tuple[Tensor, Tensor]:
183
+ """ewma((vwap - mean(vwap,10))/mean(vwap,10) delta 5, 1/20)."""
184
+ avg, am = ts_mean(panel.vwap, panel.mask, 10)
185
+ dev = (panel.vwap - avg) / avg.clamp_min(1e-9)
186
+ # delta 5
187
+ T = dev.shape[0]
188
+ if T > 5:
189
+ d = dev[5:] - dev[:-5]
190
+ dm = am[5:] & am[:-5]
191
+ else:
192
+ d = torch.zeros_like(dev)
193
+ dm = torch.zeros_like(am)
194
+ ew, em = ewma(d, dm, alpha=1.0 / 20.0)
195
+ # Pad back to [T, N]
196
+ pad = torch.zeros(5, dev.shape[1], device=dev.device)
197
+ pad_m = torch.zeros(5, dev.shape[1], dtype=torch.bool, device=dev.device)
198
+ out = torch.cat([pad, ew], dim=0)
199
+ mask_out = torch.cat([pad_m, em], dim=0)
200
+ return out * mask_out.float(), mask_out
201
+
202
+
203
+ @register_legacy_factor("better_016")
204
+ def better_016(panel: Panel) -> Tuple[Tensor, Tensor]:
205
+ """ewma((vwap - mean(vwap,6))/mean(vwap,6) delta 3, 1/12)."""
206
+ avg, am = ts_mean(panel.vwap, panel.mask, 6)
207
+ dev = (panel.vwap - avg) / avg.clamp_min(1e-9)
208
+ T = dev.shape[0]
209
+ if T > 3:
210
+ d = dev[3:] - dev[:-3]
211
+ dm = am[3:] & am[:-3]
212
+ else:
213
+ d = torch.zeros_like(dev)
214
+ dm = torch.zeros_like(am)
215
+ ew, em = ewma(d, dm, alpha=1.0 / 12.0)
216
+ pad = torch.zeros(3, dev.shape[1], device=dev.device)
217
+ pad_m = torch.zeros(3, dev.shape[1], dtype=torch.bool, device=dev.device)
218
+ out = torch.cat([pad, ew], dim=0)
219
+ mask_out = torch.cat([pad_m, em], dim=0)
220
+ return out * mask_out.float(), mask_out
221
+
222
+
223
+ @register_legacy_factor("better_017")
224
+ def better_017(panel: Panel) -> Tuple[Tensor, Tensor]:
225
+ """ewma((amount - mean(amount,6))/mean(amount,6) delta 3, 1/12)."""
226
+ amt = _amount(panel)
227
+ avg, am = ts_mean(amt, panel.mask, 6)
228
+ dev = (amt - avg) / avg.clamp_min(1.0)
229
+ T = dev.shape[0]
230
+ if T > 3:
231
+ d = dev[3:] - dev[:-3]
232
+ dm = am[3:] & am[:-3]
233
+ else:
234
+ d = torch.zeros_like(dev)
235
+ dm = torch.zeros_like(am)
236
+ ew, em = ewma(d, dm, alpha=1.0 / 12.0)
237
+ pad = torch.zeros(3, dev.shape[1], device=dev.device)
238
+ pad_m = torch.zeros(3, dev.shape[1], dtype=torch.bool, device=dev.device)
239
+ out = torch.cat([pad, ew], dim=0)
240
+ mask_out = torch.cat([pad_m, em], dim=0)
241
+ return out * mask_out.float(), mask_out
242
+
243
+
244
+ @register_legacy_factor("better_018")
245
+ def better_018(panel: Panel) -> Tuple[Tensor, Tensor]:
246
+ """ewma((amount - mean(amount,10))/mean(amount,10) delta 5, 1/20)."""
247
+ amt = _amount(panel)
248
+ avg, am = ts_mean(amt, panel.mask, 10)
249
+ dev = (amt - avg) / avg.clamp_min(1.0)
250
+ T = dev.shape[0]
251
+ if T > 5:
252
+ d = dev[5:] - dev[:-5]
253
+ dm = am[5:] & am[:-5]
254
+ else:
255
+ d = torch.zeros_like(dev)
256
+ dm = torch.zeros_like(am)
257
+ ew, em = ewma(d, dm, alpha=1.0 / 20.0)
258
+ pad = torch.zeros(5, dev.shape[1], device=dev.device)
259
+ pad_m = torch.zeros(5, dev.shape[1], dtype=torch.bool, device=dev.device)
260
+ out = torch.cat([pad, ew], dim=0)
261
+ mask_out = torch.cat([pad_m, em], dim=0)
262
+ return out * mask_out.float(), mask_out
263
+
264
+
265
+ @register_legacy_factor("better_019")
266
+ def better_019(panel: Panel) -> Tuple[Tensor, Tensor]:
267
+ """Triple-smoothed EMA of log(volume) change."""
268
+ lv = torch.log(panel.volume + 1.0)
269
+ ew1, m1 = ewma(lv, panel.mask, alpha=2.0 / 13.0)
270
+ ew2, m2 = ewma(ew1, m1, alpha=2.0 / 13.0)
271
+ ew3, m3 = ewma(ew2, m2, alpha=2.0 / 13.0)
272
+ # Change: ew3[t] / ew3[t-1] - 1
273
+ prev, pm = delay(ew3, m3, 1)
274
+ out = ew3 / prev.clamp_min(1e-9) - 1.0
275
+ m = m3 & pm
276
+ return out * m.float(), m
277
+
278
+
279
+ @register_legacy_factor("better_020")
280
+ def better_020(panel: Panel) -> Tuple[Tensor, Tensor]:
281
+ """std(|ret|/volume, 10) / mean(|ret|/volume, 10) - Amihud illiquidity CV."""
282
+ prev, pm = delay(panel.close, panel.mask, 1)
283
+ ret = (panel.close / prev.clamp_min(1e-9) - 1.0).abs()
284
+ signal = ret / panel.volume.clamp_min(1.0)
285
+ sd, sm = ts_std(signal, pm, 10)
286
+ avg, am = ts_mean(signal, pm, 10)
287
+ m = sm & am
288
+ out = sd / avg.clamp_min(1e-9)
289
+ return out * m.float(), m
290
+
291
+
292
+ @register_legacy_factor("better_021")
293
+ def better_021(panel: Panel) -> Tuple[Tensor, Tensor]:
294
+ """vwap / delay(close, 1)."""
295
+ prev, pm = delay(panel.close, panel.mask, 1)
296
+ out = panel.vwap / prev.clamp_min(1e-9)
297
+ return out * pm.float(), pm
298
+
299
+
300
+ @register_legacy_factor("better_022")
301
+ def better_022(panel: Panel) -> Tuple[Tensor, Tensor]:
302
+ """(vwap/delay(close,1) - 1) * log2(amount + 1)."""
303
+ prev, pm = delay(panel.close, panel.mask, 1)
304
+ ratio = panel.vwap / prev.clamp_min(1e-9) - 1.0
305
+ amt = _amount(panel)
306
+ out = ratio * torch.log2(amt + 1.0)
307
+ m = pm & panel.mask
308
+ return out * m.float(), m
309
+
310
+
311
+ @register_legacy_factor("better_023")
312
+ def better_023(panel: Panel) -> Tuple[Tensor, Tensor]:
313
+ """mean((vwap/delay(close,1) - 1) * log2(amount + 1), 4)."""
314
+ prev, pm = delay(panel.close, panel.mask, 1)
315
+ ratio = panel.vwap / prev.clamp_min(1e-9) - 1.0
316
+ amt = _amount(panel)
317
+ signal = ratio * torch.log2(amt + 1.0)
318
+ m = pm & panel.mask
319
+ return ts_mean(signal * m.float(), m, 4)
320
+
321
+
322
+ @register_legacy_factor("better_024")
323
+ def better_024(panel: Panel) -> Tuple[Tensor, Tensor]:
324
+ """(max(vwap-close, 5) + min(vwap-close, 5)) * Δvolume_5."""
325
+ spread = panel.vwap - panel.close
326
+ mx, mxm = ts_max(spread, panel.mask, 5)
327
+ mn, mnm = ts_min(spread, panel.mask, 5)
328
+ dv, dvm = delta(panel.volume, panel.mask, 5)
329
+ m = mxm & mnm & dvm
330
+ out = (mx + mn) * dv
331
+ return out * m.float(), m
332
+
333
+
334
+ @register_legacy_factor("better_025")
335
+ def better_025(panel: Panel) -> Tuple[Tensor, Tensor]:
336
+ """(max(vwap-close, 5) + min(vwap-close, 5)) * (volume - mean(volume, 5))."""
337
+ spread = panel.vwap - panel.close
338
+ mx, mxm = ts_max(spread, panel.mask, 5)
339
+ mn, mnm = ts_min(spread, panel.mask, 5)
340
+ avg, am = ts_mean(panel.volume, panel.mask, 5)
341
+ m = mxm & mnm & am
342
+ out = (mx + mn) * (panel.volume - avg)
343
+ return out * m.float(), m
344
+
345
+
346
+ @register_legacy_factor("better_026")
347
+ def better_026(panel: Panel) -> Tuple[Tensor, Tensor]:
348
+ """ewma((high-low)/close, 2/5) / ewma(ewma((high-low)/close, 2/5), 2/20)."""
349
+ rng = (panel.high - panel.low) / panel.close.clamp_min(1e-9)
350
+ sma1, m1 = ewma(rng, panel.mask, alpha=2.0 / 5.0)
351
+ sma2, m2 = ewma(sma1, m1, alpha=2.0 / 20.0)
352
+ m = m1 & m2
353
+ out = sma1 / sma2.clamp_min(1e-9)
354
+ return out * m.float(), m
355
+
356
+
357
+ @register_legacy_factor("better_027")
358
+ def better_027(panel: Panel) -> Tuple[Tensor, Tensor]:
359
+ """Conditional sum: if vwap < delay(close,1): |vwap/delay(close,1)-1|/log(amount+1), window=20."""
360
+ prev, pm = delay(panel.close, panel.mask, 1)
361
+ ratio = (panel.vwap / prev.clamp_min(1e-9) - 1.0).abs()
362
+ amt = _amount(panel)
363
+ signal = ratio / torch.log(amt + 1.0).clamp_min(1e-9)
364
+ cond = (panel.vwap < prev).float() * pm.float()
365
+ weighted = signal * cond
366
+ s, sm = ts_sum(weighted, pm, 20)
367
+ cnt, cm = ts_sum(cond, pm, 20)
368
+ m = sm & cm
369
+ out = s / cnt.clamp_min(1.0)
370
+ return out * m.float(), m
371
+
372
+
373
+ @register_legacy_factor("better_028")
374
+ def better_028(panel: Panel) -> Tuple[Tensor, Tensor]:
375
+ """Conditional sum: if vwap < close: |vwap/close-1|/log(amount+1), window=20."""
376
+ ratio = (panel.vwap / panel.close.clamp_min(1e-9) - 1.0).abs()
377
+ amt = _amount(panel)
378
+ signal = ratio / torch.log(amt + 1.0).clamp_min(1e-9)
379
+ cond = (panel.vwap < panel.close).float() * panel.mask.float()
380
+ weighted = signal * cond
381
+ s, sm = ts_sum(weighted, panel.mask, 20)
382
+ cnt, cm = ts_sum(cond, panel.mask, 20)
383
+ m = sm & cm
384
+ out = s / cnt.clamp_min(1.0)
385
+ return out * m.float(), m
@@ -0,0 +1,53 @@
1
+ """change_* family: short-window change-of-velocity signals (5 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 cs_rank, delta, ts_mean, ts_sum
8
+ from .legacy_factors import register_legacy_factor
9
+
10
+ Tensor = torch.Tensor
11
+
12
+
13
+ @register_legacy_factor("change_001")
14
+ def change_001(panel: Panel) -> Tuple[Tensor, Tensor]:
15
+ """1-day return minus its 5-day mean (mean-reversion proxy)."""
16
+ ret, rm = delta(panel.close, panel.mask, 1)
17
+ ret = ret / panel.close.clamp_min(1e-9)
18
+ avg, am = ts_mean(ret, rm, 5)
19
+ m = rm & am
20
+ return (ret - avg) * m.float(), m
21
+
22
+
23
+ @register_legacy_factor("change_002")
24
+ def change_002(panel: Panel) -> Tuple[Tensor, Tensor]:
25
+ """Acceleration: Δ(Δclose, 1) over 1 day."""
26
+ d1, m1 = delta(panel.close, panel.mask, 1)
27
+ d2, m2 = delta(d1, m1, 1)
28
+ return d2 / panel.close.clamp_min(1e-9), m2
29
+
30
+
31
+ @register_legacy_factor("change_003")
32
+ def change_003(panel: Panel) -> Tuple[Tensor, Tensor]:
33
+ """Volume Δ-rank: rank of (volume - ts_mean(volume, 20))."""
34
+ avg, am = ts_mean(panel.volume, panel.mask, 20)
35
+ dev = (panel.volume - avg) * am.float()
36
+ return cs_rank(dev, am)
37
+
38
+
39
+ @register_legacy_factor("change_004")
40
+ def change_004(panel: Panel) -> Tuple[Tensor, Tensor]:
41
+ """Range expansion: (high - low) / ts_mean(high - low, 20)."""
42
+ rng_ = panel.high - panel.low
43
+ avg, am = ts_mean(rng_, panel.mask, 20)
44
+ out = rng_ / avg.clamp_min(1e-9) - 1.0
45
+ return out * am.float(), am
46
+
47
+
48
+ @register_legacy_factor("change_005")
49
+ def change_005(panel: Panel) -> Tuple[Tensor, Tensor]:
50
+ """Sign flip count: ts_sum(sign(Δclose), 5)."""
51
+ d, dm = delta(panel.close, panel.mask, 1)
52
+ s = torch.sign(d)
53
+ return ts_sum(s, dm, 5)