19  高频行情特征工程综合案例

19.1 高频特征不是把日频公式搬到分钟表

日频研究通常把一行视为一个完整交易日;高频研究的一行却可能是一分钟 K 线、一次成交更新或一次盘口更新。数据的时间戳、交易阶段、累计成交量和报价单位若有一个解释错误,最后得到的“波动率”仍可能是一串看似合理的数字。因此,高频特征工程首先是市场制度与数据合同的工程,其次才是滚动计算。

本章的公司级实证只使用江苏连云港的恒瑞医药 600276.XSHG 分钟行情。当前本地 Tick 归档仅含福建与北京公司,没有符合全书地域口径的长三角证券,因此本章只执行文件级资格审计,不解码这些公司的逐笔记录,也不报告其盘口特征或经验结论。等将来归档出现合格证券后,才能在同一地域合同下追加 Tick 实证。

完成本章后,读者应能够:

  • 检查分钟 HDF5 的原生 keys、复合 dtype 与日期覆盖,并对 Tick 归档执行证券地域资格审计;

  • 按 2021 年 A 股制度区分上午连续竞价、下午连续竞价与 14:5715:00 收盘集合竞价,分别审计完整常规时段和连续竞价,并明确午间价格跳跃与收盘集合竞价是否进入特征;

  • 按证券与交易日重置日内收益,使隔夜跳跃不进入 realized variance;

  • 严格区分 realized variance 与 realized volatility,并用手工平方和复算;

  • 提取动量、波动率、量价相关与 Amihud 型非流动性,并说明未来 Tick 累计字段的时序安全差分合同;

  • 只用训练期拟合标准化和可选 PCA,写出 Parquet 后核验 schema、行数与缺失率。

本章以如下映射逐项评价正式目标;每项都要求断言、手工复算或边界解释,而不只要求复述正文。

表 19.1: 第十九章目标—活动—核心题映射
正式目标 正文活动 核心题与答案证据
原生 HDF5 与地域合同 小节 19.2 练习 19.1 的分钟 keys、字段与 Tick 资格断言
常规时段、连续竞价、收盘集合竞价与午休边界 小节 19.3 练习 19.1 与 19.4 的阶段计数和常规会话内聚合
日内收益重置 小节 19.4.1小节 19.4.2 练习 19.2 与 19.4 的开盘锚点区间数核验
RV 与 RVol 小节 19.4.3 练习 19.2 的手工平方和与量纲断言
分钟特征与未来 Tick 基线规则 小节 19.5小节 19.6 练习 19.3 与 19.4 的累计差分机制、量额和采样审计
训练期标准化、可选 PCA 与 Parquet 小节 19.7小节 19.8 练习 19.5 的拟合样本数审计与练习 19.4 的往返审计

表 19.1 汇总本节的计算或审计结果,解释时应遵循正文给出的口径与限制。

全章路线如下:小节 19.2 从原生文件和地域合同开始,小节 19.3 落实 A 股分钟时段,小节 19.4 定义收益与波动,小节 19.5 提取合规分钟特征,小节 19.6 固化未来 Tick 的准入与累计基线规则,小节 19.7 防止变换泄漏,小节 19.8 审计交付,小节 19.9小节 19.11 分别给出边界和分层检验。

19.2 环境、地域与原生文件合同

19.2.1 跨平台路径与依赖

代码块 列表 19.1 建立统一路径。h5py 只读检查分钟文件的原生 key 与 dtype;Tick 归档在通过地域资格合同以前只枚举文件名,不打开逐笔节点。

列表 19.1: 第十九章环境、时区与本地数据路径
import platform  # 识别 Windows 与 Linux 的本地数据位置。
import tempfile  # 将教学输出写入系统临时目录而不污染原始数据。
from pathlib import Path  # 安全构造 HDF5 与 Parquet 路径。
import h5py  # 按原生 HDF5 结构读取合规分钟数据。
import matplotlib.pyplot as plt  # 绘制真实高频特征的诊断图。
import numpy as np  # 计算对数收益、平方和与盘口比率。
import pandas as pd  # 组织证券—交易日特征矩阵。
from sklearn.decomposition import PCA  # 在训练期标准化结果上执行可选降维。
from sklearn.preprocessing import StandardScaler  # 只用训练期估计均值和标准差。

DATA_ROOT = Path('C:/qiufei/data' if platform.system() == 'Windows' else '/home/ubuntu/r2_data_mount/data')  # 按项目规则选择真实数据根目录。
STOCK_ROOT = DATA_ROOT / 'stock'  # 限定本章全部输入来自本地股票目录。
LOCAL_TIME_ZONE = 'Asia/Shanghai'  # 将无时区原始时间解释为中国标准时间。
plt.rcParams['font.family'] = ['Source Han Serif SC']  # 采用项目指定的中文出版字体。
plt.rcParams['axes.unicode_minus'] = False  # 正确显示负盘口不平衡和收益率。

19.2.2 用基本信息核实公司地域与 Tick 资格

表 19.2 从本地基本信息表读取公司注册省份与交易时段,并把 Tick 文件名还原为证券代码后逐一匹配。当前归档必须明确得到“两个文件、零个长三角合格文件”;若将来这个断言失败,含义是出现了可供后续开发的合格数据,而不是允许悄悄沿用旧的空集结论。

geography_columns = ['order_book_id', 'symbol', 'province', 'office_address', 'industry_name', 'trading_hours']  # 读取地域、行业与交易制度字段。
stock_geography = pd.read_hdf(STOCK_ROOT / 'stock_basic_data.h5', columns=geography_columns)  # 从本地公司基本信息表读取证据。
yrd_provinces = ['上海市', '江苏省', '浙江省', '安徽省']  # 沿用全书长三角地域口径。
main_case = stock_geography.loc[stock_geography['order_book_id'].eq('600276.XSHG')].copy()  # 提取恒瑞医药分钟主案例。
main_case['is_yrd'] = main_case['province'].isin(yrd_provinces)  # 按注册省份生成主案例地域标记。
assert len(main_case) == 1 and bool(main_case['is_yrd'].iloc[0])  # 恒瑞医药必须被唯一识别为江苏公司。
tick_archive_paths = sorted((STOCK_ROOT / 'ticks').glob('*.h5'))  # 只枚举归档文件,不解码非合格逐笔记录。
tick_archive_codes = [file_path.stem for file_path in tick_archive_paths]  # 从文件名恢复证券代码。
tick_archive = pd.DataFrame({'order_book_id': tick_archive_codes, 'file_name': [file_path.name for file_path in tick_archive_paths]})  # 建立文件级血缘表。
tick_archive = tick_archive.merge(stock_geography[['order_book_id', 'symbol', 'province']], on='order_book_id', how='left', validate='one_to_one')  # 用本地基本信息补全真实地域。
tick_archive['is_mapped'] = tick_archive['province'].notna()  # 标记文件名是否能映射到公司主数据。
tick_archive['is_yrd_eligible'] = tick_archive['province'].isin(yrd_provinces)  # 执行全书统一的长三角资格判断。
eligible_yrd_tick = tick_archive.loc[tick_archive['is_yrd_eligible']].copy()  # 形成唯一允许进入公司级 Tick 实证的文件清单。
assert len(tick_archive) == 2 and tick_archive['is_mapped'].all()  # 核验当前快照的两个 Tick 文件均可追溯到公司主数据。
assert eligible_yrd_tick.empty  # 明确证明当前归档没有长三角合格 Tick 文件。
tick_eligibility_audit = pd.DataFrame({'archive_files': [len(tick_archive)], 'mapped_files': [int(tick_archive['is_mapped'].sum())], 'yrd_eligible_files': [len(eligible_yrd_tick)]})  # 汇总归档准入结果。
tick_archive.sort_values('order_book_id')  # 输出每个文件的公司、注册省份与资格证据。
表 19.2: 分钟主案例与 Tick 归档的地域资格审计
order_book_id file_name symbol province is_mapped is_yrd_eligible
0 600660.XSHG 600660.XSHG.h5 福耀玻璃 福建省 True False
1 601288.XSHG 601288.XSHG.h5 农业银行 北京市 True False

19.2.3 分钟 HDF5 的原生 schema 与索引

分钟文件不是 Pandas HDFStore。根目录下的 data 是结构化数组,字段包括 datetime、OHLC、成交量和成交额;index 保存每个交易日及其在 data 中的起始行。表 19.3 直接检查原生 keys、形状、复合 dtype 和压缩方式。

minbar_path = STOCK_ROOT / 'minbar' / '600276.XSHG.h5'  # 定位江苏恒瑞医药的真实分钟文件。
with h5py.File(minbar_path, mode='r') as minbar_h5:  # 以只读方式打开原生 HDF5。
    minbar_keys = sorted(minbar_h5.keys())  # 检查根节点而不假定 Pandas key。
    minbar_data_shape = minbar_h5['data'].shape  # 记录完整分钟数组的行数。
    minbar_index_shape = minbar_h5['index'].shape  # 记录交易日索引的长度。
    minbar_fields = list(minbar_h5['data'].dtype.names)  # 读取结构化数组的真实字段名。
    minbar_compression = minbar_h5['data'].compression  # 记录数据集使用的压缩方式。
minbar_schema_audit = pd.DataFrame({'root_keys': [', '.join(minbar_keys)], 'data_rows': [minbar_data_shape[0]], 'index_rows': [minbar_index_shape[0]], 'fields': [', '.join(minbar_fields)], 'compression': [minbar_compression]})  # 将原生元数据整理为审计表。
minbar_schema_audit  # 输出文件合同,供后续读取逻辑核对。
表 19.3: 恒瑞医药分钟 HDF5 的原生 keys 与 schema
root_keys data_rows index_rows fields compression
0 data, index 1227600 5115 datetime, open, high, low, close, volume, tota... gzip

