Files
quant_engine/tests/test_alpha_factors.py
T
George BerkshireandMavis c04acf0ab6 feat: quant_engine 独立量化引擎(v1.2.0 重构 bootstrap)
从 research_results 抽出纯回测核心能力,形成独立成员仓。

## 模块(10 个核心)

| 模块 | 内容 |
|---|---|
| alpha_factors | 158 alpha 公式 + 24 基础算子(移植自 qlib alpha158)+ JSONB 工具 |
| execution | 执行仿真(成本/滑点/T+1/涨跌停/部分成交/价差)+ 多日 NAV + PnL 拆解 |
| indicators | 50+ 技术指标(MACD/KDJ/布林/ATR/ADX/等) |
| data_adapter | 桥接 qtdb_pro 长表与新模块(rename/long-wide/复权/vwap 代理) |
| backtest | weight-based 多日仿真 |
| metrics / perf_stats | 绩效指标 |
| factor_library | 通用方法(turnover/IC/winsorize/OLS) |
| portfolio_decomp / risk | 组合分解 + 风险指标 |
| logging | 统一 logger(标准库 + 可选 loguru) |

## 设计原则

- 零重型依赖(numpy/pandas/scipy)
- mypy strict 0 errors(12 source files)
- 394 tests passed(从 research_results 复制 + 适配)
- ruff clean

## 与 research_results 的关系

- research_results 通过 re-export wrapper 保持向后兼容(src.shared.X → quant_engine.X)
- 47 proj 的 import 路径暂时不变,后续逐步迁移
- 本仓角色:researchhub_workspace 引擎层

Co-Authored-By: Mavis <noreply@mavis.local>
2026-08-19 15:50:01 +08:00

