"""因子计算与注册表测试(合成数据、确定性断言方向与相对排序)。""" from __future__ import annotations import pandas as pd from app.quant.factors import ( FactorDef, FactorError, compute_factor, get_factor, list_factors, register, ) from conftest_quant import synthetic_daily def test_registry_builtins_present() -> None: names = {f.name for f in list_factors()} assert {"momentum_20", "momentum_60", "volatility_20", "ma_bias_20"} <= names def test_unknown_factor_raises() -> None: try: compute_factor("not_a_factor", synthetic_daily({"A": 0.0})) except FactorError: return raise AssertionError("应抛 FactorError") def test_momentum_ordering_matches_drift() -> None: daily = synthetic_daily({"AAA": 0.002, "BBB": 0.0, "CCC": -0.002}, n=160) _defn, panel = compute_factor("momentum_20", daily) tail = panel.iloc[-1] assert tail["AAA"] > tail["BBB"] > tail["CCC"] assert tail["AAA"] > 0 # 上涨股 20 日动量为正 def test_volatility_ranks_noise() -> None: # 手写:SMOOTH 每日 +0.2%;WILD 在 ±5% 间摆动 → WILD 的 20 日波动率应显著更高 from datetime import date, timedelta dates = [date(2024, 1, 1) + timedelta(days=i) for i in range(120)] rows = [] smooth, wild = 100.0, 100.0 for j, d in enumerate(dates): smooth *= 1.002 wild *= 1.05 if j % 2 == 0 else 0.95 for sym, px in (("SMOOTH", smooth), ("WILD", wild)): rows.append( { "symbol": sym, "trade_date": d, "close": px, "high": px, "low": px, "volume": 1e6, "amount": 1e8, } ) daily = pd.DataFrame(rows) _defn, panel = compute_factor("volatility_20", daily) assert float(panel["WILD"].iloc[-1]) > float(panel["SMOOTH"].iloc[-1]) * 5 def test_custom_factor_registration() -> None: @register(FactorDef("test_double_close", "close*2 测试因子", "close * 2", lookback=1)) def _fn(fields: dict[str, pd.DataFrame]) -> pd.DataFrame: return fields["close"] * 2 try: daily = synthetic_daily({"A": 0.001}, n=40) defn, panel = compute_factor("test_double_close", daily) assert defn.direction == "higher_is_better" assert float(panel.iloc[-1, 0]) > 200.0 finally: # 清理注册表,避免污染其他测试 from app.quant import factors as _factors _factors._REGISTRY.pop("test_double_close", None) # noqa: SLF001 def test_factor_def_metadata_present() -> None: defn, _fn = get_factor("momentum_60") assert defn.description assert defn.formula assert defn.lookback == 60 assert defn.direction in {"higher_is_better", "lower_is_better"}