代码块 列表 19.2 利用 index.line_no 定位 2021 年 11 月 1 日至 16 日的连续行片段,只读取 12 个真实交易日。函数输出的 datetime 是整数编码,稍后再按明确格式解析。

列表 19.2: 按原生交易日索引选择性读取恒瑞医药分钟数据
def read_minbar_date_range(file_path, start_date, end_date):  # 将原生日索引转换为连续行切片,避免读取全部历史。
    '''读取闭区间交易日内的结构化分钟记录并返回 DataFrame。'''  # 定义函数的输入日期与标准表输出。
    start_key = int(pd.Timestamp(start_date).strftime('%Y%m%d'))  # 将起始日转换为原生整数日期键。
    end_key = int(pd.Timestamp(end_date).strftime('%Y%m%d'))  # 将结束日转换为原生整数日期键。
    with h5py.File(file_path, mode='r') as minbar_file:  # 只读打开分钟 HDF5。
        day_index = minbar_file['index'][:]  # 读取仅五千余行的日期—起始行索引。
        selected_positions = np.flatnonzero((day_index['date'] >= start_key) & (day_index['date'] <= end_key))  # 定位闭区间内真实交易日。
        assert selected_positions.size > 0  # 若日期范围没有真实记录则阻断流程。
        start_row = int(day_index['line_no'][selected_positions[0]])  # 获取首个交易日的起始行。
        next_position = selected_positions[-1] + 1  # 定位结束日之后的首个交易日起点。
        stop_row = int(day_index['line_no'][next_position]) if next_position < len(day_index) else len(minbar_file['data'])  # 形成 Python 左闭右开切片。
        selected_records = minbar_file['data'][start_row:stop_row]  # 从压缩数据集只解压所需连续片段。
    return pd.DataFrame.from_records(selected_records)  # 将结构化数组转换为标准列式表。

minbar_raw = read_minbar_date_range(minbar_path, '2021-11-01', '2021-11-16')  # 读取固定的十二个真实交易日窗口。

19.3 A 股交易时段与时间戳规范化

19.3.1 分钟线:午休不是缺失值

本章样本属于 2021 年沪市股票。上海证券交易所发布的《2020 年市场资料》明确列出当时竞价交易时段:上午连续竞价为 09:3011:30,下午连续竞价为 13:0014:5714:5715:00 为收盘集合竞价;上交所 2018 年的收盘机制修订通知记录了这一边界的制度来源。规则来源只证明市场时段,不能替代本地分钟文件自身的时间戳语义核验。

本地一分钟线用区间结束时刻标记,无法从聚合柱中拆出恰好发生在 14:57:00 的边界成交;为保证 14:5715:00 从不被标作连续竞价,本章采用保守的结束戳审计:上午连续竞价戳为 09:3111:30 的 120 根,下午连续竞价戳为 13:0114:56 的 116 根,14:5715:00 四个结束戳统一归入收盘集合竞价边界桶。这个四根计数是分钟聚合数据的审计约定,不声称 14:57 结束柱内的整分钟都属于集合竞价;若需精确拆分边界,必须回到逐笔数据。完整常规交易日仍为 240 根,严格连续竞价戳为 236 根,集合竞价边界桶为 4 根。午间 11:3113:00 没有交易,不应重采样出零成交 K 线。代码块 表 19.4 将整数时间戳解析为 Asia/Shanghai 时区,并分别审计完整常规时段和三个市场阶段。

minbar = minbar_raw.copy()  # 保留原生读取结果以便追溯。
minbar['datetime'] = pd.to_datetime(minbar['datetime'].astype(str), format='%Y%m%d%H%M%S').dt.tz_localize(LOCAL_TIME_ZONE)  # 按中国标准时间解释整数分钟戳。
minbar['clock_time'] = minbar['datetime'].dt.strftime('%H:%M:%S')  # 生成便于制度过滤的本地时钟文本。
is_morning_continuous_minute = minbar['clock_time'].between('09:31:00', '11:30:00')  # 识别上午连续竞价的一分钟结束戳。
is_afternoon_continuous_minute = minbar['clock_time'].between('13:01:00', '14:56:00')  # 采用保守戳口径识别不触及14:57边界的116根下午连续竞价分钟线。
is_closing_call_minute = minbar['clock_time'].between('14:57:00', '15:00:00')  # 将14:57至15:00四个结束戳统一归入收盘集合竞价边界桶。
is_regular_minute = is_morning_continuous_minute | is_afternoon_continuous_minute | is_closing_call_minute  # 定义包含收盘集合竞价的完整常规时段。
outside_regular_rows = (~is_regular_minute).sum()  # 统计开盘集合竞价、午休或异常时段记录。
minbar = minbar.loc[is_regular_minute].copy()  # 只保留完整常规时段且不在午休插值。
minbar['market_phase'] = np.select([is_morning_continuous_minute.loc[minbar.index], is_afternoon_continuous_minute.loc[minbar.index], is_closing_call_minute.loc[minbar.index]], ['morning_continuous', 'afternoon_continuous', 'closing_call_auction'])  # 为每根分钟线赋予互斥制度阶段。
minbar['session'] = np.where(is_morning_continuous_minute.loc[minbar.index], 'morning_regular', 'afternoon_regular')  # 为常规OHLCV聚合保留两个不跨午休的120分钟会话。
minbar['trade_date'] = minbar['datetime'].dt.normalize()  # 生成带 Asia/Shanghai 时区的证券交易日。
minbar['order_book_id'] = '600276.XSHG'  # 将单文件证券身份加入标准面板键。
minbar_daily_counts = minbar.groupby('trade_date').agg(regular_minute_rows=('datetime', 'size'), continuous_auction_minute_rows=('market_phase', lambda phase: phase.ne('closing_call_auction').sum()), closing_call_minute_rows=('market_phase', lambda phase: phase.eq('closing_call_auction').sum()), first_bar=('clock_time', 'min'), last_bar=('clock_time', 'max'), zero_volume_rows=('volume', lambda series: series.eq(0).sum())).reset_index()  # 同时审计常规时段、连续竞价和收盘集合竞价覆盖。
assert minbar['clock_time'].between('11:31:00', '13:00:00').sum() == 0  # 证明午休期间没有被制造虚假分钟。
assert minbar['market_phase'].eq('closing_call_auction').equals(minbar['clock_time'].between('14:57:00', '15:00:00'))  # 阻止14:57至15:00结束戳被回标为连续竞价。
minbar_daily_counts.assign(outside_regular_rows_in_raw=outside_regular_rows)  # 输出三个阶段与原始常规时段外记录数。
表 19.4: 恒瑞医药完整常规时段、连续竞价与收盘集合竞价审计
trade_date regular_minute_rows continuous_auction_minute_rows closing_call_minute_rows first_bar last_bar zero_volume_rows outside_regular_rows_in_raw
0 2021-11-01 00:00:00+08:00 240 236 4 09:31:00 15:00:00 1 0
1 2021-11-02 00:00:00+08:00 240 236 4 09:31:00 15:00:00 1 0
2 2021-11-03 00:00:00+08:00 240 236 4 09:31:00 15:00:00 1 0
3 2021-11-04 00:00:00+08:00 240 236 4 09:31:00 15:00:00 1 0
4 2021-11-05 00:00:00+08:00 240 236 4 09:31:00 15:00:00 1 0
5 2021-11-08 00:00:00+08:00 240 236 4 09:31:00 15:00:00 1 0
6 2021-11-09 00:00:00+08:00 240 236 4 09:31:00 15:00:00 2 0
7 2021-11-10 00:00:00+08:00 240 236 4 09:31:00 15:00:00 1 0
8 2021-11-11 00:00:00+08:00 240 236 4 09:31:00 15:00:00 1 0
9 2021-11-12 00:00:00+08:00 240 236 4 09:31:00 15:00:00 1 0
10 2021-11-15 00:00:00+08:00 240 236 4 09:31:00 15:00:00 1 0
11 2021-11-16 00:00:00+08:00 240 236 4 09:31:00 15:00:00 1 0

本章的分钟主特征明确采用完整常规交易时段,所以包含收盘集合竞价;它不被命名为『连续竞价特征』。上午最后收盘价到下午首根五分钟柱收盘价的收益保留在同一交易日中,午休期间没有插值。若研究问题只允许严格连续竞价戳,应使用 market_phase != 'closing_call_auction' 的 236 根保守子样本并重新定义采样桶,不能只改图注。收盘集合竞价也可单独构造价格发现特征。

19.4 日内收益、采样与波动度量

19.4.1 隔夜收益必须显式排除

对证券 \(s\)、交易日 \(d\) 的第 \(i\) 个分钟区间,令 \(P_{s,d,0}=O_{s,d}\) 为该证券当天第一根一分钟线的开盘价,\(P_{s,d,i}\) 为第 \(i\) 根分钟线的收盘价。日内对数收益定义为

\[ r_{s,d,i}=\log P_{s,d,i}-\log P_{s,d,i-1},\qquad i=1,\ldots,n_{s,d}. \tag{19.1}\]

式 19.1 用当日真实开盘价锚定第一段收益,而不是连接前一交易日收盘。因此,完整常规交易日拥有 240 个日内区间收益,其中最后四个一分钟结束戳属于收盘集合竞价边界桶;隔夜跳跃明确排除在 realized variance 之外。表 19.5 用断言检查每个交易日第一段的分母恰为当日开盘价。

