1. 为什么需要本地解析股票数据
作为量化交易开发者,我经常遇到这样的尴尬场景:网络突然中断,但策略急需最新的股票代码表;或者高频请求交易所接口时被限制访问。这时候才意识到,过度依赖网络API是多么脆弱。其实像通达信这类主流交易软件,早就把全市场数据缓存在本地了,只是大多数人不知道如何利用。
去年我在开发一个多账户管理工具时,就深刻体会到了本地数据的价值。当时需要实时同步十几个账户的持仓股票名称,如果每次都调用在线接口,不仅速度慢,还经常触发风控。后来发现通达信安装目录下的shm.tnf和szm.tnf文件,包含了沪市和深市所有品种的代码与名称,更新频率与行情软件同步。更重要的是,这些数据随时可用,完全不受网络环境影响。
本地数据文件还有个隐藏优势——响应速度是微秒级的。我做过测试:通过网络API获取3000只股票基础信息平均需要2.3秒,而直接读取本地文件仅需8毫秒。对于需要批量处理全市场数据的场景,比如盘后分析或策略回测,这个差距会放大成小时与分钟的区别。
2. 通达信数据文件结构详解
2.1 文件存放位置与命名规则
通达信的本地数据存放在安装目录的T0002/hq_cache子文件夹下,这个路径可能会因版本不同略有变化。我建议用Everything这类文件搜索工具直接查找shm.tnf和szm.tnf这两个文件。其中:
- shm.tnf 对应沪市数据(代码以6/9开头)
- szm.tnf 对应深市数据(代码以0/3开头)
这两个文件通常有10MB左右大小,包含了股票、基金、债券、指数等所有交易品种。有趣的是,即使你不登录行情服务器,只要曾经打开过通达信软件,这些文件就会自动生成并更新。
2.2 二进制文件结构解析
用Hex编辑器打开文件,可以看到明显的分段结构。经过反复测试验证,我总结出以下格式规律:
文件头(前50字节):
- 0-39字节:最后连接的行情服务器IP(ASCII编码)
- 40-41字节:端口号(小端存储)
- 42-45字节:最后更新日期(YYYYMMDD格式)
- 46-49字节:最后更新时间(Hmmss格式)
数据体(从第50字节开始): 每条记录固定314字节,像火车车厢一样紧密排列。关键字段的偏移量如下:
- 0-5字节:股票代码(ASCII编码)
- 23-40字节:股票名称(GB2312编码)
- 276-279字节:昨日收盘价(IEEE 754浮点数)
- 285-292字节:拼音缩写(如"ZGPA"对应"中国平安")
这里有个坑要注意:股票名称字段实际只用了18字节,但测试发现部分科创板股票会出现超长名称被截断的情况。稳妥的做法是读取后手动去除\x00填充符。
3. Python实现高效解析方案
3.1 基础读取代码示例
下面这个Python函数是我经过多次优化后的稳定版本,加入了异常处理和编码转换:
import struct from pathlib import Path def parse_tdx_stock_file(file_path): stocks = [] with open(file_path, 'rb') as f: # 跳过50字节文件头 f.seek(50) # 计算总记录数(文件大小-头大小)/314 file_size = Path(file_path).stat().st_size total_records = (file_size - 50) // 314 for _ in range(total_records): try: # 读取股票代码(6字节) code_bytes = f.read(6) stock_code = code_bytes.decode('ascii').strip() # 跳过无关字段到名称位置 f.seek(17, 1) # 相对当前位置移动 # 读取股票名称(18字节) name_bytes = f.read(18) stock_name = name_bytes.decode('gb2312', errors='ignore').replace('\x00', '') # 存入结果列表 if stock_code and stock_name: stocks.append((stock_code, stock_name)) # 跳到下条记录开始位置 f.seek(314 - 6 - 17 - 18, 1) except Exception as e: print(f"解析异常:{e}") continue return stocks3.2 性能优化技巧
当处理全市场数据时(约4500只股票),原始方案需要约120ms。通过以下优化可以提升到15ms以内:
- 批量读取:一次性读取整个文件再处理,减少IO操作
with open(file_path, 'rb') as f: data = f.read()[50:] # 跳过文件头- 内存视图:使用memoryview避免切片复制
mv = memoryview(data) for i in range(0, len(data), 314): record = mv[i:i+314]- 并行处理:对于多文件处理(沪市+深市),可以用multiprocessing.Pool
在我的ThinkPad X1上,优化后的代码处理两个文件总共只需要9.2ms,比通达信软件自身的刷新速度还快。
4. 实际应用场景案例
4.1 构建本地股票代码库
我习惯用SQLite存储解析结果,方便后续查询:
import sqlite3 def build_stock_database(): conn = sqlite3.connect('stock_info.db') c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS stocks (code TEXT PRIMARY KEY, name TEXT, market TEXT)''') sh_stocks = parse_tdx_stock_file('shm.tnf') sz_stocks = parse_tdx_stock_file('szm.tnf') # 批量插入沪市股票 c.executemany("INSERT INTO stocks VALUES (?,?,?)", [(code, name, 'SH') for code, name in sh_stocks]) # 批量插入深市股票 c.executemany("INSERT INTO stocks VALUES (?,?,?)", [(code, name, 'SZ') for code, name in sz_stocks]) conn.commit() conn.close()这个本地数据库可以支持各种灵活查询,比如:
# 查找包含"科技"的创业板股票 SELECT code, name FROM stocks WHERE name LIKE '%科技%' AND code LIKE '3%'4.2 与交易系统集成
在我的自动化交易框架中,会这样使用本地数据:
- 盘前加载股票代码映射表到内存字典
- 收到行情推送时,直接通过代码查名称,避免网络请求
- 定时任务每天收盘后更新本地文件
特别是在处理Level2行情时,上交所的原始数据只包含证券代码而不带名称,这时本地查询的优势就非常明显了。实测在每秒处理3000笔逐笔委托时,能节省约40%的CPU开销。
5. 常见问题与解决方案
5.1 文件更新机制
通达信通常会在以下时机更新本地文件:
- 每日收盘后(约15:30)
- 手动点击"下载完整数据"时
- 新股上市首日开盘前
建议在程序中加入版本检查逻辑:
def get_file_update_time(file_path): with open(file_path, 'rb') as f: # 读取42-45字节的日期字段 f.seek(42) date_bytes = f.read(4) return struct.unpack('<I', date_bytes)[0] # 小端解析5.2 特殊股票处理
遇到这些情况需要特别注意:
- 退市股票:代码虽在文件中,但名称可能变为"退市XX"
- 新股临时代码:如688XXX在上市前会显示为"新股申购"
- 转板股票:深市的转板股票代码会发生变更
我的做法是在入库时增加状态字段,并通过定期与交易所列表对比来标记异常数据。
5.3 多软件数据对比
有时会发现不同软件间的名称不一致,比如:
- 通达信:"中国平安"
- 同花顺:"中国平安(601318)"
- 东方财富:"中国平安SH601318"
建议建立标准化处理流程,比如统一去除括号内容。对于量化交易来说,更重要的是保持内部一致性而非绝对准确。