Compare commits

...
Author SHA1 Message Date
ao gong 918274875b Merge remote-tracking branch 'origin/main' into codex/research-alpha158-phase4-formula-contract-20260828
CI / lite (pull_request) Successful in 8s
# Conflicts:
#	src/quant_engine/alpha_factors.py
#	tests/test_alpha_factors.py
2026-08-28 18:30:42 +08:00
ageorge156 90a43adda2 Merge remote-tracking branch 'origin/main' into codex/research-alpha158-phase3-formula-con (#10)
CI / lite (push) Successful in 8s
2026-08-28 18:29:47 +08:00
ageorge156 e72fe0a8d1 Merge remote-tracking branch 'origin/main' into codex/research-alpha158-phase2-20260827 (#9)
CI / lite (push) Successful in 7s
2026-08-28 18:28:22 +08:00
ao gong b4bd406084 feat: extend alpha formula contract
CI / lite (pull_request) Successful in 11s
2026-08-28 00:50:27 +08:00
ao gong e90687bcec feat: freeze alpha formula contract
CI / lite (pull_request) Successful in 8s
2026-08-27 23:27:15 +08:00
ao gong 7ab18432c4 feat: expand alpha operator dispatch
CI / lite (pull_request) Successful in 8s
2026-08-27 22:51:44 +08:00
ao gong 32e8bfe573 fix: bound phase1 operator windows
CI / lite (pull_request) Successful in 10s
2026-08-27 20:29:53 +08:00
ao gong eca4bd4d65 feat: add phase1 alpha operator contract
CI / lite (pull_request) Successful in 9s
2026-08-27 20:20:19 +08:00
2 changed files with 1020 additions and 2 deletions
+559 -2
View File
@@ -15,8 +15,9 @@ v1.2.0 Phase 0:5 个基础算子 + 5 个 alpha 公式(alpha001–alpha005)
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable, Mapping
from typing import Any from types import MappingProxyType
from typing import Any, cast
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@@ -344,6 +345,233 @@ def evaluate_phase1_operator(
return operator(series, window) 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 公式样本) ───────────────────────── # ── 组合算子(alpha158 公式样本) ─────────────────────────
@@ -2865,6 +3093,321 @@ def parse_alpha_formula(formula_str: str) -> dict[str, Any]:
return parsed 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))
# ── 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__ = [ __all__ = [
"rank", "rank",
"delta", "delta",
@@ -2894,6 +3437,20 @@ __all__ = [
"ALPHA158_PHASE1_OPERATOR_SPECS", "ALPHA158_PHASE1_OPERATOR_SPECS",
"list_phase1_operators", "list_phase1_operators",
"evaluate_phase1_operator", "evaluate_phase1_operator",
"ALPHA158_PHASE2_MAX_WINDOW",
"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",
"ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION",
"ALPHA158_PHASE4_FORMULA_CATALOG_SHA256",
"ALPHA158_PHASE4_FORMULA_SPECS",
"list_phase4_formulas",
"evaluate_phase4_formula",
"alpha_001", "alpha_001",
"alpha_002", "alpha_002",
"alpha_003", "alpha_003",
+461
View File
@@ -6,9 +6,17 @@ import numpy as np
import pandas as pd import pandas as pd
import pytest import pytest
import quant_engine.alpha_factors as alpha_factors_module
from quant_engine.alpha_factors import ( from quant_engine.alpha_factors import (
ALPHA158_REGISTRY, ALPHA158_REGISTRY,
ALPHA158_PHASE1_OPERATOR_SPECS, ALPHA158_PHASE1_OPERATOR_SPECS,
ALPHA158_PHASE2_OPERATOR_SPECS,
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_001,
alpha_002, alpha_002,
alpha_003, alpha_003,
@@ -168,7 +176,13 @@ from quant_engine.alpha_factors import (
alpha_157, alpha_157,
alpha_158, alpha_158,
evaluate_phase1_operator, evaluate_phase1_operator,
evaluate_phase2_operator,
evaluate_phase3_formula,
evaluate_phase4_formula,
list_phase1_operators, list_phase1_operators,
list_phase2_operators,
list_phase3_formulas,
list_phase4_formulas,
correlation, correlation,
covariance, covariance,
decay_linear, decay_linear,
@@ -1328,3 +1342,450 @@ def test_phase1_dispatch_rejects_window_above_supported_limit():
with pytest.raises(ValueError, match="maximum"): with pytest.raises(ValueError, match="maximum"):
evaluate_phase1_operator("ts_mean", values, window=2**63) 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)
# ── 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,
)
# ── 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,
)