minbar = minbar.sort_values(['order_book_id', 'trade_date', 'datetime']).copy()  # 保证每个证券日内记录严格按时间排列。
intraday_groups = minbar.groupby(['order_book_id', 'trade_date'], sort=False)  # 同时以证券和交易日定义收益边界。
minbar['row_in_day'] = intraday_groups.cumcount()  # 标记每个交易日的日内行序号。
minbar['opening_anchor'] = intraday_groups['open'].transform('first')  # 为每一分钟绑定同一交易日的真实开盘价锚点。
minbar['previous_close_1m'] = intraday_groups['close'].shift(1)  # 普通区间以前一分钟收盘价作为收益起点。
is_first_minute = minbar['row_in_day'].eq(0)  # 识别每天不能使用前日收盘价的首分钟。
minbar.loc[is_first_minute, 'previous_close_1m'] = minbar.loc[is_first_minute, 'opening_anchor']  # 用当日开盘价替换首分钟缺失分母。
minbar['log_return_1m'] = np.log(minbar['close'] / minbar['previous_close_1m'])  # 计算覆盖开盘至收盘且不含隔夜的一分钟收益。
boundary_rows = minbar.loc[is_first_minute, ['order_book_id', 'trade_date', 'opening_anchor', 'previous_close_1m', 'log_return_1m']]  # 提取每天第一段供审计。
assert np.allclose(boundary_rows['previous_close_1m'], boundary_rows['opening_anchor'], rtol=0, atol=0)  # 证明第一段只使用当天开盘锚点。
assert boundary_rows['log_return_1m'].notna().all()  # 证明开盘至首分钟收盘的价格变化已进入日内收益。
boundary_rows  # 输出每日开盘边界,便于核对隔夜排除口径。
表 19.5: 按证券与交易日重置的一分钟收益边界
order_book_id trade_date opening_anchor previous_close_1m log_return_1m
0 600276.XSHG 2021-11-01 00:00:00+08:00 49.02 49.02 -0.001633
240 600276.XSHG 2021-11-02 00:00:00+08:00 49.51 49.51 0.001816
480 600276.XSHG 2021-11-03 00:00:00+08:00 48.51 48.51 0.002059
720 600276.XSHG 2021-11-04 00:00:00+08:00 49.25 49.25 -0.003865
960 600276.XSHG 2021-11-05 00:00:00+08:00 51.25 51.25 0.003895
1200 600276.XSHG 2021-11-08 00:00:00+08:00 52.00 52.00 -0.002696
1440 600276.XSHG 2021-11-09 00:00:00+08:00 49.68 49.68 0.002212
1680 600276.XSHG 2021-11-10 00:00:00+08:00 49.81 49.81 0.003207
1920 600276.XSHG 2021-11-11 00:00:00+08:00 53.50 53.50 -0.010522
2160 600276.XSHG 2021-11-12 00:00:00+08:00 53.05 53.05 -0.000943
2400 600276.XSHG 2021-11-15 00:00:00+08:00 52.57 52.57 0.002470
2640 600276.XSHG 2021-11-16 00:00:00+08:00 53.40 53.40 0.007091

19.4.2 五分钟采样与午间跳跃

直接用一分钟价格会更容易受到买卖价差反弹和离散报价噪声影响;过度稀疏的采样又会丢失真实价格变化。这里不声称五分钟是普遍最优频率,只把它作为可复核基准。代码块 列表 19.3 以上午 09:30 和下午 13:00 为两个常规会话原点,构造 (09:30, 09:35](09:35, 09:40] 这类左开右闭区间;缺失一分钟不会让后续记录挤入错误区间。每个区间的开、高、低、收分别取 firstmaxminlast,成交量和成交额取 sum。分组不会跨越午休,但下午最后一根 (14:55, 15:00] 柱同时覆盖连续竞价尾段与收盘集合竞价,因此它属于完整常规时段特征,绝不标为连续竞价柱。收益在同一交易日内连续链接,下午第一根五分钟柱相对上午最后收盘的变化仍被保留。

列表 19.3: 按交易会话聚合任意整数分钟 OHLCV 并用当日开盘价锚定收益
def build_session_bars(minute_bars, interval_minutes):  # 统一构造一、五、十五分钟柱,避免不同频率采用不同口径。
    '''在上午和下午会话内聚合 OHLCV,并返回开盘锚定的日内收益。'''  # 明确函数输出的数据合同与收益边界。
    prepared_minutes = minute_bars.copy()  # 保留底层一分钟表供成交量和成交额守恒审计。
    session_keys = ['order_book_id', 'trade_date', 'session']  # 将午休设为不可跨越的常规会话边界。
    clock_minutes = prepared_minutes['datetime'].dt.hour * 60 + prepared_minutes['datetime'].dt.minute  # 将一分钟结束戳转换为本地日内分钟数。
    session_open_minutes = np.where(prepared_minutes['session'].eq('morning_regular'), 9 * 60 + 30, 13 * 60)  # 分别设置上午和下午常规会话原点。
    prepared_minutes['elapsed_minutes'] = clock_minutes - session_open_minutes  # 计算相对本会话开盘的真实时间距离。
    assert prepared_minutes['elapsed_minutes'].between(1, 120).all()  # 阻止开盘集合竞价、午休或收盘后记录进入常规区间。
    prepared_minutes['interval_in_session'] = (prepared_minutes['elapsed_minutes'] - 1) // interval_minutes  # 按左开右闭时钟区间分配记录而不依赖行是否缺失。
    bar_keys = session_keys + ['interval_in_session']  # 将证券、交易日、会话和区间号组成唯一柱键。
    interval_bars = prepared_minutes.groupby(bar_keys, sort=False).agg(datetime=('datetime', 'last'), open=('open', 'first'), high=('high', 'max'), low=('low', 'min'), close=('close', 'last'), volume=('volume', 'sum'), total_turnover=('total_turnover', 'sum'), source_minute_count=('datetime', 'size')).reset_index()  # 按标准 OHLCV 规则聚合真实分钟记录。
    interval_bars = interval_bars.sort_values(['order_book_id', 'trade_date', 'datetime']).reset_index(drop=True)  # 恢复完整日内顺序以链接相邻收盘价。
    day_bar_groups = interval_bars.groupby(['order_book_id', 'trade_date'], sort=False)  # 让收益只在同一证券交易日内连接。
    interval_bars['row_in_day'] = day_bar_groups.cumcount()  # 标记每天从零开始的目标柱序号。
    interval_bars['opening_anchor'] = day_bar_groups['open'].transform('first')  # 取得底层第一根一分钟线所含的当日开盘价。
    interval_bars['previous_close'] = day_bar_groups['close'].shift(1)  # 普通目标柱以前一目标柱收盘价作为收益起点。
    is_first_interval = interval_bars['row_in_day'].eq(0)  # 识别需要开盘锚点而非前日收盘的首柱。
    interval_bars.loc[is_first_interval, 'previous_close'] = interval_bars.loc[is_first_interval, 'opening_anchor']  # 补入当日开盘至首个端点的完整区间。
    return_column = f'log_return_{interval_minutes}m'  # 为不同采样频率生成一致且可辨识的收益字段。
    interval_bars[return_column] = np.log(interval_bars['close'] / interval_bars['previous_close'])  # 保留午间跳跃并排除隔夜跳跃。
    return interval_bars  # 返回 OHLCV、来源分钟数、开盘锚点和区间收益。

five_minute = build_session_bars(minbar, 5)  # 从真实一分钟底表构造 48 根五分钟 OHLCV。

表 19.6 对完整交易日断言 48 根五分钟柱和 48 个收益,并逐日核验成交量、成交额与一分钟底表守恒。每个会话的来源分钟数也必须恰为 5,从而不会把不完整尾段伪装成完整五分钟柱。

minute_session_counts = minbar.groupby(['order_book_id', 'trade_date', 'session']).size().unstack(fill_value=0)  # 汇总每个证券日的上午和下午常规分钟覆盖。
minute_session_counts['is_complete_day'] = minute_session_counts['morning_regular'].eq(120) & minute_session_counts['afternoon_regular'].eq(120)  # 依据两个120分钟常规会话识别完整日。
five_minute_counts = five_minute.groupby(['order_book_id', 'trade_date']).agg(five_minute_bars=('datetime', 'size'), five_minute_returns=('log_return_5m', 'count'), source_minutes=('source_minute_count', 'sum')).copy()  # 统计真实五分钟柱与有效收益数。
five_minute_audit = minute_session_counts.join(five_minute_counts, how='left')  # 对齐一分钟完整性与五分钟聚合结果。
complete_five_minute_audit = five_minute_audit.loc[five_minute_audit['is_complete_day']]  # 只对制度上完整的交易日执行严格数量断言。
assert complete_five_minute_audit['five_minute_bars'].eq(48).all()  # 完整日必须恰有上午 24 根和下午 24 根五分钟柱。
assert complete_five_minute_audit['five_minute_returns'].eq(48).all()  # 开盘锚点使完整日拥有 48 个而非 47 个区间收益。
assert five_minute['source_minute_count'].eq(5).all()  # 当前样本每根五分钟柱都必须由五条真实一分钟记录组成。
minute_flow_totals = minbar.groupby(['order_book_id', 'trade_date'])[['volume', 'total_turnover']].sum().rename(columns={'volume': 'volume_1m_sum', 'total_turnover': 'turnover_1m_sum'})  # 计算一分钟底表逐日量额基准。
five_minute_flow_totals = five_minute.groupby(['order_book_id', 'trade_date'])[['volume', 'total_turnover']].sum().rename(columns={'volume': 'volume_5m_sum', 'total_turnover': 'turnover_5m_sum'})  # 计算真实五分钟柱逐日量额总和。
flow_conservation_audit = minute_flow_totals.join(five_minute_flow_totals, how='inner')  # 将两个频率的同日量额并列比较。
assert np.allclose(flow_conservation_audit['volume_1m_sum'], flow_conservation_audit['volume_5m_sum'], rtol=0, atol=0)  # 成交量聚合必须无损守恒。
assert np.allclose(flow_conservation_audit['turnover_1m_sum'], flow_conservation_audit['turnover_5m_sum'], rtol=1e-12, atol=1e-6)  # 成交额仅容许浮点求和次序造成的微小误差。
five_minute_audit.join(flow_conservation_audit)  # 输出完整日、收益区间与量额守恒证据。
表 19.6: 五分钟区间数、开盘锚点与量额守恒审计
afternoon_regular morning_regular is_complete_day five_minute_bars five_minute_returns source_minutes volume_1m_sum turnover_1m_sum volume_5m_sum turnover_5m_sum
order_book_id trade_date
600276.XSHG 2021-11-01 00:00:00+08:00 120 120 True 48 48 240 48770163.0 2.405114e+09 48770163.0 2.405114e+09
2021-11-02 00:00:00+08:00 120 120 True 48 48 240 48022525.0 2.360199e+09 48022525.0 2.360199e+09
2021-11-03 00:00:00+08:00 120 120 True 48 48 240 41553719.0 2.050268e+09 41553719.0 2.050268e+09
2021-11-04 00:00:00+08:00 120 120 True 48 48 240 82335021.0 4.189626e+09 82335021.0 4.189626e+09
2021-11-05 00:00:00+08:00 120 120 True 48 48 240 92066228.0 4.838454e+09 92066228.0 4.838454e+09
2021-11-08 00:00:00+08:00 120 120 True 48 48 240 82690286.0 4.150967e+09 82690286.0 4.150967e+09
2021-11-09 00:00:00+08:00 120 120 True 48 48 240 37359483.0 1.865246e+09 37359483.0 1.865246e+09
2021-11-10 00:00:00+08:00 120 120 True 48 48 240 137161202.0 7.216854e+09 137161202.0 7.216854e+09
2021-11-11 00:00:00+08:00 120 120 True 48 48 240 77586965.0 4.111710e+09 77586965.0 4.111710e+09
2021-11-12 00:00:00+08:00 120 120 True 48 48 240 44342847.0 2.336051e+09 44342847.0 2.336051e+09
2021-11-15 00:00:00+08:00 120 120 True 48 48 240 59007205.0 3.142646e+09 59007205.0 3.142646e+09
2021-11-16 00:00:00+08:00 120 120 True 48 48 240 67674990.0 3.675390e+09 67674990.0 3.675390e+09

