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,138 @@
1
+ """Synthetic OCHLV panel generator.
2
+
3
+ The paper's empirical results use proprietary Wind / Tushare data that
4
+ external readers cannot redistribute. To make the repository
5
+ *end-to-end runnable for anyone*, this module synthesises a
6
+ realistically-shaped A-share-like universe via a multi-asset Geometric
7
+ Brownian Motion (GBM) with cross-sectional correlation, plus a
8
+ "limit-up / limit-down" simulator so the bias-correction code paths
9
+ are exercised on the synthetic data too.
10
+
11
+ Why bother making it realistic?
12
+ Random noise is useless for benchmarking factors. A synthetic panel
13
+ that obeys the same constraints as the real one (positive prices,
14
+ OCHL ordering, ~10% daily price-limit, ~5% halt probability,
15
+ cross-sectional correlation around 0.3) lets us:
16
+
17
+ * regression-test the factor engine with deterministic seeds,
18
+ * give new contributors a 30-second smoke-test path,
19
+ * publish CI runs that actually exercise the optimiser.
20
+
21
+ The generator also fills in the *optional* A-share microstructure
22
+ fields — ``amount`` (turnover), ``limit_up``, ``limit_down``,
23
+ ``last_close`` — so that code which prefers exchange-published values
24
+ over derived proxies has something realistic to consume.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass
29
+ from typing import Optional
30
+
31
+ import numpy as np
32
+ import pandas as pd
33
+ import torch
34
+
35
+ from .panel import Panel
36
+
37
+
38
+ @dataclass
39
+ class SyntheticConfig:
40
+ n_stocks: int = 200
41
+ n_dates: int = 500
42
+ start_date: str = "2020-01-02"
43
+ annual_drift: float = 0.05
44
+ annual_vol: float = 0.30
45
+ market_beta: float = 0.6
46
+ halt_prob: float = 0.005 # per stock-day
47
+ limit_pct: float = 0.10 # ±10 % A-share daily price limit
48
+ seed: int = 42
49
+ device: str = "cpu"
50
+
51
+
52
+ def make_synthetic_panel(cfg: Optional[SyntheticConfig] = None) -> Panel:
53
+ """Generate a synthetic OCHLV panel obeying A-share style constraints.
54
+
55
+ The procedure is deliberately compact:
56
+ 1. Sample a daily market log-return ``m_t ~ N(μ_m, σ_m)``.
57
+ 2. Sample idiosyncratic log-returns ``e_{t,i} ~ N(0, σ_e)``.
58
+ 3. Stock log-return ``r_{t,i} = β·m_t + e_{t,i}`` (single-factor model).
59
+ 4. Reject moves outside ±``limit_pct``: clamp and mask.
60
+ 5. Halts: Bernoulli ``halt_prob`` mask.
61
+ 6. Synthesise OCHL around close with a small intraday range.
62
+ 7. Derive ``amount = vwap*volume``, ``last_close = close[t-1]`` and
63
+ the official ±``limit_pct`` bands.
64
+
65
+ The resulting panel exercises the same masked code paths as a real
66
+ Wind feed — every test in ``tests/`` runs against this generator.
67
+ """
68
+ cfg = cfg or SyntheticConfig()
69
+ rng = np.random.default_rng(cfg.seed)
70
+
71
+ T, N = cfg.n_dates, cfg.n_stocks
72
+ daily_drift = cfg.annual_drift / 252.0
73
+ daily_vol = cfg.annual_vol / np.sqrt(252.0)
74
+
75
+ # 1. market and idiosyncratic shocks --------------------------------
76
+ market_shock = rng.normal(daily_drift, daily_vol, size=T)
77
+ idio_shock = rng.normal(0.0, daily_vol * np.sqrt(1.0 - cfg.market_beta**2), size=(T, N))
78
+ log_ret = cfg.market_beta * market_shock[:, None] + idio_shock
79
+
80
+ # 2. price-limit clamp + halt mask ---------------------------------
81
+ log_limit = np.log1p(cfg.limit_pct)
82
+ log_ret = np.clip(log_ret, -log_limit, log_limit)
83
+ halts = rng.random(size=(T, N)) < cfg.halt_prob
84
+
85
+ # 3. integrate to price -------------------------------------------
86
+ init_price = rng.uniform(5.0, 50.0, size=N).astype(np.float32)
87
+ cum_log = np.cumsum(log_ret, axis=0)
88
+ close = init_price[None, :] * np.exp(cum_log)
89
+
90
+ # 4. OCHL around close --------------------------------------------
91
+ intraday_sigma = daily_vol * 0.5
92
+ open_ = close * np.exp(rng.normal(0.0, intraday_sigma, size=close.shape))
93
+ high = np.maximum(open_, close) * (1.0 + np.abs(rng.normal(0.0, intraday_sigma * 0.5, size=close.shape)))
94
+ low = np.minimum(open_, close) * (1.0 - np.abs(rng.normal(0.0, intraday_sigma * 0.5, size=close.shape)))
95
+ vwap = (open_ + high + low + close) / 4.0
96
+ volume = rng.lognormal(mean=15.0, sigma=0.5, size=close.shape).astype(np.float32)
97
+ volume[halts] = 0.0
98
+
99
+ # 5. mask: tradable iff not halted ---------------------------------
100
+ mask = ~halts
101
+
102
+ # 6. derive optional A-share fields --------------------------------
103
+ amount = (vwap * volume).astype(np.float32)
104
+ # last_close[t] = close[t-1]; day 0 falls back to the open price so
105
+ # the ±limit_pct bands stay well-defined.
106
+ last_close = np.empty_like(close, dtype=np.float32)
107
+ last_close[0] = open_[0]
108
+ last_close[1:] = close[:-1]
109
+ # Round limit prices to two decimals to mimic exchange convention.
110
+ limit_up = np.round(last_close * (1.0 + cfg.limit_pct), 2).astype(np.float32)
111
+ limit_down = np.round(last_close * (1.0 - cfg.limit_pct), 2).astype(np.float32)
112
+
113
+ # 7. assemble Panel ------------------------------------------------
114
+ dates = pd.bdate_range(cfg.start_date, periods=T).to_numpy()
115
+ stocks = np.asarray([f"SYN{idx:05d}" for idx in range(N)])
116
+
117
+ def _t(arr: np.ndarray) -> torch.Tensor:
118
+ return torch.from_numpy(arr.astype(np.float32)).to(cfg.device)
119
+
120
+ panel = Panel.from_tensors(
121
+ dates=dates,
122
+ stocks=stocks,
123
+ fields={
124
+ "open": _t(open_),
125
+ "high": _t(high),
126
+ "low": _t(low),
127
+ "close": _t(close),
128
+ "volume": _t(volume),
129
+ "vwap": _t(vwap),
130
+ "amount": _t(amount),
131
+ "limit_up": _t(limit_up),
132
+ "limit_down": _t(limit_down),
133
+ "last_close": _t(last_close),
134
+ },
135
+ mask=torch.from_numpy(mask).to(cfg.device),
136
+ )
137
+ panel.assert_consistent()
138
+ return panel
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ import torch
6
+ from typing import Sequence, Optional, Union
7
+ import yfinance as yf
8
+
9
+ from .panel import Panel
10
+
11
+ def load_yfinance_panel(
12
+ tickers: Sequence[str],
13
+ start: str,
14
+ end: str,
15
+ device: Union[str, torch.device] = "cpu",
16
+ proxy_vwap: bool = True
17
+ ) -> Panel:
18
+ """Download data from yfinance and return a Panel.
19
+
20
+ Parameters
21
+ ----------
22
+ tickers : Sequence[str]
23
+ List of tickers to download. For A-shares, use standard suffixes (e.g., '000001.SZ', '600000.SS').
24
+ start : str
25
+ Start date (e.g. "2020-01-01").
26
+ end : str
27
+ End date (e.g. "2023-12-31").
28
+ device : str or torch.device
29
+ Where to allocate the resulting tensors.
30
+ proxy_vwap : bool
31
+ If True, estimates VWAP as (Open + Close + High + Low) / 4.
32
+ yfinance doesn't provide VWAP natively.
33
+ """
34
+ if not tickers:
35
+ raise ValueError("Tickers list cannot be empty")
36
+
37
+ df = yf.download(list(tickers), start=start, end=end)
38
+ if df.empty:
39
+ raise ValueError(f"No data returned for tickers {tickers} from {start} to {end}")
40
+
41
+ # Standardize column naming
42
+ # For single ticker, yfinance returns flat columns. For multiple, it returns MultiIndex.
43
+ if isinstance(df.columns, pd.MultiIndex):
44
+ # MultiIndex columns: level 0 is Price (e.g. 'Close', 'Open'), level 1 is Ticker
45
+ pass
46
+ else:
47
+ # Flat index: this means a single ticker was given to yf.download
48
+ if len(tickers) == 1:
49
+ df.columns = pd.MultiIndex.from_product([df.columns, [tickers[0]]])
50
+ else:
51
+ raise ValueError("Unexpected flat columns for multiple tickers")
52
+
53
+ # Extract wide DataFrames
54
+ fields_wide = {}
55
+
56
+ def get_wide_df(price_col):
57
+ if price_col in df.columns.get_level_values(0):
58
+ # Extract cross-section and sort columns by ticker order
59
+ wide = df[price_col].copy()
60
+ # If some tickers missing, add them
61
+ for t in tickers:
62
+ if t not in wide.columns:
63
+ wide[t] = np.nan
64
+ return wide[list(tickers)]
65
+ return None
66
+
67
+ open_df = get_wide_df("Open")
68
+ high_df = get_wide_df("High")
69
+ low_df = get_wide_df("Low")
70
+ close_df = get_wide_df("Close")
71
+ volume_df = get_wide_df("Volume")
72
+
73
+ if any(x is None for x in [open_df, high_df, low_df, close_df, volume_df]):
74
+ raise ValueError("Missing required OHLCV columns from yfinance data")
75
+
76
+ fields_wide["open"] = open_df
77
+ fields_wide["high"] = high_df
78
+ fields_wide["low"] = low_df
79
+ fields_wide["close"] = close_df
80
+ fields_wide["volume"] = volume_df
81
+
82
+ if proxy_vwap:
83
+ fields_wide["vwap"] = (open_df + close_df + high_df + low_df) / 4.0
84
+
85
+ dates = df.index.to_numpy()
86
+ stocks = np.array(list(tickers))
87
+
88
+ # Mask where close is not NaN
89
+ mask = (~open_df.isna() & ~high_df.isna() & ~low_df.isna() & ~close_df.isna() & ~volume_df.isna()).to_numpy()
90
+
91
+ tensors = {
92
+ name: torch.from_numpy(df_.fillna(0.0).to_numpy(dtype=np.float32).copy()).to(device)
93
+ for name, df_ in fields_wide.items()
94
+ }
95
+
96
+ return Panel.from_tensors(
97
+ dates=dates,
98
+ stocks=stocks,
99
+ fields=tensors,
100
+ mask=torch.from_numpy(mask.copy()).to(device),
101
+ )
@@ -0,0 +1,42 @@
1
+ """Tensor-vectorised factor engine + Alpha101-style library.
2
+
3
+ Design contract
4
+ ---------------
5
+ * Every primitive in :mod:`tensor_factors` accepts and returns a
6
+ ``(values, mask)`` pair; values at masked-out cells are arbitrary
7
+ (typically 0) but must not contaminate aggregates.
8
+ * Cross-sectional functions reduce along ``dim=1`` (stock axis).
9
+ * Time-series functions slide along ``dim=0`` (date axis) with a
10
+ user-supplied look-back window.
11
+ * All operations are torch ops — they run on CPU or GPU and are
12
+ autograd-friendly when needed.
13
+ """
14
+ from .tensor_factors import (
15
+ cs_rank,
16
+ cs_zscore,
17
+ ts_corr,
18
+ ts_cov,
19
+ ts_max,
20
+ ts_mean,
21
+ ts_min,
22
+ ts_rank,
23
+ ts_std,
24
+ ts_sum,
25
+ ewma,
26
+ delay,
27
+ delta,
28
+ )
29
+ from .neutralize import neutralize_cs, neutralize_industry
30
+ from .bias import limit_move_mask
31
+ from .alpha101 import compute_alpha_set, ALPHA_REGISTRY
32
+ from .legacy_factors import compute_legacy_set, LEGACY_REGISTRY
33
+
34
+ __all__ = [
35
+ "cs_rank", "cs_zscore",
36
+ "ts_corr", "ts_cov", "ts_max", "ts_mean", "ts_min",
37
+ "ts_rank", "ts_std", "ts_sum", "ewma", "delay", "delta",
38
+ "neutralize_cs", "neutralize_industry",
39
+ "limit_move_mask",
40
+ "compute_alpha_set", "ALPHA_REGISTRY",
41
+ "compute_legacy_set", "LEGACY_REGISTRY",
42
+ ]
@@ -0,0 +1,288 @@
1
+ """add_* family: additional factor variants (30 factors).
2
+
3
+ Ported from legacy Feature.py add_001-add_030.
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, _close_loc, _amount
14
+
15
+ Tensor = torch.Tensor
16
+
17
+
18
+ @register_legacy_factor("add_001")
19
+ def add_001(panel: Panel) -> Tuple[Tensor, Tensor]:
20
+ """rank(Δclose, 5) * rank(Δvolume, 5)."""
21
+ dc, dm1 = delta(panel.close, panel.mask, 5)
22
+ dv, dm2 = delta(panel.volume, panel.mask, 5)
23
+ m = dm1 & dm2
24
+ r1, _ = cs_rank(dc, m)
25
+ r2, _ = cs_rank(dv, m)
26
+ return (r1 * r2) * m.float(), m
27
+
28
+
29
+ @register_legacy_factor("add_002")
30
+ def add_002(panel: Panel) -> Tuple[Tensor, Tensor]:
31
+ """-(close - ts_mean(close, 10)) / ts_mean(close, 10) * rank(volume)."""
32
+ avg, am = ts_mean(panel.close, panel.mask, 10)
33
+ dev = -(panel.close - avg) / avg.clamp_min(1e-9)
34
+ vr, vm = cs_rank(panel.volume, panel.mask)
35
+ m = am & vm
36
+ return (dev * vr) * m.float(), m
37
+
38
+
39
+ @register_legacy_factor("add_003")
40
+ def add_003(panel: Panel) -> Tuple[Tensor, Tensor]:
41
+ """sign(Δclose, 1) * (1 + |Δclose/close|) * Δvolume/volume."""
42
+ dc, dm = delta(panel.close, panel.mask, 1)
43
+ dv, dvm = delta(panel.volume, panel.mask, 1)
44
+ m = dm & dvm
45
+ sign_dc = torch.sign(dc)
46
+ mag = 1.0 + (dc / panel.close.clamp_min(1e-9)).abs()
47
+ vol_chg = dv / panel.volume.clamp_min(1.0)
48
+ return (sign_dc * mag * vol_chg) * m.float(), m
49
+
50
+
51
+ @register_legacy_factor("add_004")
52
+ def add_004(panel: Panel) -> Tuple[Tensor, Tensor]:
53
+ """ts_rank(volume, 20) * ts_rank(-close_loc, 10)."""
54
+ vr, vm = ts_rank(panel.volume, panel.mask, 20)
55
+ cl = -_close_loc(panel)
56
+ cr, cm = ts_rank(cl, panel.mask, 10)
57
+ m = vm & cm
58
+ return (vr * cr) * m.float(), m
59
+
60
+
61
+ @register_legacy_factor("add_005")
62
+ def add_005(panel: Panel) -> Tuple[Tensor, Tensor]:
63
+ """ts_sum(close > delay(close, 1), 12) - ts_sum(close < delay(close, 1), 12)."""
64
+ prev, pm = delay(panel.close, panel.mask, 1)
65
+ up = (panel.close > prev).float() * pm.float()
66
+ dn = (panel.close < prev).float() * pm.float()
67
+ s_up, um = ts_sum(up, pm, 12)
68
+ s_dn, dm2 = ts_sum(dn, pm, 12)
69
+ m = um & dm2
70
+ return (s_up - s_dn) * m.float(), m
71
+
72
+
73
+ @register_legacy_factor("add_006")
74
+ def add_006(panel: Panel) -> Tuple[Tensor, Tensor]:
75
+ """ewma(close/delay(close,1)-1, 1/12) - close/delay(close,12)-1."""
76
+ prev1, pm1 = delay(panel.close, panel.mask, 1)
77
+ ret1 = (panel.close / prev1.clamp_min(1e-9) - 1.0) * pm1.float()
78
+ ew, em = ewma(ret1, pm1, alpha=1.0 / 12.0)
79
+ prev12, pm12 = delay(panel.close, panel.mask, 12)
80
+ ret12 = (panel.close / prev12.clamp_min(1e-9) - 1.0) * pm12.float()
81
+ m = em & pm12
82
+ return (ew - ret12) * m.float(), m
83
+
84
+
85
+ @register_legacy_factor("add_007")
86
+ def add_007(panel: Panel) -> Tuple[Tensor, Tensor]:
87
+ """corr(rank(close), rank(volume), 5)."""
88
+ cr, cm = cs_rank(panel.close, panel.mask)
89
+ vr, vm = cs_rank(panel.volume, panel.mask)
90
+ m = cm & vm
91
+ return ts_corr(cr, vr, m, 5)
92
+
93
+
94
+ @register_legacy_factor("add_008")
95
+ def add_008(panel: Panel) -> Tuple[Tensor, Tensor]:
96
+ """ts_std(close/delay(close,1)-1, 20)."""
97
+ prev, pm = delay(panel.close, panel.mask, 1)
98
+ ret = (panel.close / prev.clamp_min(1e-9) - 1.0) * pm.float()
99
+ return ts_std(ret, pm, 20)
100
+
101
+
102
+ @register_legacy_factor("add_009")
103
+ def add_009(panel: Panel) -> Tuple[Tensor, Tensor]:
104
+ """(high + low) / 2 - delay((high + low) / 2, 3)."""
105
+ mid = (panel.high + panel.low) * 0.5
106
+ prev, pm = delay(mid, panel.mask, 3)
107
+ return (mid - prev) * pm.float(), pm
108
+
109
+
110
+ @register_legacy_factor("add_010")
111
+ def add_010(panel: Panel) -> Tuple[Tensor, Tensor]:
112
+ """ts_max(high, 5) - ts_min(low, 5) scaled by close."""
113
+ mx, mm = ts_max(panel.high, panel.mask, 5)
114
+ mn, mnm = ts_min(panel.low, panel.mask, 5)
115
+ m = mm & mnm
116
+ out = (mx - mn) / panel.close.clamp_min(1e-9)
117
+ return out * m.float(), m
118
+
119
+
120
+ @register_legacy_factor("add_011")
121
+ def add_011(panel: Panel) -> Tuple[Tensor, Tensor]:
122
+ """ts_corr(vwap, volume, 10)."""
123
+ return ts_corr(panel.vwap, panel.volume, panel.mask, 10)
124
+
125
+
126
+ @register_legacy_factor("add_012")
127
+ def add_012(panel: Panel) -> Tuple[Tensor, Tensor]:
128
+ """rank(open - mean(open,10)) * rank(abs(close-vwap))."""
129
+ avg_o, am = ts_mean(panel.open, panel.mask, 10)
130
+ r1, rm1 = cs_rank((panel.open - avg_o) * am.float(), am)
131
+ diff = (panel.close - panel.vwap).abs()
132
+ r2, rm2 = cs_rank(diff, panel.mask)
133
+ m = rm1 & rm2
134
+ return (r1 * r2) * m.float(), m
135
+
136
+
137
+ @register_legacy_factor("add_013")
138
+ def add_013(panel: Panel) -> Tuple[Tensor, Tensor]:
139
+ """(high * low)^0.5 - vwap."""
140
+ out = (panel.high * panel.low).clamp_min(0).sqrt() - panel.vwap
141
+ return out * panel.mask.float(), panel.mask
142
+
143
+
144
+ @register_legacy_factor("add_014")
145
+ def add_014(panel: Panel) -> Tuple[Tensor, Tensor]:
146
+ """close - delay(close, 5) scaled by delay(close, 5)."""
147
+ prev, pm = delay(panel.close, panel.mask, 5)
148
+ out = (panel.close - prev) / prev.clamp_min(1e-9)
149
+ return out * pm.float(), pm
150
+
151
+
152
+ @register_legacy_factor("add_015")
153
+ def add_015(panel: Panel) -> Tuple[Tensor, Tensor]:
154
+ """open / delay(close, 1) - 1 (intraday gap rank)."""
155
+ prev, pm = delay(panel.close, panel.mask, 1)
156
+ gap = (panel.open / prev.clamp_min(1e-9) - 1.0)
157
+ return cs_rank(gap * pm.float(), pm)
158
+
159
+
160
+ @register_legacy_factor("add_016")
161
+ def add_016(panel: Panel) -> Tuple[Tensor, Tensor]:
162
+ """ts_max(vwap, 10) - vwap."""
163
+ mx, mm = ts_max(panel.vwap, panel.mask, 10)
164
+ out = mx - panel.vwap
165
+ return out * mm.float(), mm
166
+
167
+
168
+ @register_legacy_factor("add_017")
169
+ def add_017(panel: Panel) -> Tuple[Tensor, Tensor]:
170
+ """vwap - ts_min(vwap, 10)."""
171
+ mn, mm = ts_min(panel.vwap, panel.mask, 10)
172
+ out = panel.vwap - mn
173
+ return out * mm.float(), mm
174
+
175
+
176
+ @register_legacy_factor("add_018")
177
+ def add_018(panel: Panel) -> Tuple[Tensor, Tensor]:
178
+ """mean(abs(close - open) / (high - low + 1e-9), 5)."""
179
+ ratio = (panel.close - panel.open).abs() / (panel.high - panel.low).clamp_min(1e-9)
180
+ return ts_mean(ratio, panel.mask, 5)
181
+
182
+
183
+ @register_legacy_factor("add_019")
184
+ def add_019(panel: Panel) -> Tuple[Tensor, Tensor]:
185
+ """sum(close > open, 5) / 5 - bullishness ratio."""
186
+ bull = (panel.close > panel.open).float()
187
+ s, sm = ts_sum(bull, panel.mask, 5)
188
+ return (s / 5.0) * sm.float(), sm
189
+
190
+
191
+ @register_legacy_factor("add_020")
192
+ def add_020(panel: Panel) -> Tuple[Tensor, Tensor]:
193
+ """std(close, 10) / mean(close, 10) - coefficient of variation."""
194
+ sd, sm = ts_std(panel.close, panel.mask, 10)
195
+ avg, am = ts_mean(panel.close, panel.mask, 10)
196
+ m = sm & am
197
+ out = sd / avg.clamp_min(1e-9)
198
+ return out * m.float(), m
199
+
200
+
201
+ @register_legacy_factor("add_021")
202
+ def add_021(panel: Panel) -> Tuple[Tensor, Tensor]:
203
+ """rank(corr(close, volume, 20))."""
204
+ c, cm = ts_corr(panel.close, panel.volume, panel.mask, 20)
205
+ return cs_rank(c * cm.float(), cm)
206
+
207
+
208
+ @register_legacy_factor("add_022")
209
+ def add_022(panel: Panel) -> Tuple[Tensor, Tensor]:
210
+ """ewma(volume, 2/6) / ewma(volume, 2/24) - volume wave."""
211
+ ew1, m1 = ewma(panel.volume, panel.mask, alpha=2.0 / 6.0)
212
+ ew2, m2 = ewma(panel.volume, panel.mask, alpha=2.0 / 24.0)
213
+ m = m1 & m2
214
+ out = ew1 / ew2.clamp_min(1.0)
215
+ return out * m.float(), m
216
+
217
+
218
+ @register_legacy_factor("add_023")
219
+ def add_023(panel: Panel) -> Tuple[Tensor, Tensor]:
220
+ """close - ts_mean(close, 5) (deviation from 5-day MA)."""
221
+ avg, am = ts_mean(panel.close, panel.mask, 5)
222
+ out = (panel.close - avg) / panel.close.clamp_min(1e-9)
223
+ return out * am.float(), am
224
+
225
+
226
+ @register_legacy_factor("add_024")
227
+ def add_024(panel: Panel) -> Tuple[Tensor, Tensor]:
228
+ """close - ts_mean(close, 20) (deviation from 20-day MA)."""
229
+ avg, am = ts_mean(panel.close, panel.mask, 20)
230
+ out = (panel.close - avg) / panel.close.clamp_min(1e-9)
231
+ return out * am.float(), am
232
+
233
+
234
+ @register_legacy_factor("add_025")
235
+ def add_025(panel: Panel) -> Tuple[Tensor, Tensor]:
236
+ """mean(close, 5) / mean(close, 20)."""
237
+ m5, mm5 = ts_mean(panel.close, panel.mask, 5)
238
+ m20, mm20 = ts_mean(panel.close, panel.mask, 20)
239
+ m = mm5 & mm20
240
+ out = m5 / m20.clamp_min(1e-9)
241
+ return out * m.float(), m
242
+
243
+
244
+ @register_legacy_factor("add_026")
245
+ def add_026(panel: Panel) -> Tuple[Tensor, Tensor]:
246
+ """ts_std(volume, 5) / ts_mean(volume, 5) (volume CV)."""
247
+ sd, sm = ts_std(panel.volume, panel.mask, 5)
248
+ avg, am = ts_mean(panel.volume, panel.mask, 5)
249
+ m = sm & am
250
+ out = sd / avg.clamp_min(1.0)
251
+ return out * m.float(), m
252
+
253
+
254
+ @register_legacy_factor("add_027")
255
+ def add_027(panel: Panel) -> Tuple[Tensor, Tensor]:
256
+ """rank(-delta(close, 3) * Δvolume)."""
257
+ dc, dm1 = delta(panel.close, panel.mask, 3)
258
+ dv, dm2 = delta(panel.volume, panel.mask, 3)
259
+ m = dm1 & dm2
260
+ out = -dc * dv
261
+ return cs_rank(out * m.float(), m)
262
+
263
+
264
+ @register_legacy_factor("add_028")
265
+ def add_028(panel: Panel) -> Tuple[Tensor, Tensor]:
266
+ """(close - open) / (high - low + 1e-9)."""
267
+ out = (panel.close - panel.open) / (panel.high - panel.low).clamp_min(1e-9)
268
+ return out * panel.mask.float(), panel.mask
269
+
270
+
271
+ @register_legacy_factor("add_029")
272
+ def add_029(panel: Panel) -> Tuple[Tensor, Tensor]:
273
+ """ts_mean((close - open) / (high - low + eps), 10)."""
274
+ ratio = (panel.close - panel.open) / (panel.high - panel.low).clamp_min(1e-9)
275
+ return ts_mean(ratio, panel.mask, 10)
276
+
277
+
278
+ @register_legacy_factor("add_030")
279
+ def add_030(panel: Panel) -> Tuple[Tensor, Tensor]:
280
+ """rank(sum(ret, 5)) * rank(sum(ret, 20))."""
281
+ ret, rm = delta(panel.close, panel.mask, 1)
282
+ ret = ret / panel.close.clamp_min(1e-9)
283
+ s5, m5 = ts_sum(ret, rm, 5)
284
+ s20, m20 = ts_sum(ret, rm, 20)
285
+ m = m5 & m20
286
+ r1, _ = cs_rank(s5, m)
287
+ r2, _ = cs_rank(s20, m)
288
+ return (r1 * r2) * m.float(), m
@@ -0,0 +1,98 @@
1
+ """alpha_* family: curated WorldQuant Alpha101 signals (9 factors).
2
+
3
+ These factors are adapted from the Alpha101 formulary (Kakushadze, 2016)
4
+ and re-implemented on masked-tensor primitives. They cover momentum,
5
+ reversal, volume--price correlation, volatility, and intraday patterns.
6
+ """
7
+ from __future__ import annotations
8
+ from typing import Tuple
9
+ import torch
10
+
11
+ from ..data.panel import Panel
12
+ from .tensor_factors import (
13
+ cs_rank, cs_zscore, delta, ts_corr, ts_rank, ts_sum,
14
+ )
15
+ from .legacy_factors import register_legacy_factor
16
+
17
+ Tensor = torch.Tensor
18
+
19
+
20
+ @register_legacy_factor("alpha_001")
21
+ def alpha_001(panel: Panel) -> Tuple[Tensor, Tensor]:
22
+ """cs_rank(ts_rank(close, 5)) - 0.5 — momentum via rank of recent highs."""
23
+ z, m = ts_rank(panel.close, panel.mask, 5)
24
+ rank, _ = cs_rank(z, m)
25
+ return rank - 0.5, m
26
+
27
+
28
+ @register_legacy_factor("alpha_002")
29
+ def alpha_002(panel: Panel) -> Tuple[Tensor, Tensor]:
30
+ """-corr(rank(Δ log volume, 2), rank((C-O)/O), 6) — volume-vs-intraday reversal."""
31
+ log_vol = (panel.volume.clamp_min(1.0)).log()
32
+ d, dm = delta(log_vol, panel.mask, 2)
33
+ a, am = cs_rank(d, dm)
34
+
35
+ co = (panel.close - panel.open) / panel.open.clamp_min(1e-9)
36
+ b, bm = cs_rank(co, panel.mask)
37
+
38
+ common = am & bm
39
+ corr, cm = ts_corr(a, b, common, 6)
40
+ return -corr, cm
41
+
42
+
43
+ @register_legacy_factor("alpha_003")
44
+ def alpha_003(panel: Panel) -> Tuple[Tensor, Tensor]:
45
+ """-corr(rank(open), rank(volume), 10) — open/volume rank divergence."""
46
+ a, am = cs_rank(panel.open, panel.mask)
47
+ b, bm = cs_rank(panel.volume, panel.mask)
48
+ corr, cm = ts_corr(a, b, am & bm, 10)
49
+ return -corr, cm
50
+
51
+
52
+ @register_legacy_factor("alpha_004")
53
+ def alpha_004(panel: Panel) -> Tuple[Tensor, Tensor]:
54
+ """-ts_rank(rank(low), 9) — low-quantile mean reversion."""
55
+ r, m = cs_rank(panel.low, panel.mask)
56
+ z, zm = ts_rank(r, m, 9)
57
+ return -z, zm
58
+
59
+
60
+ @register_legacy_factor("alpha_006")
61
+ def alpha_006(panel: Panel) -> Tuple[Tensor, Tensor]:
62
+ """-corr(open, volume, 10) — open/volume rolling correlation."""
63
+ corr, cm = ts_corr(panel.open, panel.volume, panel.mask, 10)
64
+ return -corr, cm
65
+
66
+
67
+ @register_legacy_factor("alpha_007")
68
+ def alpha_007(panel: Panel) -> Tuple[Tensor, Tensor]:
69
+ """Deviation from 20-day mean, cross-sectionally z-scored."""
70
+ summed, sm = ts_sum(panel.close, panel.mask, 20)
71
+ mean = summed / 20.0
72
+ dev = (panel.close - mean) * sm
73
+ z, zm = cs_zscore(dev, sm)
74
+ return z, zm
75
+
76
+
77
+ @register_legacy_factor("alpha_012")
78
+ def alpha_012(panel: Panel) -> Tuple[Tensor, Tensor]:
79
+ """sign(Δvolume) × (−Δclose) — volume-change reversal."""
80
+ dv, dvm = delta(panel.volume, panel.mask, 1)
81
+ dc, dcm = delta(panel.close, panel.mask, 1)
82
+ out = torch.sign(dv) * (-dc)
83
+ return out, dvm & dcm
84
+
85
+
86
+ @register_legacy_factor("alpha_053")
87
+ def alpha_053(panel: Panel) -> Tuple[Tensor, Tensor]:
88
+ """-Δ(close-location, 9) — 9-day change in intraday close position."""
89
+ closeloc = ((panel.close - panel.low) - (panel.high - panel.close)) / (panel.close - panel.low + 1e-12)
90
+ d, dm = delta(closeloc, panel.mask, 9)
91
+ return -d, dm
92
+
93
+
94
+ @register_legacy_factor("alpha_101")
95
+ def alpha_101(panel: Panel) -> Tuple[Tensor, Tensor]:
96
+ """(close - open) / (high - low + eps) — intraday close location within range."""
97
+ out = (panel.close - panel.open) / (panel.high - panel.low + 1e-3)
98
+ return out * panel.mask.float(), panel.mask