Files
Auto-Trader/agent/tests/factors/test_fundamental_expansion_price_samples.py
T
Haozhe Wu d3c76819c3 fix: complete October 11-12 maintenance and research updates
Fix live option guards, grounding operand evidence, source-bound benchmarks, trade artifacts, attribution, drawdowns, valuation tolerances, and SEC point-in-time factor inputs.

Add Brazil research data and market support, 13 fundamental and valuation factors, channel progress forwarding, and opt-in ZimaOS templates. Update seven README News sections with three visible dates.

Validation: 17,635 backend tests passed; 1,084 final targeted tests and 103 frontend tests passed. Frontend build, package build, documentation checks, and CI gates passed.
2026-10-12 02:29:46 +08:00

212 lines
8.0 KiB
Python

"""Sample tests for the Tier-1 fundamental expansion factors.
Covers the four price-dependent / derived-field factors added per
``research/fundamental_factor_expansion_proposal.md`` §4 Tier 1 (#1, #5, #6, #8):
- ``fund_book_to_market`` (real FF HML characteristic)
- ``fund_fcf_yield`` (CFO - capex over market cap)
- ``fund_revenue_growth`` (schema derived field ``revenue_growth``)
- ``fund_earnings_growth`` (schema derived field ``earnings_growth``)
All compute-level tests pass hand-built panel dicts directly to ``compute()``
— never through the loader/schema — so they are independent of when the
concurrent schema change adding ``revenue_growth``/``earnings_growth`` lands.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from src.factors.base import zscore
from src.factors.registry import AlphaMeta
from src.factors.registry import get_default_registry
from src.factors.zoo.fundamental import book_to_market
from src.factors.zoo.fundamental import earnings_growth
from src.factors.zoo.fundamental import fcf_yield
from src.factors.zoo.fundamental import revenue_growth
_DATES = pd.to_datetime(["2024-01-02", "2024-01-03"])
_CODES = ["A", "B", "C"]
_PRICE_FACTORS = [book_to_market, fcf_yield]
_GROWTH_FACTORS = [revenue_growth, earnings_growth]
_ALL_FACTORS = _PRICE_FACTORS + _GROWTH_FACTORS
def _frame(rows: list[list[float]]) -> pd.DataFrame:
"""Build a 2-date x 3-symbol frame from per-date row values."""
return pd.DataFrame(rows, index=_DATES, columns=_CODES, dtype=np.float64)
def _book_to_market_panel() -> dict[str, pd.DataFrame]:
# market cap: A=1000, B=2000, C=5000; equity 500/250/100
# -> b/m: A=0.50, B=0.125, C=0.02 (strictly decreasing)
return {
"close": _frame([[10.0, 20.0, 50.0], [10.0, 20.0, 50.0]]),
"fund:total_equity": _frame([[500.0, 250.0, 100.0], [500.0, 250.0, 100.0]]),
"fund:shares_diluted": _frame([[100.0, 100.0, 100.0], [100.0, 100.0, 100.0]]),
}
def _fcf_yield_panel() -> dict[str, pd.DataFrame]:
# A: cfo=100, capex=30 -> fcf=70, mcap=10*10=100 -> yield 0.70
# B: cfo=50, capex=15 -> fcf=35, mcap=20*10=200 -> yield 0.175
# C: cfo=20, capex=25 -> fcf=-5, mcap=50*10=500 -> yield -0.01
return {
"close": _frame([[10.0, 20.0, 50.0], [10.0, 20.0, 50.0]]),
"fund:cfo": _frame([[100.0, 50.0, 20.0], [100.0, 50.0, 20.0]]),
"fund:capex": _frame([[30.0, 15.0, 25.0], [30.0, 15.0, 25.0]]),
"fund:shares_diluted": _frame([[10.0, 10.0, 10.0], [10.0, 10.0, 10.0]]),
}
def _growth_panel(field: str) -> dict[str, pd.DataFrame]:
return {field: _frame([[0.30, 0.10, -0.20], [0.30, 0.10, -0.20]])}
def _valid_panel(module) -> dict[str, pd.DataFrame]:
if module is book_to_market:
return _book_to_market_panel()
if module is fcf_yield:
return _fcf_yield_panel()
if module is revenue_growth:
return _growth_panel("fund:revenue_growth")
return _growth_panel("fund:earnings_growth")
# ---------------------------------------------------------------------------
# (a) AlphaMeta gate accepts each factor's declared columns_required
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("module", _ALL_FACTORS, ids=lambda m: m.__alpha_meta__["id"])
def test_alpha_meta_gate_accepts_declared_columns(module) -> None:
meta = AlphaMeta(**module.__alpha_meta__)
assert meta.columns_required == module.__alpha_meta__["columns_required"]
assert meta.decay_horizon == 252
assert meta.min_warmup_bars == 1
assert meta.universe == ["equity_us", "equity_cn", "equity_hk"]
assert meta.frequency == ["1d"]
def test_alpha_meta_themes_match_proposal() -> None:
assert book_to_market.__alpha_meta__["theme"] == ["value"]
assert fcf_yield.__alpha_meta__["theme"] == ["value"]
assert revenue_growth.__alpha_meta__["theme"] == ["growth"]
assert earnings_growth.__alpha_meta__["theme"] == ["growth"]
# ---------------------------------------------------------------------------
# (b) compute() numeric correctness on hand-built panels
# ---------------------------------------------------------------------------
def test_book_to_market_matches_hand_computed_ratios() -> None:
panel = _book_to_market_panel()
expected_ratios = _frame([[0.50, 0.125, 0.02], [0.50, 0.125, 0.02]])
result = book_to_market.compute(panel)
pd.testing.assert_frame_equal(result, zscore(expected_ratios))
def test_book_to_market_direction_higher_equity_to_mcap_scores_higher() -> None:
panel = _book_to_market_panel()
result = book_to_market.compute(panel)
row = result.iloc[0]
assert row["A"] > row["B"] > row["C"]
def test_fcf_yield_subtracts_capex_from_cfo() -> None:
# A: cfo 100, capex 30 -> fcf 70 over mcap 100 -> yield 0.70
panel = _fcf_yield_panel()
expected_ratios = _frame([[0.70, 0.175, -0.01], [0.70, 0.175, -0.01]])
result = fcf_yield.compute(panel)
pd.testing.assert_frame_equal(result, zscore(expected_ratios))
@pytest.mark.parametrize(
("module", "field"),
[(revenue_growth, "fund:revenue_growth"), (earnings_growth, "fund:earnings_growth")],
ids=["fund_revenue_growth", "fund_earnings_growth"],
)
def test_growth_factors_zscore_declared_field(module, field: str) -> None:
panel = _growth_panel(field)
result = module.compute(panel)
pd.testing.assert_frame_equal(result, zscore(panel[field]))
row = result.iloc[0]
assert row["A"] > row["B"] > row["C"]
# ---------------------------------------------------------------------------
# (c) NaN contract: NaN in any declared dependency -> NaN output at that cell
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("module", _ALL_FACTORS, ids=lambda m: m.__alpha_meta__["id"])
def test_nan_in_any_declared_dependency_produces_nan_output(module) -> None:
for column in module.__alpha_meta__["columns_required"]:
panel = _valid_panel(module)
panel[column] = panel[column].copy()
panel[column].iloc[0, 1] = np.nan # symbol B on date 0
result = module.compute(panel)
assert np.isnan(result.iloc[0, 1]), f"{module.__alpha_meta__['id']}: {column}"
assert np.isfinite(result.iloc[0, 0]), f"{module.__alpha_meta__['id']}: {column}"
assert np.isfinite(result.iloc[0, 2]), f"{module.__alpha_meta__['id']}: {column}"
# ---------------------------------------------------------------------------
# (d) Zero market cap / zero denominator -> NaN, never inf
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("module", _PRICE_FACTORS, ids=lambda m: m.__alpha_meta__["id"])
def test_zero_close_produces_nan_not_inf(module) -> None:
panel = _valid_panel(module)
panel["close"] = panel["close"].copy()
panel["close"].iloc[0, 1] = 0.0 # zero market cap for B on date 0
result = module.compute(panel)
assert np.isnan(result.iloc[0, 1])
assert not np.isinf(result.to_numpy(dtype=np.float64)).any()
@pytest.mark.parametrize("module", _PRICE_FACTORS, ids=lambda m: m.__alpha_meta__["id"])
def test_zero_shares_produces_nan_not_inf(module) -> None:
panel = _valid_panel(module)
panel["fund:shares_diluted"] = panel["fund:shares_diluted"].copy()
panel["fund:shares_diluted"].iloc[1, 2] = 0.0 # zero share count for C on date 1
result = module.compute(panel)
assert np.isnan(result.iloc[1, 2])
assert not np.isinf(result.to_numpy(dtype=np.float64)).any()
# ---------------------------------------------------------------------------
# Registry load: the four ids are discoverable from the default registry
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"alpha_id",
["fund_book_to_market", "fund_fcf_yield", "fund_revenue_growth", "fund_earnings_growth"],
)
def test_factor_is_loadable_from_default_registry(alpha_id: str) -> None:
registry = get_default_registry()
alpha = registry.get(alpha_id)
assert alpha.id == alpha_id