This commit is contained in:
@@ -15,6 +15,7 @@ v1.2.0 Phase 0:5 个基础算子 + 5 个 alpha 公式(alpha001–alpha005)
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -209,6 +210,138 @@ def indneutralize(series: pd.Series, groups: pd.Series) -> pd.Series:
|
|||||||
return series - series.groupby(groups).transform("mean")
|
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_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 and (
|
||||||
|
window is None
|
||||||
|
or isinstance(window, bool)
|
||||||
|
or not isinstance(window, int)
|
||||||
|
or window <= 0
|
||||||
|
):
|
||||||
|
raise ValueError(f"window must be a positive integer 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 公式样本) ─────────────────────────
|
# ── 组合算子(alpha158 公式样本) ─────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -2755,6 +2888,9 @@ __all__ = [
|
|||||||
"max_pair",
|
"max_pair",
|
||||||
"min_pair",
|
"min_pair",
|
||||||
"indneutralize",
|
"indneutralize",
|
||||||
|
"ALPHA158_PHASE1_OPERATOR_SPECS",
|
||||||
|
"list_phase1_operators",
|
||||||
|
"evaluate_phase1_operator",
|
||||||
"alpha_001",
|
"alpha_001",
|
||||||
"alpha_002",
|
"alpha_002",
|
||||||
"alpha_003",
|
"alpha_003",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import pytest
|
|||||||
|
|
||||||
from quant_engine.alpha_factors import (
|
from quant_engine.alpha_factors import (
|
||||||
ALPHA158_REGISTRY,
|
ALPHA158_REGISTRY,
|
||||||
|
ALPHA158_PHASE1_OPERATOR_SPECS,
|
||||||
alpha_001,
|
alpha_001,
|
||||||
alpha_002,
|
alpha_002,
|
||||||
alpha_003,
|
alpha_003,
|
||||||
@@ -166,6 +167,8 @@ from quant_engine.alpha_factors import (
|
|||||||
alpha_156,
|
alpha_156,
|
||||||
alpha_157,
|
alpha_157,
|
||||||
alpha_158,
|
alpha_158,
|
||||||
|
evaluate_phase1_operator,
|
||||||
|
list_phase1_operators,
|
||||||
correlation,
|
correlation,
|
||||||
covariance,
|
covariance,
|
||||||
decay_linear,
|
decay_linear,
|
||||||
@@ -1232,3 +1235,89 @@ def test_parse_alpha_formula_round_trip_jsonb():
|
|||||||
serialized = json.dumps(parsed)
|
serialized = json.dumps(parsed)
|
||||||
assert isinstance(serialized, str)
|
assert isinstance(serialized, str)
|
||||||
assert "ts_rank" in serialized
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user