Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ab18432c4 | ||
|
|
32e8bfe573 | ||
|
|
eca4bd4d65 |
@@ -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,367 @@ 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)
|
||||
|
||||
|
||||
# ── 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 公式样本) ─────────────────────────
|
||||
|
||||
|
||||
@@ -2755,6 +3117,14 @@ __all__ = [
|
||||
"max_pair",
|
||||
"min_pair",
|
||||
"indneutralize",
|
||||
"ALPHA158_PHASE1_MAX_WINDOW",
|
||||
"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",
|
||||
|
||||
@@ -8,6 +8,8 @@ import pytest
|
||||
|
||||
from quant_engine.alpha_factors import (
|
||||
ALPHA158_REGISTRY,
|
||||
ALPHA158_PHASE1_OPERATOR_SPECS,
|
||||
ALPHA158_PHASE2_OPERATOR_SPECS,
|
||||
alpha_001,
|
||||
alpha_002,
|
||||
alpha_003,
|
||||
@@ -166,6 +168,10 @@ from quant_engine.alpha_factors import (
|
||||
alpha_156,
|
||||
alpha_157,
|
||||
alpha_158,
|
||||
evaluate_phase1_operator,
|
||||
evaluate_phase2_operator,
|
||||
list_phase1_operators,
|
||||
list_phase2_operators,
|
||||
correlation,
|
||||
covariance,
|
||||
decay_linear,
|
||||
@@ -1232,3 +1238,228 @@ 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)
|
||||
|
||||
|
||||
# ── 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)
|
||||
|
||||
Reference in New Issue
Block a user