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