19.4.3 Realized variance 与 realized volatility

对一天内从开盘锚点开始的 \(N_d\) 个有效五分钟区间收益,已实现方差与已实现波动率分别为

\[ \mathrm{RV}_{s,d}^{(5m)}=\sum_{i=1}^{N_d}r_{s,d,i}^2, \tag{19.2}\]

\[ \mathrm{RVol}_{s,d}^{(5m)}=\sqrt{\mathrm{RV}_{s,d}^{(5m)}}. \tag{19.3}\]

式 19.2 定义区间收益平方和,式 式 19.3 再对其开平方。该口径属于 Andersen、Bollerslev、Diebold 与 Labys(2003)所系统建立的高频收益平方和度量框架;本章只采用其中的日度测量定义,不据此复现其预测模型。前者与收益率平方同量纲,后者与收益率同量纲。两者都没有在本章中年化;若需要年化日内波动率,可在明确平稳性假设后将 RVol 乘以 \(\sqrt{252}\),但不能把平方和本身称为“波动率”。

表 19.7 同时计算一分和五分钟 RV、RVol 与有效区间数,并对首个交易日用 NumPy 手工平方和复算。完整常规交易日的一分钟和五分钟区间数应分别为 240 和 48,因此两个频率都覆盖从当日开盘到包含收盘集合竞价的收盘端点。

minbar['squared_return_1m'] = minbar['log_return_1m'].pow(2)  # 为一分钟 realized variance 准备逐项平方。
five_minute['squared_return_5m'] = five_minute['log_return_5m'].pow(2)  # 为五分钟 realized variance 准备逐项平方。
one_minute_rv = minbar.groupby(['order_book_id', 'trade_date'])['squared_return_1m'].sum(min_count=1).rename('realized_variance_1m')  # 汇总一分钟收益平方和。
five_minute_rv = five_minute.groupby(['order_book_id', 'trade_date'])['squared_return_5m'].sum(min_count=1).rename('realized_variance_5m')  # 汇总五分钟收益平方和。
one_minute_return_count = minbar.groupby(['order_book_id', 'trade_date'])['log_return_1m'].count().rename('one_minute_return_count')  # 统计开盘锚定后的一分钟有效区间数。
five_minute_return_count = five_minute.groupby(['order_book_id', 'trade_date'])['log_return_5m'].count().rename('five_minute_return_count')  # 统计开盘锚定后的五分钟有效区间数。
rv_comparison = pd.concat([one_minute_rv, five_minute_rv, one_minute_return_count, five_minute_return_count], axis=1).reset_index()  # 对齐两个真实采样频率及其覆盖区间。
rv_comparison['realized_volatility_1m'] = np.sqrt(rv_comparison['realized_variance_1m'])  # 将一分钟平方和转换为同量纲波动率。
rv_comparison['realized_volatility_5m'] = np.sqrt(rv_comparison['realized_variance_5m'])  # 严格按 @eq-ch19-realized-volatility 开平方。
manual_trade_date = rv_comparison['trade_date'].iloc[0]  # 选择首个真实交易日作手工核验。
manual_returns = five_minute.loc[five_minute['trade_date'].eq(manual_trade_date), 'log_return_5m'].dropna().to_numpy()  # 提取该日全部有效五分钟收益。
manual_variance = np.sum(np.square(manual_returns))  # 不依赖 groupby 手工计算平方和。
reported_variance = rv_comparison.loc[rv_comparison['trade_date'].eq(manual_trade_date), 'realized_variance_5m'].iloc[0]  # 取得聚合流程的同日结果。
assert np.isclose(manual_variance, reported_variance, rtol=1e-12, atol=1e-15)  # 用高精度断言核验 RV 实现。
assert complete_five_minute_audit['source_minutes'].eq(240).all()  # 证明完整日的五分钟 RV 覆盖全部 240 条底层记录。
rv_comparison.assign(manual_variance_first_day=manual_variance, sampling_ratio_1m_to_5m=rv_comparison['realized_variance_1m'] / rv_comparison['realized_variance_5m'])  # 输出频率敏感性与手工复算证据。
表 19.7: 已实现方差、波动率与手工平方和核验
order_book_id trade_date realized_variance_1m realized_variance_5m one_minute_return_count five_minute_return_count realized_volatility_1m realized_volatility_5m manual_variance_first_day sampling_ratio_1m_to_5m
0 600276.XSHG 2021-11-01 00:00:00+08:00 0.000438 0.000268 240 48 0.020933 0.016357 0.000268 1.637854
1 600276.XSHG 2021-11-02 00:00:00+08:00 0.000471 0.000367 240 48 0.021695 0.019154 0.000268 1.282874
2 600276.XSHG 2021-11-03 00:00:00+08:00 0.000426 0.000414 240 48 0.020640 0.020359 0.000268 1.027842
3 600276.XSHG 2021-11-04 00:00:00+08:00 0.000682 0.000578 240 48 0.026115 0.024040 0.000268 1.180085
4 600276.XSHG 2021-11-05 00:00:00+08:00 0.000649 0.000772 240 48 0.025469 0.027789 0.000268 0.839987
5 600276.XSHG 2021-11-08 00:00:00+08:00 0.000652 0.000919 240 48 0.025540 0.030309 0.000268 0.710047
6 600276.XSHG 2021-11-09 00:00:00+08:00 0.000280 0.000286 240 48 0.016743 0.016921 0.000268 0.979142
7 600276.XSHG 2021-11-10 00:00:00+08:00 0.001861 0.003176 240 48 0.043134 0.056355 0.000268 0.585836
8 600276.XSHG 2021-11-11 00:00:00+08:00 0.000635 0.000678 240 48 0.025199 0.026038 0.000268 0.936569
9 600276.XSHG 2021-11-12 00:00:00+08:00 0.000667 0.000502 240 48 0.025824 0.022398 0.000268 1.329301
10 600276.XSHG 2021-11-15 00:00:00+08:00 0.000512 0.000378 240 48 0.022626 0.019445 0.000268 1.354009
11 600276.XSHG 2021-11-16 00:00:00+08:00 0.000599 0.000434 240 48 0.024469 0.020827 0.000268 1.380273

一分钟 RV 高于五分钟 RV 可能与微观结构噪声一致,但单个短样本不能证明噪声是唯一原因;跳跃、流动性变化和不同端点也会影响差异。专业研究通常绘制 volatility signature plot,在多个采样频率上比较稳定区间。

19.5 分钟级特征:动量、量价关系与流动性

19.5.1 特征定义

代码块 列表 19.4 准备逐条分量,列表 19.5 将其聚合为日度矩阵。本章构造六类互补特征:

  • intraday_momentum:当日第一根一分钟线开盘价到收盘端点的对数价格变化,完整覆盖日内持有区间且不含隔夜;

  • realized_variance_5mrealized_volatility_5m:分别衡量平方变差和同量纲波动;

  • return_volume_correlation:五分钟收益与 \(\log(1+Volume)\) 的日内相关;

  • absolute_return_volume_correlation:绝对收益与成交量的相关,用于识别剧烈价格变化是否伴随交易活跃;

  • amihud_illiquidity_scaled\(10^8\times |r|/Turnover\) 的日内均值。这是 Amihud(2002)提出的日度绝对收益—成交额比率在五分钟区间上的“Amihud 型”改写,而非对原论文日频横截面指标的原样复现;缩放只为可读性,不改变排序。

  • zero_volume_share:整根五分钟柱成交量为零的比例,与 Amihud 型指标的缺失分母分开报告。

相关系数是同日描述统计,不表示成交量导致收益。Amihud 型指标在五分钟成交额为零时没有定义,因此零成交柱单独报告,不以零替代无穷值。

