Files
sanguo_vnpy_v2/tests/factor/test_fundamental_p2_adapter.py
T

159 lines
7.3 KiB
Python
Raw 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.
# tests/factor/test_fundamental_p2_adapter.py
"""P2 批财务因子适配层: 七项目指标调研增量(PEG/PS/股息族).
口径锚(P2 任务书=七项目调研报告 §5,2026-09-22):
- D16 PEG 原料: np_ttm_yoy = TTM 净利同比(_yoy4 ttm 口径);peg_denom =
np_ttm×np_ttm_yoy(守卫 np_ttm>0 且 g>1%——增速失真不硬算,表达式层无
条件算子故守卫前移特征层;除 null 即 NaN)
- D17 PS 原料: rev_ttm = 营收 TTM(与 EP/BP/CP 同分母体系)
- E14 div_12m = 近 365 天(开区间下界)已实施现金分红/股,PIT 锚 = 除息日
(dividOperateDate,钱到股东日);除息日缺失行对现金族丢弃(宁缺毋假,
不影响 E11 送转流的公告日回退链)
- E15 div_payout = div_12m×share_capital/np_ttm(亏损派息语义反转 →
np_ttm>0 守卫,宁缺毋假)
- E16 div_streak = 截至当日的连续现金分红年数(按除息日所在年;断年重计,
纯送转/零现金事件不计年)
"""
import math
import sys, os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "vnpy_v4.4.0")))
from datetime import date
import polars as pl
import pytest
from sanguo_factor.fundamental_adapter import build_fundamental_features
from sanguo_factor.fundamental_report_features import _compute_report_features
A, B, C, BANK = "600000.SSE", "000001.SZSE", "300001.SZSE", "601398.SSE"
def _dt(day: str):
import datetime as _d
return _d.datetime.strptime(day, "%Y-%m-%d")
def val(df, vt: str, day: str, col: str):
row = df.filter((df["vt_symbol"] == vt) & (df["datetime"] == _dt(day)))
assert row.height == 1, f"grid 缺行 {vt} {day}"
v = row[col][0]
return None if v is None else float(v)
@pytest.fixture(scope="module")
def feat(synthetic_static):
return build_fundamental_features(
[A, B, C, BANK], "2021-01-01", "2024-06-30", data_dir=synthetic_static)
# ---------- D16 PEG 原料(np_ttm_yoy / peg_denom) ----------
def test_np_ttm_yoy_and_peg_denom_at_h1(feat):
# 2023-08-29 见 2023H1(i=21): ttm_np(21)=q18..21=15+18+13+14=60,
# ttm_np(17)=q14..17=15+16+11+14=56 → yoy=60/56−1=1/14
assert val(feat, A, "2023-08-29", "np_ttm_yoy") == pytest.approx(1.0 / 14.0)
# 守卫链通过(g≈7.1%>1%) → denom = 60×(1/14)
assert val(feat, A, "2023-08-29", "peg_denom") == pytest.approx(60.0 / 14.0)
def test_peg_denom_growth_floor_guard():
"""g≤1%(增速失真)→ peg_denom NaN 不硬算: 8 期持平净利的迷你报表."""
rds = [f"{y}-{m}" for y in (2021, 2022) for m in ("03-31", "06-30", "09-30", "12-31")]
base = {c: None for c in (
"rev cogs np dnp tp invest_inc fv_inc asset_imp credit_imp cfo sales_cash "
"acc_inv_cash research sale_exp int_exp tax eps fa_depr ia_amort "
"lpe_amort ua_amort capex recv_loan issue_bond pay_debt").split()}
rows = []
for i, rd in enumerate(rds):
r = {"vt_symbol": "600000.SSE",
"REPORT_DATE": date.fromisoformat(rd),
"notice_eff": date.fromisoformat(rd).replace(day=28),
"TOTAL_ASSETS": 1000.0, "TOTAL_PARENT_EQUITY": 500.0,
"ACCOUNTS_RECE": 100.0, "OTHER_RECE": 5.0, "GOODWILL": 0.0,
"SHARE_CAPITAL": 100.0, "INVENTORY": 50.0, "MONETARYFUNDS": 80.0,
"ACCOUNTS_PAYABLE": 60.0, "SHORT_LOAN": 10.0, "SHORT_FIN_PAYABLE": 0.0,
"NONCURRENT_LIAB_1YEAR": 0.0, "LONG_LOAN": 0.0, "BOND_PAYABLE": 0.0,
"LEASE_LIAB": 0.0, "UPDATE_DATE": None, "NOTICE_DATE": None,
**base}
# 累计口径: 每年 4 季各 25(=单季持平) → TTM 恒 100 → yoy=0 → 守卫触发
cum = 25.0 * (i % 4 + 1)
r.update({"rev": cum, "np": cum, "cogs": 0.6 * cum})
rows.append(r)
# 全 None 列须显式 Float64(真实 loader 有 cast,迷你帧没有 → Null dtype
# 会让 .abs()/算术炸)
out = _compute_report_features(pl.DataFrame(
rows, schema_overrides={c: pl.Float64 for c in base}))
last = out.filter(pl.col("REPORT_DATE") == date(2022, 12, 31))
assert last.height == 1
assert last["np_ttm_yoy"][0] == pytest.approx(0.0)
assert last["peg_denom"][0] is None, "g=0 ≤1% 增速失真必须 NaN(宁缺毋假)"
# ---------- D17 PS 原料(rev_ttm) ----------
def test_rev_ttm_at_h1(feat):
# ttm_rev(21) = q_rev18..21 = 150+180+130+140 = 600(与 E08 CCC 断言互证)
assert val(feat, A, "2023-08-29", "rev_ttm") == pytest.approx(600.0)
# scale=2 的 C 同窗翻倍(比率族 scale 不变性对原料列不成立,原料×scale)
assert val(feat, C, "2023-08-29", "rev_ttm") == pytest.approx(1200.0)
# ---------- E14 div_12m(除息日锚 365 天滚动) ----------
def test_div_12m_window_and_pit(feat):
# A 现金事件(除息日): 2022-06-20(1.0) 2023-03-15(2.0) 2023-09-25(3.0)
# 2023-12-05(4.0) 2024-01-10(5.0)
assert val(feat, A, "2023-03-14", "div_12m") == pytest.approx(1.0) # 只含 2022-06-20
assert val(feat, A, "2023-03-15", "div_12m") == pytest.approx(3.0) # 当日除息即计入
assert val(feat, A, "2023-09-24", "div_12m") == pytest.approx(2.0) # 2022-06-20 出窗
assert val(feat, A, "2023-12-31", "div_12m") == pytest.approx(9.0) # 2+3+4
assert val(feat, A, "2024-06-30", "div_12m") == pytest.approx(12.0) # 3+4+5
def test_div_12m_nan_before_first_event(feat):
# 首个除息日前 → NaN(域内无事件,不填 0;与窗口和为 0 区分)
assert val(feat, A, "2022-06-19", "div_12m") is None
# 无 dividend 文件的 BANK → NaN
assert val(feat, BANK, "2023-12-31", "div_12m") is None
# ---------- E15 div_payout(亏损守卫) ----------
def test_div_payout_value(feat):
# 2023-12-31(A): 见 2023Q3(i=22) → ttm_np=62, share_capital=110
# payout = 9.0×110/62
assert val(feat, A, "2023-12-31", "div_payout") == pytest.approx(9.0 * 110.0 / 62.0)
# B: div_12m=0.5(纯现金), np_ttm 同窗可算 → 值非 NaN
v = val(feat, B, "2023-12-31", "div_payout")
assert v is not None and v > 0
# ---------- E16 div_streak(连续现金分红年数) ----------
def test_div_streak_progression(feat):
assert val(feat, A, "2022-06-19", "div_streak") is None # 首事件前 NaN
assert val(feat, A, "2022-12-31", "div_streak") == 1.0 # 2022 年事件后=1
assert val(feat, A, "2023-03-15", "div_streak") == 2.0 # 2023 首事件 → 连续第 2 年
assert val(feat, A, "2023-12-31", "div_streak") == 2.0 # 同年后续事件不递增
assert val(feat, A, "2024-01-10", "div_streak") == 3.0 # 2024 首事件 → 3
def test_div_streak_gap_reset(feat):
# C 现金年份 2021/2023(2022 断档): 2023-06-01 事件时 streak 重计=1
# (若误实现为「累计分红年数」会得 2)
assert val(feat, C, "2023-06-01", "div_streak") == 1.0
assert val(feat, C, "2021-12-31", "div_streak") == 1.0
# ---------- 引用瘦身路径(仅域列请求) ----------
def test_domain_only_columns_subset(synthetic_static):
df = build_fundamental_features(
[A], "2023-12-01", "2023-12-31", data_dir=synthetic_static,
columns=["div_12m", "div_streak"])
assert df.columns == ["vt_symbol", "datetime", "div_12m", "div_streak"]
assert val(df, A, "2023-12-05", "div_12m") == pytest.approx(9.0)
assert val(df, A, "2023-12-05", "div_streak") == 2.0