Files
AlphaMaster/model_core/ops.py
T
2026-07-21 09:33:43 +08:00

738 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
model_core/ops.py -- 算子库(Operator_Library, R2)
本模块把历史上手工维护的 `OPS_CONFIG` 列表迁移为由声明式注册层
(`model_core.registry.Registry`)驱动的 `OPERATOR_REGISTRY`。所有算子先以
`OperatorSpec(name, arity, transform)` 注册进 `OPERATOR_REGISTRY`,`OPS_CONFIG`
随后作为「导出视图」由注册表派生(`[(name, transform, arity), ...]`),保持对
下游 `vocab.py` / `vm.py` 的 import 兼容与既有元组结构。
统一契约(R2.8, R2.9, R2.13):
- 形状契约:所有算子输入 `[N, T]`、输出 `[N, T]`。
- 二元/三元算子在入口校验各操作数形状一致,不一致抛 `ShapeError` 且不产出张量。
- 注册表存储 name(≤64 字符)与 arity(本模块算子均为 1/2/3)。
说明:现有算子多以 lambda 定义,`inspect` 不总能可靠解析 arity(内建函数、被
包装函数等)。注册时以显式声明的 arity 为准;二元/三元算子经形状校验包装后为
可变位置参数形式(`*operands`),注册层会跳过 arity 观测校验,从而避免对既有算子
的 `ArityMismatchError` 误报。
"""
import torch
from .registry import OperatorSpec, Registry
# ── 算子层错误类型(对应 design「错误类型模型」,归为算子层)──────────────
class ShapeError(Exception):
"""算子操作数形状不兼容或错误(R2.13)。
二元/三元算子在入口发现各操作数形状不一致时抛出,且不产出张量。
"""
def _ts_delay(x: torch.Tensor, d: int) -> torch.Tensor:
if d == 0: return x
pad = torch.zeros((x.shape[0], d), device=x.device, dtype=x.dtype)
return torch.cat([pad, x[:, :-d]], dim=1)
def _op_gate(condition: torch.Tensor, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
mask = (condition > 0).float()
return mask * x + (1.0 - mask) * y
def _op_jump(x: torch.Tensor) -> torch.Tensor:
"""降低稀疏度:阈值从 3σ 改为 1.5σ,让更多时间步有非零输出。
因果 expanding zscore:每个 t 仅用 x[:, :t+1] 计算 mean/std,
避免原 dim=1 全局聚合(含未来统计量)引入的 look-ahead bias。
P2-12 修复:t=0 时 mean=x[0]、z=0,输出恒为 tanh(-1.5)≈-0.905(与输入无关),
前几个 bar 输出几乎是常数,作为因子起点会引入系统性偏差。修复:warm-up 期
(t < min_warmup,默认 5)输出 0(中性),等累积足够样本后再用真实 zscore。
"""
N, T = x.shape
cnt = torch.arange(1, T + 1, device=x.device, dtype=x.dtype).view(1, T)
cumsum = x.cumsum(dim=1)
mean = cumsum / cnt # [N,T],t 位 = x[:,:t+1].mean()
cumsum_sq = (x * x).cumsum(dim=1)
var = (cumsum_sq / cnt) - mean * mean # E[x^2] - E[x]^2
std = var.clamp(min=1e-12).sqrt() + 1e-6
z = (x - mean) / std
out = torch.tanh(z - 1.5) # tanh 软化,不再产生全零区间
# P2-12: warm-up 期输出 0,避免 t=0 输出常数 -0.905 污染因子
min_warmup = 5
if T > min_warmup:
warmup_mask = torch.arange(T, device=x.device) < min_warmup
out[:, warmup_mask] = 0.0
return out
def _op_decay(x: torch.Tensor) -> torch.Tensor:
"""指数衰减加权:x + 0.8*x[t-1] + 0.6*x[t-2]。
P2-11 修复:原系数和=2.4 未归一化,输出幅度是输入的 2.4 倍,可能放大下游
tanh 饱和。改为归一化(除以系数和),与 _op_wma 语义一致。
"""
# 系数 [1.0, 0.8, 0.6],和=2.4,归一化为 [0.4167, 0.3333, 0.25]
return (x + 0.8 * _ts_delay(x, 1) + 0.6 * _ts_delay(x, 2)) / 2.4
def _op_wma(x: torch.Tensor) -> torch.Tensor:
"""加权移动平均(权重 3,2,1),平滑信号,减少剥头皮"""
return (3.0 * x + 2.0 * _ts_delay(x, 1) + 1.0 * _ts_delay(x, 2)) / 6.0
# ── 时序滑动窗口辅助函数(不使用 @torch.jit.script,lambda 不兼容 JIT)──────
def _ts_rolling(x: torch.Tensor, d: int) -> torch.Tensor:
"""unfold 实现因果滑动窗口,返回 [N, T, d] 的窗口张量。"""
N, T = x.shape
pad = torch.zeros(N, d - 1, device=x.device, dtype=x.dtype)
return torch.cat([pad, x], dim=1).unfold(1, d, 1) # [N, T, d]
def _ts_mean(x: torch.Tensor, d: int) -> torch.Tensor:
"""因果滑动均值,返回 [N, T]。"""
return _ts_rolling(x, d).mean(dim=-1)
def _ts_std(x: torch.Tensor, d: int) -> torch.Tensor:
"""因果滑动标准差(ddof=0),返回 [N, T],下界 1e-6。"""
w = _ts_rolling(x, d) # [N, T, d]
m = w.mean(dim=-1, keepdim=True)
std = ((w - m) ** 2).mean(dim=-1).sqrt() + 1e-6
return torch.nan_to_num(std, nan=0.0)
def _ts_rank(x: torch.Tensor, d: int) -> torch.Tensor:
"""因果滑动排名(严格小于当前值的比例),返回 [N, T],值域 [0, 1)。"""
w = _ts_rolling(x, d) # [N, T, d]
cur = w[:, :, -1:] # 当前值,[N, T, 1]
rank = (w < cur).float().mean(dim=-1) # [N, T]
return torch.nan_to_num(rank, nan=0.0)
def _ts_corr_10(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
"""x 与 y 的 10 周期因果滑动 Pearson 相关系数,返回 [N, T],值域 [-1, 1]。
当 x 或 y 在窗口内为常数(std < 1e-6)时,该位置输出 0。
"""
d = 10
wx = _ts_rolling(x, d) # [N, T, 10]
wy = _ts_rolling(y, d)
mx = wx.mean(dim=-1, keepdim=True)
my = wy.mean(dim=-1, keepdim=True)
cov = ((wx - mx) * (wy - my)).mean(dim=-1)
sx = ((wx - mx) ** 2).mean(dim=-1).sqrt() # [N, T]
sy = ((wy - my) ** 2).mean(dim=-1).sqrt()
# 常数窗口(std < 1e-6)输出 0
mask = (sx < 1e-6) | (sy < 1e-6)
corr = cov / (sx * sy + 1e-8)
corr[mask] = 0.0
return torch.nan_to_num(corr, nan=0.0, posinf=0.0, neginf=0.0)
# ── v3.0 新增算子 helper ─────────────────────────────────────────────
def _ema(x: torch.Tensor, alpha: float) -> torch.Tensor:
"""指数加权移动平均(因果)。alpha 越大越关注近期。"""
# 用递推实现太慢,用衰减权重卷积近似(窗口=20 足够)
w = min(20, x.shape[1])
weights = torch.tensor([alpha * (1 - alpha) ** i for i in range(w)],
device=x.device, dtype=x.dtype)
weights = weights / weights.sum()
pad = torch.zeros(x.shape[0], w - 1, device=x.device, dtype=x.dtype)
xp = torch.cat([pad, x], dim=1)
return torch.nn.functional.unfold(xp.unsqueeze(1), (1, w)).squeeze(1) * 0 # placeholder
# 上面的 unfold 对 1D 不直接 work,改用简单循环近似
def _ema_simple(x: torch.Tensor, span: int, exact: bool = False) -> torch.Tensor:
"""指数加权移动平均(因果),span 期。
P1-10 修复:统一使用 exact 递推路径,避免训练时 T 大走 vectorized 卷积近似、
实盘时 T 小走 exact 递推导致的 train-serve skew(虽然 max|Δ|<1e-4,但会
通过 MACD_HIST/PPO/TRIX_15/EMA_RATIO_12_26/TREND_STRENGTH_50/SUPERTREND_DIR/
SAR_DIST 等特征传播,在 tanh 阈值附近可能改变方向判定)。
实测 exact 递推在 N=1, T=10000 下耗时 < 5ms,性能损失可接受。
参数:
span: EMA 周期
exact: 保留参数兼容性,无论取值均使用 exact 递推(统一路径)
"""
alpha = 2.0 / (span + 1.0)
N, T = x.shape
if T == 0:
return x.clone()
if alpha >= 1.0:
return x.clone()
# ── 统一使用 exact 递推路径(O(N·T) 顺序累积)──────────────────
# 消除 T < 2*w_full 与 T >= 2*w_full 的路径分叉,保证训练/实盘数值一致
out = torch.zeros_like(x)
out[:, 0] = x[:, 0]
for t in range(1, T):
out[:, t] = alpha * x[:, t] + (1 - alpha) * out[:, t - 1]
return out
def _ts_quantile(x: torch.Tensor, d: int) -> torch.Tensor:
"""当前值在过去 d 期的分位数(0~1),与 TS_RANK 语义统一(严格小于)。
P1-12 修复:原实现用 (w <= cur) 包含当前值自身,值域 [1/d, 1];与
_ts_rank (w < cur) 严格小于、值域 [0, (d-1)/d] 语义不一致。统一为
严格小于,使两算子在边界值上行为一致,避免公式搜索选错"恢复算子"。
"""
w = _ts_rolling(x, d)
cur = w[:, :, -1:]
rank = (w < cur).float().mean(dim=-1)
return torch.nan_to_num(rank, nan=0.5)
def _ts_skew(x: torch.Tensor, d: int) -> torch.Tensor:
"""d 期偏度(三阶矩标准化),捕捉分布非对称性。"""
w = _ts_rolling(x, d)
m = w.mean(dim=-1, keepdim=True)
s = ((w - m) ** 2).mean(dim=-1).sqrt() + 1e-6
skew = ((w - m) ** 3).mean(dim=-1) / (s ** 3)
return torch.nan_to_num(skew, nan=0.0, posinf=0.0, neginf=0.0)
def _delta(x: torch.Tensor, d: int = 1) -> torch.Tensor:
"""d 期差分: x[t] - x[t-d],前 d 位置 0。Alpha 101 最常用算子。"""
if d == 0:
return x
out = torch.zeros_like(x)
out[:, d:] = x[:, d:] - x[:, :-d]
return out
def _ts_arg_max(x: torch.Tensor, d: int) -> torch.Tensor:
"""过去 d 期最大值的位置(归一化到 [0,1],0=最早,1=最近)。Alpha#001 核心算子。"""
w = _ts_rolling(x, d)
idx = w.argmax(dim=-1).float()
return idx / max(d - 1, 1)
def _ts_arg_min(x: torch.Tensor, d: int) -> torch.Tensor:
"""过去 d 期最小值的位置(归一化到 [0,1])。"""
w = _ts_rolling(x, d)
idx = w.argmin(dim=-1).float()
return idx / max(d - 1, 1)
def _decay_linear(x: torch.Tensor, d: int) -> torch.Tensor:
"""线性衰减加权平均(近期权重更高)。Alpha#98 核心算子。权重 = [1,2,...,d]/sum。"""
w = _ts_rolling(x, d)
weights = torch.arange(1, d + 1, dtype=x.dtype, device=x.device)
weights = weights / weights.sum()
return (w * weights).sum(dim=-1)
def _decay_exp(x: torch.Tensor, d: int, alpha: float = 0.5) -> torch.Tensor:
"""指数衰减加权平均(近期权重更高)。与 DECAY_LINEAR 对应,平滑更激进。"""
w = _ts_rolling(x, d)
weights = torch.tensor([alpha * (1 - alpha) ** i for i in range(d)],
dtype=x.dtype, device=x.device)
weights = weights / weights.sum()
return (w * weights).sum(dim=-1)
def _scale(x: torch.Tensor) -> torch.Tensor:
"""沿时间轴缩放到单位 L1 范数(Alpha#028/032 高频算子)。
scale(x)[t] = x[t] / sum(|x[1..t]|),避免未来信息用因果累积和。
"""
abs_x = x.abs()
cumsum = torch.cumsum(abs_x, dim=1) + 1e-6
return x / cumsum
def _ts_covariance(x: torch.Tensor, y: torch.Tensor, d: int) -> torch.Tensor:
"""d 期因果滑动协方差。"""
wx = _ts_rolling(x, d)
wy = _ts_rolling(y, d)
mx = wx.mean(dim=-1, keepdim=True)
my = wy.mean(dim=-1, keepdim=True)
cov = ((wx - mx) * (wy - my)).mean(dim=-1)
return torch.nan_to_num(cov, nan=0.0)
def _ts_product(x: torch.Tensor, d: int) -> torch.Tensor:
"""d 期因果滑动乘积。用对数累加避免数值爆炸:prod = exp(sum(log(x+1)))。
适合收益累积,输出接近 "过去 d 期累计收益"。
输入先 clamp 到 [-0.999, +inf),避免 log1p 在 x<=-1 时产生 NaN。
"""
x_safe = torch.clamp(x, -0.999, None)
log_x = torch.log1p(x_safe)
w = _ts_rolling(log_x, d)
# clamp 对数累加和防止 expm1 溢出到 float32 边界(>1e38)
log_sum = w.sum(dim=-1).clamp(-10.0, 10.0)
out = torch.expm1(log_sum)
return torch.nan_to_num(out, nan=0.0, posinf=0.0, neginf=0.0)
def _signed_power(x: torch.Tensor, a: float = 2.0) -> torch.Tensor:
"""带符号乘方: sign(x) * |x|^a。Alpha#001 SignedPower。保留符号同时放大极端值。"""
out = torch.sign(x) * torch.abs(x) ** a
# 防溢出:|x|^a 在链式调用中可能爆炸到 float32 边界(如 3.3e38),
# 导致后续 mean/std 计算溢出为 inf。clamp 到安全范围。
return torch.nan_to_num(out.clamp(-1e9, 1e9), nan=0.0, posinf=0.0, neginf=0.0)
# ── Cross_Sectional 算子 helper(沿 N 维,每时间步跨品种;R2.1, R2.2)───────
#
# 输入 `[N, T]`:N=品种数、T=时间步。计算沿 dim=0(N 维)逐时间步进行,
# 完全向量化(禁止逐时间步 Python 循环)。
# N=1(单品种,截面无分散)时按"恒等退化"保留输入信息,避免输出常数被公式
# 搜索误选(之前 CS_RANK/CS_SCALE 输出 0.5 常数、CS_NEUTRALIZE 输出 0 常数,
# 会污染公式语义)。全部 NaN-safe:出口 `nan_to_num`。
def _cs_rank(x: torch.Tensor) -> torch.Tensor:
"""每时间步跨品种百分位排名,值域 [0, 1](R2.1)。
对每一列(时间步)沿 N 维排名,归一化到 `[0, 1]`(rank/(N-1))。N=1 时截面
无分散,恒等退化(返回输入本身)保留时间序列信息。用双 argsort 向量化。
"""
N, T = x.shape
if N == 1:
# 单品种:恒等退化,避免常数 0.5 污染公式搜索
return torch.nan_to_num(x, nan=0.5, posinf=0.5, neginf=0.5)
order = x.argsort(dim=0) # 沿 N 维排序索引
ranks = torch.empty_like(x)
rank_vals = torch.arange(N, device=x.device, dtype=x.dtype).unsqueeze(1).expand(N, T)
ranks.scatter_(0, order, rank_vals) # ranks[order[i,t], t] = i
pct = ranks / (N - 1)
return torch.nan_to_num(pct, nan=0.5, posinf=0.5, neginf=0.5)
def _cs_scale(x: torch.Tensor) -> torch.Tensor:
"""每时间步跨品种缩放到 [0, 1]:`(x - min) / (max - min)`(R2.1)。
沿 N 维取每列的 min/max。零跨度(max==min)该列退化为 0.5;N=1 恒等退化。
完全向量化。
"""
N, T = x.shape
if N == 1:
# 单品种:恒等退化,避免常数 0.5 污染公式搜索
return torch.nan_to_num(x, nan=0.5, posinf=0.5, neginf=0.5)
mn = x.min(dim=0, keepdim=True).values # [1, T]
mx = x.max(dim=0, keepdim=True).values # [1, T]
span = mx - mn # [1, T]
zero_span = span.abs() < 1e-9 # [1, T]
span_safe = torch.where(zero_span, torch.ones_like(span), span)
out = (x - mn) / span_safe
out = torch.where(zero_span.expand_as(out), torch.full_like(out, 0.5), out)
return torch.nan_to_num(out, nan=0.5, posinf=0.5, neginf=0.5)
def _cs_neutralize(x: torch.Tensor) -> torch.Tensor:
"""每时间步减去跨品种算术均值(截面中性化,R2.2)。N=1 恒等退化。"""
N, T = x.shape
if N == 1:
# 单品种:恒等退化,避免常数 0 污染公式搜索
return torch.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)
mean = x.mean(dim=0, keepdim=True) # [1, T]
out = x - mean
return torch.nan_to_num(out, nan=0.0, posinf=0.0, neginf=0.0)
# ── 形状一致性校验包装(R2.9, R2.13)─────────────────────────────────────
def _with_shape_check(name: str, transform):
"""为二元/三元算子包装入口形状一致性校验。
调用时先校验各操作数形状完全一致,不一致抛 `ShapeError` 且不调用底层
transform(不产出张量)。包装后为可变位置参数形式,注册层将跳过 arity
观测校验(以显式声明 arity 为准)。
"""
def _checked(*operands: torch.Tensor) -> torch.Tensor:
base = operands[0].shape
for other in operands[1:]:
if other.shape != base:
raise ShapeError(
f"算子 '{name}' 操作数形状不一致: "
f"{tuple(base)} vs {tuple(other.shape)}"
)
return transform(*operands)
return _checked
# ── 初始算子定义(保持既有 44 个算子的命名、顺序与行为)──────────────────
#
# 每项为 (name, transform, arity)。此列表等价于历史 `OPS_CONFIG` 的内容与顺序,
# 用于注册进 `OPERATOR_REGISTRY`;`OPS_CONFIG` 随后由注册表派生为导出视图。
_INITIAL_OPERATORS = [
# ── 基础算子(token id = feat_offset+0~11)────────────────────────
('ADD', lambda x, y: x + y, 2),
('SUB', lambda x, y: x - y, 2),
('MUL', lambda x, y: x * y, 2),
('DIV', lambda x, y: x / (y + 1e-6), 2),
('NEG', lambda x: -x, 1),
('ABS', torch.abs, 1),
('SIGN', torch.sign, 1),
('GATE', _op_gate, 3),
('JUMP', _op_jump, 1), # 已降低稀疏度
('DECAY', _op_decay, 1),
('DELAY1', lambda x: _ts_delay(x, 1), 1),
('MAX3', lambda x: torch.max(x, torch.max(_ts_delay(x, 1), _ts_delay(x, 2))), 1),
# ── 时序算子(token id = feat_offset+12~21)───────────────────────
('TS_MEAN_5', lambda x: _ts_mean(x, 5), 1),
('TS_MEAN_10', lambda x: _ts_mean(x, 10), 1),
('TS_MEAN_20', lambda x: _ts_mean(x, 20), 1),
('TS_STD_5', lambda x: _ts_std(x, 5), 1),
('TS_STD_10', lambda x: _ts_std(x, 10), 1),
('TS_STD_20', lambda x: _ts_std(x, 20), 1),
('TS_RANK_5', lambda x: _ts_rank(x, 5), 1),
('TS_RANK_10', lambda x: _ts_rank(x, 10), 1),
('TS_RANK_20', lambda x: _ts_rank(x, 20), 1),
('TS_CORR_10', _ts_corr_10, 2),
# ── 趋势 / 动量类算子(token id = feat_offset+22~27)──────────────
# MOMENTUM_5: 短期均线 - 长期均线,捕捉趋势方向
('MOMENTUM_5', lambda x: _ts_mean(x, 5) - _ts_mean(x, 20), 1),
# MOMENTUM_10: 中期动量
('MOMENTUM_10', lambda x: _ts_mean(x, 10) - _ts_mean(x, 20), 1),
# TS_MAX_10: 10周期最大值,捕捉强势突破
('TS_MAX_10', lambda x: _ts_rolling(x, 10).max(dim=-1).values, 1),
# TS_MIN_10: 10周期最小值,捕捉弱势突破
('TS_MIN_10', lambda x: _ts_rolling(x, 10).min(dim=-1).values, 1),
# WMA: 加权移动平均,平滑信号
('WMA', _op_wma, 1),
# DELAY4: 延迟4根bar,构建中期动量差
('DELAY4', lambda x: _ts_delay(x, 4), 1),
# ── v3.0 新增算子(token id = feat_offset+28~33)──────────────────
('EMA_5', lambda x: _ema_simple(x, 5), 1),
('EMA_20', lambda x: _ema_simple(x, 20), 1),
('TS_QUANTILE_10', lambda x: _ts_quantile(x, 10), 1),
('TS_SKEW_10', lambda x: _ts_skew(x, 10), 1),
('TS_MIN_20', lambda x: _ts_rolling(x, 20).min(dim=-1).values, 1),
('TS_MAX_20', lambda x: _ts_rolling(x, 20).max(dim=-1).values, 1),
# ── v3.0 Alpha 101 + 补充算子(token id = feat_offset+34~43)──────
# Alpha 101 核心 4 个
('DELTA', lambda x: _delta(x, 1), 1),
('TS_ARG_MAX_5', lambda x: _ts_arg_max(x, 5), 1),
('TS_ARG_MIN_5', lambda x: _ts_arg_min(x, 5), 1),
('DECAY_LINEAR_5', lambda x: _decay_linear(x, 5), 1),
# 联网搜索补充 6 个
('SCALE', lambda x: _scale(x), 1),
('COVARIANCE_10', lambda x, y: _ts_covariance(x, y, 10), 2),
('PRODUCT_5', lambda x: _ts_product(x, 5), 1),
('SIGNED_POWER_2', lambda x: _signed_power(x, 2.0), 1),
('TS_DECAY_EXP_5', lambda x: _decay_exp(x, 5, 0.5), 1),
('DELTA_5', lambda x: _delta(x, 5), 1),
]
# ── Task 3.2 追加:Cross_Sectional 算子(沿 N 维,每时间步跨品种)──────────
#
# 追加在既有 44 个算子之后,保持既有算子命名/顺序/行为不变。均为 arity 1,
# 沿 dim=0(N 维)逐时间步向量化计算,NaN-safe。(R2.1, R2.2, R2.10)
_CROSS_SECTIONAL_OPERATORS = [
('CS_RANK', _cs_rank, 1), # 跨品种百分位排名 [0,1],N=1→0.5
('CS_SCALE', _cs_scale, 1), # 跨品种缩放 [0,1],零跨度/N=1→0.5
('CS_NEUTRALIZE', _cs_neutralize, 1), # 减跨品种均值,N=1→0
]
# ── 构建 OPERATOR_REGISTRY 并派生 OPS_CONFIG 导出视图 ─────────────────────
# 全局算子注册表(Operator_Library, R2)。所有算子的唯一事实来源。
OPERATOR_REGISTRY = Registry()
def _register_initial_operators(registry: Registry) -> None:
"""把初始算子注册进给定注册表。
二元/三元算子经 `_with_shape_check` 包装以在入口校验操作数形状一致性
(R2.13);一元算子直接注册。以显式声明的 arity 为准(R2.8)。
"""
for name, transform, arity in _INITIAL_OPERATORS:
fn = _with_shape_check(name, transform) if arity >= 2 else transform
registry.register_operator(
OperatorSpec(name=name, arity=arity, transform=fn)
)
def _register_cross_sectional_operators(registry: Registry) -> None:
"""把 Cross_Sectional 算子注册进给定注册表(Task 3.2)。
均为一元算子(arity 1),沿 N 维逐时间步计算;直接注册(无需形状校验包装)。
追加在初始 44 个算子之后,保持既有算子顺序在前、新算子在后(R2.1, R2.2)。
"""
for name, transform, arity in _CROSS_SECTIONAL_OPERATORS:
fn = _with_shape_check(name, transform) if arity >= 2 else transform
registry.register_operator(
OperatorSpec(name=name, arity=arity, transform=fn)
)
_register_initial_operators(OPERATOR_REGISTRY)
_register_cross_sectional_operators(OPERATOR_REGISTRY)
# `OPS_CONFIG` 现为 `OPERATOR_REGISTRY` 的导出视图,保持既有元组结构
# `[(name, transform, arity), ...]` 与下游 vocab.py / vm.py 的 import 兼容。
OPS_CONFIG = [
(spec.name, spec.transform, spec.arity)
for spec in OPERATOR_REGISTRY.operator_specs
]
# 动态断言:导出视图与注册表严格一致;且既有 44 个算子必须全部在册(不回归)。
assert len(OPS_CONFIG) == len(OPERATOR_REGISTRY.operator_specs), (
"OPS_CONFIG 导出视图与 OPERATOR_REGISTRY 长度不一致"
)
_EXPECTED_INITIAL_NAMES = {name for name, _, _ in _INITIAL_OPERATORS}
_REGISTERED_NAMES = set(OPERATOR_REGISTRY.operator_names)
assert _EXPECTED_INITIAL_NAMES <= _REGISTERED_NAMES, (
"既有算子未全部注册: "
f"{_EXPECTED_INITIAL_NAMES - _REGISTERED_NAMES}"
)
assert len(OPERATOR_REGISTRY.operator_specs) >= 44, (
f"OPERATOR_REGISTRY 至少应含 44 个既有算子,实际 {len(OPERATOR_REGISTRY.operator_specs)}"
)
# Task 3.2:Cross_Sectional 算子必须全部在册(总数随之增加)。
_EXPECTED_CS_NAMES = {name for name, _, _ in _CROSS_SECTIONAL_OPERATORS}
assert _EXPECTED_CS_NAMES <= _REGISTERED_NAMES, (
"Cross_Sectional 算子未全部注册: "
f"{_EXPECTED_CS_NAMES - _REGISTERED_NAMES}"
)
assert len(OPERATOR_REGISTRY.operator_specs) >= 44 + len(_CROSS_SECTIONAL_OPERATORS), (
"OPERATOR_REGISTRY 总数应含既有 44 个 + Cross_Sectional 算子,"
f"实际 {len(OPERATOR_REGISTRY.operator_specs)}"
)
# ── Task 3.3 追加:时序求和/极值与幅度变换算子 ────────────────────────────
#
# 新增 8 个算子(TS_SUM_5/10/20、MIN、MAX、POWER、SIGNED_LOG、SQRT),
# 追加在既有 47 个算子(44 初始 + 3 Cross_Sectional)之后,保持既有顺序不变。
# 全部算子出口 nan_to_num→0,满足形状契约 [N,T]→[N,T](R2.9, R2.10)。
# 时序求和用因果 unfold(零填充),每步 t 只用 [t-w+1..t](R2.11, R2.12)。
def _ts_sum(x: torch.Tensor, d: int) -> torch.Tensor:
"""因果滚动求和(R2.3):左侧 zero-pad + unfold,每步 t 只用 [t-w+1..t]。
部分窗口(warm-up 期 t<w)由 pad 决定——0 不含未来信息,输出有限(R2.12)。
"""
N, T = x.shape
pad = torch.zeros(N, d - 1, device=x.device, dtype=x.dtype)
return torch.cat([pad, x], dim=1).unfold(1, d, 1).sum(dim=-1)
def _power_signed(x: torch.Tensor, a: float = 2.0) -> torch.Tensor:
"""符号幂变换(R2.5):sign(x)*|x|^a,Alpha101 SignedPower 风格。
取 |x| 作为底数,避免负数的非整数幂;出口 clamp(-1e9, 1e9) 防爆炸。
"""
out = torch.sign(x) * torch.abs(x) ** a
return torch.nan_to_num(out.clamp(-1e9, 1e9), nan=0.0, posinf=0.0, neginf=0.0)
def _signed_log(x: torch.Tensor) -> torch.Tensor:
"""带符号自然对数(R2.5):sign(x)*log1p(|x|),全实数域安全,负输入不产 NaN。"""
out = torch.sign(x) * torch.log1p(torch.abs(x))
return torch.nan_to_num(out, nan=0.0, posinf=0.0, neginf=0.0)
def _signed_sqrt(x: torch.Tensor) -> torch.Tensor:
"""带符号平方根(R2.5):sign(x)*sqrt(|x|),负输入不产 NaN。"""
out = torch.sign(x) * torch.sqrt(torch.abs(x))
return torch.nan_to_num(out, nan=0.0, posinf=0.0, neginf=0.0)
# 算子列表(追加在既有 47 个之后)
_TASK33_OPERATORS = [
# 时序求和(arity 1,因果,R2.3)
('TS_SUM_5', lambda x: torch.nan_to_num(_ts_sum(x, 5), nan=0.0), 1),
('TS_SUM_10', lambda x: torch.nan_to_num(_ts_sum(x, 10), nan=0.0), 1),
('TS_SUM_20', lambda x: torch.nan_to_num(_ts_sum(x, 20), nan=0.0), 1),
# 元素级二元极值(arity 2,天然因果,R2.4)
('MIN', lambda x, y: torch.nan_to_num(torch.minimum(x, y), nan=0.0), 2),
('MAX', lambda x, y: torch.nan_to_num(torch.maximum(x, y), nan=0.0), 2),
# 幅度变换(arity 1,天然因果,R2.5)
('POWER', lambda x: _power_signed(x, 2.0), 1),
('SIGNED_LOG', _signed_log, 1),
('SQRT', _signed_sqrt, 1),
]
def _register_task33_operators(registry: Registry) -> None:
"""注册 Task 3.3 新增算子(时序求和/极值与幅度变换,R2.3–2.5)。
二元算子(MIN/MAX)经 `_with_shape_check` 包装;一元算子直接注册。
追加在既有 47 个算子之后,保持既有算子顺序在前(R2.9, R2.10)。
"""
for name, transform, arity in _TASK33_OPERATORS:
fn = _with_shape_check(name, transform) if arity >= 2 else transform
registry.register_operator(
OperatorSpec(name=name, arity=arity, transform=fn)
)
_register_task33_operators(OPERATOR_REGISTRY)
# 重新派生 OPS_CONFIG 导出视图(追加新算子后更新)
OPS_CONFIG = [
(spec.name, spec.transform, spec.arity)
for spec in OPERATOR_REGISTRY.operator_specs
]
# Task 3.3:新增算子必须全部在册
_EXPECTED_T33_NAMES = {name for name, _, _ in _TASK33_OPERATORS}
_REGISTERED_NAMES_T33 = set(OPERATOR_REGISTRY.operator_names)
assert _EXPECTED_T33_NAMES <= _REGISTERED_NAMES_T33, (
"Task 3.3 算子未全部注册: "
f"{_EXPECTED_T33_NAMES - _REGISTERED_NAMES_T33}"
)
assert len(OPERATOR_REGISTRY.operator_specs) >= 44 + len(_CROSS_SECTIONAL_OPERATORS) + len(_TASK33_OPERATORS), (
"OPERATOR_REGISTRY 总数应含既有 44 + CS 3 + Task3.3 8 个算子,"
f"实际 {len(OPERATOR_REGISTRY.operator_specs)}"
)
# ── Task 3.4 追加:归一化与条件/逻辑算子 ─────────────────────────────────
#
# 新增 11 个算子(TS_ZSCORE_10/20、WINSORIZE、CLIP、SIGMOID、TANH_SQUASH、
# GT、LT、AND、OR、IF_GT),追加在既有 55 个算子之后,保持既有顺序不变。
# 全部因果、NaN-safe(R2.6, R2.7, R2.9, R2.10, R8.2, R8.6)。
#
# 归一化算子均为 arity 1,因果;条件/逻辑算子 arity 2 或 3,形状校验。
import math as _math # noqa: E402 — 模块顶部已有 import torch;这里补 math
def _ts_zscore(x: torch.Tensor, w: int) -> torch.Tensor:
"""因果滚动 z-score(R2.6):(x - ts_mean) / (ts_std + eps)。
窗口 w 期,左侧 zero-pad + unfold,每步 t 只用 [t-w+1..t]。
std < eps 时输出 0(常数窗口安全)。
"""
windows = _ts_rolling(x, w) # [N, T, w]
m = windows.mean(dim=-1) # [N, T]
s = ((windows - m.unsqueeze(-1)) ** 2).mean(dim=-1).sqrt() + 1e-9
z = (x - m) / s
# std < eps(常数窗口)→ 输出 0
mask = s < (1e-9 + 1e-9)
z = torch.where(mask, torch.zeros_like(z), z)
return torch.nan_to_num(z, nan=0.0, posinf=0.0, neginf=0.0)
def _winsorize(x: torch.Tensor, lo: float = 0.05, hi: float = 0.95) -> torch.Tensor:
"""因果滚动分位裁剪(R2.6, WINSORIZE)。
用 _ts_rolling(x, 20) 取 per-step 的第 lo/hi 分位点(只用 ≤t 数据),
再 clamp 当前值到 [lower, upper]。严格无未来信息(因果 unfold)。
lower < upper 由 lo < hi 保证(默认 5%/95%)。
"""
w = 20
windows = _ts_rolling(x, w) # [N, T, w]
lower = torch.quantile(windows.float(), lo, dim=-1).to(x.dtype) # [N, T]
upper = torch.quantile(windows.float(), hi, dim=-1).to(x.dtype) # [N, T]
# 保证 lower < upper(零跨度时取原值)
span = upper - lower
safe_lower = torch.where(span < 1e-9, x, lower)
safe_upper = torch.where(span < 1e-9, x, upper)
out = torch.clamp(x, safe_lower, safe_upper)
return torch.nan_to_num(out, nan=0.0, posinf=0.0, neginf=0.0)
def _clip_fixed(x: torch.Tensor) -> torch.Tensor:
"""硬限幅 clamp(-3, 3)(R2.6, CLIP)。固定常数界,lower < upper。"""
return torch.clamp(x, -3.0, 3.0)
def _sigmoid_squash(x: torch.Tensor) -> torch.Tensor:
"""2*sigmoid(x)-1,squash 到 [-1, 1](R2.6)。"""
out = 2.0 * torch.sigmoid(x) - 1.0
return torch.nan_to_num(out, nan=0.0, posinf=1.0, neginf=-1.0)
def _tanh_squash(x: torch.Tensor) -> torch.Tensor:
"""tanh(x),squash 到 (-1, 1)(R2.6)。"""
out = torch.tanh(x)
return torch.nan_to_num(out, nan=0.0, posinf=1.0, neginf=-1.0)
# 条件/逻辑算子(R2.7)—— 全部形状校验(由 _with_shape_check 包装)
def _gt(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
"""(x > y).float(),形状校验(R2.7)。"""
out = (x > y).float()
return torch.nan_to_num(out, nan=0.0)
def _lt(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
"""(x < y).float(),形状校验(R2.7)。"""
out = (x < y).float()
return torch.nan_to_num(out, nan=0.0)
def _and(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
"""((x>0) & (y>0)).float(),形状校验(R2.7)。"""
out = ((x > 0) & (y > 0)).float()
return torch.nan_to_num(out, nan=0.0)
def _or(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
"""((x>0) | (y>0)).float(),形状校验(R2.7)。"""
out = ((x > 0) | (y > 0)).float()
return torch.nan_to_num(out, nan=0.0)
def _if_gt(x: torch.Tensor, y: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
"""三元选择 where(x>0, y, z)(R2.7, IF_GT, arity 3)。形状校验。
语义:条件操作数 x>0 时取 y,否则取 z。
"""
out = torch.where(x > 0, y, z)
return torch.nan_to_num(out, nan=0.0, posinf=0.0, neginf=0.0)
_TASK34_OPERATORS = [
# 归一化算子(arity 1,因果,R2.6)
('TS_ZSCORE_10', lambda x: _ts_zscore(x, 10), 1),
('TS_ZSCORE_20', lambda x: _ts_zscore(x, 20), 1),
('WINSORIZE', _winsorize, 1),
('CLIP', _clip_fixed, 1),
('SIGMOID', _sigmoid_squash, 1),
('TANH_SQUASH', _tanh_squash, 1),
# 条件/逻辑算子(R2.7)
# P1 注意:LT/GT/AND/OR 输出纯 0/1 二值,与 Neutral Band 连续因子语义冲突,
# 容易被模型利用来制造稀疏信号刷高训练分,已从词表移除。
# IF_GT 输出连续值(条件混合),保留。
('IF_GT', _if_gt, 3), # where(x>0, y, z) — 输出连续值,保留
]
def _register_task34_operators(registry: Registry) -> None:
"""注册 Task 3.4 新增算子(归一化与条件/逻辑,R2.6, R2.7)。
二元/三元算子经 `_with_shape_check` 包装;一元算子直接注册。
追加在既有 55 个算子之后,保持既有算子顺序在前(R2.9, R2.10)。
"""
for name, transform, arity in _TASK34_OPERATORS:
fn = _with_shape_check(name, transform) if arity >= 2 else transform
registry.register_operator(
OperatorSpec(name=name, arity=arity, transform=fn)
)
_register_task34_operators(OPERATOR_REGISTRY)
# 重新派生 OPS_CONFIG 导出视图(追加新算子后更新)
OPS_CONFIG = [
(spec.name, spec.transform, spec.arity)
for spec in OPERATOR_REGISTRY.operator_specs
]
# Task 3.4:新增算子必须全部在册(LT/GT/AND/OR 已移除,保留 7 个)
_EXPECTED_T34_NAMES = {name for name, _, _ in _TASK34_OPERATORS}
_REGISTERED_NAMES_T34 = set(OPERATOR_REGISTRY.operator_names)
assert _EXPECTED_T34_NAMES <= _REGISTERED_NAMES_T34, (
"Task 3.4 算子未全部注册: "
f"{_EXPECTED_T34_NAMES - _REGISTERED_NAMES_T34}"
)
_PREV_COUNT = 44 + len(_CROSS_SECTIONAL_OPERATORS) + len(_TASK33_OPERATORS)
assert len(OPERATOR_REGISTRY.operator_specs) >= _PREV_COUNT + len(_TASK34_OPERATORS), (
f"OPERATOR_REGISTRY 总数应含既有 {_PREV_COUNT} + Task3.4 {len(_TASK34_OPERATORS)} 个算子,"
f"实际 {len(OPERATOR_REGISTRY.operator_specs)}"
)