2. 金融数据基础:行情表结构、持仓表结构、基准表结构、交易日历表设计
组合风险绩效归因的基石是高质量、结构化的金融数据。本章将定义四张核心数据表:行情表、持仓表、基准表与交易日历表。这四张表构成了后续所有SQL计算与分析的数据底座,其字段设计、主键定义与索引策略直接影响归因计算的准确性与性能。
设计原则:每张表均采用“宽表+分区”模式,以交易日为分区键,避免跨表频繁JOIN;主键采用复合主键,确保数据唯一性;所有价格字段统一使用DECIMAL(18,6),避免浮点精度问题。
2.1 交易日历表(trade_calendar)
交易日历表是金融数据的时间骨架,用于判断交易日、计算持有期、对齐不同市场的交易日期。该表通常由交易所发布,包含连续多年的日期数据。
| 字段名 | 数据类型 | 约束 | 说明 |
|---|---|---|---|
| calendar_date | DATE | PRIMARY KEY | 日历日期,格式YYYY-MM-DD |
| is_trading_day | TINYINT | NOT NULL | 1=交易日,0=非交易日 |
| prev_trading_day | DATE | NULLABLE | 上一个交易日,用于计算日收益率 |
| next_trading_day | DATE | NULLABLE | 下一个交易日 |
| week_of_year | INT | NULLABLE | 年内第几周,用于周度归因 |
| month_of_year | INT | NULLABLE | 月份,用于月度归因 |
-- 交易日历表建表SQL(MySQL语法)
CREATE TABLE trade_calendar (
calendar_date DATE NOT NULL,
is_trading_day TINYINT NOT NULL COMMENT '1=交易日,0=非交易日',
prev_trading_day DATE NULL COMMENT '上一个交易日',
next_trading_day DATE NULL COMMENT '下一个交易日',
week_of_year INT NULL COMMENT '年内第几周',
month_of_year INT NULL COMMENT '月份',
PRIMARY KEY (calendar_date),
INDEX idx_trading_day (is_trading_day, calendar_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易日历表';
关键逻辑:prev_trading_day 与 next_trading_day 字段通过自关联填充,用于快速计算日收益率,避免每次查询都使用LAG/LEAD窗口函数扫描全表。
2.2 行情表(market_data)
行情表存储每个交易日每只证券的价格与成交量数据。对于组合归因,最核心的字段是复权收盘价(adj_close),用于计算收益率;总市值(market_cap)用于计算权重。
| 字段名 | 数据类型 | 约束 | 说明 |
|---|---|---|---|
| trade_date | DATE | 复合主键 | 交易日 |
| security_code | VARCHAR(20) | 复合主键 | 证券代码,如 '000300.SH' |
| open_price | DECIMAL(18,6) | NULLABLE | 开盘价 |
| high_price | DECIMAL(18,6) | NULLABLE | 最高价 |
| low_price | DECIMAL(18,6) | NULLABLE | 最低价 |
| close_price | DECIMAL(18,6) | NULLABLE | 收盘价(未复权) |
| adj_close | DECIMAL(18,6) | NOT NULL | 复权收盘价(后复权) |
| volume | BIGINT | NULLABLE | 成交量(股数) |
| turnover | DECIMAL(18,2) | NULLABLE | 成交金额(元) |
| market_cap | DECIMAL(18,2) | NULLABLE | 总市值(元),用于权重计算 |
-- 行情表建表SQL
CREATE TABLE market_data (
trade_date DATE NOT NULL,
security_code VARCHAR(20) NOT NULL,
open_price DECIMAL(18,6) NULL,
high_price DECIMAL(18,6) NULL,
low_price DECIMAL(18,6) NULL,
close_price DECIMAL(18,6) NULL,
adj_close DECIMAL(18,6) NOT NULL,
volume BIGINT NULL,
turnover DECIMAL(18,2) NULL,
market_cap DECIMAL(18,2) NULL,
PRIMARY KEY (trade_date, security_code),
INDEX idx_security (security_code, trade_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='日频行情表';
避坑提示:不要直接使用close_price计算收益率,必须使用adj_close。未复权价格在除权除息日会出现跳空,导致收益率计算错误。后复权价格保留了历史真实收益率,是归因分析的标准选择。
2.3 持仓表(portfolio_holdings)
持仓表记录每个组合在每个交易日的持仓明细。这是归因分析的核心输入表,需要精确记录每只证券的持有数量、成本与市值。对于多组合场景,增加portfolio_id字段区分不同组合。
| 字段名 | 数据类型 | 约束 | 说明 |
|---|---|---|---|
| trade_date | DATE | 复合主键 | 交易日 |
| portfolio_id | VARCHAR(20) | 复合主键 | 组合编号,如 'PF_001' |
| security_code | VARCHAR(20) | 复合主键 | 证券代码 |
| quantity | DECIMAL(18,2) | NOT NULL | 持仓数量(股/份) |
| cost_price | DECIMAL(18,6) | NULLABLE | 单位成本(元),用于计算已实现损益 |
| market_value | DECIMAL(18,2) | NOT NULL | 持仓市值(元)= quantity × adj_close |
| weight | DECIMAL(10,6) | NULLABLE | 组合权重(小数形式),由ETL计算填充 |
| asset_type | VARCHAR(10) | NULLABLE | 资产类型:'stock','bond','fund'等 |
-- 持仓表建表SQL
CREATE TABLE portfolio_holdings (
trade_date DATE NOT NULL,
portfolio_id VARCHAR(20) NOT NULL,
security_code VARCHAR(20) NOT NULL,
quantity DECIMAL(18,2) NOT NULL,
cost_price DECIMAL(18,6) NULL,
market_value DECIMAL(18,2) NOT NULL,
weight DECIMAL(10,6) NULL,
asset_type VARCHAR(10) NULL,
PRIMARY KEY (trade_date, portfolio_id, security_code),
INDEX idx_portfolio_date (portfolio_id, trade_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='组合持仓明细表';
权重计算逻辑:weight = market_value / SUM(market_value) OVER (PARTITION BY trade_date, portfolio_id)。建议在ETL阶段预计算权重字段,避免归因查询时重复计算。
2.4 基准表(benchmark_holdings)
基准表存储每个基准指数在每个交易日的成分股权重。基准表的结构与持仓表高度相似,但不需要成本字段。基准权重通常由指数编制公司发布,按日更新。
| 字段名 | 数据类型 | 约束 | 说明 |
|---|---|---|---|
| trade_date | DATE | 复合主键 | 交易日 |
| benchmark_id | VARCHAR(20) | 复合主键 | 基准编号,如 'HS300' |
| security_code | VARCHAR(20) | 复合主键 | 成分证券代码 |
| weight | DECIMAL(10,6) | NOT NULL | 基准权重(小数形式),总和为1 |
| market_cap | DECIMAL(18,2) | NULLABLE | 成分证券总市值(元) |
-- 基准表建表SQL
CREATE TABLE benchmark_holdings (
trade_date DATE NOT NULL,
benchmark_id VARCHAR(20) NOT NULL,
security_code VARCHAR(20) NOT NULL,
weight DECIMAL(10,6) NOT NULL,
market_cap DECIMAL(18,2) NULL,
PRIMARY KEY (trade_date, benchmark_id, security_code),
INDEX idx_benchmark_date (benchmark_id, trade_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='基准成分股权重表';
数据对齐技巧:在归因分析中,持仓表与基准表必须使用相同的交易日历对齐。建议在ETL阶段将持仓表与基准表按trade_date做全外连接,确保组合与基准的日期窗口完全一致,避免因交易日不同导致归因偏差。
2.5 四表关系与数据流架构
四张表通过交易日历表形成时间轴,行情表提供价格数据,持仓表与基准表提供权重数据。下图展示了四表之间的数据流与主键关联关系。
2.6 表间关联与SQL查询示例
在实际归因分析中,四张表通常通过以下方式关联:
- 持仓表 JOIN 行情表:通过
trade_date和security_code获取持仓证券的复权收盘价,计算日收益率。 - 持仓表 JOIN 基准表:通过
trade_date和security_code获取基准权重,计算主动权重(组合权重 - 基准权重)。 - 交易日历表过滤:使用
is_trading_day = 1过滤非交易日,确保数据连续性。
-- 典型查询:计算组合与基准的日收益率差异(用于Brinson归因)
SELECT
h.trade_date,
h.security_code,
h.weight AS portfolio_weight,
b.weight AS benchmark_weight,
(h.weight - b.weight) AS active_weight,
m.adj_close,
(m.adj_close / LAG(m.adj_close) OVER (
PARTITION BY h.security_code
ORDER BY h.trade_date
) - 1) AS security_return
FROM portfolio_holdings h
JOIN benchmark_holdings b
ON h.trade_date = b.trade_date
AND h.security_code = b.security_code
JOIN market_data m
ON h.trade_date = m.trade_date
AND h.security_code = m.security_code
JOIN trade_calendar c
ON h.trade_date = c.calendar_date
WHERE c.is_trading_day = 1
AND h.portfolio_id = 'PF_001'
AND h.trade_date BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY h.trade_date, h.security_code;
性能优化建议:对于千万级数据量的归因场景,建议在 trade_date 字段上建立分区表(按月或按年分区),并在 security_code 上建立二级索引。上述查询中的LAG窗口函数在分区表上执行效率更高。
2.7 数据质量检查清单
在开始归因分析前,必须对四张表执行以下数据质量检查:
| 检查项 | 涉及表 | 检查SQL | 预期结果 |
|---|---|---|---|
| 交易日完整性 | trade_calendar | 检查连续日期是否存在缺失 | 无日期空洞 |
| 价格非空 | market_data | adj_close IS NOT NULL | 0条记录 |
| 权重总和 | portfolio_holdings | SUM(weight) = 1(每个组合每日) | 偏差 < 0.0001 |
| 基准权重总和 | benchmark_holdings | SUM(weight) = 1(每个基准每日) | 偏差 < 0.0001 |
| 持仓与行情对齐 | holdings + market_data | LEFT JOIN 检查缺失 | 缺失率 < 1% |
常见数据问题:基准权重总和不为1通常是因为指数调整成分股时未同步更新;持仓权重总和不为1可能是因为现金头寸未纳入权重计算。建议在ETL阶段增加权重归一化校验,确保数据进入归因引擎前已清洗完毕。