""" 基本面因子前视回归测试:财报不得在披露日之前被回测看到。 """ import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import numpy as np import pandas as pd import pytest from factors.fundamental.pe_pb import PEFactor from factors.fundamental.roe import ROEFactor, ROETTMDeltaFactor from factors.fundamental._mapping import map_fundamental_to_daily, effective_available_dates INDICES = pd.date_range('2024-01-01', '2024-09-30', freq='B').strftime('%Y%m%d') def _daily(): return pd.DataFrame({'close': np.full(len(INDICES), 10.0)}, index=INDICES) def _fin_ann(): # 提供 ann_date(实际披露日):年报 4/25, 一季报 4/28, 中报 8/30 return pd.DataFrame({ 'end_date': ['20231231', '20240331', '20240630'], 'ann_date': ['20240425', '20240428', '20240830'], 'eps': [1.0, 1.2, 1.5], 'roe': [0.10, 0.12, 0.15], }) def test_lookahead_absent_before_disclosure(): daily = _daily() mapped = map_fundamental_to_daily(daily, _fin_ann(), 'eps') # 披露日(4/25)之前绝不可见任何财务值 → 消除前视 pre = mapped[mapped.index <= '20240424'].dropna() assert pre.empty, "披露日前出现财务值 → 前视泄漏!" # 4/25 起可见年报 eps=1.0 post = mapped[mapped.index >= '20240425'].dropna() assert abs(float(post.iloc[0]) - 1.0) < 1e-6 def test_ann_date_refines_overlap(): daily = _daily() mapped = map_fundamental_to_daily(daily, _fin_ann(), 'eps') # 4/25-4/27 是年报(1.0),4/28 起切换为一季报(1.2) band = mapped[(mapped.index >= '20240425') & (mapped.index <= '20240427')] assert (band.dropna() == 1.0).all() post = mapped[mapped.index >= '20240428'].dropna() assert abs(float(post.iloc[0]) - 1.2) < 1e-6 def test_pe_roe_no_lookahead(): daily = _daily() fin = _fin_ann() for f in [PEFactor(fin), ROEFactor(fin), ROETTMDeltaFactor(fin)]: s = f.calculate(daily) pre = s[s.index <= '20240424'].dropna() assert pre.empty, f"{f.name} 在披露日前出现值 → 前视泄漏" def test_default_disclosure_lag_without_ann_date(): """无 ann_date 时用法定滞后:年报(1231)→次年4/30。""" daily = _daily() fin = pd.DataFrame({'end_date': ['20231231'], 'eps': [1.0]}) mapped = map_fundamental_to_daily(daily, fin, 'eps') pre = mapped[mapped.index <= '20240429'].dropna() assert pre.empty, "无 ann_date 时财报不应在披露日前可见" post = mapped[mapped.index >= '20240430'].dropna() assert not post.empty