列表 19.4: 准备真实五分钟量价与流动性分量
five_minute['log_volume'] = np.log1p(five_minute['volume'])  # 对完整五分钟成交量取对数以缓和右偏并保留零成交柱。
five_minute['absolute_return_5m'] = five_minute['log_return_5m'].abs()  # 衡量方向无关的价格变化强度。
positive_turnover = five_minute['total_turnover'].where(five_minute['total_turnover'] > 0)  # 将真实五分钟成交额为零的柱保留为缺失分母。
five_minute['amihud_component_scaled'] = five_minute['absolute_return_5m'] / positive_turnover * 1e8  # 用完整五分钟成交额计算可读尺度的非流动性分量。
five_minute['is_zero_volume'] = five_minute['volume'].eq(0)  # 显式记录整根五分钟柱均没有成交的状态。
列表 19.5: 聚合恒瑞医药的日度分钟特征矩阵
daily_minute_features = five_minute.groupby(['order_book_id', 'trade_date']).agg(opening_price=('opening_anchor', 'first'), closing_price=('close', 'last'), realized_variance_5m=('squared_return_5m', 'sum'), realized_volatility_5m=('squared_return_5m', lambda series: np.sqrt(series.sum())), total_volume=('volume', 'sum'), total_turnover=('total_turnover', 'sum'), amihud_illiquidity_scaled=('amihud_component_scaled', 'mean'), zero_volume_share=('is_zero_volume', 'mean'), five_minute_return_count=('log_return_5m', 'count')).reset_index()  # 用真实五分钟 OHLCV 汇总价格、波动与流动性。
daily_minute_features['intraday_momentum'] = np.log(daily_minute_features['closing_price'] / daily_minute_features['opening_price'])  # 计算当日真实开盘到收盘且不含隔夜的动量。
return_volume_correlation = five_minute.groupby(['order_book_id', 'trade_date']).apply(lambda frame: frame['log_return_5m'].corr(frame['log_volume']), include_groups=False).rename('return_volume_correlation')  # 计算有方向收益与成交量相关。
absolute_volume_correlation = five_minute.groupby(['order_book_id', 'trade_date']).apply(lambda frame: frame['absolute_return_5m'].corr(frame['log_volume']), include_groups=False).rename('absolute_return_volume_correlation')  # 计算价格变化强度与成交量相关。
daily_minute_features = daily_minute_features.merge(return_volume_correlation.reset_index(), on=['order_book_id', 'trade_date'], validate='one_to_one')  # 关联逐日有方向量价相关。
daily_minute_features = daily_minute_features.merge(absolute_volume_correlation.reset_index(), on=['order_book_id', 'trade_date'], validate='one_to_one')  # 关联逐日绝对量价相关。
daily_flow_validation = daily_minute_features[['order_book_id', 'trade_date', 'total_volume', 'total_turnover']].merge(flow_conservation_audit.reset_index(), on=['order_book_id', 'trade_date'], validate='one_to_one')  # 按显式主键关联日度特征与一分钟量额基准。
assert np.allclose(daily_flow_validation['total_volume'], daily_flow_validation['volume_1m_sum'], rtol=0, atol=0)  # 防止下游特征矩阵退回末分钟成交量抽样口径。
assert np.allclose(daily_flow_validation['total_turnover'], daily_flow_validation['turnover_1m_sum'], rtol=1e-12, atol=1e-6)  # 防止下游特征矩阵退回末分钟成交额抽样口径。

19.5.2 真实日内路径与特征图

图 19.1 使用完整常规时段重新聚合后的真实五分钟柱收盘价,保留时区化横轴,午休在图上表现为没有观测的空档;下午最后一根柱含收盘集合竞价,不标为连续竞价。图 19.2 则使用开盘锚定后的 48 个常规时段区间收益,将重新计算的方差与波动率分面展示,避免把量纲不同的指标画在同一纵轴后误读。

first_trade_date = five_minute['trade_date'].min()  # 选择首个真实交易日作为日内结构示例。
first_day_path = five_minute.loc[five_minute['trade_date'].eq(first_trade_date)]  # 提取该日全部真实五分钟柱。
figure, axis = plt.subplots(figsize=(10, 4))  # 创建单面板日内价格图。
for session_name, session_frame in first_day_path.groupby('session'):  # 分会话绘图以避免连接午休空档。
    axis.plot(session_frame['datetime'], session_frame['close'], label=session_name, linewidth=1.6)  # 绘制真实上午和下午价格路径。
axis.set_title('恒瑞医药真实五分钟 OHLCV 收盘路径')  # 标明证券、频率与聚合口径。
axis.set_xlabel('Asia/Shanghai 时间')  # 明确时间戳时区。
axis.set_ylabel('收盘价(元)')  # 标明价格单位。
axis.legend(title='常规会话')  # 区分上午与包含收盘集合竞价的下午常规会话。
axis.grid(alpha=0.2)  # 提供克制的读数辅助线。
figure.autofmt_xdate()  # 自动旋转高频时间标签。
figure.tight_layout()  # 防止中文标题和坐标被裁切。
plt.show()  # 显示真实日内路径。
图 19.1: 恒瑞医药首个样本交易日完整常规时段的五分钟收盘路径;下午末柱含收盘集合竞价
figure, axes = plt.subplots(2, 1, figsize=(10, 6), sharex=True)  # 用分面保持方差与波动率的量纲独立。
axes[0].plot(daily_minute_features['trade_date'], daily_minute_features['realized_variance_5m'], color='#E3120B', marker='o')  # 绘制真实五分钟收益平方和。
axes[0].set_ylabel('Realized variance')  # 明确上面板是方差。
axes[0].grid(alpha=0.2)  # 添加辅助网格。
axes[1].plot(daily_minute_features['trade_date'], daily_minute_features['realized_volatility_5m'], color='#008080', marker='o')  # 绘制平方根后的同量纲波动率。
axes[1].set_ylabel('Realized volatility')  # 明确下面板是波动率。
axes[1].set_xlabel('交易日(Asia/Shanghai)')  # 说明横轴日历与时区。
axes[1].grid(alpha=0.2)  # 添加辅助网格。
figure.suptitle('同一收益序列的 RV 与 RVol 不可混称')  # 强调核心概念辨析。
figure.tight_layout()  # 优化面板间距。
plt.show()  # 显示真实特征时序。
图 19.2: 恒瑞医药完整常规时段五分钟已实现方差与已实现波动率

19.6 Tick 归档资格与未来累计基线

表 19.2 已证明当前归档的长三角 Tick 合格集合为空。因此本章到此停止公司级 Tick 实证:不打开福建或北京证券的逐笔节点,不计算价差、盘口不平衡、成交增量或日度特征。tick_eligibility_audit 是可执行的零合格文件证据;将来归档更新后,应先重新运行同一资格审计,再为合格证券新增独立的 schema、单位和交易阶段核验。

未来合格 Tick 文件的 volumetotal_turnover 若为日内累计字段,必须先在“证券—交易日”的完整原始时间序列上差分,再筛选连续竞价等研究阶段。每个交易日第一条原始记录以前的基线为零;阶段首条记录则使用它在原始序列中的真实前驱,不能在筛选后把差分填成零。这样,开盘集合竞价累计量充当首条连续竞价记录的前置基线,午休也不会制造重置。若研究阶段由多个不相邻片段组成,每个片段的首条增量仍来自完整原始序列中的直接前驱;量额守恒应按被保留更新的增量求和核验,而不是拿筛选后首末累计值直接相减。

表 19.8 用不代表任何公司的确定性机制夹具验证这一合同:差分结果非负、量与额分别守恒、开盘阶段边界保留前驱基线、午休前后不重置,并在新交易日重新以零为基线。

def add_cumulative_increments(raw_updates):  # 在阶段筛选以前还原日内累计量额。
    ordered = raw_updates.sort_values(['order_book_id', 'trade_date', 'datetime']).copy()  # 固定证券日内原始更新顺序。
    day_groups = ordered.groupby(['order_book_id', 'trade_date'], sort=False)  # 限制差分不跨证券或交易日。
    first_in_day = day_groups.cumcount().eq(0)  # 标记每天第一条原始更新。
    ordered['volume_increment'] = day_groups['volume'].diff().mask(first_in_day, ordered['volume'])  # 首条以零为基线,其余使用真实前驱。
    ordered['turnover_increment'] = day_groups['total_turnover'].diff().mask(first_in_day, ordered['total_turnover'])  # 对累计成交额执行相同规则。
    return ordered  # 返回尚未按市场阶段筛选的增量表。

baseline_rows = [('TEST', '2021-11-01', '09:29:00', 'opening_call', 100, 1000), ('TEST', '2021-11-01', '09:30:00', 'continuous', 130, 1300), ('TEST', '2021-11-01', '11:30:00', 'continuous', 150, 1520), ('TEST', '2021-11-01', '13:00:00', 'continuous', 180, 1850), ('TEST', '2021-11-01', '14:57:00', 'closing_call', 200, 2100), ('TEST', '2021-11-02', '09:29:00', 'opening_call', 80, 800), ('TEST', '2021-11-02', '09:30:00', 'continuous', 100, 1020)]  # 覆盖阶段边界、午休与跨日重置。
baseline_probe = pd.DataFrame(baseline_rows, columns=['order_book_id', 'trade_date', 'clock_time', 'market_phase', 'volume', 'total_turnover'])  # 建立最小累计字段合同。
baseline_probe['trade_date'] = pd.to_datetime(baseline_probe['trade_date'])  # 解析机制测试交易日。
baseline_probe['datetime'] = pd.to_datetime(baseline_probe['trade_date'].dt.strftime('%Y-%m-%d') + ' ' + baseline_probe['clock_time'])  # 组合日内排序时间。
baseline_probe = add_cumulative_increments(baseline_probe)  # 在任何阶段过滤前执行累计差分。
continuous_probe = baseline_probe.loc[baseline_probe['market_phase'].eq('continuous')].copy()  # 差分完成后才选择连续竞价。
assert baseline_probe[['volume_increment', 'turnover_increment']].ge(0).all().all()  # 核验量额增量均非负。
assert continuous_probe.loc[continuous_probe['datetime'].eq(pd.Timestamp('2021-11-01 09:30:00')), 'volume_increment'].iloc[0] == 30  # 首条连续竞价使用开盘集合竞价累计值作基线。
assert continuous_probe.loc[continuous_probe['datetime'].eq(pd.Timestamp('2021-11-01 13:00:00')), 'volume_increment'].iloc[0] == 30  # 午休后沿用上午最后更新而不重置。
assert continuous_probe.loc[continuous_probe['trade_date'].eq(pd.Timestamp('2021-11-01')), 'volume_increment'].sum() == 80  # 连续阶段成交量等于三次真实更新之和。
assert continuous_probe.loc[continuous_probe['trade_date'].eq(pd.Timestamp('2021-11-01')), 'turnover_increment'].sum() == 850  # 连续阶段成交额按同一基线守恒。
assert baseline_probe.loc[baseline_probe['trade_date'].eq(pd.Timestamp('2021-11-02')), 'volume_increment'].iloc[0] == 80  # 新交易日首条记录重新以零为基线。
tick_eligibility_audit  # 并列输出当前归档零合格文件的实证资格证据。
表 19.8: 未来合格 Tick 累计字段的阶段前差分机制测试
archive_files mapped_files yrd_eligible_files
0 2 2 0

