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
mlquant/features.py ADDED
@@ -0,0 +1,613 @@
1
+ """
2
+ Full 101 Alpha factors + market signal factors.
3
+ Replicates legacy/cuda_features.py using PyTorch tensors.
4
+ """
5
+ from __future__ import annotations
6
+ import torch
7
+ from torch import Tensor
8
+ from typing import Optional
9
+
10
+ # ============================================================
11
+ # Utility helpers
12
+ # ============================================================
13
+
14
+ def _rank(x: Tensor) -> Tensor:
15
+ """Cross-sectional rank normalized to [0,1]."""
16
+ sorted_idx = x.argsort(dim=-1)
17
+ ranks = torch.zeros_like(x)
18
+ ranks.scatter_(-1, sorted_idx, torch.arange(x.shape[-1], device=x.device, dtype=x.dtype).expand_as(x))
19
+ denom = max(x.shape[-1] - 1, 1)
20
+ return ranks / denom
21
+
22
+
23
+ def _ts_rank(x: Tensor, d: int) -> Tensor:
24
+ """Time-series rank over last d days."""
25
+ if x.dim() == 2:
26
+ x = x.unsqueeze(0)
27
+ T = x.shape[1]
28
+ out = torch.zeros_like(x)
29
+ for t in range(d - 1, T):
30
+ window = x[:, t - d + 1:t + 1, :]
31
+ val = x[:, t:t + 1, :]
32
+ out[:, t, :] = (window < val).sum(dim=1).float() / max(d - 1, 1)
33
+ return out.squeeze(0) if out.shape[0] == 1 else out
34
+
35
+
36
+ def _ts_max(x: Tensor, d: int) -> Tensor:
37
+ if x.dim() == 2:
38
+ x = x.unsqueeze(0)
39
+ T = x.shape[1]
40
+ out = torch.zeros_like(x)
41
+ for t in range(d - 1, T):
42
+ out[:, t, :] = x[:, t - d + 1:t + 1, :].max(dim=1).values
43
+ return out.squeeze(0) if out.shape[0] == 1 else out
44
+
45
+
46
+ def _ts_min(x: Tensor, d: int) -> Tensor:
47
+ if x.dim() == 2:
48
+ x = x.unsqueeze(0)
49
+ T = x.shape[1]
50
+ out = torch.zeros_like(x)
51
+ for t in range(d - 1, T):
52
+ out[:, t, :] = x[:, t - d + 1:t + 1, :].min(dim=1).values
53
+ return out.squeeze(0) if out.shape[0] == 1 else out
54
+
55
+
56
+ def _ts_argmax(x: Tensor, d: int) -> Tensor:
57
+ if x.dim() == 2:
58
+ x = x.unsqueeze(0)
59
+ T = x.shape[1]
60
+ out = torch.zeros_like(x)
61
+ for t in range(d - 1, T):
62
+ out[:, t, :] = x[:, t - d + 1:t + 1, :].argmax(dim=1).float()
63
+ return out.squeeze(0) if out.shape[0] == 1 else out
64
+
65
+
66
+ def _ts_argmin(x: Tensor, d: int) -> Tensor:
67
+ if x.dim() == 2:
68
+ x = x.unsqueeze(0)
69
+ T = x.shape[1]
70
+ out = torch.zeros_like(x)
71
+ for t in range(d - 1, T):
72
+ out[:, t, :] = x[:, t - d + 1:t + 1, :].argmin(dim=1).float()
73
+ return out.squeeze(0) if out.shape[0] == 1 else out
74
+
75
+
76
+ def _delta(x: Tensor, d: int) -> Tensor:
77
+ pad = torch.zeros_like(x[..., :d, :]) if x.dim() == 3 else torch.zeros_like(x[:d, :])
78
+ if x.dim() == 3:
79
+ return torch.cat([pad, x[:, d:, :] - x[:, :-d, :]], dim=1)
80
+ return torch.cat([pad, x[d:, :] - x[:-d, :]], dim=0)
81
+
82
+
83
+ def _delay(x: Tensor, d: int) -> Tensor:
84
+ pad = torch.zeros_like(x[..., :d, :]) if x.dim() == 3 else torch.zeros_like(x[:d, :])
85
+ if x.dim() == 3:
86
+ return torch.cat([pad, x[:, :-d, :]], dim=1)
87
+ return torch.cat([pad, x[:-d, :]], dim=0)
88
+
89
+
90
+ def _sma(x: Tensor, d: int) -> Tensor:
91
+ if x.dim() == 2:
92
+ x = x.unsqueeze(0)
93
+ T = x.shape[1]
94
+ out = torch.zeros_like(x)
95
+ for t in range(T):
96
+ start = max(0, t - d + 1)
97
+ out[:, t, :] = x[:, start:t + 1, :].mean(dim=1)
98
+ return out.squeeze(0) if out.shape[0] == 1 else out
99
+
100
+
101
+ def _stddev(x: Tensor, d: int) -> Tensor:
102
+ if x.dim() == 2:
103
+ x = x.unsqueeze(0)
104
+ T = x.shape[1]
105
+ out = torch.zeros_like(x)
106
+ for t in range(d - 1, T):
107
+ out[:, t, :] = x[:, t - d + 1:t + 1, :].std(dim=1)
108
+ return out.squeeze(0) if out.shape[0] == 1 else out
109
+
110
+
111
+ def _correlation(x: Tensor, y: Tensor, d: int) -> Tensor:
112
+ if x.dim() == 2:
113
+ x = x.unsqueeze(0)
114
+ y = y.unsqueeze(0)
115
+ T = x.shape[1]
116
+ out = torch.zeros_like(x)
117
+ for t in range(d - 1, T):
118
+ xw = x[:, t - d + 1:t + 1, :]
119
+ yw = y[:, t - d + 1:t + 1, :]
120
+ xm = xw - xw.mean(dim=1, keepdim=True)
121
+ ym = yw - yw.mean(dim=1, keepdim=True)
122
+ num = (xm * ym).sum(dim=1)
123
+ den = (xm.pow(2).sum(dim=1) * ym.pow(2).sum(dim=1)).sqrt()
124
+ out[:, t, :] = num / den.clamp(min=1e-8)
125
+ return out.squeeze(0) if out.shape[0] == 1 else out
126
+
127
+
128
+ def _covariance(x: Tensor, y: Tensor, d: int) -> Tensor:
129
+ if x.dim() == 2:
130
+ x = x.unsqueeze(0)
131
+ y = y.unsqueeze(0)
132
+ T = x.shape[1]
133
+ out = torch.zeros_like(x)
134
+ for t in range(d - 1, T):
135
+ xw = x[:, t - d + 1:t + 1, :]
136
+ yw = y[:, t - d + 1:t + 1, :]
137
+ xm = xw - xw.mean(dim=1, keepdim=True)
138
+ ym = yw - yw.mean(dim=1, keepdim=True)
139
+ out[:, t, :] = (xm * ym).mean(dim=1)
140
+ return out.squeeze(0) if out.shape[0] == 1 else out
141
+
142
+
143
+ def _ts_sum(x: Tensor, d: int) -> Tensor:
144
+ if x.dim() == 2:
145
+ x = x.unsqueeze(0)
146
+ T = x.shape[1]
147
+ out = torch.zeros_like(x)
148
+ for t in range(T):
149
+ start = max(0, t - d + 1)
150
+ out[:, t, :] = x[:, start:t + 1, :].sum(dim=1)
151
+ return out.squeeze(0) if out.shape[0] == 1 else out
152
+
153
+
154
+ def _product(x: Tensor, d: int) -> Tensor:
155
+ if x.dim() == 2:
156
+ x = x.unsqueeze(0)
157
+ T = x.shape[1]
158
+ out = torch.ones_like(x)
159
+ for t in range(d - 1, T):
160
+ out[:, t, :] = x[:, t - d + 1:t + 1, :].prod(dim=1)
161
+ return out.squeeze(0) if out.shape[0] == 1 else out
162
+
163
+
164
+ def _decay_linear(x: Tensor, d: int) -> Tensor:
165
+ if x.dim() == 2:
166
+ x = x.unsqueeze(0)
167
+ T = x.shape[1]
168
+ weights = torch.arange(1, d + 1, dtype=x.dtype, device=x.device).float()
169
+ weights = weights / weights.sum()
170
+ out = torch.zeros_like(x)
171
+ for t in range(d - 1, T):
172
+ out[:, t, :] = (x[:, t - d + 1:t + 1, :] * weights.view(1, -1, 1)).sum(dim=1)
173
+ return out.squeeze(0) if out.shape[0] == 1 else out
174
+
175
+
176
+ def _sign(x: Tensor) -> Tensor:
177
+ return torch.sign(x)
178
+
179
+
180
+ def _log(x: Tensor) -> Tensor:
181
+ return torch.log(x.clamp(min=1e-8))
182
+
183
+
184
+ def _abs(x: Tensor) -> Tensor:
185
+ return x.abs()
186
+
187
+
188
+ # placeholder for scale (cross-sectional normalize to sum abs = 1)
189
+ def _scale(x: Tensor, a: float = 1.0) -> Tensor:
190
+ s = x.abs().sum(dim=-1, keepdim=True).clamp(min=1e-8)
191
+ return x / s * a
192
+
193
+
194
+ # ============================================================
195
+ # Main compute function (skeleton - implementations added below)
196
+ # ============================================================
197
+
198
+ def compute_alpha101(
199
+ open: Tensor,
200
+ high: Tensor,
201
+ low: Tensor,
202
+ close: Tensor,
203
+ volume: Tensor,
204
+ returns: Optional[Tensor] = None,
205
+ vwap: Optional[Tensor] = None,
206
+ ) -> Tensor:
207
+ """Compute 101 alpha factors + market signal factors.
208
+
209
+ Args:
210
+ open, high, low, close, volume: shape (T, N) or (B, T, N)
211
+ returns: if None, computed from close
212
+ vwap: if None, approximated as (high+low+close)/3
213
+
214
+ Returns:
215
+ Tensor of shape (..., T, N, num_factors)
216
+ """
217
+ # Ensure 3D
218
+ squeeze = False
219
+ if open.dim() == 2:
220
+ squeeze = True
221
+ open = open.unsqueeze(0)
222
+ high = high.unsqueeze(0)
223
+ low = low.unsqueeze(0)
224
+ close = close.unsqueeze(0)
225
+ volume = volume.unsqueeze(0)
226
+
227
+ B, T, N = close.shape
228
+
229
+ if returns is None:
230
+ returns = torch.zeros_like(close)
231
+ returns[:, 1:, :] = close[:, 1:, :] / close[:, :-1, :].clamp(min=1e-8) - 1
232
+ elif returns.dim() == 2:
233
+ returns = returns.unsqueeze(0)
234
+
235
+ if vwap is None:
236
+ vwap = (high + low + close) / 3
237
+ elif vwap.dim() == 2:
238
+ vwap = vwap.unsqueeze(0)
239
+
240
+ factors = []
241
+
242
+ # Alpha#001 to Alpha#101 + market signals will be appended
243
+ # (implementations filled in via replace_in_file)
244
+ factors.append(_alpha001(close, returns))
245
+ factors.append(_alpha002(open, close, volume))
246
+ factors.append(_alpha003(open, close, volume))
247
+ factors.append(_alpha004(low, volume))
248
+ factors.append(_alpha005(open, close, vwap))
249
+ factors.append(_alpha006(open, volume))
250
+ factors.append(_alpha007(close, volume, returns))
251
+ factors.append(_alpha008(open, close, returns))
252
+ factors.append(_alpha009(close))
253
+ factors.append(_alpha010(close, returns))
254
+ factors.append(_alpha011(close, volume, vwap))
255
+ factors.append(_alpha012(close, volume))
256
+ factors.append(_alpha013(close, volume))
257
+ factors.append(_alpha014(open, close, volume, returns))
258
+ factors.append(_alpha015(high, volume))
259
+ factors.append(_alpha016(close, volume))
260
+ factors.append(_alpha017(close, volume))
261
+ factors.append(_alpha018(open, close))
262
+ factors.append(_alpha019(close, returns))
263
+ factors.append(_alpha020(open, high, low, close))
264
+ factors.append(_alpha021(open, high, low, close, volume))
265
+ factors.append(_alpha022(high, close, volume))
266
+ factors.append(_alpha023(high, close))
267
+ factors.append(_alpha024(close))
268
+ factors.append(_alpha025(close, returns, vwap, volume))
269
+ factors.append(_alpha026(close, volume))
270
+ factors.append(_alpha027(close, volume))
271
+ factors.append(_alpha028(high, low, close, volume))
272
+ factors.append(_alpha029(close, returns))
273
+ factors.append(_alpha030(close, volume))
274
+ factors.append(_alpha031(close, low, volume))
275
+ factors.append(_alpha032(close, volume, vwap))
276
+ factors.append(_alpha033(open, close))
277
+ factors.append(_alpha034(close, returns))
278
+ factors.append(_alpha035(high, low, close, volume, returns))
279
+ factors.append(_alpha036(open, close, volume, vwap, returns))
280
+ factors.append(_alpha037(open, close))
281
+ factors.append(_alpha038(high, close))
282
+ factors.append(_alpha039(close, volume, returns))
283
+ factors.append(_alpha040(high, volume))
284
+ factors.append(_alpha041(high, low, vwap))
285
+ factors.append(_alpha042(close, vwap))
286
+ factors.append(_alpha043(close, volume))
287
+ factors.append(_alpha044(high, volume))
288
+ factors.append(_alpha045(close, volume))
289
+ factors.append(_alpha046(close))
290
+ factors.append(_alpha047(high, close, volume, vwap))
291
+ factors.append(_alpha048(close, returns))
292
+ factors.append(_alpha049(close))
293
+ factors.append(_alpha050(volume, vwap))
294
+ # Market signal factors
295
+ factors.append(_market_ma_signal(close, 5, 20))
296
+ factors.append(_market_ma_signal(close, 10, 60))
297
+ factors.append(_market_volatility(returns, 20))
298
+ factors.append(_market_momentum(close, 20))
299
+ factors.append(_market_volume_signal(volume, 20))
300
+
301
+ result = torch.stack(factors, dim=-1) # (B, T, N, F)
302
+ if squeeze:
303
+ result = result.squeeze(0)
304
+ return result
305
+
306
+
307
+ # ============================================================
308
+ # Alpha factor implementations (first 20 + market signals)
309
+ # ============================================================
310
+
311
+ def _alpha001(close: Tensor, returns: Tensor) -> Tensor:
312
+ """rank(ts_argmax(sign(delta(close,1)) < 0 ? stddev(returns,20) : close, 5))"""
313
+ cond = _sign(_delta(close, 1)) < 0
314
+ inner = torch.where(cond, _stddev(returns, 20), close)
315
+ return _rank(_ts_argmax(inner, 5))
316
+
317
+
318
+ def _alpha002(open: Tensor, close: Tensor, volume: Tensor) -> Tensor:
319
+ """-1 * correlation(rank(delta(log(volume),2)), rank((close-open)/open), 6)"""
320
+ return -1 * _correlation(_rank(_delta(_log(volume), 2)), _rank((close - open) / open.clamp(min=1e-8)), 6)
321
+
322
+
323
+ def _alpha003(open: Tensor, close: Tensor, volume: Tensor) -> Tensor:
324
+ """-1 * correlation(rank(open), rank(volume), 10)"""
325
+ return -1 * _correlation(_rank(open), _rank(volume), 10)
326
+
327
+
328
+ def _alpha004(low: Tensor, volume: Tensor) -> Tensor:
329
+ """-1 * ts_rank(rank(low), 9)"""
330
+ return -1 * _ts_rank(_rank(low), 9)
331
+
332
+
333
+ def _alpha005(open: Tensor, close: Tensor, vwap: Tensor) -> Tensor:
334
+ """rank(open - ts_sum(vwap,10)/10) * (-1 * abs(rank(close - vwap)))"""
335
+ return _rank(open - _sma(vwap, 10)) * (-1 * _abs(_rank(close - vwap)))
336
+
337
+
338
+ def _alpha006(open: Tensor, volume: Tensor) -> Tensor:
339
+ """-1 * correlation(open, volume, 10)"""
340
+ return -1 * _correlation(open, volume, 10)
341
+
342
+
343
+ def _alpha007(close: Tensor, volume: Tensor, returns: Tensor) -> Tensor:
344
+ """adv20 = sma(volume,20); cond = adv20 < volume
345
+ if cond: (-1*ts_rank(abs(delta(close,7)),60))*sign(delta(close,7)) else -1"""
346
+ adv20 = _sma(volume, 20)
347
+ cond = adv20 < volume
348
+ inner = (-1 * _ts_rank(_abs(_delta(close, 7)), 60)) * _sign(_delta(close, 7))
349
+ return torch.where(cond, inner, torch.tensor(-1.0, device=close.device))
350
+
351
+
352
+ def _alpha008(open: Tensor, close: Tensor, returns: Tensor) -> Tensor:
353
+ """-1 * rank(ts_sum(open,5) * ts_sum(returns,5) - delay(ts_sum(open,5)*ts_sum(returns,5),10))"""
354
+ inner = _ts_sum(open, 5) * _ts_sum(returns, 5)
355
+ return -1 * _rank(inner - _delay(inner, 10))
356
+
357
+
358
+ def _alpha009(close: Tensor) -> Tensor:
359
+ """if 0<ts_min(delta(close,1),5): delta(close,1)
360
+ elif ts_max(delta(close,1),5)<0: delta(close,1) else -1*delta(close,1)"""
361
+ d = _delta(close, 1)
362
+ cond1 = _ts_min(d, 5) > 0
363
+ cond2 = _ts_max(d, 5) < 0
364
+ return torch.where(cond1 | cond2, d, -1 * d)
365
+
366
+
367
+ def _alpha010(close: Tensor, returns: Tensor) -> Tensor:
368
+ """rank(if 0<ts_min(delta(close,1),4): delta(close,1) elif ts_max(delta(close,1),4)<0: delta(close,1) else -1*delta(close,1))"""
369
+ d = _delta(close, 1)
370
+ cond1 = _ts_min(d, 4) > 0
371
+ cond2 = _ts_max(d, 4) < 0
372
+ return _rank(torch.where(cond1 | cond2, d, -1 * d))
373
+
374
+
375
+ def _alpha011(close: Tensor, volume: Tensor, vwap: Tensor) -> Tensor:
376
+ """((rank(ts_max(vwap-close,3))+ rank(ts_min(vwap-close,3)))*rank(delta(volume,3)))"""
377
+ return (_rank(_ts_max(vwap - close, 3)) + _rank(_ts_min(vwap - close, 3))) * _rank(_delta(volume, 3))
378
+
379
+
380
+ def _alpha012(close: Tensor, volume: Tensor) -> Tensor:
381
+ """sign(delta(volume,1)) * (-1*delta(close,1))"""
382
+ return _sign(_delta(volume, 1)) * (-1 * _delta(close, 1))
383
+
384
+
385
+ def _alpha013(close: Tensor, volume: Tensor) -> Tensor:
386
+ """-1 * rank(covariance(rank(close),rank(volume),5))"""
387
+ return -1 * _rank(_covariance(_rank(close), _rank(volume), 5))
388
+
389
+
390
+ def _alpha014(open: Tensor, close: Tensor, volume: Tensor, returns: Tensor) -> Tensor:
391
+ """-1 * rank(delta(returns,3)) * correlation(open, volume, 10)"""
392
+ return -1 * _rank(_delta(returns, 3)) * _correlation(open, volume, 10)
393
+
394
+
395
+ def _alpha015(high: Tensor, volume: Tensor) -> Tensor:
396
+ """-1 * ts_sum(rank(correlation(rank(high), rank(volume), 3)), 3)"""
397
+ return -1 * _ts_sum(_rank(_correlation(_rank(high), _rank(volume), 3)), 3)
398
+
399
+
400
+ def _alpha016(close: Tensor, volume: Tensor) -> Tensor:
401
+ """-1 * rank(covariance(rank(close), rank(volume), 5))"""
402
+ return -1 * _rank(_covariance(_rank(close), _rank(volume), 5))
403
+
404
+
405
+ def _alpha017(close: Tensor, volume: Tensor) -> Tensor:
406
+ """rank(-1 * ts_rank(close, 10)) * rank(delta(delta(close,1),1)) * rank(ts_rank(volume/sma(volume,20),5))"""
407
+ return _rank(-1 * _ts_rank(close, 10)) * _rank(_delta(_delta(close, 1), 1)) * _rank(_ts_rank(volume / _sma(volume, 20).clamp(min=1e-8), 5))
408
+
409
+
410
+ def _alpha018(open: Tensor, close: Tensor) -> Tensor:
411
+ """-1 * rank(stddev(abs(close-open), 5) + (close-open) + correlation(close,open,10))"""
412
+ return -1 * _rank(_stddev(_abs(close - open), 5) + (close - open) + _correlation(close, open, 10))
413
+
414
+
415
+ def _alpha019(close: Tensor, returns: Tensor) -> Tensor:
416
+ """(-1*sign(close-delay(close,7)+delta(close,7))) * (1+rank(1+ts_sum(returns,250)))"""
417
+ return (-1 * _sign(close - _delay(close, 7) + _delta(close, 7))) * (1 + _rank(1 + _ts_sum(returns, 250)))
418
+
419
+
420
+ def _alpha020(open: Tensor, high: Tensor, low: Tensor, close: Tensor) -> Tensor:
421
+ """rank(open - delay(high,1)) * rank(open-delay(close,1)) * rank(open - delay(low,1))"""
422
+ return _rank(open - _delay(high, 1)) * _rank(open - _delay(close, 1)) * _rank(open - _delay(low, 1))
423
+
424
+
425
+ # ============================================================
426
+ # Alpha factor implementations (21-50)
427
+ # ============================================================
428
+
429
+ def _alpha021(open: Tensor, high: Tensor, low: Tensor, close: Tensor, volume: Tensor) -> Tensor:
430
+ c1 = _sma(close, 8) + _stddev(close, 8) < _sma(close, 2)
431
+ c2 = _sma(volume, 20) / volume < 1
432
+ inner = torch.where(c1, torch.tensor(-1.0, device=close.device),
433
+ torch.where(c2, torch.tensor(1.0, device=close.device),
434
+ torch.tensor(-1.0, device=close.device)))
435
+ return inner
436
+
437
+ def _alpha022(high: Tensor, close: Tensor, volume: Tensor) -> Tensor:
438
+ return -1 * _delta(_correlation(high, volume, 5), 5) * _rank(_stddev(close, 20))
439
+
440
+ def _alpha023(high: Tensor, close: Tensor) -> Tensor:
441
+ cond = _sma(high, 20) < high
442
+ return torch.where(cond, -1 * _delta(high, 2), torch.zeros_like(high))
443
+
444
+ def _alpha024(close: Tensor) -> Tensor:
445
+ cond = _delta(_sma(close, 100), 100) / _delay(close, 100).clamp(min=1e-8) <= 0.05
446
+ return torch.where(cond, -1 * _delta(close, 3), -1 * (close - _ts_min(close, 100)))
447
+
448
+ def _alpha025(close: Tensor, returns: Tensor, vwap: Tensor, volume: Tensor) -> Tensor:
449
+ adv20 = _sma(volume, 20)
450
+ return _rank(-1 * returns * adv20 * vwap * (close - _delay(close, 1)))
451
+
452
+ def _alpha026(close: Tensor, volume: Tensor) -> Tensor:
453
+ return -1 * _ts_max(_correlation(_ts_rank(volume, 5), _ts_rank(close, 5), 5), 3)
454
+
455
+ def _alpha027(close: Tensor, volume: Tensor) -> Tensor:
456
+ val = _correlation(_rank(volume), _rank(close), 6)
457
+ cond = _rank(val) < 2.5
458
+ return torch.where(cond, torch.tensor(-1.0, device=close.device), _rank(val))
459
+
460
+ def _alpha028(high: Tensor, low: Tensor, close: Tensor, volume: Tensor) -> Tensor:
461
+ adv20 = _sma(volume, 20)
462
+ return _scale(_correlation(adv20, low, 5) + (high + low) / 2 - close)
463
+
464
+ def _alpha029(close: Tensor, returns: Tensor) -> Tensor:
465
+ return _ts_min(_rank(_rank(_scale(_log(_ts_sum(_rank(_rank(-1 * _rank(_delta(close - 1, 5)))), 2))))), 5)
466
+
467
+ def _alpha030(close: Tensor, volume: Tensor) -> Tensor:
468
+ d = _delta(close, 1)
469
+ return _rank(_sign(d) + _sign(_delay(d, 1)) + _sign(_delay(d, 2))) * _ts_sum(volume, 5) / _ts_sum(volume, 20).clamp(min=1e-8)
470
+
471
+ def _alpha031(close: Tensor, low: Tensor, volume: Tensor) -> Tensor:
472
+ adv20 = _sma(volume, 20)
473
+ return _rank(_decay_linear(-1 * _rank(_delta(close, 10)), 10)) + _rank(-1 * _delta(close, 3)) + _sign(_scale(_correlation(adv20, low, 12)))
474
+
475
+ def _alpha032(close: Tensor, volume: Tensor, vwap: Tensor) -> Tensor:
476
+ return _scale(_sma(close, 7) - close) + 20 * _scale(_correlation(vwap, _delay(close, 5), 230))
477
+
478
+ def _alpha033(open: Tensor, close: Tensor) -> Tensor:
479
+ return _rank(-1 + open / close.clamp(min=1e-8))
480
+
481
+ def _alpha034(close: Tensor, returns: Tensor) -> Tensor:
482
+ return _rank(2 - _rank(_stddev(returns, 2) / _stddev(returns, 5).clamp(min=1e-8)) + _rank(_delta(close, 1)))
483
+
484
+ def _alpha035(high: Tensor, low: Tensor, close: Tensor, volume: Tensor, returns: Tensor) -> Tensor:
485
+ return _ts_rank(volume, 32) * (1 - _ts_rank(close + high - low, 16)) * (1 - _ts_rank(returns, 32))
486
+
487
+ def _alpha036(open: Tensor, close: Tensor, volume: Tensor, vwap: Tensor, returns: Tensor) -> Tensor:
488
+ adv20 = _sma(volume, 20)
489
+ return 2.21 * _rank(_correlation(close - open, _delay(volume, 1), 15)) + 0.7 * _rank(open - close) + 0.73 * _rank(_ts_rank(_delay(-1 * returns, 6), 5)) + _rank(_abs(_correlation(vwap, adv20, 6))) + 0.6 * _rank((_sma(close, 200) - open) * (close - open))
490
+
491
+ def _alpha037(open: Tensor, close: Tensor) -> Tensor:
492
+ return _rank(_correlation(_delay(open - close, 1), close, 200)) + _rank(open - close)
493
+
494
+ def _alpha038(high: Tensor, close: Tensor) -> Tensor:
495
+ return -1 * _rank(_ts_rank(close, 10)) * _rank(close / high.clamp(min=1e-8))
496
+
497
+ def _alpha039(close: Tensor, volume: Tensor, returns: Tensor) -> Tensor:
498
+ adv20 = _sma(volume, 20)
499
+ return -1 * _rank(_delta(close, 7) * (1 - _rank(_decay_linear(volume / adv20.clamp(min=1e-8), 9)))) * (1 + _rank(_ts_sum(returns, 250)))
500
+
501
+ def _alpha040(high: Tensor, volume: Tensor) -> Tensor:
502
+ return -1 * _rank(_stddev(high, 10)) * _correlation(high, volume, 10)
503
+
504
+ def _alpha041(high: Tensor, low: Tensor, vwap: Tensor) -> Tensor:
505
+ return (high * low) ** 0.5 - vwap
506
+
507
+ def _alpha042(close: Tensor, vwap: Tensor) -> Tensor:
508
+ return _rank(vwap - close) / _rank(vwap + close).clamp(min=1e-8)
509
+
510
+ def _alpha043(close: Tensor, volume: Tensor) -> Tensor:
511
+ adv20 = _sma(volume, 20)
512
+ return _ts_rank(volume / adv20.clamp(min=1e-8), 20) * _ts_rank(-1 * _delta(close, 7), 8)
513
+
514
+ def _alpha044(high: Tensor, volume: Tensor) -> Tensor:
515
+ return -1 * _correlation(high, _rank(volume), 5)
516
+
517
+ def _alpha045(close: Tensor, volume: Tensor) -> Tensor:
518
+ return -1 * _rank(_sma(_delay(close, 5), 20)) * _correlation(close, volume, 2) * _rank(_correlation(_ts_sum(close, 5), _ts_sum(close, 20), 2))
519
+
520
+ def _alpha046(close: Tensor) -> Tensor:
521
+ cond = 0.25 < _delta(close, 20) / _delay(close, 20).clamp(min=1e-8)
522
+ return torch.where(cond, torch.tensor(-1.0, device=close.device), _delta(close, 1))
523
+
524
+ def _alpha047(high: Tensor, close: Tensor, volume: Tensor, vwap: Tensor) -> Tensor:
525
+ adv20 = _sma(volume, 20)
526
+ return _rank(1 / close.clamp(min=1e-8)) * volume / adv20.clamp(min=1e-8) * high * _rank(high - close) / _sma(high, 5).clamp(min=1e-8) - _rank(vwap - _delay(vwap, 5))
527
+
528
+ def _alpha048(close: Tensor, returns: Tensor) -> Tensor:
529
+ # indneutralize simplified as rank
530
+ return _rank(_correlation(_delta(close, 1), _delta(_delay(close, 1), 1), 250)) * _rank(_delta(close, 1)) / close.clamp(min=1e-8)
531
+
532
+ def _alpha049(close: Tensor) -> Tensor:
533
+ cond = _delta(close, 1) > _delay(_delta(close, 1), 1)
534
+ return torch.where(cond, _delta(close, 1), torch.tensor(0.0, device=close.device))
535
+
536
+ def _alpha050(volume: Tensor, vwap: Tensor) -> Tensor:
537
+ return -1 * _ts_max(_rank(_correlation(_rank(volume), _rank(vwap), 5)), 5)
538
+
539
+
540
+ # ============================================================
541
+ # Market signal factors
542
+ # ============================================================
543
+
544
+ def _market_ma_signal(close: Tensor, short_w: int, long_w: int) -> Tensor:
545
+ """MA crossover signal: short MA - long MA, normalized."""
546
+ ma_short = _sma(close, short_w)
547
+ ma_long = _sma(close, long_w)
548
+ return (ma_short - ma_long) / ma_long.clamp(min=1e-8)
549
+
550
+
551
+ def _market_volatility(returns: Tensor, window: int) -> Tensor:
552
+ """Rolling volatility."""
553
+ return _stddev(returns, window)
554
+
555
+
556
+ def _market_momentum(close: Tensor, window: int) -> Tensor:
557
+ """Momentum: current price / price window days ago - 1."""
558
+ return close / _delay(close, window).clamp(min=1e-8) - 1
559
+
560
+
561
+ def _market_volume_signal(volume: Tensor, window: int) -> Tensor:
562
+ """Volume ratio signal: current volume / SMA(volume, window)."""
563
+ return volume / _sma(volume, window).clamp(min=1e-8)
564
+
565
+
566
+ # ============================================================
567
+ # Data loading utility
568
+ # ============================================================
569
+
570
+ def load_ohlcv_csv(path: str, device: str = "cpu") -> dict:
571
+ """Load OHLCV data from a CSV file.
572
+
573
+ Expected columns: date, open, high, low, close, volume
574
+ Optionally: symbol (for multi-stock), vwap
575
+
576
+ Returns dict with keys: open, high, low, close, volume, returns, vwap
577
+ Each tensor has shape (T, N) where N is number of stocks.
578
+ """
579
+ import pandas as pd
580
+
581
+ df = pd.read_csv(path, parse_dates=["date"])
582
+
583
+ if "symbol" in df.columns:
584
+ # Multi-stock pivot
585
+ symbols = sorted(df["symbol"].unique())
586
+ pivot = df.pivot(index="date", columns="symbol")
587
+ o = torch.tensor(pivot["open"][symbols].values, dtype=torch.float32, device=device)
588
+ h = torch.tensor(pivot["high"][symbols].values, dtype=torch.float32, device=device)
589
+ l = torch.tensor(pivot["low"][symbols].values, dtype=torch.float32, device=device)
590
+ c = torch.tensor(pivot["close"][symbols].values, dtype=torch.float32, device=device)
591
+ v = torch.tensor(pivot["volume"][symbols].values, dtype=torch.float32, device=device)
592
+ else:
593
+ # Single stock
594
+ o = torch.tensor(df["open"].values, dtype=torch.float32, device=device).unsqueeze(-1)
595
+ h = torch.tensor(df["high"].values, dtype=torch.float32, device=device).unsqueeze(-1)
596
+ l = torch.tensor(df["low"].values, dtype=torch.float32, device=device).unsqueeze(-1)
597
+ c = torch.tensor(df["close"].values, dtype=torch.float32, device=device).unsqueeze(-1)
598
+ v = torch.tensor(df["volume"].values, dtype=torch.float32, device=device).unsqueeze(-1)
599
+
600
+ # Compute returns
601
+ ret = torch.zeros_like(c)
602
+ ret[1:] = c[1:] / c[:-1].clamp(min=1e-8) - 1
603
+
604
+ # VWAP
605
+ if "vwap" in df.columns:
606
+ if "symbol" in df.columns:
607
+ vwap = torch.tensor(pivot["vwap"][symbols].values, dtype=torch.float32, device=device)
608
+ else:
609
+ vwap = torch.tensor(df["vwap"].values, dtype=torch.float32, device=device).unsqueeze(-1)
610
+ else:
611
+ vwap = (h + l + c) / 3
612
+
613
+ return {"open": o, "high": h, "low": l, "close": c, "volume": v, "returns": ret, "vwap": vwap}
@@ -0,0 +1,8 @@
1
+ """Neural networks + sign-aware regression losses."""
2
+ from .nets import MLPRegressor, TransformerRegressor
3
+ from .losses import AdjMSELoss, ICLoss, RankICLoss
4
+
5
+ __all__ = [
6
+ "MLPRegressor", "TransformerRegressor",
7
+ "AdjMSELoss", "ICLoss", "RankICLoss",
8
+ ]