ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

郑商所日统计数据解析:从TXT到量化因子构建全流程

郑商所日统计数据解析:从TXT到量化因子构建全流程 做商品期货量化的人几乎都绕不开交易所公开的日度行情文件。今天这篇“量化投资从0开始”系列第15篇我拿郑商所每天收盘后发布的日统计数据来拆一拆。这个文件是研究国内商品期货最“朴素”也最可靠的数据源之一甚至不夸张地说很多看起来复杂的量价因子、仓位因子、情绪指标最后都能从这份纯文本文件里倒推出来。我身边不少做CTA和跨期套利的同行每天下午固定时间要干的头一件事就是把它拉到本地跑一遍更新流程。后面我们就从文件结构、下载方式、字段语义、因子构造到常见坑完整过一遍。1. 郑商所日统计数据到底是什么1.1 这份数据解决了什么问题郑商所的全称是郑州商品交易所国内四大期货交易所之一主要上市品种是农产品和部分化工品比如白糖、棉花、菜粕、PTA、甲醇、玻璃、纯碱这些。每个交易日收盘之后交易所会把当天所有合约的成交、持仓、价格信息汇总整理成一个固定格式的TXT文本放到官网上这个就是日统计数据。从量化角度来说它能解决一个很基础也很头疼的问题统一、规范、免费的历史行情怎么来。市面上很多商业数据源要花钱而且字段封装得五花八门交易所官网这份文件虽然是“裸数据”但它的字段定义是交易所官方口径用来做因子回测和策略校准天然就是权威参考。另一个容易被忽视的作用是数据对齐。你做跨品种套利、跨期套利、或者算涨跌停价格收盘价、结算价、涨跌、持仓量这些字段必须在一个统一的文件里对齐。郑商所这份日统计表恰好就是按“品种合约日期”粒度输出的拉下来直接就能当底表用。1.2 能用来做什么适合谁有这个文件在手你可以做三件很典型的事维护自己的日线数据库替代或者校验商业数据源计算持仓变化、成交量变化、结算价与收盘价乖离等基础因子做合约换月、主力连续合约拼接、跨期价差分析的前置数据清洗。所以这个系列面向的对象很明确已经会一点Python和Pandas想开始接触国内商品期货量化的朋友。如果你完全没碰过期货也没关系只要理解“每天交易所发布一张表我们把表读进来变成DataFrame”后续的事情就顺理成章了。我自己最早接触国内品种数据就是从这种TXT文件开始的当时觉得丑后来觉得是真香。2. 文件格式、下载地址与字段语义2.1 文件存放规律与真正的下载地址郑商所官网公布的历史数据走的是静态文件方式路径规律非常工整http://www.czce.com.cn/cn/DFSStaticFiles/Future/YYYY/YYYYMMDD/FutureDataDaily.txt其中 YYYY 是年份YYYYMMDD 是交易日。比如你想下载2024年6月26日的数据URL就是http://www.czce.com.cn/cn/DFSStaticFiles/Future/2024/20240626/FutureDataDaily.txt这个规律的好处是你不需要解析网页、不需要登录、不需要接口文档只要用一个循环把日期拼出来就能批量拉历史数据。坏处是它确实“太裸”了文件头、编码、字段宽度都可能因为网站改版而变化我后面会专门讲怎么避坑。另外郑商所还有一种带查询参数的历史数据下载方式用reqDate参数去取但那个接口往往只保留最近一段时间的记录而且字段格式在不同年份也有过调整。日常使用我最推荐的就是上面的静态文件地址稳定性足够好。注意下载时一定要用HTTP的GET请求同时建议在请求头里带一个正常的 User-Agent有些时候反爬策略会拦截空UA的请求。别问我是怎么知道的。2.2 TXT文件内部长什么样下载回来后它是一个TXT文本从最表面看内容大致是品种代码 品种名称 合约代码 前结算价 今开盘 最高价 最低价 今收盘 结算价 涨跌1 涨跌2 成交量(手) 持仓量(手) 持仓量增减 成交额(万元) 交割结算价 SR 白糖 SR607 5301 5301 5301 5301 5301 5301 0 0 0 0 0 0.00 5301 ... 交易所公告...注意不同年份这个文件头可能不同有些版本前面会多个几行说明有些版本列名会变成中文逗号分隔。所以写解析脚本时不建议写死表头在第几行而是应该先读前几行找到真正的列名行再往下解析。数据字段里最要命的三个字段是“结算价”“今收盘”“涨跌1/涨跌2”。很多人刚上手会默认“今收盘”就是当日结算依据其实不是。国内商品期货的当日盈亏结算用的是该合约当日成交价格按成交量加权计算出来的“结算价”它和收盘价经常有几十个点的差异。2.3 各字段的量化用途下面这张表是我自己整理的重点字段速查字段含义量化用途品种代码品种缩写如SR、CF、TA、MA、FG、SA分组、跨品种分析合约代码具体合约如SR609区分到期月份前结算价上一交易日结算价计算涨跌幅基准今开盘/最高/最低/收盘当日OHLCK线、技术指标结算价当日加权均价保证金、盈亏、隔夜跳空基准涨跌1今收盘 - 前结算价单日涨跌幅度涨跌2今结算价 - 前结算价结算口径涨跌成交量(手)当日成交总手数流动性、换手因子持仓量(手)当日收盘后的总持仓仓位、资金流因子持仓量增减当日持仓变化增仓/减仓信号成交额(万元)成交金额活跃度、市场深度我给一个具体的例子。假设你想识别“放量增仓上涨”的合约那你要看的就是涨跌1 0成交量较过去5日均值放大持仓量增减 0。这三条用这份文件里的字段直接就能算不需要任何外部数据。2.4 一个容易搞混的交易日概念郑商所有夜盘交易比如白糖、PTA、甲醇这些品种夜盘是晚上21点到23点。夜盘结束后数据在会计上归属于下一个交易日。举个例子周五晚上的夜盘成交会算进下周一的日统计数据里周一夜盘才算周二的。这个如果不注意你自己做“按天聚合”的时候就会把周五夜盘和周五日盘混在一起导致因子值序列错乱。3. 实操过程从零写一个郑商所日统计解析器3.1 环境准备我这里默认用的是 Python 3.9 和 Pandas 1.5如果你还没有装直接pip install pandas requests就行。后面所有代码我不整复杂框架一个脚本打到尾方便你复制出去改着用。3.2 第一步下载当日的TXT文件先写一个下载函数。核心就是把日期拼进URL然后加个重试和保存逻辑。import requests import time from pathlib import Path def download_czce_daily(trade_date: str, save_dir: str ./data) - Path: trade_date: 格式 YYYYMMDD例如 20240626 year trade_date[:4] url ( fhttp://www.czce.com.cn/cn/DFSStaticFiles/Future/ f{year}/{trade_date}/FutureDataDaily.txt ) headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } Path(save_dir).mkdir(parentsTrue, exist_okTrue) local_path Path(save_dir) / f{trade_date}.txt # 简单重试三次 for attempt in range(3): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: resp.encoding utf-8 local_path.write_bytes(resp.content) return local_path else: print(f[{trade_date}] HTTP {resp.status_code}) except Exception as e: print(f[{trade_date}] 下载异常: {e}) time.sleep(1.5) raise RuntimeError(f下载失败: {trade_date})为什么resp.encoding utf-8而不是 GBK因为这几年郑商所官网做过改版历史文件很多已经变成UTF-8了老一点的年份可能是GBK。所以下载时我干脆先原样存字节流解析时再去判断编码。这里把resp.content直接写文件就是保留原始字节避免在下载阶段就做了错误的转码。3.3 第二步解析TXT为DataFrame先别急着pd.read_csv因为这个文件不是标准CSV列之间可能是多个空格也可能有制表符还可能混入中文表头。稳妥的方式是先读取文本找到正文的起始位置。import pandas as pd def parse_czce_daily(file_path: Path) - pd.DataFrame: # 先读原始字节探测编码 raw file_path.read_bytes() for enc in (utf-8-sig, gbk, gb18030): try: text raw.decode(enc) break except UnicodeDecodeError: continue else: raise ValueError(无法识别的编码) lines text.splitlines() # 找列名行通常包含“合约代码”这四个字 header_idx None for i, line in enumerate(lines): if 合约代码 in line: header_idx i break if header_idx is None: raise ValueError(未找到表头行) # 跳过表头之前的内容用正则按空白符切分 from io import StringIO df pd.read_csv( StringIO(\n.join(lines[header_idx:])), sepr\s, enginepython, encodingutf-8, dtypestr, ) # 把列名两边的空格清一下 df.columns [c.strip() for c in df.columns] return df这里用了dtypestr因为合并后的列经常有“—”“NaN”之类的占位符直接读数值会报错。后续要转数值的列再统一pd.to_numeric(..., errorscoerce)。3.4 第三步字段清洗与类型转换拿到原始DataFrame后需要做这么几件事去掉行首行尾的空列把“-”“—”“None”这些占位符变成NaN成交量、持仓量、价格字段全部转为数值型增加一列日期方便后续拼接历史数据。def clean_czce_df(df: pd.DataFrame, trade_date: str) - pd.DataFrame: # 去掉完全为空的列 df df.dropna(axis1, howall) # 去掉完全为空的行 df df.dropna(axis0, howall) numeric_cols [ 前结算价, 今开盘, 最高价, 最低价, 今收盘, 结算价, 涨跌1, 涨跌2, 成交量(手), 持仓量(手), 持仓量增减, 成交额(万元), 交割结算价 ] for col in numeric_cols: if col in df.columns: df[col] pd.to_numeric( df[col].replace({-: None, —: None, None: None}), errorscoerce ) df[品种代码] df[品种代码].astype(str).str.strip() df[合约代码] df[合约代码].astype(str).str.strip() df[日期] trade_date return df这个清洗看着简单但它决定了后续所有因子计算能不能一次跑通。我自己后来把这个逻辑封装成了一个函数每天定时任务直接调用极大减少了手工整理的时间。3.5 第四步批量拉历史数据如果你想回测过去两年每天拼URL、下载、解析循环跑就行。但是要记得控制频率毕竟交易所服务器不是给你一个人准备的。import datetime as dt def make_trade_dates(start: str, end: str): 粗略生成交易日序列只跳过周末不停用节假日节假日文件会404 更严谨的做法是自己维护一个交易日历。 s dt.date.fromisoformat(start) e dt.date.fromisoformat(end) while s e: if s.weekday() 5: yield s.strftime(%Y%m%d) s dt.timedelta(days1) all_dfs [] for day in make_trade_dates(2024-01-01, 2024-06-30): try: file_path download_czce_daily(day, save_dir./data) df parse_czce_daily(file_path) df clean_czce_df(df, day) all_dfs.append(df) except RuntimeError: # 可能是节假日或者当天数据未发布 continue history pd.concat(all_dfs, ignore_indexTrue)注意这个“跳过周末”的做法并不严谨因为节假日它也会去请求然后碰到404。实际生产中我建议维护一个交易日历或者直接根据持仓量不为空的文件合集反推交易日。反推的办法就是哪些日期路径能成功下载到文件哪些就是交易日。这个方法虽然不是官方日历但在90%的场景下都够用。4. 从日统计数据到可用的量化信号4.1 信号一结算价与收盘价的乖离我个人认为结算价和收盘价之间的差值是这份文件里最被低估的字段组合。结算价是全天成交的加权平均价而收盘价是最后时刻的成交价。如果收盘价远高于结算价说明尾盘买盘非常强势这种情况常出现在资金抢筹的品种上。构造方法很简单history[收盘结算乖离] (history[今收盘] - history[结算价]) / history[结算价]你可以按“品种代码 日期”做截面排序把乖离最大的几个品种当成一种短期动量信号。我实测下来在流动性好的主力合约上这个因子单独用效果一般但叠加成交量放大条件后对趋势跟踪类策略有不错的辅助作用。4.2 信号二增仓方向一致性期货市场有句老话叫“增仓上涨看多、增仓下跌看空、减仓上涨谨慎、减仓下跌有支撑”。这句话翻译成因子就是把持仓量增减和涨跌方向放在一起看。history[持仓变化] history[持仓量增减] history[价格变化] history[涨跌1] history[增仓上涨] ( (history[持仓变化] 0) (history[价格变化] 0) ).astype(int)如果用历史数据统计在“增仓上涨”这个条件出现后次日继续上涨的概率是否高于基准你可以直接在DataFrame上做滚动统计。这就是最朴素的“资金流验证”。当然单看一个品种容易过拟合我通常会把全市场所有活跃合约合在一起做横截面统计。4.3 信号三成交持仓比成交持仓比衡量的是合约的换手活跃程度。计算公式是成交量 / 持仓量。这个比值高说明短线资金活跃比值低说明市场沉淀度较高价格更容易被少量资金推动。history[成交持仓比] history[成交量(手)] / (history[持仓量(手)] 1e-12)这里加1e-12是为了防止除零。用量化人之间的话说这是一个“必须写进代码里的细节”因为新上市合约首日可能没有持仓直接除就会得到inf。4.4 信号四主力合约识别与换月做国内商品期货最麻烦的一件事就是确定“主力合约”。交易所文件里没有“主力”这个标签只有合约列表。常用的方法是每天取同一品种持仓量最大的合约作为当日主力合约。idx history.groupby([日期, 品种代码])[持仓量(手)].idxmax() main_contracts history.loc[idx].copy()然后如果你要做连续价格序列还需要处理换月。最简单的方式是在主力合约切换的当天用“新主力合约的涨跌幅”来替代拼接避免价格跳空造成的假信号。这个方法看起来很土但在日度频率上非常实用。4.5 进阶配合会员持仓排名数据日统计数据里只有总量没有席位信息。如果你想看得更深一步比如某品种前20名会员是净多还是净空那就需要再去抓交易所的“会员持仓排名”文件。郑商所同样有对应的每日排名文件一般在官网“交易排名”栏目下。把“日统计数据”和“会员持仓排名”按日期、合约做合并之后就能构造出类似“前20席净持仓变化”的经典资金流因子。这个可以作为本系列后续内容单独展开。5. 常见问题与排查技巧实录5.1 问题速查表现象原因解决办法下载时返回404当天不是交易日或数据尚未发布先查交易日历再做时间等待文件里中文全部乱码当前文件是GBK编码代码默认用了UTF-8用gbk或gb18030重试解码解析出来的列数不对定宽空格被当成多列或者表头被拆分先找表头行再用sepr\s数字列变成全NaN文件用“-”表示缺失Pandas默认读成字符串用replace把“-”替换成None收盘价和结算价对不上把两个字段理解成同一个概念明确结算价是加权均价收盘价是最后一笔周五数据里总有奇怪数字夜盘归属到下一交易日按交易日重采样不要按自然日重采样历史数据缺了一个月仅用reqDate接口拉历史只能拉到近一年用静态文件URL批量拉取或每日定时存档5.2 高频踩坑点逐个说第一个坑盲目用pd.read_csv直接读TXT。很多早期教程这么写但因为表头前可能有说明行文件解析会错位。一定要先定位“合约代码”所在的行再从这个行开始读。第二个坑股票思维带到期货。股票有前复权、后复权期货不存在复权但存在合约换月。如果你把不同月份的合约价格拼在一起画曲线不做换月处理那画出来的“连续图”一眼假。处理方式是换月当天只保留新合约通过涨跌幅连续化。第三个坑收盘后才去定时下载却不知道发布延迟。郑商所日统计数据通常在收盘后几个小时内发布但具体时间不固定。我自己跑定时任务时一般会设置“下午5点开始每10分钟拉一次直到成功为止”避免数据没发布就以为网络出问题。第四个坑忽略文件编码的年份差异。2020年的文件和2024年的文件编码可能完全不同。批量拉历史时必须做编码探测不能写死一种。我上面的解析函数里已经演示了用for enc in (utf-8-sig, gbk, gb18030)循环尝试。第五个坑直接用“持仓量增减”字段做当日变化量却不校验。这个字段偶尔会出现缺失或异常尤其是新品种上市、老品种退市时。清洗时建议用今日持仓 - 昨日持仓交叉验证对不上的再剔除。第六个坑合约代码大小写问题。郑商所的合约代码一般是两个大写字母加数字比如SR609但某些年份的文件里可能出现小写。解析时最好统一upper()。5.3 独家技巧如何自建交易日历与其到处找交易日历不如自己造一个。方法是先跑一遍make_trade_dates把所有周末之外的日期都尝试下载一次能下载到文件的就是交易日。把这些日期存成一个CSV每次回测时直接用这个日历不进交易所服务器反复请求。这样既省时间也避免触发频率限制。我在本地就是这么维护的每天早上定时任务跑一次下载同时把成功下载的日期追加到交易日历里。日积月累日历越来越完整历史数据也不断在本地积累以后要回溯策略再也不愁没数据。反过来如果一开始不存档等到两三年后发现需要某段历史再回头去交易所拉对方挂了404你也只能干瞪眼。6. 关于自动化和后续扩展的一些个人建议做这类数据工程我建议你从第一天就养成“全天候定时存档”的习惯。不管你现在用不用得上历史数据先把每天的日统计数据拉下来存好成本极低收益却在后期呈指数级放大的。我见过太多人一开始只拉自己关注的几个品种后来换策略方向需要别的品种历史数据结果发现早期没存只能手工补痛苦程度谁补谁知道。另一点日统计数据处理熟练之后建议把目光扩大到其他交易所。上期所、大商所、中金所、广期所的数据发布方式大同小异但文件格式细节各有各的坑。一个交易所跑通剩下的其实就是复制粘贴后改字段名的事。还有郑商所除了每日行情TXT还提供每天的交割结算价表、仓单日报表、会员持仓排名表这些都可以用类似的下载方案拿到。最后说一个我在实际使用中的体会处理这种“官方裸数据”最大的障碍从来不是计算本身而是对字段语义的敬畏。拿到一个字段先别急着写公式先把交易所文档翻明白再用几天的真实数据手工验算一遍再去上因子。我自己在“结算价和收盘价乖离”这个因子上栽过跟头就是因为早期直接用收盘价做结算依据导致止损单经常被莫名其妙的跳空扫掉。后来把文件里的结算价字段对齐到回测逻辑里整个系统才稳下来。你也动手跑一遍应该能感受到这种“突然开窍”的时刻。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进