19.7 标准化、可选 PCA 与时间泄漏

19.7.1 训练期拟合,后期只变换

标准化使用 \(z_{j,t}=(x_{j,t}-\hat\mu_{j,train})/\hat\sigma_{j,train}\)。如果均值和标准差由全样本估计,未来分布会泄露到早期特征。PCA 同样只能在训练期标准化矩阵上拟合协方差结构。代码块 列表 19.6 按日期排序,将前 70% 交易日作为教学训练期,后 30% 只用于变换;这不是模型效果评估,只是演示正确的信息边界。

列表 19.6: 仅在早期交易日拟合恒瑞医药标准化与 PCA
minute_feature_columns = ['intraday_momentum', 'realized_variance_5m', 'realized_volatility_5m', 'return_volume_correlation', 'absolute_return_volume_correlation', 'amihud_illiquidity_scaled', 'zero_volume_share']  # 固定进入缩放器的分钟特征 schema。
minute_feature_matrix = daily_minute_features.dropna(subset=minute_feature_columns).sort_values('trade_date').reset_index(drop=True)  # 不插补含义不清的日内相关缺失。
minute_split_position = max(2, int(len(minute_feature_matrix) * 0.7))  # 以前 70% 交易日构造训练期并保证至少两日。
minute_train_rows = np.arange(len(minute_feature_matrix)) < minute_split_position  # 生成严格按时间排序的训练掩码。
minute_scaler = StandardScaler()  # 创建用于分钟特征的 Z-score 变换器。
minute_scaler.fit(minute_feature_matrix.loc[minute_train_rows, minute_feature_columns])  # 只从早期交易日学习均值与标准差。
minute_standardized = minute_scaler.transform(minute_feature_matrix[minute_feature_columns])  # 用同一训练期参数变换全部日期。
minute_z_columns = [f'{column_name}_z' for column_name in minute_feature_columns]  # 为标准化特征建立可审计后缀。
minute_feature_matrix[minute_z_columns] = minute_standardized  # 将无泄漏标准化结果写回日度矩阵。
minute_pca = PCA(n_components=2)  # 选择两个主成分作为可选低维摘要。
minute_pca.fit(minute_feature_matrix.loc[minute_train_rows, minute_z_columns])  # 只用训练期标准化矩阵学习载荷。
minute_feature_matrix[['principal_component_1', 'principal_component_2']] = minute_pca.transform(minute_feature_matrix[minute_z_columns])  # 将训练期载荷应用到所有日期。
assert minute_feature_matrix.loc[minute_train_rows, 'trade_date'].max() < minute_feature_matrix.loc[~minute_train_rows, 'trade_date'].min()  # 证明拟合期严格早于保留期。

19.7.2 PCA 解释不是经济命名

表 19.9 报告训练期解释方差比与载荷。主成分的正负号本身可以整体翻转,不具有经济含义;只有观察多个原始特征在同一成分上的相对载荷,才能谨慎描述其可能代表的“波动—流动性”共同变化。短样本 PCA 主要是接口和泄漏边界示范,不能被解释为稳定因子。

pca_loadings = pd.DataFrame(minute_pca.components_.T, index=minute_feature_columns, columns=['principal_component_1', 'principal_component_2'])  # 将特征向量按原始特征排列。
pca_loadings['feature_name'] = pca_loadings.index  # 将索引转换为可展示字段。
pca_loadings['explained_variance_ratio_pc1'] = minute_pca.explained_variance_ratio_[0]  # 记录第一主成分训练期解释方差比。
pca_loadings['explained_variance_ratio_pc2'] = minute_pca.explained_variance_ratio_[1]  # 记录第二主成分训练期解释方差比。
pca_loadings.reset_index(drop=True)  # 输出载荷与解释方差证据。
表 19.9: 训练期分钟特征 PCA 的载荷与解释方差
principal_component_1 principal_component_2 feature_name explained_variance_ratio_pc1 explained_variance_ratio_pc2
0 0.521421 0.157202 intraday_momentum 0.51192 0.307161
1 0.460161 -0.385671 realized_variance_5m 0.51192 0.307161
2 0.454640 -0.401641 realized_volatility_5m 0.51192 0.307161
3 0.450633 0.406702 return_volume_correlation 0.51192 0.307161
4 0.281010 0.578170 absolute_return_volume_correlation 0.51192 0.307161
5 -0.166246 0.406870 amihud_illiquidity_scaled 0.51192 0.307161
6 0.000000 0.000000 zero_volume_share 0.51192 0.307161

19.8 Parquet 输出与往返审计

特征输出不是 to_parquet 调用结束即完成。稳定的数据产品至少需要主键、时区、字段类型、缺失率、训练边界和读取往返测试。代码块 列表 19.7 将合规分钟矩阵写入系统临时目录,表 19.10 重新读取并审计字段,同时断言行数、列序和 dtype 完全一致。生产项目应把路径替换为版本化的派生数据目录,并记录代码提交与输入快照哈希。

列表 19.7: 写出并重新读取长三角分钟特征 Parquet
temporary_output_root = Path(tempfile.gettempdir())  # 使用系统临时目录避免改写只读原始数据集。
minute_parquet_path = temporary_output_root / 'ch19_yrd_minute_features.parquet'  # 为长三角分钟特征定义独立输出文件。
minute_feature_matrix.to_parquet(minute_parquet_path, index=False)  # 以列式格式写出恒瑞医药特征矩阵。
minute_roundtrip = pd.read_parquet(minute_parquet_path)  # 从 Parquet 重新读取分钟矩阵以验证可恢复性。
assert minute_roundtrip.dtypes.equals(minute_feature_matrix.dtypes)  # 核验分钟 schema 在往返后保持一致。
assert minute_roundtrip.shape == minute_feature_matrix.shape  # 核验分钟行列数没有变化。
parquet_schema_audit = pd.DataFrame({'dataset': 'yrd_minute', 'column_name': minute_roundtrip.columns, 'dtype': minute_roundtrip.dtypes.astype(str).to_numpy(), 'missing_rate': minute_roundtrip.isna().mean().to_numpy()})  # 汇总长三角分钟输出的逐列合同。
parquet_schema_audit  # 输出全部字段类型与缺失率,避免只展示成功写入消息。
表 19.10: Parquet schema、缺失率与往返一致性审计
dataset column_name dtype missing_rate
0 yrd_minute order_book_id object 0.0
1 yrd_minute trade_date datetime64[ns, Asia/Shanghai] 0.0
2 yrd_minute opening_price float64 0.0
3 yrd_minute closing_price float64 0.0
4 yrd_minute realized_variance_5m float64 0.0
5 yrd_minute realized_volatility_5m float64 0.0
6 yrd_minute total_volume float64 0.0
7 yrd_minute total_turnover float64 0.0
8 yrd_minute amihud_illiquidity_scaled float64 0.0
9 yrd_minute zero_volume_share float64 0.0
10 yrd_minute five_minute_return_count int64 0.0
11 yrd_minute intraday_momentum float64 0.0
12 yrd_minute return_volume_correlation float64 0.0
13 yrd_minute absolute_return_volume_correlation float64 0.0
14 yrd_minute intraday_momentum_z float64 0.0
15 yrd_minute realized_variance_5m_z float64 0.0
16 yrd_minute realized_volatility_5m_z float64 0.0
17 yrd_minute return_volume_correlation_z float64 0.0
18 yrd_minute absolute_return_volume_correlation_z float64 0.0
19 yrd_minute amihud_illiquidity_scaled_z float64 0.0
20 yrd_minute zero_volume_share_z float64 0.0
21 yrd_minute principal_component_1 float64 0.0
22 yrd_minute principal_component_2 float64 0.0

标准化列和主成分应当无缺失,因为拟合前已显式剔除必要输入缺失;原始辅助列若有缺失则应在 表 19.10 中如实出现。读者不应使用全表 fillna(0),因为零价差、零相关和“无法计算”具有完全不同的经济含义。

19.9 适用边界与进一步研究

本章的结论边界包括:

  • 公司级样本只有恒瑞医药的十二个真实分钟交易日,足以验证数据工程和公式,不能估计稳定的横截面因子收益;

  • 本地 Tick 文件没有长三角公司,所以本章只报告归档资格审计与通用累计差分机制测试,不报告任何非长三角公司的 Tick 结果;

  • 五分钟采样降低但不消除微观结构噪声。本章分钟 RV 覆盖含收盘集合竞价的完整常规时段;若改为只研究严格连续竞价戳,必须重新构造 236 根保守分钟子样本及其频率网格,不能沿用 240 根结果换名;

  • 将来取得合格 Tick 数据后,还需确认累计字段单位、盘口字段缩放、逐笔更新是否等间隔以及阶段边界,不能把本章的机制夹具当成经验结果;

  • 午间跳跃与收盘集合竞价被保留、隔夜跳跃被排除。若研究开盘价格发现或纯连续竞价波动,应另建特征,不能悄悄改变本章 RV 定义;

  • 特征标准化和 PCA 只展示时间安全流程。任何预测模型仍需滚动或扩展窗口、交易成本、停牌可交易性和样本外检验。

