From fd3014c286ea2b0759ddc0f3b045f5560d700c74 Mon Sep 17 00:00:00 2001 From: ageorge156 Date: Fri, 28 Aug 2026 18:26:14 +0800 Subject: [PATCH 1/5] fix: bound phase1 operator windows (#8) --- src/quant_engine/alpha_factors.py | 139 ++++++++++++++++++++++++++++++ tests/test_alpha_factors.py | 96 +++++++++++++++++++++ 2 files changed, 235 insertions(+) diff --git a/src/quant_engine/alpha_factors.py b/src/quant_engine/alpha_factors.py index 3926ce2..2079417 100644 --- a/src/quant_engine/alpha_factors.py +++ b/src/quant_engine/alpha_factors.py @@ -15,6 +15,7 @@ v1.2.0 Phase 0:5 个基础算子 + 5 个 alpha 公式(alpha001–alpha005) from __future__ import annotations +from collections.abc import Callable from typing import Any import numpy as np @@ -209,6 +210,140 @@ def indneutralize(series: pd.Series, groups: pd.Series) -> pd.Series: return series - series.groupby(groups).transform("mean") +# ── Phase 1 operator contract ────────────────────────── + +# This is deliberately a small, stable surface for downstream research +# orchestration. The full alpha158 formula catalogue can continue to grow, +# while callers use one validated dispatch entry point for the first ten +# deterministic building blocks. +ALPHA158_PHASE1_MAX_WINDOW = 252 + +ALPHA158_PHASE1_OPERATOR_SPECS: dict[str, dict[str, Any]] = { + "rank": { + "name": "rank", + "formula": "rank(series)", + "inputs": ["series"], + "windowed": False, + }, + "delta": { + "name": "delta", + "formula": "delta(series, window)", + "inputs": ["series"], + "windowed": True, + }, + "ts_mean": { + "name": "ts_mean", + "formula": "ts_mean(series, window)", + "inputs": ["series"], + "windowed": True, + }, + "ts_std": { + "name": "ts_std", + "formula": "ts_std(series, window)", + "inputs": ["series"], + "windowed": True, + }, + "ts_rank": { + "name": "ts_rank", + "formula": "ts_rank(series, window)", + "inputs": ["series"], + "windowed": True, + }, + "correlation": { + "name": "correlation", + "formula": "correlation(series, secondary, window)", + "inputs": ["series", "secondary"], + "windowed": True, + }, + "ts_min": { + "name": "ts_min", + "formula": "ts_min(series, window)", + "inputs": ["series"], + "windowed": True, + }, + "ts_max": { + "name": "ts_max", + "formula": "ts_max(series, window)", + "inputs": ["series"], + "windowed": True, + }, + "ts_sum": { + "name": "ts_sum", + "formula": "ts_sum(series, window)", + "inputs": ["series"], + "windowed": True, + }, + "decay_linear": { + "name": "decay_linear", + "formula": "decay_linear(series, window)", + "inputs": ["series"], + "windowed": True, + }, +} + +_PHASE1_OPERATOR_FUNCTIONS: dict[str, Callable[..., pd.Series]] = { + "rank": rank, + "delta": delta, + "ts_mean": ts_mean, + "ts_std": ts_std, + "ts_rank": ts_rank, + "correlation": correlation, + "ts_min": ts_min, + "ts_max": ts_max, + "ts_sum": ts_sum, + "decay_linear": decay_linear, +} + + +def list_phase1_operators() -> tuple[str, ...]: + """Return the deterministic Phase 1 operator names in stable order.""" + return tuple(ALPHA158_PHASE1_OPERATOR_SPECS) + + +def evaluate_phase1_operator( + name: str, + series: pd.Series, + secondary: pd.Series | None = None, + *, + window: int | None = None, +) -> pd.Series: + """Evaluate one of the ten Phase 1 operators with a validated contract. + + ``window`` is required for time-series operators and forbidden for the + cross-sectional ``rank`` operator. Binary ``correlation`` also requires + a same-index secondary series so that callers cannot silently introduce + alignment-dependent results. + """ + if name not in ALPHA158_PHASE1_OPERATOR_SPECS: + raise KeyError(f"operator {name!r} not registered") + + is_windowed = bool(ALPHA158_PHASE1_OPERATOR_SPECS[name]["windowed"]) + if is_windowed: + if isinstance(window, bool) or not isinstance(window, int) or window <= 0: + raise ValueError(f"window must be a positive integer for {name}") + if window > ALPHA158_PHASE1_MAX_WINDOW: + raise ValueError( + f"window exceeds maximum supported value {ALPHA158_PHASE1_MAX_WINDOW} for {name}" + ) + if not is_windowed and window is not None: + raise ValueError(f"window is not supported for {name}") + + if name == "correlation": + if secondary is None: + raise ValueError("secondary is required for correlation") + if not series.index.equals(secondary.index): + raise ValueError("secondary index must align with series") + return correlation(series, secondary, window) # type: ignore[arg-type] + + if secondary is not None: + raise ValueError(f"secondary is not supported for {name}") + + operator = _PHASE1_OPERATOR_FUNCTIONS[name] + if name == "rank": + return operator(series) + return operator(series, window) + + # ── 组合算子(alpha158 公式样本) ───────────────────────── @@ -2755,6 +2890,10 @@ __all__ = [ "max_pair", "min_pair", "indneutralize", + "ALPHA158_PHASE1_MAX_WINDOW", + "ALPHA158_PHASE1_OPERATOR_SPECS", + "list_phase1_operators", + "evaluate_phase1_operator", "alpha_001", "alpha_002", "alpha_003", diff --git a/tests/test_alpha_factors.py b/tests/test_alpha_factors.py index ddfef21..6c3fc09 100644 --- a/tests/test_alpha_factors.py +++ b/tests/test_alpha_factors.py @@ -8,6 +8,7 @@ import pytest from quant_engine.alpha_factors import ( ALPHA158_REGISTRY, + ALPHA158_PHASE1_OPERATOR_SPECS, alpha_001, alpha_002, alpha_003, @@ -166,6 +167,8 @@ from quant_engine.alpha_factors import ( alpha_156, alpha_157, alpha_158, + evaluate_phase1_operator, + list_phase1_operators, correlation, covariance, decay_linear, @@ -1232,3 +1235,96 @@ def test_parse_alpha_formula_round_trip_jsonb(): serialized = json.dumps(parsed) assert isinstance(serialized, str) assert "ts_rank" in serialized + + +# ── v1.2.0 Phase 1: deterministic operator dispatch contract ────────────── + + +def test_phase1_operator_catalog_is_explicit_and_serializable(): + """Phase 1 exposes a stable, JSON-friendly catalog for downstream callers.""" + import json + + expected = { + "rank", + "delta", + "ts_mean", + "ts_std", + "ts_rank", + "correlation", + "ts_min", + "ts_max", + "ts_sum", + "decay_linear", + } + assert set(list_phase1_operators()) == expected + assert set(ALPHA158_PHASE1_OPERATOR_SPECS) == expected + json.dumps(ALPHA158_PHASE1_OPERATOR_SPECS) + for name, spec in ALPHA158_PHASE1_OPERATOR_SPECS.items(): + assert spec["name"] == name + assert isinstance(spec["inputs"], list) + assert isinstance(spec["formula"], str) + + +def test_phase1_unary_operators_preserve_index_and_are_deterministic(): + values = pd.Series([1.0, 2.0, 3.0, 4.0], index=["a", "b", "c", "d"]) + + first = evaluate_phase1_operator("rank", values) + second = evaluate_phase1_operator("rank", values) + + pd.testing.assert_series_equal(first, second) + assert first.index.equals(values.index) + assert first.iloc[-1] == pytest.approx(1.0) + + +@pytest.mark.parametrize( + ("name", "window"), + [ + ("delta", 2), + ("ts_mean", 2), + ("ts_std", 2), + ("ts_rank", 2), + ("ts_min", 2), + ("ts_max", 2), + ("ts_sum", 2), + ("decay_linear", 2), + ], +) +def test_phase1_windowed_operators_require_explicit_window(name: str, window: int): + values = pd.Series([1.0, 2.0, 3.0, 4.0]) + + result = evaluate_phase1_operator(name, values, window=window) + + assert result.index.equals(values.index) + with pytest.raises(ValueError, match="window"): + evaluate_phase1_operator(name, values) + with pytest.raises(ValueError, match="positive integer"): + evaluate_phase1_operator(name, values, window=1.5) # type: ignore[arg-type] + + +def test_phase1_binary_correlation_requires_aligned_secondary_input(): + values = pd.Series([1.0, 2.0, 3.0, 4.0]) + other = pd.Series([4.0, 3.0, 2.0, 1.0]) + + result = evaluate_phase1_operator("correlation", values, other, window=2) + + assert result.iloc[-1] == pytest.approx(-1.0) + with pytest.raises(ValueError, match="secondary"): + evaluate_phase1_operator("correlation", values, window=2) + + +def test_phase1_dispatch_rejects_unknown_or_unused_arguments(): + values = pd.Series([1.0, 2.0, 3.0]) + + with pytest.raises(KeyError, match="not registered"): + evaluate_phase1_operator("unknown", values) + with pytest.raises(ValueError, match="window"): + evaluate_phase1_operator("rank", values, window=2) + with pytest.raises(ValueError, match="secondary"): + evaluate_phase1_operator("rank", values, values) + + +def test_phase1_dispatch_rejects_window_above_supported_limit(): + values = pd.Series([1.0, 2.0, 3.0]) + + with pytest.raises(ValueError, match="maximum"): + evaluate_phase1_operator("ts_mean", values, window=2**63) From e72fe0a8d1451cc90d06dbc5b0621e42ba348e5c Mon Sep 17 00:00:00 2001 From: ageorge156 Date: Fri, 28 Aug 2026 18:28:22 +0800 Subject: [PATCH 2/5] Merge remote-tracking branch 'origin/main' into codex/research-alpha158-phase2-20260827 (#9) --- src/quant_engine/alpha_factors.py | 231 ++++++++++++++++++++++++++++++ tests/test_alpha_factors.py | 135 +++++++++++++++++ 2 files changed, 366 insertions(+) diff --git a/src/quant_engine/alpha_factors.py b/src/quant_engine/alpha_factors.py index 2079417..4ed8773 100644 --- a/src/quant_engine/alpha_factors.py +++ b/src/quant_engine/alpha_factors.py @@ -344,6 +344,233 @@ def evaluate_phase1_operator( return operator(series, window) +# ── Phase 2 cumulative operator contract ────────────────────────────── + +# Phase 2 is cumulative: downstream callers can upgrade to one dispatch +# surface covering every existing alpha158 building block, while Phase 1 +# names, metadata, ordering, and evaluation remain unchanged. +ALPHA158_PHASE2_MAX_WINDOW = ALPHA158_PHASE1_MAX_WINDOW + +ALPHA158_PHASE2_OPERATOR_SPECS: dict[str, dict[str, Any]] = { + name: { + **spec, + "parameters": ["window"] if bool(spec["windowed"]) else [], + } + for name, spec in ALPHA158_PHASE1_OPERATOR_SPECS.items() +} +ALPHA158_PHASE2_OPERATOR_SPECS.update( + { + "ts_argmin": { + "name": "ts_argmin", + "formula": "ts_argmin(series, window)", + "inputs": ["series"], + "parameters": ["window"], + "windowed": True, + }, + "ts_argmax": { + "name": "ts_argmax", + "formula": "ts_argmax(series, window)", + "inputs": ["series"], + "parameters": ["window"], + "windowed": True, + }, + "product": { + "name": "product", + "formula": "product(series, window)", + "inputs": ["series"], + "parameters": ["window"], + "windowed": True, + }, + "returns": { + "name": "returns", + "formula": "returns(series)", + "inputs": ["series"], + "parameters": [], + "windowed": False, + }, + "scale": { + "name": "scale", + "formula": "scale(series)", + "inputs": ["series"], + "parameters": [], + "windowed": False, + }, + "signed_power": { + "name": "signed_power", + "formula": "signed_power(series, exponent)", + "inputs": ["series"], + "parameters": ["exponent"], + "windowed": False, + }, + "stddev": { + "name": "stddev", + "formula": "stddev(series, window)", + "inputs": ["series"], + "parameters": ["window"], + "windowed": True, + }, + "covariance": { + "name": "covariance", + "formula": "covariance(series, secondary, window)", + "inputs": ["series", "secondary"], + "parameters": ["window"], + "windowed": True, + }, + "log": { + "name": "log", + "formula": "log(series)", + "inputs": ["series"], + "parameters": [], + "windowed": False, + }, + "abs_series": { + "name": "abs_series", + "formula": "abs_series(series)", + "inputs": ["series"], + "parameters": [], + "windowed": False, + }, + "sign": { + "name": "sign", + "formula": "sign(series)", + "inputs": ["series"], + "parameters": [], + "windowed": False, + }, + "max_pair": { + "name": "max_pair", + "formula": "max_pair(series, secondary)", + "inputs": ["series", "secondary"], + "parameters": [], + "windowed": False, + }, + "min_pair": { + "name": "min_pair", + "formula": "min_pair(series, secondary)", + "inputs": ["series", "secondary"], + "parameters": [], + "windowed": False, + }, + "indneutralize": { + "name": "indneutralize", + "formula": "indneutralize(series, groups)", + "inputs": ["series", "groups"], + "parameters": [], + "windowed": False, + }, + } +) + +_PHASE2_OPERATOR_FUNCTIONS: dict[str, Callable[..., pd.Series]] = { + **_PHASE1_OPERATOR_FUNCTIONS, + "ts_argmin": ts_argmin, + "ts_argmax": ts_argmax, + "product": product, + "returns": returns, + "scale": scale, + "signed_power": signed_power, + "stddev": stddev, + "covariance": covariance, + "log": log, + "abs_series": abs_series, + "sign": sign, + "max_pair": max_pair, + "min_pair": min_pair, + "indneutralize": indneutralize, +} + +_PHASE2_WINDOWED_OPERATORS = frozenset( + name for name, spec in ALPHA158_PHASE2_OPERATOR_SPECS.items() if bool(spec["windowed"]) +) +_PHASE2_BINARY_OPERATORS = frozenset({"correlation", "covariance", "max_pair", "min_pair"}) + + +def list_phase2_operators() -> tuple[str, ...]: + """Return all Phase 2 operator names in stable cumulative order.""" + return tuple(ALPHA158_PHASE2_OPERATOR_SPECS) + + +def _validate_phase2_window(name: str, window: int | None) -> int: + if isinstance(window, bool) or not isinstance(window, int) or window <= 0: + raise ValueError(f"window must be a positive integer for {name}") + if window > ALPHA158_PHASE2_MAX_WINDOW: + raise ValueError( + f"window exceeds maximum supported value {ALPHA158_PHASE2_MAX_WINDOW} for {name}" + ) + return window + + +def evaluate_phase2_operator( + name: str, + series: pd.Series, + secondary: pd.Series | None = None, + *, + window: int | None = None, + exponent: float | None = None, + groups: pd.Series | None = None, +) -> pd.Series: + """Evaluate any existing alpha158 building block through a strict contract. + + Phase 2 rejects implicit alignment, missing required arguments, unused + arguments, unbounded windows, and non-finite exponents before dispatch. + """ + if name not in ALPHA158_PHASE2_OPERATOR_SPECS: + raise KeyError(f"operator {name!r} not registered") + if not isinstance(series, pd.Series): + raise TypeError("series must be a pandas Series") + + validated_window: int | None = None + if name in _PHASE2_WINDOWED_OPERATORS: + validated_window = _validate_phase2_window(name, window) + elif window is not None: + raise ValueError(f"window is not supported for {name}") + + if name in _PHASE2_BINARY_OPERATORS: + if secondary is None: + raise ValueError(f"secondary is required for {name}") + if not isinstance(secondary, pd.Series): + raise TypeError("secondary must be a pandas Series") + if not series.index.equals(secondary.index): + raise ValueError("secondary index must align with series") + elif secondary is not None: + raise ValueError(f"secondary is not supported for {name}") + + validated_exponent: float | None = None + if name == "signed_power": + if ( + isinstance(exponent, bool) + or not isinstance(exponent, (int, float)) + or not np.isfinite(exponent) + ): + raise ValueError("exponent must be a finite number for signed_power") + validated_exponent = float(exponent) + elif exponent is not None: + raise ValueError(f"exponent is not supported for {name}") + + if name == "indneutralize": + if groups is None: + raise ValueError("groups is required for indneutralize") + if not isinstance(groups, pd.Series): + raise TypeError("groups must be a pandas Series") + if not series.index.equals(groups.index): + raise ValueError("groups index must align with series") + elif groups is not None: + raise ValueError(f"groups is not supported for {name}") + + operator = _PHASE2_OPERATOR_FUNCTIONS[name] + if name == "signed_power": + return operator(series, validated_exponent) + if name == "indneutralize": + return operator(series, groups) + if name in {"correlation", "covariance"}: + return operator(series, secondary, validated_window) + if name in {"max_pair", "min_pair"}: + return operator(series, secondary) + if validated_window is not None: + return operator(series, validated_window) + return operator(series) + + # ── 组合算子(alpha158 公式样本) ───────────────────────── @@ -2894,6 +3121,10 @@ __all__ = [ "ALPHA158_PHASE1_OPERATOR_SPECS", "list_phase1_operators", "evaluate_phase1_operator", + "ALPHA158_PHASE2_MAX_WINDOW", + "ALPHA158_PHASE2_OPERATOR_SPECS", + "list_phase2_operators", + "evaluate_phase2_operator", "alpha_001", "alpha_002", "alpha_003", diff --git a/tests/test_alpha_factors.py b/tests/test_alpha_factors.py index 6c3fc09..4174d3d 100644 --- a/tests/test_alpha_factors.py +++ b/tests/test_alpha_factors.py @@ -9,6 +9,7 @@ import pytest from quant_engine.alpha_factors import ( ALPHA158_REGISTRY, ALPHA158_PHASE1_OPERATOR_SPECS, + ALPHA158_PHASE2_OPERATOR_SPECS, alpha_001, alpha_002, alpha_003, @@ -168,7 +169,9 @@ from quant_engine.alpha_factors import ( alpha_157, alpha_158, evaluate_phase1_operator, + evaluate_phase2_operator, list_phase1_operators, + list_phase2_operators, correlation, covariance, decay_linear, @@ -1328,3 +1331,135 @@ def test_phase1_dispatch_rejects_window_above_supported_limit(): with pytest.raises(ValueError, match="maximum"): evaluate_phase1_operator("ts_mean", values, window=2**63) + + +# ── v1.2.0 Phase 2: cumulative deterministic operator contract ───────────── + + +def test_phase2_operator_catalog_is_cumulative_stable_and_serializable(): + """Phase 2 exposes all existing building blocks without changing Phase 1.""" + import json + + phase1 = list_phase1_operators() + expected_phase2 = ( + *phase1, + "ts_argmin", + "ts_argmax", + "product", + "returns", + "scale", + "signed_power", + "stddev", + "covariance", + "log", + "abs_series", + "sign", + "max_pair", + "min_pair", + "indneutralize", + ) + + assert list_phase2_operators() == expected_phase2 + assert tuple(ALPHA158_PHASE2_OPERATOR_SPECS) == expected_phase2 + assert tuple(ALPHA158_PHASE1_OPERATOR_SPECS) == phase1 + json.dumps(ALPHA158_PHASE2_OPERATOR_SPECS) + for name, spec in ALPHA158_PHASE2_OPERATOR_SPECS.items(): + assert spec["name"] == name + assert isinstance(spec["inputs"], list) + assert isinstance(spec["parameters"], list) + assert isinstance(spec["formula"], str) + + +@pytest.mark.parametrize("name", ["ts_argmin", "ts_argmax", "product", "stddev"]) +def test_phase2_windowed_unary_dispatch_is_deterministic(name: str): + values = pd.Series([3.0, 1.0, 4.0, 2.0], index=["a", "b", "c", "d"]) + + first = evaluate_phase2_operator(name, values, window=3) + second = evaluate_phase2_operator(name, values, window=3) + + pd.testing.assert_series_equal(first, second) + assert first.index.equals(values.index) + with pytest.raises(ValueError, match="window"): + evaluate_phase2_operator(name, values) + + +@pytest.mark.parametrize("name", ["returns", "scale", "log", "abs_series", "sign"]) +def test_phase2_unary_dispatch_rejects_unused_arguments(name: str): + values = pd.Series([1.0, 2.0, 4.0], index=["a", "b", "c"]) + + result = evaluate_phase2_operator(name, values) + + assert result.index.equals(values.index) + with pytest.raises(ValueError, match="window"): + evaluate_phase2_operator(name, values, window=2) + with pytest.raises(ValueError, match="secondary"): + evaluate_phase2_operator(name, values, secondary=values) + + +@pytest.mark.parametrize( + ("name", "window"), + [("correlation", 2), ("covariance", 2), ("max_pair", None), ("min_pair", None)], +) +def test_phase2_binary_dispatch_requires_aligned_secondary(name: str, window: int | None): + values = pd.Series([1.0, 2.0, 3.0], index=["a", "b", "c"]) + secondary = pd.Series([3.0, 2.0, 1.0], index=values.index) + + result = evaluate_phase2_operator(name, values, secondary=secondary, window=window) + + assert result.index.equals(values.index) + with pytest.raises(ValueError, match="secondary is required"): + evaluate_phase2_operator(name, values, window=window) + with pytest.raises(ValueError, match="secondary index"): + evaluate_phase2_operator( + name, + values, + secondary=secondary.rename(index={"c": "z"}), + window=window, + ) + + +def test_phase2_signed_power_requires_finite_numeric_exponent(): + values = pd.Series([-4.0, 0.0, 9.0]) + + result = evaluate_phase2_operator("signed_power", values, exponent=0.5) + + pd.testing.assert_series_equal(result, pd.Series([-2.0, 0.0, 3.0])) + for exponent in (None, True, float("inf"), float("nan"), "2"): + with pytest.raises(ValueError, match="exponent"): + evaluate_phase2_operator( # type: ignore[arg-type] + "signed_power", + values, + exponent=exponent, + ) + + +def test_phase2_indneutralize_requires_aligned_groups(): + values = pd.Series([1.0, 3.0, 10.0, 14.0], index=["a", "b", "c", "d"]) + groups = pd.Series(["x", "x", "y", "y"], index=values.index) + + result = evaluate_phase2_operator("indneutralize", values, groups=groups) + + pd.testing.assert_series_equal(result, pd.Series([-1.0, 1.0, -2.0, 2.0], index=values.index)) + with pytest.raises(ValueError, match="groups is required"): + evaluate_phase2_operator("indneutralize", values) + with pytest.raises(ValueError, match="groups index"): + evaluate_phase2_operator( + "indneutralize", + values, + groups=groups.rename(index={"d": "z"}), + ) + + +def test_phase2_dispatch_validates_primary_series_and_unused_parameters(): + values = pd.Series([1.0, 2.0, 3.0]) + + with pytest.raises(TypeError, match="series must be a pandas Series"): + evaluate_phase2_operator("rank", [1.0, 2.0, 3.0]) # type: ignore[arg-type] + with pytest.raises(KeyError, match="not registered"): + evaluate_phase2_operator("unknown", values) + with pytest.raises(ValueError, match="exponent"): + evaluate_phase2_operator("rank", values, exponent=2.0) + with pytest.raises(ValueError, match="groups"): + evaluate_phase2_operator("rank", values, groups=pd.Series(["x", "x", "x"])) + with pytest.raises(ValueError, match="maximum"): + evaluate_phase2_operator("product", values, window=253) From 90a43adda2c2509d0a92168ab5a85e512f534b52 Mon Sep 17 00:00:00 2001 From: ageorge156 Date: Fri, 28 Aug 2026 18:29:47 +0800 Subject: [PATCH 3/5] Merge remote-tracking branch 'origin/main' into codex/research-alpha158-phase3-formula-con (#10) --- src/quant_engine/alpha_factors.py | 188 +++++++++++++++++++++++++++++- tests/test_alpha_factors.py | 178 ++++++++++++++++++++++++++++ 2 files changed, 364 insertions(+), 2 deletions(-) diff --git a/src/quant_engine/alpha_factors.py b/src/quant_engine/alpha_factors.py index 4ed8773..f71fcc8 100644 --- a/src/quant_engine/alpha_factors.py +++ b/src/quant_engine/alpha_factors.py @@ -15,8 +15,9 @@ v1.2.0 Phase 0:5 个基础算子 + 5 个 alpha 公式(alpha001–alpha005) from __future__ import annotations -from collections.abc import Callable -from typing import Any +from collections.abc import Callable, Mapping +from types import MappingProxyType +from typing import Any, cast import numpy as np import pandas as pd @@ -3092,6 +3093,184 @@ def parse_alpha_formula(formula_str: str) -> dict[str, Any]: return parsed +# ── Phase 3 formula contract: frozen alpha001-alpha050 surface ────────────── + +# Formula functions remain the implementation source of truth. This contract +# freezes their callable surface separately from formula dependencies so that +# historical compatibility-only arguments remain explicit without rewriting +# formulas or changing direct-call APIs. +ALPHA158_PHASE3_FORMULA_CONTRACT_VERSION = "1.0.0" +ALPHA158_PHASE3_FORMULA_CATALOG_SHA256 = ( + "9a3360e5ee77a85a35d3c2fdab1efaa531fa0c263a2cb2bb5b965a1fb96fe1bd" +) + +_PHASE3_FORMULA_FUNCTIONS: dict[str, Callable[..., pd.Series]] = { + "alpha_001": alpha_001, + "alpha_002": alpha_002, + "alpha_003": alpha_003, + "alpha_004": alpha_004, + "alpha_005": alpha_005, + "alpha_006": alpha_006, + "alpha_007": alpha_007, + "alpha_008": alpha_008, + "alpha_009": alpha_009, + "alpha_010": alpha_010, + "alpha_011": alpha_011, + "alpha_012": alpha_012, + "alpha_013": alpha_013, + "alpha_014": alpha_014, + "alpha_015": alpha_015, + "alpha_016": alpha_016, + "alpha_017": alpha_017, + "alpha_018": alpha_018, + "alpha_019": alpha_019, + "alpha_020": alpha_020, + "alpha_021": alpha_021, + "alpha_022": alpha_022, + "alpha_023": alpha_023, + "alpha_024": alpha_024, + "alpha_025": alpha_025, + "alpha_026": alpha_026, + "alpha_027": alpha_027, + "alpha_028": alpha_028, + "alpha_029": alpha_029, + "alpha_030": alpha_030, + "alpha_031": alpha_031, + "alpha_032": alpha_032, + "alpha_033": alpha_033, + "alpha_034": alpha_034, + "alpha_035": alpha_035, + "alpha_036": alpha_036, + "alpha_037": alpha_037, + "alpha_038": alpha_038, + "alpha_039": alpha_039, + "alpha_040": alpha_040, + "alpha_041": alpha_041, + "alpha_042": alpha_042, + "alpha_043": alpha_043, + "alpha_044": alpha_044, + "alpha_045": alpha_045, + "alpha_046": alpha_046, + "alpha_047": alpha_047, + "alpha_048": alpha_048, + "alpha_049": alpha_049, + "alpha_050": alpha_050, +} + +_PHASE3_FORMULA_INPUT_OVERRIDES: dict[str, list[str]] = { + "alpha_011": ["close", "high", "low"], + "alpha_035": ["volume"], + "alpha_036": ["close"], + "alpha_040": ["high", "low"], + "alpha_042": ["close"], + "alpha_043": ["volume"], +} + +_PHASE3_INPUT_CATEGORIES = { + 1: "single", + 2: "pair", + 3: "triple", + 4: "quadruple", +} + + +def _phase3_call_inputs(function: Callable[..., pd.Series]) -> list[str]: + import inspect + + parameters = list(inspect.signature(function).parameters.values()) + if any( + parameter.kind is not inspect.Parameter.POSITIONAL_OR_KEYWORD + or parameter.default is not inspect.Parameter.empty + for parameter in parameters + ): + raise RuntimeError(f"unsupported formula signature for {function.__name__}") + return ["open" if parameter.name == "open_" else parameter.name for parameter in parameters] + + +def _phase3_string_list(meta: dict[str, Any], field: str, alpha_id: str) -> list[str]: + value = meta[field] + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise RuntimeError(f"{field} must be a list of strings for {alpha_id}") + return list(value) + + +def _build_phase3_formula_specs() -> dict[str, dict[str, Any]]: + specs: dict[str, dict[str, Any]] = {} + for alpha_id, function in _PHASE3_FORMULA_FUNCTIONS.items(): + meta = ALPHA158_REGISTRY[alpha_id] + call_inputs = _phase3_call_inputs(function) + formula_inputs = _PHASE3_FORMULA_INPUT_OVERRIDES.get(alpha_id, call_inputs) + input_category = _PHASE3_INPUT_CATEGORIES.get(len(call_inputs)) + if input_category is None: + raise RuntimeError(f"unsupported formula input count for {alpha_id}") + specs[alpha_id] = { + "name": alpha_id, + "contract_version": ALPHA158_PHASE3_FORMULA_CONTRACT_VERSION, + "formula": meta["formula"], + "category": meta["category"], + "complexity": meta["complexity"], + "parameters": _phase3_string_list(meta, "params", alpha_id), + "description": meta["description"], + "references": _phase3_string_list(meta, "references", alpha_id), + "call_inputs": list(call_inputs), + "formula_inputs": list(formula_inputs), + "input_category": input_category, + } + return specs + + +def _freeze_phase3_formula_specs( + specs: dict[str, dict[str, Any]], +) -> Mapping[str, Mapping[str, Any]]: + frozen_specs: dict[str, Mapping[str, Any]] = {} + for alpha_id, spec in specs.items(): + frozen_specs[alpha_id] = MappingProxyType( + {field: tuple(value) if isinstance(value, list) else value for field, value in spec.items()} + ) + return MappingProxyType(frozen_specs) + + +ALPHA158_PHASE3_FORMULA_SPECS: Mapping[str, Mapping[str, Any]] = ( + _freeze_phase3_formula_specs(_build_phase3_formula_specs()) +) + + +def list_phase3_formulas() -> tuple[str, ...]: + """Return the frozen alpha001-alpha050 formula IDs in stable order.""" + return tuple(ALPHA158_PHASE3_FORMULA_SPECS) + + +def evaluate_phase3_formula(name: str, **inputs: pd.Series) -> pd.Series: + """Evaluate a Phase 3 formula with an exact, alignment-safe input contract.""" + if name not in ALPHA158_PHASE3_FORMULA_SPECS: + raise KeyError(f"formula {name!r} not registered") + + spec = ALPHA158_PHASE3_FORMULA_SPECS[name] + required_inputs = cast(tuple[str, ...], spec["call_inputs"]) + missing_inputs = [field for field in required_inputs if field not in inputs] + unexpected_inputs = sorted(field for field in inputs if field not in required_inputs) + if missing_inputs or unexpected_inputs: + details: list[str] = [] + if missing_inputs: + details.append(f"missing inputs {missing_inputs}") + if unexpected_inputs: + details.append(f"unexpected inputs {unexpected_inputs}") + raise ValueError(f"invalid inputs for {name}: {'; '.join(details)}") + + for field in required_inputs: + if not isinstance(inputs[field], pd.Series): + raise TypeError(f"{field} must be a pandas Series") + + primary_field = required_inputs[0] + primary = inputs[primary_field] + for field in required_inputs[1:]: + if not primary.index.equals(inputs[field].index): + raise ValueError(f"{field} index must align with {primary_field}") + + function = _PHASE3_FORMULA_FUNCTIONS[name] + return function(*(inputs[field] for field in required_inputs)) + + __all__ = [ "rank", "delta", @@ -3125,6 +3304,11 @@ __all__ = [ "ALPHA158_PHASE2_OPERATOR_SPECS", "list_phase2_operators", "evaluate_phase2_operator", + "ALPHA158_PHASE3_FORMULA_CONTRACT_VERSION", + "ALPHA158_PHASE3_FORMULA_CATALOG_SHA256", + "ALPHA158_PHASE3_FORMULA_SPECS", + "list_phase3_formulas", + "evaluate_phase3_formula", "alpha_001", "alpha_002", "alpha_003", diff --git a/tests/test_alpha_factors.py b/tests/test_alpha_factors.py index 4174d3d..de3044f 100644 --- a/tests/test_alpha_factors.py +++ b/tests/test_alpha_factors.py @@ -6,10 +6,14 @@ import numpy as np import pandas as pd import pytest +import quant_engine.alpha_factors as alpha_factors_module from quant_engine.alpha_factors import ( ALPHA158_REGISTRY, ALPHA158_PHASE1_OPERATOR_SPECS, ALPHA158_PHASE2_OPERATOR_SPECS, + ALPHA158_PHASE3_FORMULA_CATALOG_SHA256, + ALPHA158_PHASE3_FORMULA_CONTRACT_VERSION, + ALPHA158_PHASE3_FORMULA_SPECS, alpha_001, alpha_002, alpha_003, @@ -170,8 +174,10 @@ from quant_engine.alpha_factors import ( alpha_158, evaluate_phase1_operator, evaluate_phase2_operator, + evaluate_phase3_formula, list_phase1_operators, list_phase2_operators, + list_phase3_formulas, correlation, covariance, decay_linear, @@ -1463,3 +1469,175 @@ def test_phase2_dispatch_validates_primary_series_and_unused_parameters(): evaluate_phase2_operator("rank", values, groups=pd.Series(["x", "x", "x"])) with pytest.raises(ValueError, match="maximum"): evaluate_phase2_operator("product", values, window=253) + + +# ── Alpha158 Phase 3: versioned alpha001-alpha050 formula contract ────────── + + +def _phase3_market_inputs() -> dict[str, pd.Series]: + positions = np.arange(80, dtype=float) + index = pd.RangeIndex(len(positions), name="row") + open_ = pd.Series(100.0 + positions * 0.2 + np.sin(positions / 4.0), index=index) + close = pd.Series(100.5 + positions * 0.18 + np.cos(positions / 5.0), index=index) + high = pd.Series(np.maximum(open_, close) + 1.0, index=index) + low = pd.Series(np.minimum(open_, close) - 1.0, index=index) + volume = pd.Series(1_000.0 + positions**1.3 + 20.0 * np.sin(positions / 3.0), index=index) + vwap = (open_ + close + high + low) / 4.0 + return { + "open": open_, + "close": close, + "high": high, + "low": low, + "volume": volume, + "vwap": vwap, + } + + +def test_phase3_formula_catalog_is_versioned_exact_and_content_addressed(): + import hashlib + import json + from collections import Counter + + expected_ids = tuple(f"alpha_{number:03d}" for number in range(1, 51)) + expected_fields = { + "name", + "contract_version", + "formula", + "category", + "complexity", + "parameters", + "description", + "references", + "call_inputs", + "formula_inputs", + "input_category", + } + + assert ALPHA158_PHASE3_FORMULA_CONTRACT_VERSION == "1.0.0" + assert list_phase3_formulas() == expected_ids + assert tuple(ALPHA158_PHASE3_FORMULA_SPECS) == expected_ids + assert Counter( + spec["input_category"] for spec in ALPHA158_PHASE3_FORMULA_SPECS.values() + ) == {"single": 19, "pair": 26, "triple": 4, "quadruple": 1} + + for alpha_id, spec in ALPHA158_PHASE3_FORMULA_SPECS.items(): + assert set(spec) == expected_fields + assert spec["name"] == alpha_id + assert spec["contract_version"] == ALPHA158_PHASE3_FORMULA_CONTRACT_VERSION + assert spec["formula"] == ALPHA158_REGISTRY[alpha_id]["formula"] + + serializable_specs = { + alpha_id: { + field: list(value) if isinstance(value, tuple) else value + for field, value in spec.items() + } + for alpha_id, spec in ALPHA158_PHASE3_FORMULA_SPECS.items() + } + encoded = json.dumps( + serializable_specs, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ).encode() + assert hashlib.sha256(encoded).hexdigest() == ALPHA158_PHASE3_FORMULA_CATALOG_SHA256 + assert ALPHA158_PHASE3_FORMULA_CATALOG_SHA256 == ( + "9a3360e5ee77a85a35d3c2fdab1efaa531fa0c263a2cb2bb5b965a1fb96fe1bd" + ) + + +def test_phase3_formula_catalog_is_recursively_immutable(): + import operator + + with pytest.raises(TypeError): + operator.setitem(ALPHA158_PHASE3_FORMULA_SPECS, "alpha_001", {}) + with pytest.raises(TypeError): + operator.setitem( + ALPHA158_PHASE3_FORMULA_SPECS["alpha_001"], + "formula", + "changed", + ) + with pytest.raises(TypeError): + operator.setitem( + ALPHA158_PHASE3_FORMULA_SPECS["alpha_001"]["call_inputs"], + 0, + "volume", + ) + + +def test_phase3_catalog_freezes_callable_signatures_without_rewriting_formulas(): + import inspect + + legacy_formula_input_differences = { + "alpha_011": ("close", "high", "low"), + "alpha_035": ("volume",), + "alpha_036": ("close",), + "alpha_040": ("high", "low"), + "alpha_042": ("close",), + "alpha_043": ("volume",), + } + + for alpha_id, spec in ALPHA158_PHASE3_FORMULA_SPECS.items(): + function = getattr(alpha_factors_module, alpha_id) + signature_inputs = tuple( + "open" if name == "open_" else name + for name in inspect.signature(function).parameters + ) + assert spec["call_inputs"] == signature_inputs + assert spec["formula_inputs"] == legacy_formula_input_differences.get( + alpha_id, + signature_inputs, + ) + + assert ALPHA158_PHASE3_FORMULA_SPECS["alpha_035"]["call_inputs"] == ( + "close", + "volume", + ) + assert ALPHA158_REGISTRY["alpha_035"]["inputs"] == ["volume"] + + +def test_phase3_dispatch_matches_all_existing_alpha001_alpha050_functions(): + inputs = _phase3_market_inputs() + + for alpha_id, spec in ALPHA158_PHASE3_FORMULA_SPECS.items(): + call_inputs = spec["call_inputs"] + function = getattr(alpha_factors_module, alpha_id) + expected = function(*(inputs[name] for name in call_inputs)) + actual = evaluate_phase3_formula( + alpha_id, + **{name: inputs[name] for name in reversed(call_inputs)}, + ) + pd.testing.assert_series_equal(actual, expected) + + +def test_phase3_dispatch_rejects_unknown_missing_extra_and_non_series_inputs(): + inputs = _phase3_market_inputs() + + with pytest.raises(KeyError, match="not registered"): + evaluate_phase3_formula("alpha_051", close=inputs["close"]) + with pytest.raises(ValueError, match=r"missing inputs.*volume"): + evaluate_phase3_formula("alpha_005", close=inputs["close"]) + with pytest.raises(ValueError, match=r"unexpected inputs.*vwap"): + evaluate_phase3_formula( + "alpha_005", + close=inputs["close"], + volume=inputs["volume"], + vwap=inputs["vwap"], + ) + with pytest.raises(TypeError, match="close must be a pandas Series"): + evaluate_phase3_formula( # type: ignore[arg-type] + "alpha_005", + close=[1.0, 2.0], + volume=inputs["volume"], + ) + + +def test_phase3_dispatch_rejects_implicit_series_alignment(): + inputs = _phase3_market_inputs() + misaligned_volume = inputs["volume"].rename(index={79: 80}) + + with pytest.raises(ValueError, match="volume index must align with close"): + evaluate_phase3_formula( + "alpha_005", + close=inputs["close"], + volume=misaligned_volume, + ) From 03e38d5123c3879073405831c7647887eac21d83 Mon Sep 17 00:00:00 2001 From: ageorge156 Date: Fri, 28 Aug 2026 18:30:57 +0800 Subject: [PATCH 4/5] Merge remote-tracking branch 'origin/main' into codex/research-alpha158-phase4-formula-con (#11) --- src/quant_engine/alpha_factors.py | 142 ++++++++++++++++++++++++++++ tests/test_alpha_factors.py | 148 ++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) diff --git a/src/quant_engine/alpha_factors.py b/src/quant_engine/alpha_factors.py index f71fcc8..4cfe7db 100644 --- a/src/quant_engine/alpha_factors.py +++ b/src/quant_engine/alpha_factors.py @@ -3271,6 +3271,143 @@ def evaluate_phase3_formula(name: str, **inputs: pd.Series) -> pd.Series: return function(*(inputs[field] for field in required_inputs)) +# ── Phase 4 formula contract: frozen alpha051-alpha100 surface ────────────── + +# Phase 4 extends the versioned formula contract without mutating the Phase 3 +# catalogue, digest, dispatch surface, or the existing formula functions. +ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION = "1.0.0" +ALPHA158_PHASE4_FORMULA_CATALOG_SHA256 = ( + "858daf5e2abab5063fc28fcf7c79936096e2c76dde582fc7bab78b3458f17054" +) + +_PHASE4_FORMULA_FUNCTIONS: dict[str, Callable[..., pd.Series]] = { + "alpha_051": alpha_051, + "alpha_052": alpha_052, + "alpha_053": alpha_053, + "alpha_054": alpha_054, + "alpha_055": alpha_055, + "alpha_056": alpha_056, + "alpha_057": alpha_057, + "alpha_058": alpha_058, + "alpha_059": alpha_059, + "alpha_060": alpha_060, + "alpha_061": alpha_061, + "alpha_062": alpha_062, + "alpha_063": alpha_063, + "alpha_064": alpha_064, + "alpha_065": alpha_065, + "alpha_066": alpha_066, + "alpha_067": alpha_067, + "alpha_068": alpha_068, + "alpha_069": alpha_069, + "alpha_070": alpha_070, + "alpha_071": alpha_071, + "alpha_072": alpha_072, + "alpha_073": alpha_073, + "alpha_074": alpha_074, + "alpha_075": alpha_075, + "alpha_076": alpha_076, + "alpha_077": alpha_077, + "alpha_078": alpha_078, + "alpha_079": alpha_079, + "alpha_080": alpha_080, + "alpha_081": alpha_081, + "alpha_082": alpha_082, + "alpha_083": alpha_083, + "alpha_084": alpha_084, + "alpha_085": alpha_085, + "alpha_086": alpha_086, + "alpha_087": alpha_087, + "alpha_088": alpha_088, + "alpha_089": alpha_089, + "alpha_090": alpha_090, + "alpha_091": alpha_091, + "alpha_092": alpha_092, + "alpha_093": alpha_093, + "alpha_094": alpha_094, + "alpha_095": alpha_095, + "alpha_096": alpha_096, + "alpha_097": alpha_097, + "alpha_098": alpha_098, + "alpha_099": alpha_099, + "alpha_100": alpha_100, +} + +_PHASE4_INPUT_CATEGORIES = { + 1: "single", + 2: "pair", + 3: "triple", + 4: "quadruple", + 5: "quintuple", +} + + +def _build_phase4_formula_specs() -> dict[str, dict[str, Any]]: + specs: dict[str, dict[str, Any]] = {} + for alpha_id, function in _PHASE4_FORMULA_FUNCTIONS.items(): + meta = ALPHA158_REGISTRY[alpha_id] + call_inputs = _phase3_call_inputs(function) + formula_inputs = _phase3_string_list(meta, "inputs", alpha_id) + input_category = _PHASE4_INPUT_CATEGORIES.get(len(call_inputs)) + if input_category is None: + raise RuntimeError(f"unsupported formula input count for {alpha_id}") + specs[alpha_id] = { + "name": alpha_id, + "contract_version": ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION, + "formula": meta["formula"], + "category": meta["category"], + "complexity": meta["complexity"], + "parameters": _phase3_string_list(meta, "params", alpha_id), + "description": meta["description"], + "references": _phase3_string_list(meta, "references", alpha_id), + "call_inputs": list(call_inputs), + "formula_inputs": formula_inputs, + "input_category": input_category, + } + return specs + + +ALPHA158_PHASE4_FORMULA_SPECS: Mapping[str, Mapping[str, Any]] = ( + _freeze_phase3_formula_specs(_build_phase4_formula_specs()) +) + + +def list_phase4_formulas() -> tuple[str, ...]: + """Return the frozen alpha051-alpha100 formula IDs in stable order.""" + return tuple(ALPHA158_PHASE4_FORMULA_SPECS) + + +def evaluate_phase4_formula(name: str, **inputs: pd.Series) -> pd.Series: + """Evaluate a Phase 4 formula with an exact, alignment-safe input contract.""" + if name not in ALPHA158_PHASE4_FORMULA_SPECS: + raise KeyError(f"formula {name!r} not registered") + + spec = ALPHA158_PHASE4_FORMULA_SPECS[name] + required_inputs = cast(tuple[str, ...], spec["call_inputs"]) + missing_inputs = [field for field in required_inputs if field not in inputs] + unexpected_inputs = sorted(field for field in inputs if field not in required_inputs) + if missing_inputs or unexpected_inputs: + details: list[str] = [] + if missing_inputs: + details.append(f"missing inputs {missing_inputs}") + if unexpected_inputs: + details.append(f"unexpected inputs {unexpected_inputs}") + raise ValueError(f"invalid inputs for {name}: {'; '.join(details)}") + + for field in required_inputs: + if not isinstance(inputs[field], pd.Series): + raise TypeError(f"{field} must be a pandas Series") + + primary_field = required_inputs[0] + primary = inputs[primary_field] + for field in required_inputs[1:]: + if not primary.index.equals(inputs[field].index): + raise ValueError(f"{field} index must align with {primary_field}") + + function = _PHASE4_FORMULA_FUNCTIONS[name] + return function(*(inputs[field] for field in required_inputs)) + + __all__ = [ "rank", "delta", @@ -3309,6 +3446,11 @@ __all__ = [ "ALPHA158_PHASE3_FORMULA_SPECS", "list_phase3_formulas", "evaluate_phase3_formula", + "ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION", + "ALPHA158_PHASE4_FORMULA_CATALOG_SHA256", + "ALPHA158_PHASE4_FORMULA_SPECS", + "list_phase4_formulas", + "evaluate_phase4_formula", "alpha_001", "alpha_002", "alpha_003", diff --git a/tests/test_alpha_factors.py b/tests/test_alpha_factors.py index de3044f..8552078 100644 --- a/tests/test_alpha_factors.py +++ b/tests/test_alpha_factors.py @@ -14,6 +14,9 @@ from quant_engine.alpha_factors import ( ALPHA158_PHASE3_FORMULA_CATALOG_SHA256, ALPHA158_PHASE3_FORMULA_CONTRACT_VERSION, ALPHA158_PHASE3_FORMULA_SPECS, + ALPHA158_PHASE4_FORMULA_CATALOG_SHA256, + ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION, + ALPHA158_PHASE4_FORMULA_SPECS, alpha_001, alpha_002, alpha_003, @@ -175,9 +178,11 @@ from quant_engine.alpha_factors import ( evaluate_phase1_operator, evaluate_phase2_operator, evaluate_phase3_formula, + evaluate_phase4_formula, list_phase1_operators, list_phase2_operators, list_phase3_formulas, + list_phase4_formulas, correlation, covariance, decay_linear, @@ -1641,3 +1646,146 @@ def test_phase3_dispatch_rejects_implicit_series_alignment(): close=inputs["close"], volume=misaligned_volume, ) + + +# ── Alpha158 Phase 4: versioned alpha051-alpha100 formula contract ────────── + + +def test_phase4_formula_catalog_is_versioned_exact_and_content_addressed(): + import hashlib + import json + from collections import Counter + + expected_ids = tuple(f"alpha_{number:03d}" for number in range(51, 101)) + expected_fields = { + "name", + "contract_version", + "formula", + "category", + "complexity", + "parameters", + "description", + "references", + "call_inputs", + "formula_inputs", + "input_category", + } + + assert ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION == "1.0.0" + assert list_phase4_formulas() == expected_ids + assert tuple(ALPHA158_PHASE4_FORMULA_SPECS) == expected_ids + assert Counter( + spec["input_category"] for spec in ALPHA158_PHASE4_FORMULA_SPECS.values() + ) == {"single": 1, "pair": 27, "triple": 11, "quadruple": 10, "quintuple": 1} + + for alpha_id, spec in ALPHA158_PHASE4_FORMULA_SPECS.items(): + assert set(spec) == expected_fields + assert spec["name"] == alpha_id + assert spec["contract_version"] == ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION + assert spec["formula"] == ALPHA158_REGISTRY[alpha_id]["formula"] + + serializable_specs = { + alpha_id: { + field: list(value) if isinstance(value, tuple) else value + for field, value in spec.items() + } + for alpha_id, spec in ALPHA158_PHASE4_FORMULA_SPECS.items() + } + encoded = json.dumps( + serializable_specs, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ).encode() + assert hashlib.sha256(encoded).hexdigest() == ALPHA158_PHASE4_FORMULA_CATALOG_SHA256 + assert ALPHA158_PHASE4_FORMULA_CATALOG_SHA256 == ( + "858daf5e2abab5063fc28fcf7c79936096e2c76dde582fc7bab78b3458f17054" + ) + + +def test_phase4_formula_catalog_is_recursively_immutable(): + import operator + + with pytest.raises(TypeError): + operator.setitem(ALPHA158_PHASE4_FORMULA_SPECS, "alpha_051", {}) + with pytest.raises(TypeError): + operator.setitem( + ALPHA158_PHASE4_FORMULA_SPECS["alpha_051"], + "formula", + "changed", + ) + with pytest.raises(TypeError): + operator.setitem( + ALPHA158_PHASE4_FORMULA_SPECS["alpha_051"]["call_inputs"], + 0, + "volume", + ) + + +def test_phase4_catalog_freezes_callable_and_formula_inputs(): + import inspect + + for alpha_id, spec in ALPHA158_PHASE4_FORMULA_SPECS.items(): + function = getattr(alpha_factors_module, alpha_id) + signature_inputs = tuple( + "open" if name == "open_" else name + for name in inspect.signature(function).parameters + ) + assert spec["call_inputs"] == signature_inputs + assert spec["formula_inputs"] == tuple(ALPHA158_REGISTRY[alpha_id]["inputs"]) + + assert ALPHA158_PHASE4_FORMULA_SPECS["alpha_055"]["call_inputs"] == ( + "open", + "high", + "low", + "volume", + "close", + ) + + +def test_phase4_dispatch_matches_all_existing_alpha051_alpha100_functions(): + inputs = _phase3_market_inputs() + + for alpha_id, spec in ALPHA158_PHASE4_FORMULA_SPECS.items(): + call_inputs = spec["call_inputs"] + function = getattr(alpha_factors_module, alpha_id) + expected = function(*(inputs[name] for name in call_inputs)) + actual = evaluate_phase4_formula( + alpha_id, + **{name: inputs[name] for name in reversed(call_inputs)}, + ) + pd.testing.assert_series_equal(actual, expected) + + +def test_phase4_dispatch_rejects_unknown_missing_extra_and_non_series_inputs(): + inputs = _phase3_market_inputs() + + with pytest.raises(KeyError, match="not registered"): + evaluate_phase4_formula("alpha_050", close=inputs["close"]) + with pytest.raises(ValueError, match=r"missing inputs.*low"): + evaluate_phase4_formula("alpha_051", high=inputs["high"]) + with pytest.raises(ValueError, match=r"unexpected inputs.*vwap"): + evaluate_phase4_formula( + "alpha_051", + high=inputs["high"], + low=inputs["low"], + vwap=inputs["vwap"], + ) + with pytest.raises(TypeError, match="high must be a pandas Series"): + evaluate_phase4_formula( # type: ignore[arg-type] + "alpha_051", + high=[1.0, 2.0], + low=inputs["low"], + ) + + +def test_phase4_dispatch_rejects_implicit_series_alignment(): + inputs = _phase3_market_inputs() + misaligned_low = inputs["low"].rename(index={79: 80}) + + with pytest.raises(ValueError, match="low index must align with high"): + evaluate_phase4_formula( + "alpha_051", + high=inputs["high"], + low=misaligned_low, + ) From 2bc8aea43545ea220a1f209e34ddda6bfaf8649f Mon Sep 17 00:00:00 2001 From: ageorge156 Date: Fri, 28 Aug 2026 18:32:18 +0800 Subject: [PATCH 5/5] Merge remote-tracking branch 'origin/main' into codex/research-alpha158-phase5-formula-con (#12) --- src/quant_engine/alpha_factors.py | 136 +++++++++++++++++++++++ tests/test_alpha_factors.py | 173 ++++++++++++++++++++++++++++++ 2 files changed, 309 insertions(+) diff --git a/src/quant_engine/alpha_factors.py b/src/quant_engine/alpha_factors.py index 4cfe7db..760dda8 100644 --- a/src/quant_engine/alpha_factors.py +++ b/src/quant_engine/alpha_factors.py @@ -3408,6 +3408,137 @@ def evaluate_phase4_formula(name: str, **inputs: pd.Series) -> pd.Series: return function(*(inputs[field] for field in required_inputs)) +# ── Phase 5 formula contract: frozen alpha101-alpha150 surface ────────────── + +# Phase 5 extends the versioned formula contract without mutating any earlier +# catalogue, digest, dispatch surface, or existing formula implementation. +ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION = "1.0.0" +ALPHA158_PHASE5_FORMULA_CATALOG_SHA256 = ( + "3368796169c9fbd39c4a34ea137e569964b15882fbf4de25124790d548db6533" +) + +_PHASE5_FORMULA_FUNCTIONS: dict[str, Callable[..., pd.Series]] = { + "alpha_101": alpha_101, + "alpha_102": alpha_102, + "alpha_103": alpha_103, + "alpha_104": alpha_104, + "alpha_105": alpha_105, + "alpha_106": alpha_106, + "alpha_107": alpha_107, + "alpha_108": alpha_108, + "alpha_109": alpha_109, + "alpha_110": alpha_110, + "alpha_111": alpha_111, + "alpha_112": alpha_112, + "alpha_113": alpha_113, + "alpha_114": alpha_114, + "alpha_115": alpha_115, + "alpha_116": alpha_116, + "alpha_117": alpha_117, + "alpha_118": alpha_118, + "alpha_119": alpha_119, + "alpha_120": alpha_120, + "alpha_121": alpha_121, + "alpha_122": alpha_122, + "alpha_123": alpha_123, + "alpha_124": alpha_124, + "alpha_125": alpha_125, + "alpha_126": alpha_126, + "alpha_127": alpha_127, + "alpha_128": alpha_128, + "alpha_129": alpha_129, + "alpha_130": alpha_130, + "alpha_131": alpha_131, + "alpha_132": alpha_132, + "alpha_133": alpha_133, + "alpha_134": alpha_134, + "alpha_135": alpha_135, + "alpha_136": alpha_136, + "alpha_137": alpha_137, + "alpha_138": alpha_138, + "alpha_139": alpha_139, + "alpha_140": alpha_140, + "alpha_141": alpha_141, + "alpha_142": alpha_142, + "alpha_143": alpha_143, + "alpha_144": alpha_144, + "alpha_145": alpha_145, + "alpha_146": alpha_146, + "alpha_147": alpha_147, + "alpha_148": alpha_148, + "alpha_149": alpha_149, + "alpha_150": alpha_150, +} + + +def _build_phase5_formula_specs() -> dict[str, dict[str, Any]]: + specs: dict[str, dict[str, Any]] = {} + for alpha_id, function in _PHASE5_FORMULA_FUNCTIONS.items(): + meta = ALPHA158_REGISTRY[alpha_id] + call_inputs = _phase3_call_inputs(function) + formula_inputs = _phase3_string_list(meta, "inputs", alpha_id) + input_category = _PHASE4_INPUT_CATEGORIES.get(len(call_inputs)) + if input_category is None: + raise RuntimeError(f"unsupported formula input count for {alpha_id}") + specs[alpha_id] = { + "name": alpha_id, + "contract_version": ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION, + "formula": meta["formula"], + "category": meta["category"], + "complexity": meta["complexity"], + "parameters": _phase3_string_list(meta, "params", alpha_id), + "description": meta["description"], + "references": _phase3_string_list(meta, "references", alpha_id), + "call_inputs": list(call_inputs), + "formula_inputs": formula_inputs, + "input_category": input_category, + } + return specs + + +ALPHA158_PHASE5_FORMULA_SPECS: Mapping[str, Mapping[str, Any]] = ( + _freeze_phase3_formula_specs(_build_phase5_formula_specs()) +) + + +def list_phase5_formulas() -> tuple[str, ...]: + """Return the frozen alpha101-alpha150 formula IDs in stable order.""" + return tuple(ALPHA158_PHASE5_FORMULA_SPECS) + + +def evaluate_phase5_formula(name: str, **inputs: pd.Series) -> pd.Series: + """Evaluate a Phase 5 formula with an exact, alignment-safe input contract.""" + if name not in ALPHA158_PHASE5_FORMULA_SPECS: + raise KeyError(f"formula {name!r} not registered") + + spec = ALPHA158_PHASE5_FORMULA_SPECS[name] + required_inputs = cast(tuple[str, ...], spec["call_inputs"]) + missing_inputs = [field for field in required_inputs if field not in inputs] + unexpected_inputs = sorted(field for field in inputs if field not in required_inputs) + if missing_inputs or unexpected_inputs: + details: list[str] = [] + if missing_inputs: + details.append(f"missing inputs {missing_inputs}") + if unexpected_inputs: + details.append(f"unexpected inputs {unexpected_inputs}") + raise ValueError(f"invalid inputs for {name}: {'; '.join(details)}") + + for field in required_inputs: + if not isinstance(inputs[field], pd.Series): + raise TypeError(f"{field} must be a pandas Series") + + primary_field = required_inputs[0] + primary = inputs[primary_field] + for field in required_inputs[1:]: + if len(inputs[field]) != len(primary): + raise ValueError(f"{field} length must match {primary_field}") + if not primary.index.equals(inputs[field].index): + raise ValueError(f"{field} index must align with {primary_field}") + + function = _PHASE5_FORMULA_FUNCTIONS[name] + return function(*(inputs[field] for field in required_inputs)) + + __all__ = [ "rank", "delta", @@ -3451,6 +3582,11 @@ __all__ = [ "ALPHA158_PHASE4_FORMULA_SPECS", "list_phase4_formulas", "evaluate_phase4_formula", + "ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION", + "ALPHA158_PHASE5_FORMULA_CATALOG_SHA256", + "ALPHA158_PHASE5_FORMULA_SPECS", + "list_phase5_formulas", + "evaluate_phase5_formula", "alpha_001", "alpha_002", "alpha_003", diff --git a/tests/test_alpha_factors.py b/tests/test_alpha_factors.py index 8552078..4b607f3 100644 --- a/tests/test_alpha_factors.py +++ b/tests/test_alpha_factors.py @@ -17,6 +17,9 @@ from quant_engine.alpha_factors import ( ALPHA158_PHASE4_FORMULA_CATALOG_SHA256, ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION, ALPHA158_PHASE4_FORMULA_SPECS, + ALPHA158_PHASE5_FORMULA_CATALOG_SHA256, + ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION, + ALPHA158_PHASE5_FORMULA_SPECS, alpha_001, alpha_002, alpha_003, @@ -179,10 +182,12 @@ from quant_engine.alpha_factors import ( evaluate_phase2_operator, evaluate_phase3_formula, evaluate_phase4_formula, + evaluate_phase5_formula, list_phase1_operators, list_phase2_operators, list_phase3_formulas, list_phase4_formulas, + list_phase5_formulas, correlation, covariance, decay_linear, @@ -1789,3 +1794,171 @@ def test_phase4_dispatch_rejects_implicit_series_alignment(): high=inputs["high"], low=misaligned_low, ) + + +# ── Alpha158 Phase 5: versioned alpha101-alpha150 formula contract ────────── + + +def test_phase5_formula_catalog_is_versioned_exact_and_content_addressed(): + import hashlib + import json + from collections import Counter + + expected_ids = tuple(f"alpha_{number:03d}" for number in range(101, 151)) + expected_fields = { + "name", + "contract_version", + "formula", + "category", + "complexity", + "parameters", + "description", + "references", + "call_inputs", + "formula_inputs", + "input_category", + } + + assert ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION == "1.0.0" + assert list_phase5_formulas() == expected_ids + assert tuple(ALPHA158_PHASE5_FORMULA_SPECS) == expected_ids + assert Counter( + spec["input_category"] for spec in ALPHA158_PHASE5_FORMULA_SPECS.values() + ) == {"pair": 33, "triple": 14, "quadruple": 3} + + for alpha_id, spec in ALPHA158_PHASE5_FORMULA_SPECS.items(): + assert set(spec) == expected_fields + assert spec["name"] == alpha_id + assert spec["contract_version"] == ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION + assert spec["formula"] == ALPHA158_REGISTRY[alpha_id]["formula"] + + serializable_specs = { + alpha_id: { + field: list(value) if isinstance(value, tuple) else value + for field, value in spec.items() + } + for alpha_id, spec in ALPHA158_PHASE5_FORMULA_SPECS.items() + } + encoded = json.dumps( + serializable_specs, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ).encode() + assert hashlib.sha256(encoded).hexdigest() == ALPHA158_PHASE5_FORMULA_CATALOG_SHA256 + assert ALPHA158_PHASE5_FORMULA_CATALOG_SHA256 == ( + "3368796169c9fbd39c4a34ea137e569964b15882fbf4de25124790d548db6533" + ) + + +def test_phase5_formula_catalog_is_recursively_immutable(): + import operator + + with pytest.raises(TypeError): + operator.setitem(ALPHA158_PHASE5_FORMULA_SPECS, "alpha_101", {}) + with pytest.raises(TypeError): + operator.setitem( + ALPHA158_PHASE5_FORMULA_SPECS["alpha_101"], + "formula", + "changed", + ) + with pytest.raises(TypeError): + operator.setitem( + ALPHA158_PHASE5_FORMULA_SPECS["alpha_101"]["call_inputs"], + 0, + "volume", + ) + + +def test_phase5_catalog_freezes_callable_and_formula_inputs(): + import inspect + + for alpha_id, spec in ALPHA158_PHASE5_FORMULA_SPECS.items(): + function = getattr(alpha_factors_module, alpha_id) + signature_inputs = tuple( + "open" if name == "open_" else name + for name in inspect.signature(function).parameters + ) + assert spec["call_inputs"] == signature_inputs + assert spec["formula_inputs"] == tuple(ALPHA158_REGISTRY[alpha_id]["inputs"]) + + assert ALPHA158_PHASE5_FORMULA_SPECS["alpha_101"]["call_inputs"] == ( + "close", + "high", + "low", + ) + + +def test_phase5_dispatch_matches_all_existing_alpha101_alpha150_functions(): + inputs = _phase3_market_inputs() + + for alpha_id, spec in ALPHA158_PHASE5_FORMULA_SPECS.items(): + call_inputs = spec["call_inputs"] + function = getattr(alpha_factors_module, alpha_id) + expected = function(*(inputs[name] for name in call_inputs)) + actual = evaluate_phase5_formula( + alpha_id, + **{name: inputs[name] for name in reversed(call_inputs)}, + ) + pd.testing.assert_series_equal(actual, expected) + + +def test_phase5_dispatch_rejects_unknown_missing_extra_and_non_series_inputs(): + inputs = _phase3_market_inputs() + + with pytest.raises(KeyError, match="not registered"): + evaluate_phase5_formula("alpha_100", close=inputs["close"]) + with pytest.raises(KeyError, match="not registered"): + evaluate_phase5_formula("alpha_151", close=inputs["close"]) + with pytest.raises(ValueError, match=r"missing inputs.*low"): + evaluate_phase5_formula( + "alpha_101", + close=inputs["close"], + high=inputs["high"], + ) + with pytest.raises(ValueError, match=r"unexpected inputs.*vwap"): + evaluate_phase5_formula( + "alpha_101", + close=inputs["close"], + high=inputs["high"], + low=inputs["low"], + vwap=inputs["vwap"], + ) + with pytest.raises(TypeError, match="high must be a pandas Series"): + evaluate_phase5_formula( # type: ignore[arg-type] + "alpha_101", + close=inputs["close"], + high=[1.0, 2.0], + low=inputs["low"], + ) + + +def test_phase5_dispatch_rejects_length_and_index_alignment_errors(): + inputs = _phase3_market_inputs() + shorter_low = inputs["low"].iloc[:-1] + misaligned_high = inputs["high"].rename(index={79: 80}) + + with pytest.raises(ValueError, match="low length must match close"): + evaluate_phase5_formula( + "alpha_101", + close=inputs["close"], + high=inputs["high"], + low=shorter_low, + ) + with pytest.raises(ValueError, match="high index must align with close"): + evaluate_phase5_formula( + "alpha_101", + close=inputs["close"], + high=misaligned_high, + low=inputs["low"], + ) + + +def test_phase5_contract_is_publicly_exported(): + assert { + "ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION", + "ALPHA158_PHASE5_FORMULA_CATALOG_SHA256", + "ALPHA158_PHASE5_FORMULA_SPECS", + "list_phase5_formulas", + "evaluate_phase5_formula", + } <= set(alpha_factors_module.__all__)