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
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}
|