19.10 本章小结

高频特征的可信度来自一组明确约束:先核验证券地域资格、原生 keys 和 dtype;再按 2021 年制度区分上午连续竞价、下午连续竞价和 14:5715:00 收盘集合竞价;完整常规时段与连续竞价必须分别审计;OHLCV 在明确的常规会话内按 first/max/min/last/sum/sum 聚合;每个交易日以真实开盘价锚定首个区间;RV 是平方和,RVol 是其平方根;成交量和成交额在频率转换前后必须守恒;分钟成交额为零仍保留为缺失含义;未来合格 Tick 的累计字段必须先在完整日内序列差分再筛选阶段;标准化和 PCA 只在过去拟合;Parquet 写出后必须往返核验。当前 Tick 合格集合为空,因此停止公司级逐笔实证本身也是合格的数据结论边界。

19.11 分层练习与完整解答

19.11.1 基础:验证原生合同与交易时段

练习 19.1

先验证分钟文件包含 data/index 根 key 与 OHLCV 复合字段,并验证 Tick 文件名都能映射到基本信息且当前没有长三角合格文件;再验证恒瑞医药每个完整常规交易日包含上午常规会话 120 条、下午常规会话 120 条,其中上午连续竞价戳 120 条、下午严格连续竞价戳 116 条、14:5715:00 收盘集合竞价边界桶 4 条,且不存在午休记录。报告不完整交易日,而不是静默删除。

完整解答

required_minbar_fields = {'datetime', 'open', 'high', 'low', 'close', 'volume', 'total_turnover'}  # 声明分钟OHLCV算法依赖的最低字段合同。
assert {'data', 'index'}.issubset(minbar_keys) and required_minbar_fields.issubset(minbar_fields)  # 阻止把原生分钟文件误当Pandas HDFStore。
assert tick_archive['is_mapped'].all() and eligible_yrd_tick.empty  # 核验 Tick 归档可追溯且没有越界进入公司级实证。
session_count_table = minbar.groupby(['trade_date', 'session']).size().unstack(fill_value=0)  # 分别统计上午和下午常规会话分钟数。
phase_count_table = minbar.groupby(['trade_date', 'market_phase']).size().unstack(fill_value=0)  # 分别统计两个连续竞价阶段与收盘集合竞价。
session_count_table['regular_total'] = session_count_table['morning_regular'] + session_count_table['afternoon_regular']  # 计算包含收盘集合竞价的常规分钟总数。
phase_count_table['continuous_total'] = phase_count_table['morning_continuous'] + phase_count_table['afternoon_continuous']  # 计算不含收盘集合竞价的连续竞价分钟数。
session_count_table['is_complete_regular'] = session_count_table['morning_regular'].eq(120) & session_count_table['afternoon_regular'].eq(120) & session_count_table['regular_total'].eq(240)  # 标记完整常规交易日。
phase_count_table['is_complete_continuous'] = phase_count_table['morning_continuous'].eq(120) & phase_count_table['afternoon_continuous'].eq(116) & phase_count_table['continuous_total'].eq(236)  # 标记保守结束戳口径下的完整连续竞价覆盖。
phase_count_table['is_complete_closing_call'] = phase_count_table['closing_call_auction'].eq(4)  # 要求14:57至15:00四个结束戳归入集合竞价边界桶。
lunch_rows = minbar['clock_time'].between('11:31:00', '13:00:00').sum()  # 独立统计午休期间记录。
assert lunch_rows == 0  # 若午休被插值或误读则阻断答案。
phase_audit_table = session_count_table.join(phase_count_table, how='inner')  # 将完整常规时段与竞价阶段按交易日对齐。
phase_audit_table['is_complete'] = phase_audit_table[['is_complete_regular', 'is_complete_continuous', 'is_complete_closing_call']].all(axis=1)  # 要求三套计数同时成立。
assert minbar.loc[minbar['clock_time'].between('14:57:00', '15:00:00'), 'market_phase'].eq('closing_call_auction').all()  # 防止14:57至15:00误标为连续竞价。
incomplete_days = phase_audit_table.loc[~phase_audit_table['is_complete']].reset_index()  # 保留所有不完整交易日供人工调查。
phase_audit_table  # 输出240根常规、236根严格连续竞价戳、4根收盘集合竞价边界桶的逐日证据。
表 19.11: 练习 19.1:原生合同、完整常规时段与三个竞价阶段核验
afternoon_regular morning_regular regular_total is_complete_regular afternoon_continuous closing_call_auction morning_continuous continuous_total is_complete_continuous is_complete_closing_call is_complete
trade_date
2021-11-01 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True
2021-11-02 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True
2021-11-03 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True
2021-11-04 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True
2021-11-05 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True
2021-11-08 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True
2021-11-09 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True
2021-11-10 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True
2021-11-11 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True
2021-11-12 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True
2021-11-15 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True
2021-11-16 00:00:00+08:00 120 120 240 True 116 4 120 236 True True True

表 19.11 若对应的 incomplete_days 非空,应检查临时停牌、早收市或数据缺口;不能为了得到 240 行而插值价格和成交量,也不能把 14:5715:00 四个结束戳并入『连续竞价』计数。

19.11.2 进阶:手工复算 RV 与 RVol

练习 19.2

选择 realized variance 最大的交易日,用该日五分钟收益手工计算 RV 与 RVol,并验证 \(\mathrm{RVol}^2=\mathrm{RV}\)

完整解答

maximum_rv_row = rv_comparison.nlargest(1, 'realized_variance_5m').iloc[0]  # 找到真实样本中的最大五分钟 RV 日。
maximum_rv_date = maximum_rv_row['trade_date']  # 提取该交易日作为手工核验对象。
maximum_rv_returns = five_minute.loc[five_minute['trade_date'].eq(maximum_rv_date), 'log_return_5m'].dropna().to_numpy()  # 取得该日全部有效日内收益。
manual_rv = np.square(maximum_rv_returns).sum()  # 按 @eq-ch19-realized-variance 手工求平方和。
manual_rvol = np.sqrt(manual_rv)  # 按 @eq-ch19-realized-volatility 计算平方根。
assert len(maximum_rv_returns) == 48  # 完整交易日必须覆盖开盘锚点开始的全部 48 个五分钟区间。
assert np.isclose(manual_rv, maximum_rv_row['realized_variance_5m'], rtol=1e-12, atol=1e-15)  # 核验手工值与聚合值一致。
assert np.isclose(manual_rvol ** 2, manual_rv, rtol=1e-12, atol=1e-15)  # 核验方差与波动率的量纲关系。
pd.DataFrame({'trade_date': [maximum_rv_date], 'five_minute_returns': [len(maximum_rv_returns)], 'manual_realized_variance': [manual_rv], 'manual_realized_volatility': [manual_rvol]})  # 输出最大波动日的完整关键结果。
表 19.12: 练习 19.2:最大波动日的手工 RV 与 RVol
trade_date five_minute_returns manual_realized_variance manual_realized_volatility
0 2021-11-10 00:00:00+08:00 48 0.003176 0.056355

表 19.12 中的 five_minute_returns 应为 48,与五分钟柱数相同。第一条收益以当天开盘价为分母;隔夜收益被排除,但开盘至 09:35 的价格变化没有遗漏。

19.11.3 应用:审计累计字段的阶段前基线

练习 19.3

解释为什么累计成交量和成交额必须先在完整日内更新序列上差分,再筛选连续竞价。使用 表 19.8 的机制夹具,复核开盘阶段边界、午休、跨日重置、非负性以及量额守恒;同时确认当前归档没有可进入公司级实证的长三角 Tick 文件。

完整解答

first_continuous = continuous_probe.groupby(['order_book_id', 'trade_date'], sort=False).first().reset_index()  # 提取每天首条连续竞价更新。
first_raw = baseline_probe.groupby(['order_book_id', 'trade_date'], sort=False).first().reset_index()  # 提取每天第一条完整原始更新。
baseline_comparison = first_continuous.merge(first_raw[['order_book_id', 'trade_date', 'volume']], on=['order_book_id', 'trade_date'], suffixes=('_continuous', '_raw'), validate='one_to_one')  # 对齐阶段首条与真实日初基线。
baseline_comparison['expected_first_continuous_increment'] = baseline_comparison['volume_continuous'] - baseline_comparison['volume_raw']  # 由完整原始前驱复算阶段首条增量。
assert np.allclose(baseline_comparison['volume_increment'], baseline_comparison['expected_first_continuous_increment'])  # 阻止筛选后把阶段首条错误置零。
assert continuous_probe[['volume_increment', 'turnover_increment']].ge(0).all().all()  # 再次核验保留阶段内量额增量非负。
assert eligible_yrd_tick.empty  # 确认机制测试没有被包装成非合格公司的经验结果。
baseline_comparison[['trade_date', 'volume_raw', 'volume_continuous', 'volume_increment', 'expected_first_continuous_increment']]  # 输出阶段边界复算证据。
表 19.13: 练习 19.3:Tick 资格与累计基线复核
trade_date volume_raw volume_continuous volume_increment expected_first_continuous_increment
0 2021-11-01 100 130 30.0 30
1 2021-11-02 80 100 20.0 20

表 19.13 中,阶段首条增量等于该条累计值减去完整原始序列中的前驱累计值,而不是零。午休没有成交记录,不构成新交易日,所以下午首条仍承接上午最后更新;只有证券或交易日变化才把基线重置为零。当前归档资格为空意味着答案只能证明算法合同,不能给出公司盘口特征。

