说明:本提交是工作区中此前的未提交工作(在 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 模式的主要性能瓶颈,建议下一轮优化。
1095 lines
45 KiB
Python
1095 lines
45 KiB
Python
"""``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)
|