Files
qlib/backend/tests/test_quant_factors.py
Simon e9f59d3cf8 feat(backend): Phase 2 研究引擎 — ResearchSpec / 因子 / 评估 / 低频回测 / 引擎抽象
- domain:ResearchSpec(universe/factors/selection/rebalance/costs 校验)+ 标准化 BacktestResult / FactorTestReport
- 因子引擎:注册表 + 元数据,内置 9 个行情因子(momentum/volatility/量比/乖离/反转),支持自定义注册;只用行情字段规避未来函数
- 评估:横截面 IC / RankIC(rank+pearson 免 scipy)/ ICIR / 分层收益
- 回测:TopK 等权低频,无未来函数记账(t 收盘成交、自 t+1 计收益),成本/涨跌停/停牌约束,未建模项显式写入 unimplemented(AGENT §24)
- 引擎抽象 QuantEngine + LocalEngine(pandas 默认实现);qlib_adapter 桥接占位 —— pyqlib 无 aarch64+cp312 wheel(ROADMAP 已备注)
- 真实链路冒烟:600519 2024 月度动量回测闭环产出标准结果
- 测试 60 passed / ruff clean
2026-09-06 17:08:00 +08:00

90 lines
2.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""因子计算与注册表测试(合成数据、确定性断言方向与相对排序)。"""
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"}