19.11.4 综合挑战:一、五、十五分钟稳健性与输出审计

练习 19.4

基于包含收盘集合竞价的完整常规时段,在不跨午休的前提下构造真实十五分钟 OHLCV,以当日开盘价锚定首个区间,重新计算日度 RV 与 RVol,并与一、五分钟结果比较。断言完整日分别拥有 240、48、16 个收益区间,且十五分钟成交量、成交额仍与一分钟底表守恒;明确下午最后一根柱不是纯连续竞价柱。最后将比较表写成 Parquet,重新读取并验证 schema 与缺失率。

完整解答

代码块 列表 19.8 先构造时段安全的十五分钟 OHLCV 与收益,再由 表 19.14 完成交付审计。

列表 19.8: 练习 19.4:十五分钟 RV 的时段安全构造
fifteen_minute = build_session_bars(minbar, 15)  # 按与五分钟相同的完整常规会话口径聚合十五分钟柱。
fifteen_minute['squared_return_15m'] = fifteen_minute['log_return_15m'].pow(2)  # 构造十五分钟收益平方分量。
fifteen_minute_features = fifteen_minute.groupby(['order_book_id', 'trade_date']).agg(realized_variance_15m=('squared_return_15m', 'sum'), fifteen_minute_return_count=('log_return_15m', 'count'), volume_15m_sum=('volume', 'sum'), turnover_15m_sum=('total_turnover', 'sum')).reset_index()  # 汇总十五分钟波动与量额守恒字段。
fifteen_minute_features['realized_volatility_15m'] = np.sqrt(fifteen_minute_features['realized_variance_15m'])  # 将十五分钟 RV 转为同量纲 RVol。
complete_trade_dates = complete_five_minute_audit.reset_index()[['order_book_id', 'trade_date']]  # 复用一分钟会话完整性审计确定严格比较样本。
complete_fifteen_features = complete_trade_dates.merge(fifteen_minute_features, on=['order_book_id', 'trade_date'], validate='one_to_one')  # 对齐完整日的十五分钟结果。
assert complete_fifteen_features['fifteen_minute_return_count'].eq(16).all()  # 完整日必须拥有上午八段和下午八段十五分钟收益。
fifteen_flow_audit = fifteen_minute_features.merge(minute_flow_totals.reset_index(), on=['order_book_id', 'trade_date'], validate='one_to_one')  # 按证券和日期显式对齐十五分钟与一分钟量额。
assert np.allclose(fifteen_flow_audit['volume_15m_sum'], fifteen_flow_audit['volume_1m_sum'], rtol=0, atol=0)  # 核验十五分钟成交量没有因降采样丢失。
assert np.allclose(fifteen_flow_audit['turnover_15m_sum'], fifteen_flow_audit['turnover_1m_sum'], rtol=1e-12, atol=1e-6)  # 核验十五分钟成交额与一分钟底表守恒。
sampling_robustness = rv_comparison.merge(fifteen_minute_features, on=['order_book_id', 'trade_date'], validate='one_to_one')  # 对齐同一证券交易日的一、五、十五分钟频率。
sampling_robustness['rv_15m_to_5m_ratio'] = sampling_robustness['realized_variance_15m'] / sampling_robustness['realized_variance_5m']  # 量化采样频率敏感性。
robustness_path = Path(tempfile.gettempdir()) / 'ch19_sampling_robustness.parquet'  # 将练习输出放入系统临时目录。
sampling_robustness.to_parquet(robustness_path, index=False)  # 写出真实频率稳健性表。
sampling_roundtrip = pd.read_parquet(robustness_path)  # 重新读取输出以执行交付审计。
assert sampling_roundtrip.dtypes.equals(sampling_robustness.dtypes)  # 核验全部字段类型保持一致。
assert sampling_roundtrip.isna().mean().equals(sampling_robustness.isna().mean())  # 核验缺失模式没有变化。
assert sampling_roundtrip.loc[sampling_roundtrip['trade_date'].isin(complete_trade_dates['trade_date']), 'one_minute_return_count'].eq(240).all()  # 核验完整日的一分钟频率覆盖 240 个开盘锚定区间。
sampling_roundtrip[['trade_date', 'one_minute_return_count', 'five_minute_return_count', 'fifteen_minute_return_count', 'realized_variance_1m', 'realized_variance_5m', 'realized_variance_15m', 'realized_volatility_1m', 'realized_volatility_5m', 'realized_volatility_15m', 'rv_15m_to_5m_ratio']]  # 输出逐日三频率波动与覆盖区间比较。
表 19.14: 练习 19.4:一、五、十五分钟 RV 与 RVol 的 Parquet 往返审计
trade_date one_minute_return_count five_minute_return_count fifteen_minute_return_count realized_variance_1m realized_variance_5m realized_variance_15m realized_volatility_1m realized_volatility_5m realized_volatility_15m rv_15m_to_5m_ratio
0 2021-11-01 00:00:00+08:00 240 48 16 0.000438 0.000268 0.000206 0.020933 0.016357 0.014350 0.769661
1 2021-11-02 00:00:00+08:00 240 48 16 0.000471 0.000367 0.000616 0.021695 0.019154 0.024827 1.680064
2 2021-11-03 00:00:00+08:00 240 48 16 0.000426 0.000414 0.000571 0.020640 0.020359 0.023892 1.377262
3 2021-11-04 00:00:00+08:00 240 48 16 0.000682 0.000578 0.000779 0.026115 0.024040 0.027915 1.348395
4 2021-11-05 00:00:00+08:00 240 48 16 0.000649 0.000772 0.000933 0.025469 0.027789 0.030552 1.208693
5 2021-11-08 00:00:00+08:00 240 48 16 0.000652 0.000919 0.000861 0.025540 0.030309 0.029343 0.937213
6 2021-11-09 00:00:00+08:00 240 48 16 0.000280 0.000286 0.000072 0.016743 0.016921 0.008508 0.252838
7 2021-11-10 00:00:00+08:00 240 48 16 0.001861 0.003176 0.007498 0.043134 0.056355 0.086591 2.360925
8 2021-11-11 00:00:00+08:00 240 48 16 0.000635 0.000678 0.000304 0.025199 0.026038 0.017438 0.448501
9 2021-11-12 00:00:00+08:00 240 48 16 0.000667 0.000502 0.000272 0.025824 0.022398 0.016482 0.541478
10 2021-11-15 00:00:00+08:00 240 48 16 0.000512 0.000378 0.000331 0.022626 0.019445 0.018189 0.875034
11 2021-11-16 00:00:00+08:00 240 48 16 0.000599 0.000434 0.000324 0.024469 0.020827 0.018008 0.747571

若十五分钟 RV 系统性低于五分钟或一分钟 RV,可能是更稀疏采样减少了噪声,也可能平滑了真实高频变化。由于三个频率现在覆盖相同的当日开盘至收盘区间,差异不再来自遗漏首段收益;正确结论仍是“估计对采样频率敏感”,而不是自动宣布某个频率是真实波动率。

19.11.5 审计挑战:训练期标准化与可选 PCA

练习 19.5

证明分钟特征的 StandardScaler 和 PCA 都只看见前 70% 的训练日期:核验拟合样本数、手工复算一个保留期 Z 分数,并用已拟合载荷复算同一行的主成分。说明『可选 PCA』在特征交付中的含义。

完整解答

表 19.15 汇总本节的计算或审计结果,解释时应遵循正文给出的口径与限制。

minute_training_count = int(minute_train_rows.sum())  # 取得严格早期训练日期数量。
assert minute_scaler.n_samples_seen_ == minute_training_count  # 核验缩放器没有看见保留期记录。
assert minute_pca.n_samples_ == minute_training_count  # 核验PCA协方差只由训练期标准化矩阵估计。
first_holdout_position = int(np.flatnonzero(~minute_train_rows)[0])  # 定位第一条未来保留期记录。
first_holdout_raw = minute_feature_matrix.loc[first_holdout_position, minute_feature_columns].to_numpy(dtype=float)  # 提取未标准化的未来特征。
manual_holdout_z = (first_holdout_raw - minute_scaler.mean_) / minute_scaler.scale_  # 只用训练均值与尺度手工变换未来行。
stored_holdout_z = minute_feature_matrix.loc[first_holdout_position, minute_z_columns].to_numpy(dtype=float)  # 取得流水线已保存的未来Z分数。
np.testing.assert_allclose(manual_holdout_z, stored_holdout_z, rtol=1e-12, atol=1e-12)  # 核验保留期没有重新拟合尺度。
manual_holdout_pc = minute_pca.transform(stored_holdout_z.reshape(1, -1))[0]  # 用训练期载荷变换同一未来行。
stored_holdout_pc = minute_feature_matrix.loc[first_holdout_position, ['principal_component_1', 'principal_component_2']].to_numpy(dtype=float)  # 取得已保存主成分。
np.testing.assert_allclose(manual_holdout_pc, stored_holdout_pc, rtol=1e-12, atol=1e-12)  # 核验未来行没有参与载荷估计。
preprocessing_audit = pd.DataFrame({'训练行数': [minute_training_count], '缩放器拟合行数': [minute_scaler.n_samples_seen_], 'PCA拟合行数': [minute_pca.n_samples_], '首个保留期日期': [minute_feature_matrix.loc[first_holdout_position, 'trade_date']]})  # 汇总时间安全证据。
preprocessing_audit  # 输出训练期拟合审计结果。
表 19.15: 练习 19.5:训练期 StandardScaler 与 PCA 审计
训练行数 缩放器拟合行数 PCA拟合行数 首个保留期日期
0 8 8 8 2021-11-11 00:00:00+08:00

『可选 PCA』表示原始且已标准化的可解释特征仍是主交付,主成分只是可另行选择的低维摘要;是否使用及保留几个成分必须在训练期评价流程内决定。不能先用全样本选择成分数,也不能用 PCA 载荷给出未经检验的经济因子命名。