60dd84ad4a
- E08 ccc: 365×(AR均值/REV_TTM + INV均值/COGS_TTM − AP均值/COGS_TTM), 均值=t与t−4期末平均(_mean4),ACCOUNTS_PAYABLE 入 balance 读取清单, 金融股置 NaN(NAS 实测列名核实) - E11 send_total_12m: dividend 回退链 PIT(Plan→PreNotice→AgmPum→Plan, 全空行丢弃),cum(d)−cum(d−365) 两端 asof 实现开区间滚动窗;域内无事件 →NaN 不填 0(与缺文件区分) - E12 gdhs_chg: 自建事件表(代码,截止日)→户数,同键取最新公告,相邻事件 Δln,公告日期 asof;不用「增减比例」列;实测截止日列名无第二连字符(双名兼容) - E13 topholder_chg: 只读 factor_cache/top_holders_agg.parquet,期→法定 披露截止(Q1→04-30/H1→08-31/Q3→10-31/年报→次年04-30)保守 PIT - D14 ep_vb/bp_vb: vb 按年文件+2026-01-01~08-12 缺口 static/valuation(ak 中文列)倒数补;实测 exchange=SH/SZ(SSE/SZSE 全称变体兼容);1/pe 守卫 0→NaN - 四域引用列驱动加载(未引用不读盘)+缺域一次性 warning 优雅降级全 NaN - conftest 四域合成 fixture;NAS 真数据冒烟通过(300750 ccc=12.5d/ 000333 ccc=−9.2d/600519 ep=1/32PE)
329 lines
19 KiB
Python
329 lines
19 KiB
Python
"""Test configuration for factor module tests."""
|
||
import sys
|
||
import os
|
||
|
||
# Add vnpy source to path
|
||
_VNPY_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "vnpy_v4.4.0"))
|
||
if _VNPY_SRC not in sys.path:
|
||
sys.path.insert(0, _VNPY_SRC)
|
||
|
||
|
||
# ==================== 合成财务静态域(财务因子批测试共用) ====================
|
||
# 6 只股 × 24 报告期(2018Q1~2023Q4;2018-2019 为 16 季滚动窗/5 年 CAGR 预热史),
|
||
# NOTICE_DATE 错位: Q1→当年4-28 / H1→当年8-29 / Q3→当年10-27 / 年报→次年4-25
|
||
# 数值全部手算可验证(断言用),公式见各 build 函数内注释。
|
||
from datetime import date, datetime
|
||
|
||
import polars as pl
|
||
|
||
REPORT_DATES = [
|
||
f"{y}-{m}" for y in (2018, 2019, 2020, 2021, 2022, 2023) for m in ("03-31", "06-30", "09-30", "12-31")
|
||
]
|
||
|
||
# 单季 REV/NP 序列(累计=年内前缀和): 报告期索引 i=0..23(2018Q1..2023Q4)
|
||
# 2020 起的段与 P0 期完全一致(旧断言零回归);2018-2019 为新增预热史
|
||
REV_Q = ([70, 100, 110, 120] + [80, 110, 120, 130] # 2018 / 2019
|
||
+ [90, 130, 140, 150] + [100, 120, 150, 160] # 2020 / 2021
|
||
+ [110, 140, 150, 180] + [130, 140, 170, 180]) # 2022 / 2023
|
||
NP_Q = ([7, 10, 11, 12] + [8, 11, 12, 13] # 2018 / 2019
|
||
+ [9, 13, 14, 15] + [10, 12, 15, 16] # 2020 / 2021
|
||
+ [11, 14, 15, 18] + [13, 14, 17, 18]) # 2022 / 2023
|
||
|
||
# 资产负债表(时点存量,随报告期线性演化;2021Q1 起算,idx=i-12)
|
||
TA_OF = lambda i: 1000 + 50 * max(i - 12, 0)
|
||
EQ_OF = lambda i: 500 + 20 * max(i - 12, 0)
|
||
AR_OF = lambda i: 100 + 10 * max(i - 12, 0)
|
||
AP_OF = lambda i: 120 + 8 * max(i - 12, 0) # P1-B: 应付账款(E08 CCC 分子)
|
||
SC_OF = lambda i: 100 if i < 20 else 110 # 2023 起股本扩张 10%(NSI=0.1)
|
||
|
||
SYN_STOCKS = {
|
||
"600000.SH": {"vt": "600000.SSE", "scale": 1.0,
|
||
"drop_periods": [], "null_notice_periods": []},
|
||
"000001.SZ": {"vt": "000001.SZSE", "scale": 1.0,
|
||
"drop_periods": ["2022-03-31"], # 缺 2022Q1 → 单季差分/TTM 链 NaN
|
||
"null_notice_periods": ["2022-09-30"]}, # NOTICE_DATE 缺失 → 该报告期跳过
|
||
"300001.SZ": {"vt": "300001.SZSE", "scale": 2.0,
|
||
"drop_periods": [], "null_notice_periods": []},
|
||
# 三只分块等值测试扩容股(全史无残缺,不同 scale 增截面多样性)
|
||
"600004.SH": {"vt": "600004.SSE", "scale": 0.5,
|
||
"drop_periods": [], "null_notice_periods": []},
|
||
"000333.SZ": {"vt": "000333.SZSE", "scale": 1.7,
|
||
"drop_periods": [], "null_notice_periods": []},
|
||
"300124.SZ": {"vt": "300124.SZSE", "scale": 3.0,
|
||
"drop_periods": [], "null_notice_periods": []},
|
||
# 银行模板股(OPERATE_COST 缺失 → _is_fin;INVENTORY 缺失测 null 传播):
|
||
# 盈利质量/成长/费用类特征应置 NaN,盈利能力/估值族保留(§7 红线 5)
|
||
"601398.SH": {"vt": "601398.SSE", "scale": 4.0, "bank": True,
|
||
"drop_periods": [], "null_notice_periods": []},
|
||
}
|
||
|
||
|
||
def _notice_date(report_date: str) -> str:
|
||
"""A 股典型披露节奏(年报次年 4-25 / 一季报 4-28 / 中报 8-29 / 三季报 10-27)."""
|
||
y, m = int(report_date[:4]), int(report_date[5:7])
|
||
if m == 3:
|
||
return f"{y}-04-28"
|
||
if m == 6:
|
||
return f"{y}-08-29"
|
||
if m == 9:
|
||
return f"{y}-10-27"
|
||
return f"{y + 1}-04-25"
|
||
|
||
|
||
def _cum_in_year(q_values: list[float], i: int) -> float:
|
||
"""报告期 i 的年内累计值 = 当年前几季单季之和."""
|
||
year_start = (i // 4) * 4
|
||
return float(sum(q_values[year_start:i + 1]))
|
||
|
||
|
||
def build_synthetic_static(root: str) -> str:
|
||
"""写合成静态域 parquet 树(data_dir),返回 static 根目录路径.
|
||
|
||
目录结构与 NAS 一致: static/{income,balance,cashflow}/{code}.{SH|SZ}_{table}.parquet
|
||
forecast 按报告期全市场文件: static/forecast/{YYYYMMDD}_forecast.parquet
|
||
"""
|
||
static_dir = os.path.join(str(root), "static")
|
||
for table in ("income", "balance", "cashflow"):
|
||
os.makedirs(os.path.join(static_dir, table), exist_ok=True)
|
||
|
||
for file_code, spec in SYN_STOCKS.items():
|
||
s, drop, null_notice = spec["scale"], set(spec["drop_periods"]), set(spec["null_notice_periods"])
|
||
is_bank = spec.get("bank", False)
|
||
income_rows, balance_rows, cashflow_rows = [], [], []
|
||
for i, rd in enumerate(REPORT_DATES):
|
||
if rd in drop:
|
||
continue
|
||
notice = None if rd in null_notice else _notice_date(rd)
|
||
rev_c, np_c = s * _cum_in_year(REV_Q, i), s * _cum_in_year(NP_Q, i)
|
||
common = {
|
||
"REPORT_DATE": f"{rd} 00:00:00",
|
||
"NOTICE_DATE": (f"{notice} 00:00:00" if notice else None),
|
||
"UPDATE_DATE": f"{notice} 00:00:00" if notice else None,
|
||
}
|
||
income_rows.append({**common,
|
||
"TOTAL_OPERATE_INCOME": rev_c,
|
||
# 银行模板无营业成本(OPERATE_COST 缺失 → 金融股判定)
|
||
"OPERATE_COST": None if is_bank else 0.6 * rev_c,
|
||
"PARENT_NETPROFIT": np_c, "DEDUCT_PARENT_NETPROFIT": 0.9 * np_c,
|
||
"TOTAL_PROFIT": 1.1 * np_c, "INVEST_INCOME": 0.05 * np_c,
|
||
"FAIRVALUE_CHANGE_INCOME": 0.01 * np_c,
|
||
"ASSET_IMPAIRMENT_LOSS": 0.02 * np_c, "CREDIT_IMPAIRMENT_LOSS": 0.01 * np_c,
|
||
# P1 新原料(比率设计成可手算: τ=0.25 / INT=0.2·NP / 研发5% / 销售8%)
|
||
"RESEARCH_EXPENSE": 0.05 * rev_c, "SALE_EXPENSE": 0.08 * rev_c,
|
||
"FE_INTEREST_EXPENSE": 0.2 * np_c, "INCOME_TAX": 0.275 * np_c,
|
||
"BASIC_EPS": _cum_in_year(NP_Q, i) / SC_OF(i)}) # EPS 与 scale 无关
|
||
# IBD 只给 SHORT_LOAN 一列(其余组件列缺失,测 schema 缺列容错)
|
||
balance_rows.append({**common,
|
||
"TOTAL_ASSETS": s * TA_OF(i), "TOTAL_PARENT_EQUITY": s * EQ_OF(i),
|
||
"ACCOUNTS_RECE": s * AR_OF(i), "OTHER_RECE": s * (5 + max(i - 12, 0)),
|
||
"GOODWILL": 50.0, "SHARE_CAPITAL": float(SC_OF(i)),
|
||
"SHORT_LOAN": s * (100 + max(i - 12, 0)),
|
||
# P1 新原料: 存货/货币资金(银行无存货 → null 传播)
|
||
"INVENTORY": None if is_bank else s * (200 + 5 * max(i - 12, 0)),
|
||
# P1-B: 应付账款(银行无应付账款概念 → null 传播,CCC 剔金融)
|
||
"ACCOUNTS_PAYABLE": None if is_bank else s * AP_OF(i),
|
||
"MONETARYFUNDS": s * (150 + 10 * max(i - 12, 0))})
|
||
cashflow_rows.append({**common,
|
||
"NETCASH_OPERATE": 1.2 * np_c, "SALES_SERVICES": 1.05 * rev_c,
|
||
"ACCEPT_INVEST_CASH": 0.1 * rev_c,
|
||
# P1 新原料(DA 合计 = 0.10·REV / capex 15% / 债务净发行 = +0.10·REV)
|
||
"FA_IR_DEPR": 0.06 * rev_c, "IA_AMORTIZE": 0.02 * rev_c,
|
||
"LPE_AMORTIZE": 0.01 * rev_c, "USERIGHT_ASSET_AMORTIZE": 0.01 * rev_c,
|
||
"CONSTRUCT_LONG_ASSET": 0.15 * rev_c,
|
||
"RECEIVE_LOAN_CASH": 0.3 * rev_c, "ISSUE_BOND": 0.05 * rev_c,
|
||
"PAY_DEBT_CASH": 0.25 * rev_c})
|
||
|
||
for table, rows in (("income", income_rows), ("balance", balance_rows), ("cashflow", cashflow_rows)):
|
||
pl.DataFrame(rows).write_parquet(
|
||
os.path.join(static_dir, table, f"{file_code}_{table}.parquet"))
|
||
|
||
# forecast: 按报告期全市场文件(中文列,归母净利润行优先 + 无净利润行 fallback)
|
||
# 预测数值 = 预告中值(元): A=30(实际 27 → beat=-0.1) / B=40(beat=-0.325)
|
||
os.makedirs(os.path.join(static_dir, "forecast"), exist_ok=True)
|
||
pl.DataFrame([
|
||
{"股票代码": "600000", "预测指标": "归属于上市公司股东的净利润",
|
||
"预测数值": 30.0, "业绩变动幅度": 56.79, "预告类型": "预增", "公告日期": date(2023, 7, 15)},
|
||
{"股票代码": "000001", "预测指标": "归属于上市公司股东的净利润",
|
||
"预测数值": 40.0, "业绩变动幅度": -30.0, "预告类型": "预减", "公告日期": date(2023, 7, 20)},
|
||
{"股票代码": "300001", "预测指标": "营业收入",
|
||
"预测数值": 999.0, "业绩变动幅度": 5.0, "预告类型": "略增", "公告日期": date(2023, 7, 10)},
|
||
# 北交所两段前缀(92x/43x)→ .BJSE 映射用例
|
||
{"股票代码": "920001", "预测指标": "归属于上市公司股东的净利润",
|
||
"预测数值": 50.0, "业绩变动幅度": 20.0, "预告类型": "预增", "公告日期": date(2023, 7, 18)},
|
||
{"股票代码": "430047", "预测指标": "净利润",
|
||
"预测数值": 20.0, "业绩变动幅度": 10.0, "预告类型": "预增", "公告日期": date(2023, 7, 19)},
|
||
]).write_parquet(os.path.join(static_dir, "forecast", "20230630_forecast.parquet"))
|
||
pl.DataFrame([
|
||
{"股票代码": "600000", "预测指标": "净利润",
|
||
"预测数值": 65.0, "业绩变动幅度": 100.0, "预告类型": "扭亏", "公告日期": date(2023, 10, 15)},
|
||
]).write_parquet(os.path.join(static_dir, "forecast", "20230930_forecast.parquet"))
|
||
# 迟到预告用例(F07 锚语义): 2022 年报披露 2023-04-25,预告公告 2023-05-10
|
||
# 晚于披露日 → 兑现差锚 = max(披露日, 公告日) = 05-10(实际 NP 58,中值 55)
|
||
pl.DataFrame([
|
||
{"股票代码": "600000", "预测指标": "归属于上市公司股东的净利润",
|
||
"预测数值": 55.0, "业绩变动幅度": 15.0, "预告类型": "预增", "公告日期": date(2023, 5, 10)},
|
||
]).write_parquet(os.path.join(static_dir, "forecast", "20221231_forecast.parquet"))
|
||
|
||
# ==================== P1-B 批四新域(dividend/gdhs/top_holders/vb) ====================
|
||
# 契约=NAS 实测 2026-09-08: dividend 日期/数值列为字符串('' 为空);
|
||
# gdhs 截止日列名「股东户数统计截止日-本次」(无第二连字符);vb date 为字符串。
|
||
|
||
# ---- dividend 按股文件(A/B 两只;C/BANK 无文件 → send_total_12m NaN)----
|
||
# A 事件(eff = 回退链首个非空公告日):
|
||
# 2022-06-15 送转 0.2 / 2023-03-10 送 0.5 / 2023-09-20 送 0.3
|
||
# 2023-12-01(仅 dividPreNoticeDate,回退链用例) 送 0.4
|
||
# 全链空行(dividStocksPs=9.9 哨兵,应被丢弃)
|
||
# 2024-01-05 送 1.0(迟到事件,2024 起才可见)
|
||
os.makedirs(os.path.join(static_dir, "dividend"), exist_ok=True)
|
||
pl.DataFrame([
|
||
{"code": "sh.600000", "dividPreNoticeDate": "", "dividAgmPumDate": "",
|
||
"dividPlanAnnounceDate": "2022-06-15", "dividPlanDate": "",
|
||
"dividStocksPs": "0.200000", "dividCashPsBeforeTax": "1.000000"},
|
||
{"code": "sh.600000", "dividPreNoticeDate": "", "dividAgmPumDate": "",
|
||
"dividPlanAnnounceDate": "2023-03-10", "dividPlanDate": "",
|
||
"dividStocksPs": "0.500000", "dividCashPsBeforeTax": "2.000000"},
|
||
{"code": "sh.600000", "dividPreNoticeDate": "", "dividAgmPumDate": "",
|
||
"dividPlanAnnounceDate": "2023-09-20", "dividPlanDate": "",
|
||
"dividStocksPs": "0.300000", "dividCashPsBeforeTax": "3.000000"},
|
||
{"code": "sh.600000", "dividPreNoticeDate": "2023-12-01", "dividAgmPumDate": "",
|
||
"dividPlanAnnounceDate": "", "dividPlanDate": "",
|
||
"dividStocksPs": "0.400000", "dividCashPsBeforeTax": "4.000000"},
|
||
{"code": "sh.600000", "dividPreNoticeDate": "", "dividAgmPumDate": "",
|
||
"dividPlanAnnounceDate": "", "dividPlanDate": "",
|
||
"dividStocksPs": "9.900000", "dividCashPsBeforeTax": "9.900000"},
|
||
{"code": "sh.600000", "dividPreNoticeDate": "", "dividAgmPumDate": "",
|
||
"dividPlanAnnounceDate": "2024-01-05", "dividPlanDate": "",
|
||
"dividStocksPs": "1.000000", "dividCashPsBeforeTax": "5.000000"},
|
||
]).write_parquet(os.path.join(static_dir, "dividend", "600000.SH_dividend.parquet"))
|
||
# B: 纯现金分红事件(dividStocksPs=0 → send_total_12m = 0 非 NaN)
|
||
pl.DataFrame([
|
||
{"code": "sz.000001", "dividPreNoticeDate": "", "dividAgmPumDate": "",
|
||
"dividPlanAnnounceDate": "2023-03-10", "dividPlanDate": "",
|
||
"dividStocksPs": "0.000000", "dividCashPsBeforeTax": "0.500000"},
|
||
]).write_parquet(os.path.join(static_dir, "dividend", "000001.SZ_dividend.parquet"))
|
||
|
||
# ---- gdhs 按期文件(事件表: 截止日→户数,PIT=公告日期)----
|
||
os.makedirs(os.path.join(static_dir, "gdhs"), exist_ok=True)
|
||
|
||
def _gdhs_rows(items):
|
||
return [{"代码": c, "名称": f"股{c}", "股东户数-本次": h,
|
||
"股东户数统计截止日-本次": cutoff, "公告日期": ann,
|
||
"股东户数-增减比例": -1.0} # 噪声列(红线: 不可直接用,应无影响)
|
||
for c, h, cutoff, ann in items]
|
||
|
||
pl.DataFrame(_gdhs_rows([
|
||
("000001", 46000, date(2022, 6, 30), date(2022, 7, 20)),
|
||
])).write_parquet(os.path.join(static_dir, "gdhs", "20220630_gdhs.parquet"))
|
||
pl.DataFrame(_gdhs_rows([
|
||
("600000", 84000, date(2022, 9, 30), date(2022, 10, 20)), # A 首事件(Δ=null)
|
||
("000001", 48000, date(2022, 9, 30), date(2022, 10, 20)),
|
||
])).write_parquet(os.path.join(static_dir, "gdhs", "20220930_gdhs.parquet"))
|
||
pl.DataFrame(_gdhs_rows([
|
||
("600000", 80000, date(2022, 12, 31), date(2023, 1, 10)),
|
||
("000001", 50000, date(2022, 12, 31), date(2023, 1, 10)),
|
||
])).write_parquet(os.path.join(static_dir, "gdhs", "20221231_gdhs.parquet"))
|
||
pl.DataFrame(_gdhs_rows([
|
||
("600000", 72000, date(2023, 3, 31), date(2023, 4, 20)),
|
||
])).write_parquet(os.path.join(static_dir, "gdhs", "20230331_gdhs.parquet"))
|
||
# 同 (股, 截止日) 两行不同公告日 → 去重取最新公告(08-01 修订取代 07-20)
|
||
pl.DataFrame(_gdhs_rows([
|
||
("600000", 68000, date(2023, 6, 30), date(2023, 7, 25)),
|
||
("000001", 50000, date(2023, 6, 30), date(2023, 7, 20)),
|
||
]) + _gdhs_rows([
|
||
("000001", 50500, date(2023, 6, 30), date(2023, 8, 1)),
|
||
])).write_parquet(os.path.join(static_dir, "gdhs", "20230630_gdhs.parquet"))
|
||
pl.DataFrame(_gdhs_rows([
|
||
("600000", 77760, date(2023, 9, 30), date(2023, 10, 15)),
|
||
])).write_parquet(os.path.join(static_dir, "gdhs", "20230930_gdhs.parquet"))
|
||
|
||
# ---- top_holders: 原始按股×期小文件树(供预聚合脚本测试)+ 聚合产物 ----
|
||
# 600000 五期占比合计: 50/57/60/65/62;000001 单期 40(单期 → Δ=null)
|
||
th_dir = os.path.join(static_dir, "top_holders")
|
||
os.makedirs(th_dir, exist_ok=True)
|
||
_TH_SUMS = {("600000.SH", "20220630"): 5.0, ("600000.SH", "20220930"): 5.7,
|
||
("600000.SH", "20221231"): 6.0, ("600000.SH", "20230331"): 6.5,
|
||
("600000.SH", "20230630"): 6.2, ("000001.SZ", "20221231"): 4.0}
|
||
for (file_code, period), per_row in _TH_SUMS.items():
|
||
rows = []
|
||
for r in range(10):
|
||
rows.append({
|
||
"名次": r + 1, "股东名称": f"股东{r}", "股东性质": "基金",
|
||
"股份类型": "流通A股", "持股数": 1000 + r,
|
||
"占总流通股本持股比例": (None if (file_code == "600000.SH" and r == 9)
|
||
else per_row),
|
||
# 增减混合类型(「不变」字符串+数值字符串,coalesce/求和不受影响)
|
||
"增减": "不变" if r % 2 else f"{1000 + r}", "变动比率": 0.0,
|
||
})
|
||
pl.DataFrame(rows).write_parquet(
|
||
os.path.join(th_dir, f"{file_code}_{period}_top_holders.parquet"))
|
||
# 聚合产物(adapter 只读这个;与脚本对同一小树的输出逐值一致)
|
||
root = os.path.dirname(static_dir)
|
||
agg_dir = os.path.join(root, "factor_cache")
|
||
os.makedirs(agg_dir, exist_ok=True)
|
||
pl.DataFrame([
|
||
{"file_code": fc, "period": datetime.strptime(p, "%Y%m%d").date(),
|
||
"hold_pct": 10.0 * v, "n_holders": 10}
|
||
for (fc, p), v in _TH_SUMS.items()
|
||
]).write_parquet(os.path.join(agg_dir, "top_holders_agg.parquet"))
|
||
|
||
# ---- valuation_baostock 按年文件(与 static 同级的兄弟目录)----
|
||
vb_dir = os.path.join(root, "valuation_baostock")
|
||
os.makedirs(vb_dir, exist_ok=True)
|
||
pl.DataFrame([
|
||
{"symbol": "600000", "exchange": "SH", "date": "2023-08-25",
|
||
"peTTM": 12.0, "psTTM": 6.0, "pcfNcfTTM": 8.0, "pbMRQ": 2.4,
|
||
"turn": 0.5, "pctChg": 1.0, "isST": 0},
|
||
{"symbol": "600000", "exchange": "SH", "date": "2023-08-28",
|
||
"peTTM": 11.0, "psTTM": 6.0, "pcfNcfTTM": 8.0, "pbMRQ": 2.2,
|
||
"turn": 0.5, "pctChg": 1.0, "isST": 0},
|
||
{"symbol": "600000", "exchange": "SH", "date": "2023-08-29",
|
||
"peTTM": 10.0, "psTTM": 6.0, "pcfNcfTTM": 8.0, "pbMRQ": 2.0,
|
||
"turn": 0.5, "pctChg": 1.0, "isST": 0},
|
||
{"symbol": "000001", "exchange": "SZ", "date": "2023-08-29",
|
||
"peTTM": 20.0, "psTTM": 6.0, "pcfNcfTTM": 8.0, "pbMRQ": 0.8,
|
||
"turn": 0.5, "pctChg": 1.0, "isST": 0},
|
||
# 亏损股: baostock 不给负 pe → NaN;pe=0 守卫用例
|
||
{"symbol": "300001", "exchange": "SZ", "date": "2023-08-29",
|
||
"peTTM": None, "psTTM": 6.0, "pcfNcfTTM": 8.0, "pbMRQ": 5.0,
|
||
"turn": 0.5, "pctChg": 1.0, "isST": 0},
|
||
{"symbol": "000001", "exchange": "SZ", "date": "2023-08-30",
|
||
"peTTM": 0.0, "psTTM": 6.0, "pcfNcfTTM": 8.0, "pbMRQ": 0.8,
|
||
"turn": 0.5, "pctChg": 1.0, "isST": 0},
|
||
# exchange 全称变体(SSE/SZSE 旧契约形态)一行 → 映射兼容锁定
|
||
{"symbol": "600004", "exchange": "SSE", "date": "2023-08-29",
|
||
"peTTM": 40.0, "psTTM": 6.0, "pcfNcfTTM": 8.0, "pbMRQ": 4.0,
|
||
"turn": 0.5, "pctChg": 1.0, "isST": 0},
|
||
]).write_parquet(os.path.join(vb_dir, "2023.parquet"))
|
||
# 2026 文件: 08-13 起(bs 日喂起点;01-01~08-12 缺口由 static/valuation 补)
|
||
pl.DataFrame([
|
||
{"symbol": "600000", "exchange": "SH", "date": "2026-08-13",
|
||
"peTTM": 20.0, "psTTM": 6.0, "pcfNcfTTM": 8.0, "pbMRQ": 4.0,
|
||
"turn": 0.5, "pctChg": 1.0, "isST": 0},
|
||
{"symbol": "600000", "exchange": "SH", "date": "2026-08-14",
|
||
"peTTM": 19.0, "psTTM": 6.0, "pcfNcfTTM": 8.0, "pbMRQ": 3.9,
|
||
"turn": 0.5, "pctChg": 1.0, "isST": 0},
|
||
]).write_parquet(os.path.join(vb_dir, "2026.parquet"))
|
||
|
||
# ---- static/valuation(ak em 中文列;仅补 vb 2026-01-01~08-12 缺口)----
|
||
os.makedirs(os.path.join(static_dir, "valuation"), exist_ok=True)
|
||
pl.DataFrame([
|
||
{"数据日期": date(2026, 1, 5), "PE(TTM)": 25.0, "市净率": 5.0},
|
||
{"数据日期": date(2026, 8, 12), "PE(TTM)": 24.0, "市净率": 4.8},
|
||
]).write_parquet(os.path.join(static_dir, "valuation", "600000.SH_valuation.parquet"))
|
||
return static_dir
|
||
|
||
|
||
import pytest
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def synthetic_static(tmp_path_factory) -> str:
|
||
"""session 级合成静态域根目录(test_fundamental_* 共用)."""
|
||
return build_synthetic_static(tmp_path_factory.mktemp("fund_static"))
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def np_q_series() -> list[float]:
|
||
"""合成归母净利单季序列(SUE 期望值独立重算用)."""
|
||
return list(NP_Q)
|