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