Files
ggx/src/hdiv/data/schema.py
T
simon cf6d4d2c56 功能:每日动态股票池回测(--mode daily)+ 每日增量同步 + PIT 批量取数层
说明:本提交是工作区中此前的未提交工作(在 14ec0c6 之后产生),**非本次会话所写**,
按用户要求整理并推送。已做安全检查(无明文凭据、无大文件、.env/logs/output 仍被忽略),
并完成可执行范围内的测试验证(见「测试」一节)。

## 新增能力

1) `hdiv backtest --mode daily --start <日期>`
   - src/hdiv/backtest/daily.py:两趟式(先逐日选股,再复用既有引擎模拟)
   - 每个交易日按当日可见数据重建股票池(PIT),每个交易日判断买卖点
   - `pool_exit_action`:hold(只减不加、不因掉出池子而清仓)/ sell(掉出即清仓)
   - `profile_on_trade`:买卖决策发生时计算并留痕个股画像,**不区分是否在当日池内**
     (卖出/减仓同样留痕,否则「为什么卖」缺证据)
   - 与 walkforward 的分工:daily 是一条连续路径的推演,不是过拟合检验;
     因此不使用训练段、不冻结分布,阈值口径一律 rolling
   - 拒绝 `--universe-run`(daily 的定义就是逐日重筛,冻结池与之矛盾)

2) PIT 批量取数层 src/hdiv/universe/pit.py
   - PitRepo 继承 Repo,**只重写取数**(按区块批量预载 + 逐日内存切片),
     派生逻辑(最新一期财报合并、单位归一化、支付率口径等)一行不重写
     —— 以保证与逐日单点查询**结果等价**
   - 候选集预剪枝:用「不可能通过」的边界条件提前排除,文档论证为精确等价而非近似
   - src/hdiv/universe/daily.py:每日动态筛选器(仍然调用既有 selector 与四个 Filter)

3) 每日增量同步 `hdiv sync daily`
   - src/hdiv/data/sync/daily.py:只抓「库里还没有的那几天」,
     按「当日股票数 ≥ 当年规模阈值」判定缺口,不重拉历史、不覆盖既有行;
     支持 `--dry-run` 先看待抓清单
   - deploy/daily-sync.sh、deploy/install-sync-schedule.sh、
     deploy/com.hddiv.sync.plist.example(launchd 每天 17:00)
   - 新表 hd_daily_universe(逐日入选成员留痕)+ sql/hd_daily_universe.sql + schema.py
     (该表已存在于库中,`ddl plan` 返回 0 个待执行动作)

4) Web 与文档
   - 前端支持 daily 模式记录下钻(web/app.js、web/app.css、web/index.html、
     web/favicon.svg)
   - README / docs/user-guide.md / docs/implementation-status.md 同步更新:
     三种回测模式的取舍、daily 的成本说明(6.7 年约 1.5 小时)与调优手段

## 测试

tests/ 共 500 项(新增 tests/test_daily.py 43 项、tests/test_sync_daily.py 36 项)。

已验证通过:
- 排除上述两个新文件的 **421 项:全部通过(pytest 退出码 0)**
- 两个新文件的**非 DB 单元测试 60 项:全部通过**

未能在合理时间内跑完:
- 两个新文件中 **19 项 DB 标记的重型测试**。实测瓶颈是一条**无界全表扫描**:
  `SELECT ... FROM hd_cashflow WHERE ann_date <= :asof ORDER BY symbol, end_date, ann_date`
  (31 万行,无 symbol/报告期下限)。全量套件跑到 161 项时已耗时 20 分钟、
  0 失败,按该速率预计需 3 小时以上,因此改为分档验证。
- 旁证:库中存在 3 次成功的 daily 端到端运行(2026-10-05 10:05 / 10:32 / 11:03,
  区间 2024-03-01~03-15),说明该路径可正常完成。

## 已知待改进

- 上述 `hd_cashflow`(及同类「按 ann_date 上界取全历史」)的查询缺
  symbol / 报告期下限,是 daily 模式的主要性能瓶颈,建议下一轮优化。
2026-10-05 11:57:13 +08:00