1235 lines
38 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.
"""src/shared/alpha_factors.py 单元测试(v1.2.0 Phase 0 第一批)。"""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from quant_engine.alpha_factors import (
ALPHA158_REGISTRY,
alpha_001,
alpha_002,
alpha_003,
alpha_004,
alpha_005,
alpha_006,
alpha_007,
alpha_008,
alpha_009,
alpha_010,
alpha_011,
alpha_012,
alpha_013,
alpha_014,
alpha_015,
alpha_016,
alpha_017,
alpha_018,
alpha_019,
alpha_020,
alpha_021,
alpha_022,
alpha_023,
alpha_024,
alpha_025,
alpha_026,
alpha_027,
alpha_028,
alpha_029,
alpha_030,
alpha_031,
alpha_032,
alpha_033,
alpha_034,
alpha_035,
alpha_036,
alpha_037,
alpha_038,
alpha_039,
alpha_040,
alpha_041,
alpha_042,
alpha_043,
alpha_044,
alpha_045,
alpha_046,
alpha_047,
alpha_048,
alpha_049,
alpha_050,
alpha_051,
alpha_052,
alpha_053,
alpha_054,
alpha_055,
alpha_056,
alpha_057,
alpha_058,
alpha_059,
alpha_060,
alpha_061,
alpha_062,
alpha_063,
alpha_064,
alpha_065,
alpha_066,
alpha_067,
alpha_068,
alpha_069,
alpha_070,
alpha_071,
alpha_072,
alpha_073,
alpha_074,
alpha_075,
alpha_076,
alpha_077,
alpha_078,
alpha_079,
alpha_080,
alpha_081,
alpha_082,
alpha_083,
alpha_084,
alpha_085,
alpha_086,
alpha_087,
alpha_088,
alpha_089,
alpha_090,
alpha_091,
alpha_092,
alpha_093,
alpha_094,
alpha_095,
alpha_096,
alpha_097,
alpha_098,
alpha_099,
alpha_100,
alpha_101,
alpha_102,
alpha_103,
alpha_104,
alpha_105,
alpha_106,
alpha_107,
alpha_108,
alpha_109,
alpha_110,
alpha_111,
alpha_112,
alpha_113,
alpha_114,
alpha_115,
alpha_116,
alpha_117,
alpha_118,
alpha_119,
alpha_120,
alpha_121,
alpha_122,
alpha_123,
alpha_124,
alpha_125,
alpha_126,
alpha_127,
alpha_128,
alpha_129,
alpha_130,
alpha_131,
alpha_132,
alpha_133,
alpha_134,
alpha_135,
alpha_136,
alpha_137,
alpha_138,
alpha_139,
alpha_140,
alpha_141,
alpha_142,
alpha_143,
alpha_144,
alpha_145,
alpha_146,
alpha_147,
alpha_148,
alpha_149,
alpha_150,
alpha_151,
alpha_152,
alpha_153,
alpha_154,
alpha_155,
alpha_156,
alpha_157,
alpha_158,
correlation,
covariance,
decay_linear,
delta,
get_alpha_meta,
indneutralize,
log,
max_pair,
min_pair,
product,
rank,
returns,
scale,
sign,
signed_power,
stddev,
ts_argmax,
ts_argmin,
ts_max,
ts_mean,
ts_min,
ts_rank,
ts_std,
ts_sum,
)
# ── rank 基础算子 ──────────────────────────────
def test_rank_uniform_input():
"""全相同输入 → 所有位置相同(pandas average tie-breaking)。"""
s = pd.Series([1.0, 1.0, 1.0, 1.0])
result = rank(s)
# pandas rank default method='average': ties get average rank
# 4 ties at positions 1,2,3,4 → average 2.5 → 2.5/4 = 0.625
expected = pd.Series([0.625, 0.625, 0.625, 0.625])
pd.testing.assert_series_equal(result, expected)
def test_rank_basic():
"""5 个升序值 → 0.2, 0.4, 0.6, 0.8, 1.0(pandas rank pct=True)。"""
s = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
result = rank(s)
expected = pd.Series([0.2, 0.4, 0.6, 0.8, 1.0])
pd.testing.assert_series_equal(result, expected)
def test_rank_empty():
"""空 series 返回空 series。"""
s = pd.Series([], dtype=float)
result = rank(s)
assert len(result) == 0
# ── delta 基础算子 ──────────────────────────────
def test_delta_basic():
"""5 期 delta。"""
s = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
result = delta(s, 5)
expected = pd.Series([np.nan, np.nan, np.nan, np.nan, np.nan, 5.0])
pd.testing.assert_series_equal(result, expected)
def test_delta_zero():
"""delta n=0 → 全部 NaN(无前移)。"""
s = pd.Series([1.0, 2.0, 3.0])
result = delta(s, 0)
# shift(0) is identity, so result is all zeros except first NaN
assert result.iloc[0] == 0.0 or pd.isna(result.iloc[0])
def test_delta_negative_raises():
"""delta n<0 应报错。"""
s = pd.Series([1.0, 2.0, 3.0])
with pytest.raises(ValueError, match="non-negative"):
delta(s, -1)
# ── ts_mean 基础算子 ──────────────────────────────
def test_ts_mean_basic():
"""20 期均值。"""
s = pd.Series(range(20), dtype=float)
result = ts_mean(s, 5)
# 最后 5 个值 15,16,17,18,19 均值 17
assert result.iloc[-1] == 17.0
def test_ts_mean_n_1_raises():
"""ts_mean n<=0 应报错。"""
s = pd.Series([1.0, 2.0, 3.0])
with pytest.raises(ValueError, match="positive"):
ts_mean(s, 0)
# ── ts_std 基础算子 ──────────────────────────────
def test_ts_std_basic():
"""常量 series → std = 0。"""
s = pd.Series([5.0] * 20)
result = ts_std(s, 5)
assert result.iloc[-1] == 0.0
def test_ts_std_n_1_raises():
"""ts_std n<=0 应报错。"""
s = pd.Series([1.0, 2.0, 3.0])
with pytest.raises(ValueError, match="positive"):
ts_std(s, -1)
# ── ts_rank 基础算子 ──────────────────────────────
def test_ts_rank_basic():
"""时序 rank:最后一个值在窗口内应排 1.0。"""
s = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
result = ts_rank(s, 5)
# 最后一个值 5 在 [1,2,3,4,5] 内 rank = 1.0
assert result.iloc[-1] == 1.0
def test_ts_rank_n_1_raises():
"""ts_rank n<=0 应报错。"""
s = pd.Series([1.0, 2.0, 3.0])
with pytest.raises(ValueError, match="positive"):
ts_rank(s, 0)
# ── correlation 基础算子 ──────────────────────────────
def test_correlation_perfect_positive():
"""完全正相关 → 1.0。"""
s1 = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0])
s2 = s1 * 2 + 1
result = correlation(s1, s2, 5)
assert result.iloc[-1] == pytest.approx(1.0)
def test_correlation_perfect_negative():
"""完全负相关 → -1.0。"""
s1 = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0])
s2 = -s1
result = correlation(s1, s2, 5)
assert result.iloc[-1] == pytest.approx(-1.0)
def test_correlation_n_1_raises():
"""correlation n<=0 应报错。"""
s1 = pd.Series([1.0, 2.0, 3.0])
s2 = pd.Series([3.0, 2.0, 1.0])
with pytest.raises(ValueError, match="positive"):
correlation(s1, s2, 0)
# ── alpha_001 公式 ──────────────────────────────
def test_alpha_001_smoke():
"""alpha_001 端到端 smoke test。"""
idx = pd.MultiIndex.from_product(
[["d1", "d2", "d3", "d4", "d5"], ["s1", "s2", "s3"]],
names=["date", "stock"],
)
close = pd.Series(np.random.rand(15), index=idx)
result = alpha_001(close)
assert isinstance(result, pd.Series)
assert result.shape == close.shape
def test_alpha_001_constant_input():
"""常量输入 → ts_rank 在窗口内全 0.5 → rank 后仍全相同。"""
s = pd.Series([5.0] * 10)
result = alpha_001(s)
# 全 0.5 输入 → ts_rank = 0.5 → rank 在 6 个 0.5 之间取平均
# 6 个 ties at positions 1..6 → average 3.5 → 3.5/6 ≈ 0.5833
expected_value = 3.5 / 6.0
valid = result.dropna()
for v in valid:
assert v == pytest.approx(expected_value)
# ── alpha_002 公式 ──────────────────────────────
def test_alpha_002_matches_delta():
"""alpha_002 应等于 delta(close, 5)。"""
close = pd.Series(range(20), dtype=float)
assert alpha_002(close).equals(delta(close, 5))
# ── alpha_003 公式 ──────────────────────────────
def test_alpha_003_matches_ts_mean():
"""alpha_003 应等于 ts_mean(close, 20)。"""
close = pd.Series(range(30), dtype=float)
assert alpha_003(close).equals(ts_mean(close, 20))
# ── alpha_004 公式 ──────────────────────────────
def test_alpha_004_matches_ts_std():
"""alpha_004 应等于 ts_std(close, 20)。"""
close = pd.Series(range(30), dtype=float)
assert alpha_004(close).equals(ts_std(close, 20))
# ── alpha_005 公式 ──────────────────────────────
def test_alpha_005_matches_correlation():
"""alpha_005 应等于 correlation(close, volume, 10)。"""
close = pd.Series(range(20), dtype=float)
volume = pd.Series([1.0] * 10 + [2.0] * 10)
assert alpha_005(close, volume).equals(correlation(close, volume, 10))
# ── 元数据 ──────────────────────────────
def test_alpha_registry_complete():
"""158 个 alpha 算子全部在 registry 中(v1.2.0 第八批后——达成完整 alpha158)。"""
expected = {f"alpha_{i:03d}" for i in range(1, 159)}
assert set(ALPHA158_REGISTRY.keys()) == expected
def test_alpha_registry_required_fields():
"""每个 registry 条目都包含必需字段。"""
required = {"formula", "category", "complexity", "params", "description", "inputs"}
for alpha_id, meta in ALPHA158_REGISTRY.items():
assert required <= set(meta.keys()), f"{alpha_id} missing fields"
def test_get_alpha_meta_success():
"""已知 alpha_id 返回完整 meta。"""
meta = get_alpha_meta("alpha_001")
assert meta["formula"] == "rank(ts_rank(close, 5))"
assert meta["category"] == "momentum_rank"
def test_get_alpha_meta_unknown():
"""未知 alpha_id 抛 KeyError。"""
with pytest.raises(KeyError, match="alpha_999"):
get_alpha_meta("alpha_999")
def test_alpha_meta_jsonb_serializable():
"""元数据可以 JSON 序列化(PostgreSQL JSONB 落库前置验证)。"""
import json
for alpha_id, meta in ALPHA158_REGISTRY.items():
# 抛异常即失败
json.dumps(meta)
# ── v1.2.0 第二批基础算子(ts_min / ts_max / ts_sum / ts_argmin / ts_argmax / decay_linear / product / returns / scale / signed_power) ──
def test_ts_min_basic():
"""5 期滚动最小值。"""
s = pd.Series([5.0, 3.0, 8.0, 1.0, 6.0, 2.0])
assert ts_min(s, 5).iloc[-1] == 1.0
def test_ts_max_basic():
"""5 期滚动最大值。"""
s = pd.Series([5.0, 3.0, 8.0, 1.0, 6.0, 2.0])
assert ts_max(s, 5).iloc[-1] == 8.0
def test_ts_sum_basic():
"""5 期滚动求和。"""
s = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
assert ts_sum(s, 5).iloc[-1] == 20.0 # 2+3+4+5+6
def test_ts_argmin_first_position():
"""最小值在窗口起点 → argmin = 0。"""
s = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
assert ts_argmin(s, 5).iloc[-1] == 0.0
def test_ts_argmax_last_position():
"""最大值在窗口终点 → argmax = 4。"""
s = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
assert ts_argmax(s, 5).iloc[-1] == 4.0
def test_decay_linear_recent_weighted():
"""线性衰减:近期权重大。"""
s = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
weights = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
weights /= weights.sum() # [0.0667, 0.1333, 0.2, 0.2667, 0.3333]
expected = float(np.dot(s.values, weights))
assert decay_linear(s, 5).iloc[-1] == pytest.approx(expected)
def test_product_basic():
"""5 期滚动乘积。"""
s = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
assert product(s, 5).iloc[-1] == 120.0
def test_returns_basic():
"""简单收益率。"""
s = pd.Series([100.0, 110.0, 121.0])
assert returns(s).iloc[1] == pytest.approx(0.1)
assert returns(s).iloc[2] == pytest.approx(0.1)
def test_scale_sum_to_one():
"""scale 让 |x| 之和 = 1。"""
s = pd.Series([1.0, -2.0, 3.0, -4.0])
result = scale(s)
assert result.abs().sum() == pytest.approx(1.0)
def test_scale_zero_input():
"""scale 全 0 输入 → 原样返回(避免除零)。"""
s = pd.Series([0.0, 0.0, 0.0])
result = scale(s)
assert (result == 0.0).all()
def test_signed_power_positive():
"""正数 signed_power(2) → x^2。"""
s = pd.Series([3.0, 4.0])
result = signed_power(s, 2)
assert result.iloc[0] == pytest.approx(9.0)
assert result.iloc[1] == pytest.approx(16.0)
def test_signed_power_negative():
"""负数 signed_power(2) → -|x|^2。"""
s = pd.Series([-3.0])
result = signed_power(s, 2)
assert result.iloc[0] == pytest.approx(-9.0)
def test_ts_min_n_1_raises():
"""ts_min n<=0 应报错。"""
with pytest.raises(ValueError, match="positive"):
ts_min(pd.Series([1.0]), 0)
def test_ts_max_n_1_raises():
with pytest.raises(ValueError, match="positive"):
ts_max(pd.Series([1.0]), -1)
def test_decay_linear_n_1_raises():
with pytest.raises(ValueError, match="positive"):
decay_linear(pd.Series([1.0]), 0)
# ── v1.2.0 第二批 alpha 公式(alpha_006 – alpha_015) ─────
def test_alpha_006_smoke():
"""alpha_006 = rank(close - open) smoke test。"""
open_ = pd.Series([10.0, 11.0, 12.0])
close = pd.Series([11.0, 10.0, 13.0]) # diff: +1, -1, +1
result = alpha_006(open_, close)
# diff 序列 [1, -1, 1] → pandas rank pct=True:ranks [2.5, 1, 2.5] → pct [0.833, 0.333, 0.833]
assert result.iloc[0] == pytest.approx(5.0 / 6)
assert result.iloc[1] == pytest.approx(1.0 / 3)
assert result.iloc[2] == pytest.approx(5.0 / 6)
def test_alpha_007_volume_trend():
"""alpha_007 = mean(volume, 7) - mean(volume, 14)。"""
volume = pd.Series(range(20), dtype=float)
result = alpha_007(volume)
expected_last = volume.iloc[-7:].mean() - volume.iloc[-14:].mean()
assert result.iloc[-1] == pytest.approx(expected_last)
def test_alpha_008_smoke():
"""alpha_008 = rank(delta(vwap, 5))。"""
vwap = pd.Series(range(20), dtype=float)
result = alpha_008(vwap)
assert isinstance(result, pd.Series)
def test_alpha_009_min_rank():
"""alpha_009 = rank(ts_min(low, 5))。"""
low = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
result = alpha_009(low)
# 5 期 ts_min 都 = 1.0 → rank 全相同
valid = result.dropna()
assert valid.nunique() == 1
def test_alpha_010_max_rank():
"""alpha_010 = rank(ts_max(high, 5))。"""
high = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
result = alpha_010(high)
# 5 期 ts_max 都 = 5.0 → rank 全相同
valid = result.dropna()
assert valid.nunique() == 1
def test_alpha_011_constant_range():
"""alpha_011 高=低时(无振幅)→ 返回 0(safe_diff 处理)。"""
open_ = pd.Series([10.0, 11.0])
close = pd.Series([10.0, 11.0])
high = pd.Series([10.0, 11.0])
low = pd.Series([10.0, 11.0])
result = alpha_011(open_, close, high, low)
assert (result == 0.0).all()
def test_alpha_011_bullish():
"""alpha_011 阳线 close > open → 正值(多头强度)。"""
open_ = pd.Series([10.0] * 5)
close = pd.Series([10.5] * 5)
high = pd.Series([11.0] * 5)
low = pd.Series([9.5] * 5)
result = alpha_011(open_, close, high, low)
# close - low = 1.0; high - close = 0.5; high - low = 1.5
# (1.0 - 0.5) / 1.5 = 0.333
for v in result:
assert v == pytest.approx(1.0 / 3.0)
def test_alpha_012_volume_rank_change():
"""alpha_012 = rank(volume) - rank(volume.shift(5))。"""
volume = pd.Series(range(20), dtype=float)
result = alpha_012(volume)
# 前 5 个应为 NaN
assert result.iloc[:5].isna().all()
def test_alpha_013_returns_rank_change():
"""alpha_013 = rank(returns) - rank(returns.shift(3))。"""
close = pd.Series(range(20), dtype=float)
result = alpha_013(close)
# 前 4 个 NaN(returns 第一行 + shift(3) 3 行)
assert result.iloc[:4].isna().all()
def test_alpha_014_combined_momentum():
"""alpha_014 = -rank(delta(returns(open_), 3)) * correlation(open_, volume, 10)。"""
open_ = pd.Series(range(20), dtype=float)
volume = pd.Series([1.0] * 20)
result = alpha_014(open_, volume)
assert isinstance(result, pd.Series)
def test_alpha_015_high_volume_combo():
"""alpha_015 = -1 * ts_sum(rank(correlation(rank(high), rank(volume), 5)), 5)。
完全相关输入(high 和 volume 同方向)→ correlation = 1.0;
但 ts_sum 5 期窗口内 rank(1.0) 因 tie-breaking 在 [0, 1] 之间分布,
所以最后 5 期总和约为 -3.6875(实测值)。
"""
high = pd.Series(range(20), dtype=float)
volume = pd.Series(range(20), dtype=float)
result = alpha_015(high, volume)
# 取最后一个完全有效值(rolling warm-up 后)
valid_last = result.dropna().iloc[-1]
# 实测:-3.6875(具体数值取决于 pandas rank tie-breaking)
assert valid_last < 0 # 公式有 -1 系数
assert valid_last > -5.5 # 不会低于完全负相关
assert valid_last == pytest.approx(-3.6875, abs=1e-4)
def test_get_alpha_meta_alpha_006_to_015():
"""alpha_006 到 alpha_015 全部在 registry 中。"""
for i in range(6, 16):
alpha_id = f"alpha_{i:03d}"
meta = get_alpha_meta(alpha_id)
assert "formula" in meta
assert "inputs" in meta
assert len(meta["inputs"]) >= 1
# ── v1.2.0 第三批基础算子(stddev / covariance / log / abs / sign / max_pair / min_pair / indneutralize) ──
def test_stddev_alias():
"""stddev 与 ts_std 同义。"""
s = pd.Series(range(20), dtype=float)
assert stddev(s, 5).equals(ts_std(s, 5))
def test_covariance_perfect():
"""完全正相关 → 协方差 ≈ var(x)。"""
s1 = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
s2 = s1 * 2 + 1
cov = covariance(s1, s2, 5).iloc[-1]
var = s1.var()
# cov = corr * std1 * std2 = 1 * std1 * (2*std1) = 2*var
assert cov == pytest.approx(2 * var)
def test_log_basic():
"""log(e) ≈ 1。"""
s = pd.Series([1.0, np.e, np.e**2])
assert log(s).iloc[0] == pytest.approx(0.0)
assert log(s).iloc[1] == pytest.approx(1.0)
assert log(s).iloc[2] == pytest.approx(2.0)
def test_log_negative_input():
"""log(<=0) → NaN。"""
s = pd.Series([-1.0, 0.0, 1.0])
assert pd.isna(log(s).iloc[0])
assert pd.isna(log(s).iloc[1])
assert log(s).iloc[2] == pytest.approx(0.0)
def test_abs_basic():
"""abs_series: |−3| = 3。"""
from quant_engine.alpha_factors import abs_series
assert abs_series(pd.Series([-3.0, 0.0, 5.0])).tolist() == [3.0, 0.0, 5.0]
def test_sign_basic():
"""sign 函数。"""
assert sign(pd.Series([-5.0, 0.0, 5.0])).tolist() == [-1.0, 0.0, 1.0]
def test_max_pair():
"""逐元素 max。"""
s1 = pd.Series([1.0, 5.0, 3.0])
s2 = pd.Series([4.0, 2.0, 6.0])
assert max_pair(s1, s2).tolist() == [4.0, 5.0, 6.0]
def test_min_pair():
"""逐元素 min。"""
s1 = pd.Series([1.0, 5.0, 3.0])
s2 = pd.Series([4.0, 2.0, 6.0])
assert min_pair(s1, s2).tolist() == [1.0, 2.0, 3.0]
def test_indneutralize():
"""组内中性化:减去组均值。"""
s = pd.Series([1.0, 2.0, 3.0, 4.0], index=["a", "a", "b", "b"])
groups = pd.Series(["g1", "g1", "g2", "g2"], index=s.index)
result = indneutralize(s, groups)
# g1 均值 = 1.5;g2 均值 = 3.5
assert result.iloc[0] == pytest.approx(-0.5)
assert result.iloc[1] == pytest.approx(0.5)
assert result.iloc[2] == pytest.approx(-0.5)
assert result.iloc[3] == pytest.approx(0.5)
# ── v1.2.0 第三批 alpha 公式(alpha_016 – alpha_035) ─────
def test_alpha_016_smoke():
"""alpha_016 = rank(decay_linear(correlation(rank(high), rank(volume), 5), 5))。"""
high = pd.Series(np.random.rand(30).cumsum())
volume = pd.Series(np.random.rand(30) * 100 + 1.0)
result = alpha_016(high, volume)
assert isinstance(result, pd.Series)
assert result.dropna().shape[0] > 0
def test_alpha_017_smoke():
"""alpha_017 smoke test。"""
close = pd.Series(np.random.rand(30).cumsum() + 10)
volume = pd.Series(np.random.rand(30) * 100 + 1.0)
result = alpha_017(close, volume)
assert isinstance(result, pd.Series)
def test_alpha_018_smoke():
"""alpha_018 smoke test。"""
open_ = pd.Series(np.random.rand(30).cumsum() + 10)
volume = pd.Series(np.random.rand(30) * 100 + 1.0)
result = alpha_018(open_, volume)
assert isinstance(result, pd.Series)
def test_alpha_019_smoke():
"""alpha_019 smoke test。"""
close = pd.Series(np.random.rand(30).cumsum() + 10)
open_ = pd.Series(np.random.rand(30).cumsum() + 10)
result = alpha_019(close, open_)
assert isinstance(result, pd.Series)
def test_alpha_020_smoke():
"""alpha_020 smoke test。"""
open_ = pd.Series(np.random.rand(30).cumsum() + 10)
close = pd.Series(np.random.rand(30).cumsum() + 10)
result = alpha_020(open_, close)
assert isinstance(result, pd.Series)
def test_alpha_021_volume_ratio():
"""alpha_021 = ts_mean(volume, 20) / ts_mean(volume, 60)。"""
volume = pd.Series([1.0] * 60 + [3.0] * 20)
result = alpha_021(volume).dropna()
# 后 20 个 volume=3, 前 40 个=1, ts_mean 20 = 3, ts_mean 60 = (40+60)/60 = 100/60
expected = 3.0 / (100.0 / 60.0)
assert result.iloc[-1] == pytest.approx(expected)
def test_alpha_021_zero_long_avg():
"""alpha_021 分母为 0 → NaN(safe 处理)。"""
volume = pd.Series([0.0] * 80)
result = alpha_021(volume)
assert result.dropna().empty
def test_alpha_022_smoke():
"""alpha_022 smoke test。"""
high = pd.Series(np.random.rand(50).cumsum() + 10)
volume = pd.Series(np.random.rand(50) * 100 + 1.0)
close = pd.Series(np.random.rand(50).cumsum() + 10)
result = alpha_022(high, volume, close)
assert result.dropna().shape[0] > 0
def test_alpha_023_smoke():
"""alpha_023 = -1 * ts_mean(delta(close, 5), 20) * delta(close, 5) / close。"""
close = pd.Series(np.random.rand(50).cumsum() + 10)
result = alpha_023(close)
assert result.dropna().shape[0] > 0
def test_alpha_023_zero_close():
"""close 中含 0 → 跳过(safe_close)。"""
close = pd.Series([10.0, 11.0, 0.0, 12.0, 13.0] * 10)
result = alpha_023(close)
# 不抛异常;NaN 在 0 处
assert not result.isna().all()
def test_alpha_024_smoke():
"""alpha_024 smoke test。"""
close = pd.Series(np.random.rand(50).cumsum() + 10)
volume = pd.Series(np.random.rand(50) * 100 + 1.0)
result = alpha_024(close, volume)
assert result.dropna().shape[0] > 0
def test_alpha_025_smoke():
"""alpha_025 = rank(decay_linear(correlation(vwap, volume, 4), 8))。"""
vwap = pd.Series(np.random.rand(50).cumsum() + 10)
volume = pd.Series(np.random.rand(50) * 100 + 1.0)
result = alpha_025(vwap, volume)
assert result.dropna().shape[0] > 0
def test_alpha_026_smoke():
"""alpha_026 smoke test。"""
open_ = pd.Series(np.random.rand(50).cumsum() + 10)
volume = pd.Series(np.random.rand(50) * 100 + 1.0)
close = pd.Series(np.random.rand(50).cumsum() + 10)
result = alpha_026(open_, volume, close)
assert result.dropna().shape[0] > 0
def test_alpha_027_smoke():
"""alpha_027 smoke test。"""
close = pd.Series(np.random.rand(50).cumsum() + 10)
volume = pd.Series(np.random.rand(50) * 100 + 1.0)
result = alpha_027(close, volume)
assert result.dropna().shape[0] > 0
def test_alpha_028_scale():
"""alpha_028 = scale(...) 应满足 |sum| ≈ 1。"""
close = pd.Series(np.random.rand(50).cumsum() + 10)
open_ = pd.Series(np.random.rand(50).cumsum() + 10)
result = alpha_028(close, open_)
valid = result.dropna()
assert valid.abs().sum() == pytest.approx(1.0, abs=1e-6)
def test_alpha_029_smoke():
"""alpha_029 嵌套组合 smoke。"""
close = pd.Series(np.random.rand(50).cumsum() + 10)
result = alpha_029(close)
assert result.dropna().shape[0] > 0
def test_alpha_030_smoke():
"""alpha_030 = -1 * rank(decay_linear(correlation(rank(low), rank(volume), 5), 5))。"""
low = pd.Series(np.random.rand(50).cumsum() + 5)
volume = pd.Series(np.random.rand(50) * 100 + 1.0)
result = alpha_030(low, volume)
assert result.dropna().shape[0] > 0
def test_alpha_031_smoke():
"""alpha_031 smoke test。"""
close = pd.Series(np.random.rand(50).cumsum() + 10)
volume = pd.Series(np.random.rand(50) * 100 + 1.0)
result = alpha_031(close, volume)
assert result.dropna().shape[0] > 0
def test_alpha_032_scale():
"""alpha_032 = scale(...) → |sum| ≈ 1。"""
close = pd.Series(np.random.rand(50).cumsum() + 10)
volume = pd.Series(np.random.rand(50) * 100 + 1.0)
result = alpha_032(close, volume)
valid = result.dropna()
assert valid.abs().sum() == pytest.approx(1.0, abs=1e-6)
def test_alpha_033_scale():
"""alpha_033 = scale(...) → |sum| ≈ 1。"""
close = pd.Series(np.random.rand(50).cumsum() + 10)
result = alpha_033(close)
valid = result.dropna()
assert valid.abs().sum() == pytest.approx(1.0, abs=1e-6)
def test_alpha_034_volume_ratio():
"""alpha_034 = ts_mean(volume, 12) / ts_mean(volume, 26)。"""
volume = pd.Series([1.0] * 50)
result = alpha_034(volume).dropna()
# 全部 = 1,ratio = 1.0
assert result.iloc[-1] == pytest.approx(1.0)
def test_alpha_035_volume_ratio():
"""alpha_035 = ts_mean(volume, 6) / ts_mean(volume, 12)。"""
close = pd.Series([1.0] * 30)
volume = pd.Series([1.0] * 30)
result = alpha_035(close, volume).dropna()
assert result.iloc[-1] == pytest.approx(1.0)
# ── v1.2.0 第四批 alpha 公式(alpha_036 – alpha_060) ─────
# 共 25 个 smoke test(大多数是组合公式,行为正确即可)
@pytest.mark.parametrize(
"alpha_fn,args_fn",
[
("alpha_036", lambda c, v, o: (o, c)),
("alpha_037", lambda c, v, o: (o, c)),
("alpha_038", lambda c, v, o: (c,)),
("alpha_039", lambda c, v, o: (v,)),
("alpha_040", lambda c, v, o: (o * 1.1, c * 0.9, v)),
("alpha_041", lambda c, v, o: (o * 1.1, c * 0.9)),
("alpha_042", lambda c, v, o: (c, o * 1.1, c * 0.9)),
("alpha_043", lambda c, v, o: (c, v)),
("alpha_044", lambda c, v, o: (o, v)),
("alpha_045", lambda c, v, o: (c, v)),
("alpha_046", lambda c, v, o: (c,)),
("alpha_047", lambda c, v, o: (v, c)),
("alpha_048", lambda c, v, o: (c,)),
("alpha_049", lambda c, v, o: (c, v)),
("alpha_050", lambda c, v, o: (c, v)),
("alpha_051", lambda c, v, o: (o * 1.1, c * 0.9)),
("alpha_052", lambda c, v, o: (c,)),
("alpha_053", lambda c, v, o: (c, o * 1.1, c * 0.9)),
("alpha_054", lambda c, v, o: (o, c, c * 0.9)),
("alpha_055", lambda c, v, o: (o, o * 1.1, c * 0.9, v, c)),
("alpha_056", lambda c, v, o: (c, v)),
("alpha_057", lambda c, v, o: (c, v)),
("alpha_058", lambda c, v, o: (v, c)),
("alpha_059", lambda c, v, o: (c, v)),
("alpha_060", lambda c, v, o: (c, v)),
],
)
def test_alpha_036_to_060_smoke(alpha_fn, args_fn):
"""alpha_036 到 alpha_060 全部能跑通(smoke test)。"""
import sys
module = sys.modules["quant_engine.alpha_factors"]
fn = getattr(module, alpha_fn)
close = pd.Series(np.random.rand(60).cumsum() + 10)
volume = pd.Series(np.random.rand(60) * 100 + 1.0)
open_ = close.shift(1).fillna(close.iloc[0])
args = args_fn(close, volume, open_)
result = fn(*args)
assert isinstance(result, pd.Series)
# 大多数公式应该至少有少量有效值
assert result.dropna().shape[0] > 0
# ── v1.2.0 第五批 alpha 公式(alpha_061 – alpha_085) ─────
@pytest.mark.parametrize(
"alpha_fn,args_fn",
[
("alpha_061", lambda c, v, o, h, lo: (h, lo, v)),
("alpha_062", lambda c, v, o, h, lo: (c, v)),
("alpha_063", lambda c, v, o, h, lo: (c, v)),
("alpha_064", lambda c, v, o, h, lo: (o, c, v)),
("alpha_065", lambda c, v, o, h, lo: (c, v)),
("alpha_066", lambda c, v, o, h, lo: (c, v)),
("alpha_067", lambda c, v, o, h, lo: (c, h, lo, v)),
("alpha_068", lambda c, v, o, h, lo: (c, v)),
("alpha_069", lambda c, v, o, h, lo: (c, v)),
("alpha_070", lambda c, v, o, h, lo: (c, h, lo, v)),
("alpha_071", lambda c, v, o, h, lo: (o, c, lo, v)),
("alpha_072", lambda c, v, o, h, lo: (c, h, lo, v)),
("alpha_073", lambda c, v, o, h, lo: (c, v)),
("alpha_074", lambda c, v, o, h, lo: (c, h, lo, v)),
("alpha_075", lambda c, v, o, h, lo: (c, v)),
("alpha_076", lambda c, v, o, h, lo: (c, v)),
("alpha_077", lambda c, v, o, h, lo: (h, lo, v)),
("alpha_078", lambda c, v, o, h, lo: (c, h, lo, v)),
("alpha_079", lambda c, v, o, h, lo: (c, h, lo)),
("alpha_080", lambda c, v, o, h, lo: (o, c, v)),
("alpha_081", lambda c, v, o, h, lo: (c, v)),
("alpha_082", lambda c, v, o, h, lo: (o, v)),
("alpha_083", lambda c, v, o, h, lo: (h, lo, v)),
("alpha_084", lambda c, v, o, h, lo: (c, v)),
("alpha_085", lambda c, v, o, h, lo: (c, v)),
],
)
def test_alpha_061_to_085_smoke(alpha_fn, args_fn):
"""alpha_061 到 alpha_085 smoke test。"""
import sys
module = sys.modules["quant_engine.alpha_factors"]
fn = getattr(module, alpha_fn)
close = pd.Series(np.random.rand(60).cumsum() + 10)
volume = pd.Series(np.random.rand(60) * 100 + 1.0)
high = close * 1.05
low = close * 0.95
open_ = close.shift(1).fillna(close.iloc[0])
args = args_fn(close, volume, open_, high, low)
result = fn(*args)
assert isinstance(result, pd.Series)
assert result.dropna().shape[0] > 0
def test_get_alpha_meta_alpha_061_to_085():
"""alpha_061 到 alpha_085 全部在 registry 中。"""
for i in range(61, 86):
alpha_id = f"alpha_{i:03d}"
meta = get_alpha_meta(alpha_id)
assert "formula" in meta
assert "inputs" in meta
assert len(meta["inputs"]) >= 1
# ── v1.2.0 第六批 alpha 公式(alpha_086 – alpha_110) ─────
@pytest.mark.parametrize(
"alpha_fn,args_fn",
[
("alpha_086", lambda c, v, o, h, lo: (c, v)),
("alpha_087", lambda c, v, o, h, lo: (o, c, v)),
("alpha_088", lambda c, v, o, h, lo: (c, v)),
("alpha_089", lambda c, v, o, h, lo: (h, lo, v)),
("alpha_090", lambda c, v, o, h, lo: (c, v)),
("alpha_091", lambda c, v, o, h, lo: (c, h, lo, v)),
("alpha_092", lambda c, v, o, h, lo: (c, h, lo, v)),
("alpha_093", lambda c, v, o, h, lo: (c, v)),
("alpha_094", lambda c, v, o, h, lo: (c, v)),
("alpha_095", lambda c, v, o, h, lo: (c, v)),
("alpha_096", lambda c, v, o, h, lo: (c, v)),
("alpha_097", lambda c, v, o, h, lo: (c, h, lo, v)),
("alpha_098", lambda c, v, o, h, lo: (c, v)),
("alpha_099", lambda c, v, o, h, lo: (h, lo, v)),
("alpha_100", lambda c, v, o, h, lo: (c, h, lo, v)),
("alpha_101", lambda c, v, o, h, lo: (c, h, lo)),
("alpha_102", lambda c, v, o, h, lo: (c, v)),
("alpha_103", lambda c, v, o, h, lo: (c, v)),
("alpha_104", lambda c, v, o, h, lo: (c, h, lo, v)),
("alpha_105", lambda c, v, o, h, lo: (c, v)),
("alpha_106", lambda c, v, o, h, lo: (c, v)),
("alpha_107", lambda c, v, o, h, lo: (c, v)),
("alpha_108", lambda c, v, o, h, lo: (h, lo, v)),
("alpha_109", lambda c, v, o, h, lo: (c, h, lo, v)),
("alpha_110", lambda c, v, o, h, lo: (c, h, lo, v)),
],
)
def test_alpha_086_to_110_smoke(alpha_fn, args_fn):
"""alpha_086 到 alpha_110 smoke test。"""
import sys
module = sys.modules["quant_engine.alpha_factors"]
fn = getattr(module, alpha_fn)
close = pd.Series(np.random.rand(60).cumsum() + 10)
volume = pd.Series(np.random.rand(60) * 100 + 1.0)
high = close * 1.05
low = close * 0.95
open_ = close.shift(1).fillna(close.iloc[0])
args = args_fn(close, volume, open_, high, low)
result = fn(*args)
assert isinstance(result, pd.Series)
assert result.dropna().shape[0] > 0
# ── v1.2.0 第七批 alpha 公式(alpha_111 – alpha_135) ─────
@pytest.mark.parametrize(
"alpha_id,args_fn",
[
(
f"alpha_{i:03d}",
lambda c, v, o, h, lo, _i=i: (
(c, v) if _i % 3 == 0 else (c, v) if _i % 3 == 1 else (h, lo, v)
),
)
for i in range(111, 136)
],
)
def test_alpha_111_to_135_smoke(alpha_id, args_fn):
"""alpha_111 到 alpha_135 smoke test(模板化 25 个)。"""
import sys
fn = getattr(sys.modules["quant_engine.alpha_factors"], alpha_id)
close = pd.Series(np.random.rand(60).cumsum() + 10)
volume = pd.Series(np.random.rand(60) * 100 + 1.0)
high = close * 1.05
low = close * 0.95
open_ = close.shift(1).fillna(close.iloc[0])
args = args_fn(close, volume, open_, high, low)
result = fn(*args)
assert isinstance(result, pd.Series)
assert result.dropna().shape[0] > 0
# ── v1.2.0 第八批 alpha 公式(alpha_136 – alpha_158) ─────
# 这是 alpha158 完整版的最后一批——达成 158 个公式
@pytest.mark.parametrize(
"alpha_id",
[f"alpha_{i:03d}" for i in range(136, 159)],
)
def test_alpha_136_to_158_smoke(alpha_id):
"""alpha_136 到 alpha_158 smoke test(达成 158 完整版)。"""
import sys
fn = getattr(sys.modules["quant_engine.alpha_factors"], alpha_id)
close = pd.Series(np.random.rand(60).cumsum() + 10)
volume = pd.Series(np.random.rand(60) * 100 + 1.0)
high = close * 1.05
low = close * 0.95
args = fn.__code__.co_varnames[: fn.__code__.co_argcount]
args_series = {
"close": close,
"volume": volume,
"high": high,
"low": low,
"open": close.shift(1).fillna(close.iloc[0]),
}
fn_args = tuple(args_series[a] for a in args if a in args_series)
result = fn(*fn_args)
assert isinstance(result, pd.Series)
assert result.dropna().shape[0] > 0
def test_alpha_158_complete():
"""确认 alpha158 完整版:158 个算子全部可用 + 全部元数据。"""
assert len(ALPHA158_REGISTRY) == 158
for i in range(1, 159):
alpha_id = f"alpha_{i:03d}"
meta = get_alpha_meta(alpha_id)
assert "formula" in meta
assert "inputs" in meta
assert len(meta["inputs"]) >= 1
# ── v1.2.0 Phase 2: dump/load / JSONB 兼容工具 ─────
def test_dump_alpha_registry_jsonl(tmp_path):
"""dump_alpha_registry_jsonl 应产生 JSONL 文件。"""
from quant_engine.alpha_factors import dump_alpha_registry_jsonl, load_alpha_registry_jsonl
output_path = tmp_path / "alpha_registry.jsonl"
count = dump_alpha_registry_jsonl(str(output_path))
assert count == 158
assert output_path.exists()
# 加载回来验证
loaded = load_alpha_registry_jsonl(str(output_path))
assert len(loaded) == 158
assert "alpha_001" in loaded
assert "alpha_158" in loaded
def test_dump_load_roundtrip(tmp_path):
"""dump + load 应保持数据完整性。"""
from quant_engine.alpha_factors import (
ALPHA158_REGISTRY,
dump_alpha_registry_jsonl,
load_alpha_registry_jsonl,
)
output_path = tmp_path / "roundtrip.jsonl"
dump_alpha_registry_jsonl(str(output_path))
loaded = load_alpha_registry_jsonl(str(output_path))
# 抽样验证
for alpha_id in ["alpha_001", "alpha_050", "alpha_158"]:
assert alpha_id in loaded
original = ALPHA158_REGISTRY[alpha_id]
# 注意:load 后 id 字段已剥离,其余字段应完全一致
for key in original:
assert loaded[alpha_id].get(key) == original[key], f"Mismatch in {alpha_id}.{key}"
def test_alpha_registry_to_pg_rows():
"""alpha_registry_to_pg_rows 应输出 PG INSERT 行结构。"""
from quant_engine.alpha_factors import alpha_registry_to_pg_rows
rows = alpha_registry_to_pg_rows()
assert len(rows) == 158
# 验证第一行结构
row = rows[0]
assert "name" in row
assert "formula" in row
assert "metadata" in row
assert "version" in row
assert row["version"] == "1.2.0"
assert isinstance(row["metadata"], dict)
assert "category" in row["metadata"]
def test_parse_alpha_formula_basic():
"""parse_alpha_formula 应提取函数名与变量。"""
from quant_engine.alpha_factors import parse_alpha_formula
parsed = parse_alpha_formula("rank(ts_rank(close, 5))")
assert "raw" in parsed
assert "operators" in parsed
assert "functions" in parsed
assert "variables" in parsed
assert "close" in parsed["variables"]
assert "rank" in parsed["operators"]
def test_parse_alpha_formula_complex():
"""复杂公式解析应识别多函数 + 多变量。"""
from quant_engine.alpha_factors import parse_alpha_formula
parsed = parse_alpha_formula("-1 * rank(decay_linear(correlation(close, volume, 10), 5))")
# 应识别 correlation, decay_linear 函数调用
func_names = [f["name"] for f in parsed["functions"]]
assert "correlation" in func_names
assert "decay_linear" in func_names
# 应识别 close, volume 变量
assert "close" in parsed["variables"]
assert "volume" in parsed["variables"]
def test_parse_alpha_formula_round_trip_jsonb():
"""parse_alpha_formula 输出可 json.dumps → JSONB 落库。"""
import json
from quant_engine.alpha_factors import parse_alpha_formula
parsed = parse_alpha_formula("rank(decay_linear(rank(ts_rank(close, 5)), 5))")
# 应能 JSON 序列化
serialized = json.dumps(parsed)
assert isinstance(serialized, str)
assert "ts_rank" in serialized