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 四表关系与数据流架构

四张表通过交易日历表形成时间轴,行情表提供价格数据,持仓表与基准表提供权重数据。下图展示了四表之间的数据流与主键关联关系。

金融数据四表关系与数据流架构 交易日历表 trade_calendar 行情表 market_data 持仓表 portfolio_holdings 基准表 benchmark_holdings 组合风险绩效归因计算 Brinson归因 / 风险分解 / 业绩归因 对齐交易日 时间轴 对齐交易日 价格数据 组合权重 基准权重 核心关联:持仓表与基准表通过 trade_date + security_code 关联行情表获取价格;交易日历表确保所有日期对齐

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阶段增加权重归一化校验,确保数据进入归因引擎前已清洗完毕。