1095 lines
45 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.
"""``hd_*`` 表结构定义(DDL 单一事实来源)。
设计约束(development-plan.md §3):
- 所有表 ``hd_`` 前缀,与 qlib 既有表共存于同一 schema,但不注册进其 Alembic 版本链;
- 只使用 ``CREATE TABLE IF NOT EXISTS`` 与 ``ALTER TABLE ... ADD COLUMN``,**从不 DROP/TRUNCATE**;
- 汇总表(画像/回测/敏感性)以 ``run_id`` 为逻辑分区键,重跑产生新 run_id,旧结果永久保留。
DDL 是幂等的:``apply_ddl()`` 可反复调用。
"""
from __future__ import annotations
from dataclasses import dataclass, field
CHARSET = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
@dataclass(frozen=True)
class Migration:
"""一次性的结构变更(幂等、可重复执行)。
背景:MySQL/MariaDB 的 UNIQUE 索引**不约束 NULL**(多行 NULL 视为互不相同),
因此凡是参与唯一键的列都不能为 NULL,否则会产生静默重复。
"""
name: str
sql: str
# 仅当该索引**不存在**时执行(用于 ADD UNIQUE KEY)
when_index_missing: str | None = None
# 仅当该索引**存在**时执行(用于 DROP 冗余索引)
when_index_exists: str | None = None
# 仅当该列**不存在**时执行(用于 ADD COLUMN)
when_column_missing: str | None = None
# 仅当该列**当前可为 NULL** 时执行(用于 MODIFY ... NOT NULL)
when_column_nullable: str | None = None
# 仅当该列当前最大长度**小于**给定值时执行(用于安全加长 VARCHAR)
when_column_max_length_lt: tuple[str, int] | None = None
# 仅当表为空时执行(杜绝任何数据损失)
requires_empty: bool = False
@dataclass(frozen=True)
class Table:
name: str
ddl: str
comment: str
# 后续版本新增的列(用于对既有库做幂等 ADD COLUMN)
added_columns: dict[str, str] = field(default_factory=dict)
# 结构迁移(永不删除数据)
migrations: tuple[Migration, ...] = ()
# ---------------------------------------------------------------------------
# A. 数据同步层
# ---------------------------------------------------------------------------
T_DIVIDEND = Table(
name="hd_dividend",
comment="分红送转全明细(plan.md §14/§30 的数据基座)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_dividend` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`symbol` VARCHAR(12) NOT NULL,
`end_date` DATE NOT NULL COMMENT '分红对应报告期',
`ann_date` DATE NULL COMMENT '预案公告日',
`imp_ann_date` DATE NULL COMMENT '实施公告日 —— PIT 关键字段',
`div_proc` VARCHAR(16) NOT NULL COMMENT '预案/股东大会通过/实施/取消',
`dedup_key` VARCHAR(80) NOT NULL COMMENT '去重键:NULL 不参与 MySQL 唯一约束,故显式构造',
`stk_div` DECIMAL(16,6) NULL COMMENT '每股送转股',
`stk_bo_rate` DECIMAL(16,6) NULL COMMENT '每股送股比例',
`stk_co_rate` DECIMAL(16,6) NULL COMMENT '每股转增比例',
`cash_div` DECIMAL(16,6) NULL COMMENT '每股税后现金分红(元)',
`cash_div_tax` DECIMAL(16,6) NULL COMMENT '每股税前现金分红(元)—— 连续性判定口径',
`record_date` DATE NULL COMMENT '股权登记日',
`ex_date` DATE NULL COMMENT '除权除息日',
`pay_date` DATE NULL COMMENT '派息日',
`div_listdate` DATE NULL COMMENT '红股上市日',
`base_share` DECIMAL(24,4) NULL COMMENT '基准股本(万股)',
`source` VARCHAR(16) NOT NULL DEFAULT 'tushare',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_div_key` (`dedup_key`),
KEY `ix_hd_div_ex` (`symbol`,`ex_date`),
KEY `ix_hd_div_pit` (`symbol`,`imp_ann_date`),
KEY `ix_hd_div_proc` (`div_proc`,`ex_date`)
) {CHARSET} COMMENT='分红送转全明细'
""",
migrations=(
Migration(
name="dividend_dedup_key",
sql=(
"ALTER TABLE `hd_dividend` "
"ADD COLUMN `dedup_key` VARCHAR(80) NOT NULL DEFAULT '' "
"COMMENT '去重键:NULL 不参与 MySQL 唯一约束,故显式构造' AFTER `div_proc`"
),
when_column_missing="dedup_key",
),
Migration(
name="dividend_unique_key",
sql="ALTER TABLE `hd_dividend` ADD UNIQUE KEY `uq_hd_div_key` (`dedup_key`)",
when_index_missing="uq_hd_div_key",
),
Migration(
name="dividend_drop_null_key",
sql="ALTER TABLE `hd_dividend` DROP INDEX `uq_hd_div`",
when_index_exists="uq_hd_div",
),
),
)
T_FINA = Table(
name="hd_fina_indicator",
comment="扩展财务指标(G4;带 announce_date 以支持 PIT)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_fina_indicator` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`symbol` VARCHAR(12) NOT NULL,
`end_date` DATE NOT NULL COMMENT '报告期',
`ann_date` DATE NOT NULL COMMENT '公告日 —— PIT 关键字段',
`eps` DECIMAL(16,6) NULL,
`roe` DECIMAL(16,6) NULL COMMENT '净资产收益率 %',
`roe_waa` DECIMAL(16,6) NULL COMMENT '加权平均 ROE %',
`roe_dt` DECIMAL(16,6) NULL COMMENT '扣非 ROE %',
`roa` DECIMAL(16,6) NULL,
`roic` DECIMAL(16,6) NULL COMMENT '投入资本回报率 %',
`grossprofit_margin` DECIMAL(16,6) NULL COMMENT '毛利率 %',
`netprofit_margin` DECIMAL(16,6) NULL COMMENT '净利率 %',
`debt_to_assets` DECIMAL(16,6) NULL COMMENT '资产负债率 %',
`current_ratio` DECIMAL(16,6) NULL,
`quick_ratio` DECIMAL(16,6) NULL,
`ocf_to_or` DECIMAL(16,6) NULL COMMENT '经营现金流/营收 %',
`ocf_to_opincome` DECIMAL(16,6) NULL COMMENT '经营现金流/经营利润 %',
`ocf_to_profit` DECIMAL(16,6) NULL COMMENT '经营现金流/净利润(盈利质量)',
`netdebt` DECIMAL(24,4) NULL,
`interestdebt` DECIMAL(24,4) NULL,
`ebit` DECIMAL(24,4) NULL,
`ebitda` DECIMAL(24,4) NULL,
`interest_cover` DECIMAL(16,6) NULL COMMENT '利息保障倍数',
`fcff` DECIMAL(24,4) NULL COMMENT '企业自由现金流',
`fcfe` DECIMAL(24,4) NULL COMMENT '股权自由现金流',
`source` VARCHAR(16) NOT NULL DEFAULT 'tushare',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_fina` (`symbol`,`end_date`,`ann_date`),
KEY `ix_hd_fina_pit` (`symbol`,`ann_date`),
KEY `ix_hd_fina_end` (`end_date`)
) {CHARSET} COMMENT='扩展财务指标(PIT)'
""",
)
T_CASHFLOW = Table(
name="hd_cashflow",
comment="现金流量表(G4;FCF 与分红覆盖的来源)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_cashflow` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`symbol` VARCHAR(12) NOT NULL,
`end_date` DATE NOT NULL,
`ann_date` DATE NOT NULL,
`report_type` VARCHAR(4) NOT NULL DEFAULT '1'
COMMENT '1=合并报表 2=单季合并 6=母公司;实测全为 1',
`n_cashflow_act` DECIMAL(24,4) NULL COMMENT '经营活动现金流净额',
`n_cashflow_inv_act` DECIMAL(24,4) NULL COMMENT '投资活动现金流净额',
`n_cash_flows_fnc_act` DECIMAL(24,4) NULL COMMENT '筹资活动现金流净额',
`c_pay_dist_dpcp_int_exp` DECIMAL(24,4) NULL COMMENT '分配股利/利润/偿付利息支付的现金',
`free_cashflow` DECIMAL(24,4) NULL COMMENT 'Tushare 口径自由现金流',
`c_fr_sale_sg_ar` DECIMAL(24,4) NULL COMMENT '销售商品收到的现金',
`source` VARCHAR(16) NOT NULL DEFAULT 'tushare',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_cf_key` (`symbol`,`end_date`,`ann_date`,`report_type`),
KEY `ix_hd_cf_pit` (`symbol`,`ann_date`)
) {CHARSET} COMMENT='现金流量表'
""",
migrations=(
Migration(
name="cashflow_report_type_column",
sql=(
"ALTER TABLE `hd_cashflow` ADD COLUMN `report_type` VARCHAR(4) "
"NOT NULL DEFAULT '1' "
"COMMENT '1=合并报表 2=单季合并 6=母公司;实测全为 1' AFTER `ann_date`"
),
when_column_missing="report_type",
),
Migration(
name="cashflow_report_type_key",
sql="ALTER TABLE `hd_cashflow` ADD UNIQUE KEY `uq_hd_cf_key` "
"(`symbol`,`end_date`,`ann_date`,`report_type`)",
when_index_missing="uq_hd_cf_key",
),
Migration(
name="cashflow_drop_old_key",
sql="ALTER TABLE `hd_cashflow` DROP INDEX `uq_hd_cf`",
when_index_exists="uq_hd_cf",
),
),
)
T_BALANCE = Table(
name="hd_balancesheet",
comment="资产负债表(G4;负债率与净资产判定的来源)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_balancesheet` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`symbol` VARCHAR(12) NOT NULL,
`end_date` DATE NOT NULL,
`ann_date` DATE NOT NULL,
`report_type` VARCHAR(4) NOT NULL DEFAULT '1'
COMMENT '1=合并报表 2=单季合并 6=母公司;实测全为 1',
`total_assets` DECIMAL(24,4) NULL,
`total_liab` DECIMAL(24,4) NULL,
`total_hldr_eqy_exc_min_int` DECIMAL(24,4) NULL COMMENT '归母净资产',
`total_cur_assets` DECIMAL(24,4) NULL,
`total_cur_liab` DECIMAL(24,4) NULL,
`money_cap` DECIMAL(24,4) NULL COMMENT '货币资金',
`goodwill` DECIMAL(24,4) NULL COMMENT '商誉',
`source` VARCHAR(16) NOT NULL DEFAULT 'tushare',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_bs_key` (`symbol`,`end_date`,`ann_date`,`report_type`),
KEY `ix_hd_bs_pit` (`symbol`,`ann_date`)
) {CHARSET} COMMENT='资产负债表'
""",
migrations=(
Migration(
name="balancesheet_report_type_column",
sql=(
"ALTER TABLE `hd_balancesheet` ADD COLUMN `report_type` VARCHAR(4) "
"NOT NULL DEFAULT '1' "
"COMMENT '1=合并报表 2=单季合并 6=母公司;实测全为 1' AFTER `ann_date`"
),
when_column_missing="report_type",
),
Migration(
name="balancesheet_report_type_key",
sql="ALTER TABLE `hd_balancesheet` ADD UNIQUE KEY `uq_hd_bs_key` "
"(`symbol`,`end_date`,`ann_date`,`report_type`)",
when_index_missing="uq_hd_bs_key",
),
Migration(
name="balancesheet_drop_old_key",
sql="ALTER TABLE `hd_balancesheet` DROP INDEX `uq_hd_bs`",
when_index_exists="uq_hd_bs",
),
),
)
T_INCOME = Table(
name="hd_income",
comment="利润表(G4;分红支付率的分母来源)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_income` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`symbol` VARCHAR(12) NOT NULL,
`end_date` DATE NOT NULL,
`ann_date` DATE NOT NULL,
`report_type` VARCHAR(4) NOT NULL DEFAULT '1'
COMMENT '1=合并报表 2=单季合并 6=母公司;实测全为 1',
`total_revenue` DECIMAL(24,4) NULL,
`revenue` DECIMAL(24,4) NULL,
`operate_profit` DECIMAL(24,4) NULL,
`n_income` DECIMAL(24,4) NULL COMMENT '净利润(含少数股东)',
`n_income_attr_p` DECIMAL(24,4) NULL COMMENT '归母净利润',
`basic_eps` DECIMAL(16,6) NULL,
`source` VARCHAR(16) NOT NULL DEFAULT 'tushare',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_inc_key` (`symbol`,`end_date`,`ann_date`,`report_type`),
KEY `ix_hd_inc_pit` (`symbol`,`ann_date`)
) {CHARSET} COMMENT='利润表'
""",
migrations=(
Migration(
name="income_report_type_column",
sql=(
"ALTER TABLE `hd_income` ADD COLUMN `report_type` VARCHAR(4) "
"NOT NULL DEFAULT '1' "
"COMMENT '1=合并报表 2=单季合并 6=母公司;实测全为 1' AFTER `ann_date`"
),
when_column_missing="report_type",
),
Migration(
name="income_report_type_key",
sql="ALTER TABLE `hd_income` ADD UNIQUE KEY `uq_hd_inc_key` "
"(`symbol`,`end_date`,`ann_date`,`report_type`)",
when_index_missing="uq_hd_inc_key",
),
Migration(
name="income_drop_old_key",
sql="ALTER TABLE `hd_income` DROP INDEX `uq_hd_inc`",
when_index_exists="uq_hd_inc",
),
),
)
T_INDEX_DAILY = Table(
name="hd_index_daily",
comment="基准指数行情(G5;plan.md §28.4 基准比较)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_index_daily` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`index_code` VARCHAR(16) NOT NULL,
`index_name` VARCHAR(64) NULL,
`trade_date` DATE NOT NULL,
`open` DECIMAL(16,4) NULL,
`high` DECIMAL(16,4) NULL,
`low` DECIMAL(16,4) NULL,
`close` DECIMAL(16,4) NULL,
`pre_close` DECIMAL(16,4) NULL,
`pct_chg` DECIMAL(12,6) NULL,
`vol` DECIMAL(24,4) NULL,
`amount` DECIMAL(24,4) NULL,
`source` VARCHAR(16) NOT NULL DEFAULT 'tushare',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_idx` (`index_code`,`trade_date`),
KEY `ix_hd_idx_date` (`trade_date`)
) {CHARSET} COMMENT='基准指数行情'
""",
)
T_SUSPEND = Table(
name="hd_suspend",
comment="停牌记录(G6;成交可行性约束)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_suspend` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`symbol` VARCHAR(12) NOT NULL,
`trade_date` DATE NOT NULL,
`suspend_type` VARCHAR(4) NOT NULL COMMENT 'S=停牌 R=复牌',
`suspend_timing` VARCHAR(48) NULL,
`source` VARCHAR(16) NOT NULL DEFAULT 'tushare',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_susp` (`symbol`,`trade_date`,`suspend_type`),
KEY `ix_hd_susp_date` (`trade_date`)
) {CHARSET} COMMENT='停牌记录'
""",
migrations=(
Migration(
name="suspend_timing_widen",
sql=(
"ALTER TABLE `hd_suspend` MODIFY COLUMN `suspend_timing` VARCHAR(48) NULL"
),
when_column_max_length_lt=("suspend_timing", 48),
),
),
)
T_LIMIT = Table(
name="hd_limit",
comment="涨跌停价(G6;plan.md §29 成交现实性)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_limit` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`symbol` VARCHAR(12) NOT NULL,
`trade_date` DATE NOT NULL,
`up_limit` DECIMAL(16,4) NULL,
`down_limit` DECIMAL(16,4) NULL,
`source` VARCHAR(16) NOT NULL DEFAULT 'tushare',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_limit` (`symbol`,`trade_date`),
KEY `ix_hd_limit_date` (`trade_date`)
) {CHARSET} COMMENT='涨跌停价'
""",
)
T_SYNC_LOG = Table(
name="hd_sync_log",
comment="本项目同步台账(含回补前后行数基线,供审计)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_sync_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`job` VARCHAR(64) NOT NULL COMMENT '同步器名',
`api` VARCHAR(96) NULL COMMENT 'Tushare 接口名(交错模式为 a+b+c 形式)',
`params_json` TEXT NULL,
`started_at` DATETIME NOT NULL,
`finished_at` DATETIME NULL,
`status` VARCHAR(16) NOT NULL DEFAULT 'RUNNING' COMMENT 'RUNNING/OK/FAIL/SKIP',
`rows_written` BIGINT NOT NULL DEFAULT 0,
`rows_before` BIGINT NULL COMMENT '回补前基线行数',
`rows_after` BIGINT NULL COMMENT '回补后行数(审计:只增不减)',
`data_start` DATE NULL,
`data_end` DATE NULL,
`failure_reason` TEXT NULL,
PRIMARY KEY (`id`),
KEY `ix_hd_synclog_job` (`job`,`started_at`),
KEY `ix_hd_synclog_status` (`status`)
) {CHARSET} COMMENT='同步台账'
""",
migrations=(
Migration(
name="sync_log_api_widen",
sql=(
"ALTER TABLE `hd_sync_log` MODIFY COLUMN `api` VARCHAR(96) NULL "
"COMMENT 'Tushare 接口名(交错模式为 a+b+c 形式)'"
),
when_column_max_length_lt=("api", 96),
),
),
)
T_AUDIT = Table(
name="hd_data_audit",
comment="数据完整性问题清单(P0 验收依据)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_data_audit` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`audit_id` VARCHAR(32) NOT NULL,
`check_code` VARCHAR(32) NOT NULL COMMENT 'G1..G6 或自定义检查码',
`check_name` VARCHAR(128) NOT NULL,
`severity` VARCHAR(16) NOT NULL COMMENT 'OK/WARN/FAIL',
`table_name` VARCHAR(64) NULL,
`metric_json` TEXT NULL,
`message` TEXT NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `ix_hd_audit_run` (`audit_id`),
KEY `ix_hd_audit_code` (`check_code`,`severity`)
) {CHARSET} COMMENT='数据审计结果'
""",
)
# ---------------------------------------------------------------------------
# B. 股票池层
# ---------------------------------------------------------------------------
T_UNIVERSE_RUN = Table(
name="hd_universe_run",
comment="一次筛选运行(股票池快照头)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_universe_run` (
`run_id` VARCHAR(32) NOT NULL,
`name` VARCHAR(64) NOT NULL,
`asof_date` DATE NOT NULL,
`mode` VARCHAR(16) NOT NULL COMMENT 'point_in_time/latest',
`config_json` MEDIUMTEXT NOT NULL COMMENT '生效配置快照(含 override 后)',
`config_hash` VARCHAR(64) NOT NULL,
`data_version` VARCHAR(64) NULL,
`code_version` VARCHAR(32) NULL,
`member_count` INT NOT NULL DEFAULT 0,
`candidate_count` INT NOT NULL DEFAULT 0 COMMENT '进入筛选的候选总数',
`stats_json` TEXT NULL COMMENT '逐滤网淘汰统计',
`status` VARCHAR(16) NOT NULL DEFAULT 'OK' COMMENT 'OK/WARN/FAIL',
`display_name` VARCHAR(128) NULL COMMENT '用户在界面上给这条筛选记录起的名字',
`notes` VARCHAR(500) NULL COMMENT '用户备注',
`archived_at` DATETIME NULL COMMENT '归档时间;NULL=未归档',
`deleted_at` DATETIME NULL COMMENT '软删除时间;NULL=未删除(不做物理删除)',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`run_id`),
KEY `ix_hd_uni_run_asof` (`asof_date`),
KEY `ix_hd_uni_run_hash` (`config_hash`),
KEY `ix_hd_uni_run_state` (`deleted_at`,`archived_at`,`created_at`)
) {CHARSET} COMMENT='股票池运行头'
""",
added_columns={
"display_name": "VARCHAR(128) NULL COMMENT '用户在界面上给这条筛选记录起的名字'",
"notes": "VARCHAR(500) NULL COMMENT '用户备注'",
"archived_at": "DATETIME NULL COMMENT '归档时间;NULL=未归档'",
"deleted_at": "DATETIME NULL COMMENT '软删除时间;NULL=未删除(不做物理删除)'",
},
migrations=(
Migration(
name="universe_run_state_index",
sql=(
"ALTER TABLE `hd_universe_run` ADD KEY `ix_hd_uni_run_state` "
"(`deleted_at`,`archived_at`,`created_at`)"
),
when_index_missing="ix_hd_uni_run_state",
),
),
)
T_UNIVERSE_MEMBER = Table(
name="hd_universe_member",
comment="股票池成员 + 逐滤网命中留痕(可回答「为什么没选上」)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_universe_member` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`run_id` VARCHAR(32) NOT NULL,
`symbol` VARCHAR(12) NOT NULL,
`name` VARCHAR(64) NULL,
`industry` VARCHAR(64) NULL,
`passed` TINYINT(1) NOT NULL COMMENT '1=入选 0=被淘汰',
`fail_stage` VARCHAR(32) NULL COMMENT '首个未通过的滤网',
`fail_reason` VARCHAR(255) NULL,
`values_json` TEXT NULL COMMENT '各滤网实际取值快照',
`filter_json` TEXT NULL COMMENT '各滤网通过位',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_uni_mem` (`run_id`,`symbol`),
KEY `ix_hd_uni_mem_pass` (`run_id`,`passed`),
KEY `ix_hd_uni_mem_sym` (`symbol`)
) {CHARSET} COMMENT='股票池成员留痕'
""",
)
T_DAILY_UNIVERSE = Table(
name="hd_daily_universe",
comment="每日动态股票池成员(daily 模式「每日选股」的留痕)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_daily_universe` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`run_id` VARCHAR(32) NOT NULL COMMENT '所属回测 run_id',
`trade_date` DATE NOT NULL COMMENT '该交易日的选股结果',
`symbol` VARCHAR(12) NOT NULL,
`name` VARCHAR(64) NULL,
`industry` VARCHAR(64) NULL,
`dividend_yield` DECIMAL(18,8) NULL COMMENT '入选当日股息率(池内排序口径)',
`total_mv` DECIMAL(24,4) NULL,
`roe_avg` DECIMAL(18,8) NULL,
`listed_count` INT NULL COMMENT '当日市场候选数(未预剪枝)',
`candidate_count` INT NULL COMMENT '当日实际参与筛选的候选数(已预剪枝)',
`values_json` TEXT NULL COMMENT '入选时的关键因子快照',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_daily_uni` (`run_id`,`trade_date`,`symbol`),
KEY `ix_hd_daily_uni_date` (`trade_date`),
KEY `ix_hd_daily_uni_sym` (`symbol`,`trade_date`)
) {CHARSET} COMMENT='每日动态股票池成员'
""",
# 建表时已含上述列;这里的 added_columns 是为了让**先建表、后加列**的库
# 也能幂等补上(CREATE TABLE IF NOT EXISTS 不会修改已存在的表)。
added_columns={
"listed_count": "INT NULL COMMENT '当日市场候选数(未预剪枝)'",
},
)
# ---------------------------------------------------------------------------
# C. 因子与画像层
# ---------------------------------------------------------------------------
T_FACTOR_SNAPSHOT = Table(
name="hd_factor_snapshot",
comment="决策时点因子值(长表;避免 1.9 亿行日频面板,见 plan §3.2)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_factor_snapshot` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`snapshot_id` VARCHAR(32) NOT NULL COMMENT '一次快照(通常绑定 universe run 或 rebalance 日)',
`asof_date` DATE NOT NULL,
`symbol` VARCHAR(12) NOT NULL,
`factor_code` VARCHAR(48) NOT NULL,
`value` DECIMAL(24,8) NULL,
`source` VARCHAR(32) NULL COMMENT 'computed/dv_ttm/daily_basic 等口径来源',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_fac` (`snapshot_id`,`symbol`,`factor_code`),
KEY `ix_hd_fac_asof` (`asof_date`,`factor_code`),
KEY `ix_hd_fac_sym` (`symbol`,`factor_code`,`asof_date`)
) {CHARSET} COMMENT='决策时点因子快照'
""",
)
T_PROFILE_RUN = Table(
name="hd_profile_run",
comment="画像运行头",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_profile_run` (
`run_id` VARCHAR(32) NOT NULL,
`name` VARCHAR(64) NOT NULL,
`asof_date` DATE NOT NULL,
`universe_run_id` VARCHAR(32) NULL COMMENT '来源股票池 run',
`config_json` MEDIUMTEXT NOT NULL,
`config_hash` VARCHAR(64) NOT NULL,
`data_version` VARCHAR(64) NULL,
`symbol_count` INT NOT NULL DEFAULT 0,
`status` VARCHAR(16) NOT NULL DEFAULT 'OK',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`run_id`),
KEY `ix_hd_prof_run_asof` (`asof_date`),
KEY `ix_hd_prof_run_hash` (`config_hash`)
) {CHARSET} COMMENT='画像运行头'
""",
)
T_PROFILE_STAT = Table(
name="hd_profile_stat",
comment="逐股逐指标历史分布统计(plan.md §9 明确要求 P10/P25/P50/P75/P90)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_profile_stat` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`run_id` VARCHAR(32) NOT NULL,
`symbol` VARCHAR(12) NOT NULL,
`metric_code` VARCHAR(48) NOT NULL,
`window_years` INT NOT NULL DEFAULT 0 COMMENT '0=全历史;NULL 会绕过唯一约束故禁用',
`n_obs` INT NOT NULL DEFAULT 0,
`min_value` DECIMAL(24,8) NULL,
`max_value` DECIMAL(24,8) NULL,
`mean_value` DECIMAL(24,8) NULL,
`median_value` DECIMAL(24,8) NULL,
`std_value` DECIMAL(24,8) NULL,
`p10` DECIMAL(24,8) NULL,
`p25` DECIMAL(24,8) NULL,
`p50` DECIMAL(24,8) NULL,
`p75` DECIMAL(24,8) NULL,
`p90` DECIMAL(24,8) NULL,
`extra_percentiles_json` TEXT NULL COMMENT '配置中超出默认 5 档的分位',
`current_value` DECIMAL(24,8) NULL COMMENT 'asof 当日取值',
`current_percentile` DECIMAL(12,6) NULL COMMENT '当前值在历史分布中的分位 %',
`status` VARCHAR(16) NOT NULL DEFAULT 'OK' COMMENT 'OK/INSUFFICIENT',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_prof_stat` (`run_id`,`symbol`,`metric_code`,`window_years`),
KEY `ix_hd_prof_stat_sym` (`symbol`,`metric_code`),
KEY `ix_hd_prof_stat_q` (`run_id`,`metric_code`,`current_percentile`)
) {CHARSET} COMMENT='个股历史分布统计'
""",
migrations=(
Migration(
name="profile_stat_window_not_null",
sql=(
"ALTER TABLE `hd_profile_stat` MODIFY COLUMN `window_years` "
"INT NOT NULL DEFAULT 0 COMMENT '0=全历史;NULL 会绕过唯一约束故禁用'"
),
when_column_nullable="window_years",
),
),
)
T_PROFILE_SERIES = Table(
name="hd_profile_series",
comment="画像时间序列(供 HTML 画图;仅筛选后股票,体积可控)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_profile_series` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`run_id` VARCHAR(32) NOT NULL,
`symbol` VARCHAR(12) NOT NULL,
`metric_code` VARCHAR(48) NOT NULL,
`series_date` DATE NOT NULL,
`value` DECIMAL(24,8) NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_prof_ser` (`run_id`,`symbol`,`metric_code`,`series_date`),
KEY `ix_hd_prof_ser_sym` (`run_id`,`symbol`,`metric_code`)
) {CHARSET} COMMENT='个股画像序列'
""",
)
T_PROFILE_SCORE = Table(
name="hd_profile_score",
comment="安全边际分项得分(plan.md §15)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_profile_score` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`run_id` VARCHAR(32) NOT NULL,
`symbol` VARCHAR(12) NOT NULL,
`score_code` VARCHAR(48) NOT NULL COMMENT 'dividend_yield/valuation/financial_quality/balance_sheet/dividend_quality/composite',
`score` DECIMAL(12,6) NULL,
`detail_json` TEXT NULL COMMENT '分项明细与所用锚点',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_prof_score` (`run_id`,`symbol`,`score_code`),
KEY `ix_hd_prof_score_v` (`run_id`,`score_code`,`score`)
) {CHARSET} COMMENT='安全边际得分'
""",
)
# ---------------------------------------------------------------------------
# D. 策略层
# ---------------------------------------------------------------------------
T_STRATEGY = Table(
name="hd_strategy",
comment="策略身份与版本(plan.md §38/§39 生命周期)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_strategy` (
`strategy_id` VARCHAR(32) NOT NULL,
`name` VARCHAR(64) NOT NULL,
`version` VARCHAR(16) NOT NULL,
`status` VARCHAR(16) NOT NULL DEFAULT 'DRAFT'
COMMENT 'DRAFT/RESEARCH/BACKTEST/VALIDATED/PAPER/LIVE/ARCHIVED',
`description` VARCHAR(500) NOT NULL DEFAULT '',
`yaml_path` VARCHAR(255) NULL,
`config_json` MEDIUMTEXT NOT NULL,
`config_hash` VARCHAR(64) NOT NULL,
`parent_id` VARCHAR(32) NULL COMMENT '上一版本 strategy_id',
`spec_type` VARCHAR(16) NOT NULL DEFAULT 'yaml',
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`strategy_id`,`version`),
UNIQUE KEY `uq_hd_strat_hash` (`config_hash`),
KEY `ix_hd_strat_name` (`name`)
) {CHARSET} COMMENT='策略版本'
""",
)
T_STRATEGY_PARAM = Table(
name="hd_strategy_param",
comment="策略参数扁平化(path → value),便于跨版本 diff 与敏感性扫描",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_strategy_param` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`strategy_id` VARCHAR(32) NOT NULL,
`version` VARCHAR(16) NOT NULL,
`param_path` VARCHAR(255) NOT NULL,
`param_value` VARCHAR(500) NULL,
`value_type` VARCHAR(16) NULL COMMENT 'int/float/str/bool/null',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_sparam` (`strategy_id`,`version`,`param_path`),
KEY `ix_hd_sparam_path` (`param_path`)
) {CHARSET} COMMENT='策略参数扁平表'
""",
)
# ---------------------------------------------------------------------------
# E. 回测层
# ---------------------------------------------------------------------------
T_BT_RUN = Table(
name="hd_backtest_run",
comment="回测运行头(plan.md §47 原则 6:可复现四元组)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_backtest_run` (
`run_id` VARCHAR(32) NOT NULL,
`strategy_id` VARCHAR(32) NOT NULL,
`strategy_version` VARCHAR(16) NOT NULL,
`mode` VARCHAR(16) NOT NULL DEFAULT 'single'
COMMENT 'single/in_sample/out_of_sample/walkforward_window',
`wf_id` VARCHAR(32) NULL,
`window_index` INT NULL,
`start_date` DATE NOT NULL,
`end_date` DATE NOT NULL,
`initial_capital` DECIMAL(24,4) NOT NULL,
`final_capital` DECIMAL(24,4) NULL,
`total_dividend_net` DECIMAL(24,4) NOT NULL DEFAULT 0 COMMENT '累计现金分红(税后)',
`total_dividend_tax` DECIMAL(24,4) NOT NULL DEFAULT 0 COMMENT '累计代扣红利税',
`total_fees` DECIMAL(24,4) NOT NULL DEFAULT 0 COMMENT '累计交易费用',
`cash_final` DECIMAL(24,4) NULL COMMENT '期末现金(用于资金对账)',
`strategy_config_json` MEDIUMTEXT NOT NULL,
`config_hash` VARCHAR(64) NOT NULL,
`backtest_config_json` TEXT NULL,
`cost_json` TEXT NULL,
`data_version` VARCHAR(64) NULL,
`code_version` VARCHAR(32) NULL,
`seed` INT NULL,
`status` VARCHAR(16) NOT NULL DEFAULT 'OK' COMMENT 'OK/FAIL/PARTIAL',
`unimplemented_json` TEXT NULL COMMENT '未建模部分显式声明(如涨跌停近似)',
`display_name` VARCHAR(128) NULL COMMENT '用户命名',
`notes` VARCHAR(500) NULL COMMENT '用户备注',
`universe_run_id` VARCHAR(32) NULL COMMENT '关联的股票池筛选记录(--universe-run 指定时写入)',
`archived_at` DATETIME NULL COMMENT '归档时间;NULL=未归档',
`deleted_at` DATETIME NULL COMMENT '软删除时间;NULL=未删除',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`run_id`),
KEY `ix_hd_bt_strategy` (`strategy_id`,`strategy_version`),
KEY `ix_hd_bt_wf` (`wf_id`,`window_index`),
KEY `ix_hd_bt_period` (`start_date`,`end_date`),
KEY `ix_hd_bt_universe` (`universe_run_id`),
KEY `ix_hd_bt_state` (`deleted_at`,`archived_at`,`created_at`)
) {CHARSET} COMMENT='回测运行头'
""",
# 既有库通过 added_columns 幂等补列(CREATE TABLE IF NOT EXISTS 不会改已存在的表)
added_columns={
"total_dividend_net": "DECIMAL(24,4) NOT NULL DEFAULT 0 COMMENT '累计现金分红(税后)'",
"total_dividend_tax": "DECIMAL(24,4) NOT NULL DEFAULT 0 COMMENT '累计代扣红利税'",
"total_fees": "DECIMAL(24,4) NOT NULL DEFAULT 0 COMMENT '累计交易费用'",
"cash_final": "DECIMAL(24,4) NULL COMMENT '期末现金(用于资金对账)'",
"display_name": "VARCHAR(128) NULL COMMENT '用户命名'",
"notes": "VARCHAR(500) NULL COMMENT '用户备注'",
"universe_run_id": "VARCHAR(32) NULL COMMENT '关联的股票池筛选记录'",
"archived_at": "DATETIME NULL COMMENT '归档时间;NULL=未归档'",
"deleted_at": "DATETIME NULL COMMENT '软删除时间;NULL=未删除'",
},
migrations=(
Migration(
name="bt_run_universe_index",
sql="ALTER TABLE `hd_backtest_run` ADD KEY `ix_hd_bt_universe` (`universe_run_id`)",
when_index_missing="ix_hd_bt_universe",
),
Migration(
name="bt_run_state_index",
sql=(
"ALTER TABLE `hd_backtest_run` ADD KEY `ix_hd_bt_state` "
"(`deleted_at`,`archived_at`,`created_at`)"
),
when_index_missing="ix_hd_bt_state",
),
),
)
T_BT_EQUITY = Table(
name="hd_backtest_equity",
comment="逐日净值曲线(含基准对齐,plan.md §28.4)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_backtest_equity` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`run_id` VARCHAR(32) NOT NULL,
`trade_date` DATE NOT NULL,
`nav` DECIMAL(24,8) NOT NULL COMMENT '组合净值(初始=1)',
`total_value` DECIMAL(24,4) NOT NULL,
`cash` DECIMAL(24,4) NOT NULL,
`position_value` DECIMAL(24,4) NOT NULL,
`daily_return` DECIMAL(18,10) NULL,
`cum_return` DECIMAL(18,10) NULL,
`drawdown` DECIMAL(18,10) NULL COMMENT '相对历史高点回撤(负值)',
`holding_count` INT NOT NULL DEFAULT 0,
`benchmark_code` VARCHAR(16) NULL,
`benchmark_close` DECIMAL(16,4) NULL,
`benchmark_nav` DECIMAL(24,8) NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_bt_eq` (`run_id`,`trade_date`),
KEY `ix_hd_bt_eq_date` (`trade_date`)
) {CHARSET} COMMENT='回测净值曲线'
""",
)
T_BT_POSITION = Table(
name="hd_backtest_position",
comment="逐日持仓快照",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_backtest_position` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`run_id` VARCHAR(32) NOT NULL,
`trade_date` DATE NOT NULL,
`symbol` VARCHAR(12) NOT NULL,
`quantity` DECIMAL(20,4) NOT NULL,
`avg_cost` DECIMAL(16,4) NULL,
`close` DECIMAL(16,4) NULL,
`market_value` DECIMAL(24,4) NULL,
`weight` DECIMAL(12,8) NULL,
`unrealized_pnl` DECIMAL(24,4) NULL,
`holding_days` INT NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_bt_pos` (`run_id`,`trade_date`,`symbol`),
KEY `ix_hd_bt_pos_sym` (`run_id`,`symbol`)
) {CHARSET} COMMENT='逐日持仓'
""",
)
T_BT_TRADE = Table(
name="hd_backtest_trade",
comment="逐笔成交(plan.md §32;reason_json 为 AI 可解释性关键)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_backtest_trade` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`run_id` VARCHAR(32) NOT NULL,
`trade_id` VARCHAR(32) NOT NULL,
`symbol` VARCHAR(12) NOT NULL,
`signal_date` DATE NOT NULL,
`execution_date` DATE NOT NULL,
`side` VARCHAR(4) NOT NULL COMMENT 'BUY/SELL',
`price` DECIMAL(16,4) NOT NULL,
`quantity` DECIMAL(20,4) NOT NULL,
`amount` DECIMAL(24,4) NOT NULL,
`commission` DECIMAL(16,4) NOT NULL DEFAULT 0,
`stamp_tax` DECIMAL(16,4) NOT NULL DEFAULT 0,
`transfer_fee` DECIMAL(16,4) NOT NULL DEFAULT 0,
`slippage_cost` DECIMAL(16,4) NOT NULL DEFAULT 0,
`total_cost` DECIMAL(16,4) NOT NULL DEFAULT 0,
`realized_pnl` DECIMAL(24,4) NULL,
`holding_days` INT NULL,
`reason_json` TEXT NULL COMMENT 'BUY/SELL 的完整理由快照',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_bt_trade` (`run_id`,`trade_id`),
KEY `ix_hd_bt_trade_sym` (`run_id`,`symbol`,`execution_date`),
KEY `ix_hd_bt_trade_date` (`execution_date`)
) {CHARSET} COMMENT='回测成交流水'
""",
)
T_BT_SIGNAL = Table(
name="hd_backtest_signal",
comment="逐信号(含 executed/skip_reason,用于区分「想买」与「买到了」)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_backtest_signal` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`run_id` VARCHAR(32) NOT NULL,
`symbol` VARCHAR(12) NOT NULL,
`signal_date` DATE NOT NULL,
`signal_type` VARCHAR(8) NOT NULL COMMENT 'BUY/SELL/HOLD/WATCH',
`yield_value` DECIMAL(18,8) NULL COMMENT '触发时的股息率',
`yield_percentile` DECIMAL(12,6) NULL COMMENT '触发时的历史分位 %',
`target_weight` DECIMAL(12,8) NULL,
`price` DECIMAL(16,4) NULL,
`executed` TINYINT(1) NOT NULL DEFAULT 0,
`skip_reason` VARCHAR(64) NULL COMMENT 'LIMIT_UP/LIMIT_DOWN/SUSPENDED/NO_CASH/MIN_COMMISSION',
`reason_json` TEXT NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `ix_hd_bt_sig` (`run_id`,`symbol`,`signal_date`),
KEY `ix_hd_bt_sig_type` (`run_id`,`signal_type`,`executed`)
) {CHARSET} COMMENT='回测信号流水'
""",
)
T_BT_METRIC = Table(
name="hd_backtest_metric",
comment="绩效指标长表(scope 区分样本内/外/窗口;plan.md §28)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_backtest_metric` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`run_id` VARCHAR(32) NOT NULL,
`scope` VARCHAR(24) NOT NULL COMMENT 'all/train/test/window_k/year_YYYY',
`category` VARCHAR(16) NOT NULL COMMENT 'return/risk/trade/benchmark',
`metric_code` VARCHAR(48) NOT NULL,
`metric_value` DECIMAL(24,8) NULL,
`benchmark_code` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '空串=策略自身;NULL 会绕过唯一约束故禁用',
`unit` VARCHAR(16) NULL COMMENT 'ratio/pct/money/count/days',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_bt_metric` (`run_id`,`scope`,`metric_code`,`benchmark_code`),
KEY `ix_hd_bt_metric_code` (`metric_code`),
KEY `ix_hd_bt_metric_scope` (`run_id`,`scope`,`category`)
) {CHARSET} COMMENT='回测绩效指标'
""",
migrations=(
Migration(
name="bt_metric_benchmark_not_null",
sql=(
"ALTER TABLE `hd_backtest_metric` MODIFY COLUMN `benchmark_code` "
"VARCHAR(16) NOT NULL DEFAULT '' "
"COMMENT '空串=策略自身;NULL 会绕过唯一约束故禁用'"
),
when_column_nullable="benchmark_code",
),
),
)
T_WF_RUN = Table(
name="hd_walkforward_run",
comment="Walk-forward 运行头(plan.md §23)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_walkforward_run` (
`wf_id` VARCHAR(32) NOT NULL,
`strategy_id` VARCHAR(32) NOT NULL,
`strategy_version` VARCHAR(16) NOT NULL,
`config_json` MEDIUMTEXT NOT NULL,
`config_hash` VARCHAR(64) NOT NULL,
`scheme` VARCHAR(16) NOT NULL COMMENT 'rolling/expanding',
`train_years` INT NOT NULL,
`test_years` INT NOT NULL,
`step_months` INT NOT NULL,
`window_count` INT NOT NULL DEFAULT 0,
`data_version` VARCHAR(64) NULL,
`code_version` VARCHAR(32) NULL,
`status` VARCHAR(16) NOT NULL DEFAULT 'OK',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`wf_id`),
KEY `ix_hd_wf_strategy` (`strategy_id`,`strategy_version`)
) {CHARSET} COMMENT='Walk-forward 运行头'
""",
)
T_WF_WINDOW = Table(
name="hd_walkforward_window",
comment="Walk-forward 每个窗口(train 定参 → test 冻结,plan.md §25)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_walkforward_window` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`wf_id` VARCHAR(32) NOT NULL,
`window_index` INT NOT NULL,
`train_start` DATE NOT NULL,
`train_end` DATE NOT NULL,
`test_start` DATE NOT NULL,
`test_end` DATE NOT NULL,
`frozen_params_json` TEXT NOT NULL COMMENT '由 train 段确定、在 test 段冻结的参数',
`train_run_id` VARCHAR(32) NULL,
`test_run_id` VARCHAR(32) NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_wf_win` (`wf_id`,`window_index`),
KEY `ix_hd_wf_win_period` (`test_start`,`test_end`)
) {CHARSET} COMMENT='Walk-forward 窗口'
""",
)
# ---------------------------------------------------------------------------
# F. 分析层
# ---------------------------------------------------------------------------
T_SENS_RUN = Table(
name="hd_sensitivity_run",
comment="参数敏感性扫描(plan.md §27 过拟合控制)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_sensitivity_run` (
`sens_id` VARCHAR(32) NOT NULL,
`base_strategy_id` VARCHAR(32) NOT NULL,
`base_version` VARCHAR(16) NOT NULL,
`sweep_json` TEXT NOT NULL COMMENT '扫描网格定义',
`point_count` INT NOT NULL DEFAULT 0,
`period_start` DATE NULL,
`period_end` DATE NULL,
`data_version` VARCHAR(64) NULL,
`code_version` VARCHAR(32) NULL,
`status` VARCHAR(16) NOT NULL DEFAULT 'OK',
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`sens_id`),
KEY `ix_hd_sens_strategy` (`base_strategy_id`,`base_version`)
) {CHARSET} COMMENT='敏感性扫描头'
""",
)
T_SENS_POINT = Table(
name="hd_sensitivity_point",
comment="敏感性扫描的每个参数组合及其绩效",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_sensitivity_point` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`sens_id` VARCHAR(32) NOT NULL,
`point_index` INT NOT NULL,
`param_values_json` TEXT NOT NULL,
`param_key` VARCHAR(128) NULL COMMENT '单参数扫描时的主键(便于画曲线)',
`run_id` VARCHAR(32) NULL COMMENT '对应回测 run',
`cagr` DECIMAL(18,8) NULL,
`total_return` DECIMAL(18,8) NULL,
`max_drawdown` DECIMAL(18,8) NULL,
`sharpe` DECIMAL(18,8) NULL,
`calmar` DECIMAL(18,8) NULL,
`sortino` DECIMAL(18,8) NULL,
`trade_count` INT NULL,
`turnover` DECIMAL(18,8) NULL,
`extra_json` TEXT NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_hd_sens_pt` (`sens_id`,`point_index`),
KEY `ix_hd_sens_pt_key` (`sens_id`,`param_key`)
) {CHARSET} COMMENT='敏感性扫描点'
""",
)
# ---------------------------------------------------------------------------
# G. 报告层
# ---------------------------------------------------------------------------
T_REPORT = Table(
name="hd_report",
comment="报告元数据(含 SQL 溯源,保证页面数字可复现)",
ddl=f"""
CREATE TABLE IF NOT EXISTS `hd_report` (
`report_id` VARCHAR(32) NOT NULL,
`report_type` VARCHAR(24) NOT NULL COMMENT 'audit/universe/profile/backtest/walkforward/sensitivity/strategy/index',
`title` VARCHAR(255) NOT NULL,
`html_path` VARCHAR(500) NOT NULL,
`source_run_ids` VARCHAR(500) NULL COMMENT '逗号分隔的来源 run_id',
`context_json` MEDIUMTEXT NULL COMMENT '渲染上下文快照',
`sql_provenance_json` MEDIUMTEXT NULL COMMENT '页面数据的来源 SQL(可复现性)',
`data_version` VARCHAR(64) NULL,
`generated_at` DATETIME NOT NULL,
`file_bytes` BIGINT NULL,
PRIMARY KEY (`report_id`),
KEY `ix_hd_report_type` (`report_type`,`generated_at`)
) {CHARSET} COMMENT='报告元数据'
""",
)
ALL_TABLES: tuple[Table, ...] = (
# A. 数据同步层
T_DIVIDEND,
T_FINA,
T_CASHFLOW,
T_BALANCE,
T_INCOME,
T_INDEX_DAILY,
T_SUSPEND,
T_LIMIT,
T_SYNC_LOG,
T_AUDIT,
# B. 股票池层
T_UNIVERSE_RUN,
T_UNIVERSE_MEMBER,
T_DAILY_UNIVERSE,
# C. 因子与画像层
T_FACTOR_SNAPSHOT,
T_PROFILE_RUN,
T_PROFILE_STAT,
T_PROFILE_SERIES,
T_PROFILE_SCORE,
# D. 策略层
T_STRATEGY,
T_STRATEGY_PARAM,
# E. 回测层
T_BT_RUN,
T_BT_EQUITY,
T_BT_POSITION,
T_BT_TRADE,
T_BT_SIGNAL,
T_BT_METRIC,
T_WF_RUN,
T_WF_WINDOW,
# F. 分析层
T_SENS_RUN,
T_SENS_POINT,
# G. 报告层
T_REPORT,
)
TABLE_NAMES: tuple[str, ...] = tuple(t.name for t in ALL_TABLES)