
做日内策略回测的时候我发现一个很扎心的问题手头的分钟K线根本不够用。策略在1分钟图上回测出来很漂亮但放到真实盘口里挂单成交的逻辑完全不是那么回事。后来我意识到缺的正是股票 tick 级实时行情——也就是每一笔逐笔成交的原始记录。于是我用 Python 做了几个月的折腾目标是零成本把 tick 级数据抓到本地跑通订阅、落盘、清洗的完整链路。这篇文章就是这次实战的完整记录适合量化入门、做日内策略研究、或者想搞盘口分析但不想一开始就掏钱买数据的朋友。先说结论零成本抓取 tick 级行情完全可行但前提是你得搞清楚“tick”到底意味着什么、免费数据源有哪些坑、以及代码层面哪些地方必须做防护。这篇文章我把选型思路、核心代码、踩坑记录一次性讲透。1. tick级行情到底值不值得抓先拆开这三层数据1.1 K线是剧照tick是连续视频数据层级一次看清很多人对行情数据的理解停留在K线上觉得“有了1分钟K线就等于有了日内全部信息”这个误解在实盘和回测中都会吃亏。打个比方K线相当于一部电影的剧照集每隔1分钟给你一张照片tick级行情则是完整的连续视频流每一笔成交都留下了记录。剧照集能让你看出剧情的大致走向但导演剪辑时的一个跳帧、一帧画面里的微妙表情变化只有视频流里才有。在交易所层面行情数据大致分三个层级基础快照每隔几秒推送一次当前的最新价、成交量、买卖五档盘口。这就是你在股票软件上看到的实时报价严格说属于静态切片。Level-2快照比基础快照频率更高能看到十档行情、委托队列等更细的数据国内通常是付费服务。逐笔成交tick by tick每一笔实际成交的记录包含成交时间、成交价、成交量、成交编号有的还带主动买卖方向。这才是真正意义上的 tick 级数据。这篇文章里的“tick级实时行情”指的是逐笔成交这一层。要强调的是很多公开免费接口返回的其实是基础快照只是你以较高频率轮询它才能在视觉上“重建”出tick级的变动轨迹。严格意义上的逐笔成交需要用支持推送的接口才能拿到。这两个方案我后面都会给出代码它们的使用场景完全不同。1.2 这些场景才真正需要tick数据先别急着写代码我遇到过不少朋友一听说能抓tick就兴奋结果抓了一周数据也不知道拿来干嘛。这里我先泼盆冷水如果你的策略是日线选股、周线波段或者只是每天看看涨跌tick数据对你毫无意义徒增存储负担。真正需要tick级行情的场景我总结下来主要是这四类日内高频/超短线策略持仓时间以分钟甚至秒计算分钟K线里开盘后第一秒的剧烈波动被平均掉了你的进出场逻辑和回测结果对不上。盘口微观结构研究想分析大单挂撤、买卖压力变化、主力资金是不是在偷偷吃货必须看逐笔成交和盘口的变化过程快照数据不够用。滑点与冲击成本估算回测时如果只用收盘价或均价成交会严重低估真实交易成本。有了逐笔成交你才能模拟出“我这个单子砸下去成交价会被推到哪一档”。算法交易与拆单策略把大单拆成小单需要实时感知流动性和盘口深度变化分钟级数据完全做不到。反过来如果你的需求只是“盘中提醒某个价格突破”普通快照轮询就够了不必上tick级方案。这一点一定要想清楚否则你的项目会陷入“为抓数据而抓数据”的泥潭。1.3 零成本的三条路我最后只留了两条圈子里说到免费行情通常绕不开这几条路公开HTTP行情接口比如新浪财经的 hq.sinajs.cn、腾讯财经的 qt.gtimg.cn。它们会返回最新的快照数据包含现价、成交量、五档盘口等。好处是零门槛、不需要注册账号坏处是只能轮询拿不到真正的逐笔成交而且频率拉太高容易被限制。量化平台/终端的免费模拟账号比如天勤量化TqSdk、掘金、聚宽等注册模拟账号后可以通过接口实时订阅行情。TqSdk走的是WebSocket推送能拿到逐笔成交这是我试下来最接近“零成本拿真tick”的方案。自己搭行情采集服务比如连接券商的行情服务器或者用一些开源网关程序。这个方案门槛高、稳定性差对个人研究和学习来说性价比太低我直接放弃了。实际测试之后我保留了两条路线主力方案用TqSdk免费模拟账号拿逐笔tick推送备选方案用新浪HTTP接口做高频快照轮询。前者用于正经的逐笔研究和策略回测后者用于做轻量级监控和价格提醒。接下来我会详细讲为什么这样选以及每一步具体怎么落地。2. 数据源选型与方案设计为什么我拿TqSdk当主力2.1 三个免费数据源横向对比在动手写代码之前我先把三个数据源从头到尾对比了一遍避免最后发现路线选错了白干半个月。这个对比表基于我这几个月的实测字段和结论都偏个人经验但大概率能帮你少踩坑。对比项新浪公开HTTP接口腾讯公开HTTP接口TqSdk免费模拟账号数据层级基础快照最新价五档基础快照最新价五档逐笔成交tick五档快照获取方式HTTP轮询HTTP轮询WebSocket实时推送更新粒度每次请求返回当前最新快照每次请求返回当前最新快照每一笔成交/每一次盘口变动推送注册门槛无无需要注册免费模拟账号请求限制实测不宜超过每秒1次频繁会断同样有隐性频率限制有连接数限制但推送本身不限频适合场景价格提醒、轻量监控、快速实验同左字段解析略有不同策略回测、逐笔研究、盘口微观分析主要风险接口字段偶尔变动、IP被临时限制同左模拟账号有效期、连接数限制从这个表能看出来HTTP接口和TqSdk根本不是一个层级的东西。HTTP接口适合“秒级快照”TqSdk才是“真正的tick推送”。如果你做的是日内策略回测或者盘口分析老老实实上TqSdk如果你只是想搞个盘中预警新浪或腾讯接口五分钟就能搞定没必要杀鸡用牛刀。2.2 主力方案的设计逻辑推送优于轮询我最终选择TqSdk作为主力方案最核心的一个原因是逐笔成交用HTTP轮询是不可能稳定拿到的。你想想看某一秒内可能成交了50笔你轮询一次只能拿到当前的最新快照中间49笔的信息就永远丢了。就算你把轮询频率提到每秒10次还是会有遗漏而且大概率触发接口风控。WebSocket推送则是行情服务器主动把每一笔变动推给你理论上不会漏这就是推送和轮询的本质区别。TqSdk的另一个优势是数据字段比较完整。它返回的行情对象里除了大家最熟悉的 last_price最新价、last_volume最新成交量还有五档的 bid_price、bid_volume、ask_price、ask_volume以及行情时间戳 datetime。这些字段足够我做逐笔成交落盘、五档快照重建和后续的盘口分析。当然免费模拟账号也有限制。我实测下来同一时间能维持的连接数有限而且模拟账号登录有并发限制多个脚本同时跑容易报错。我的做法是一个脚本里尽量用单进程订阅多个合约而不是开一堆脚本各连各的这样既能绕开连接数限制省下的资源还能做点清洗和监控。2.3 准备工作Python环境、依赖、模拟账号一分钟搞定先交代一下环境不用太复杂我在Windows和Linux上都跑过流程完全一致Python 3.8及以上推荐3.10或3.11太老的版本对某些依赖支持不好。用 pip 安装 TqSdkpip install tqsdk如果要跑HTTP轮询方案还需要pip install requestsTqSdk的免费模拟账号需要在官网注册注册之后会给你一个账号和密码直接用这个信息登录API。注意这不是实盘账号只是官方提供的模拟交易环境但行情数据本身是真实的对学习研究来说完全够用。安装完之后先做个快速验证确认依赖和账号都没问题from tqsdk import TqApi, TqAuth # 替换成你自己的模拟账号密码 api TqApi(authTqAuth(你的模拟账号, 你的模拟密码)) print(连接成功当前行情服务器时间:, api.get_quote(SHFE.rb2510).datetime) api.close()这里有个细节合约代码的格式是“交易所代码.合约代码”。SHFE.rb2510表示上海期货交易所的螺纹钢2510合约。合约月份是会不断变化的你实操时一定要替换成当前正在交易的主力合约代码。股票的话类似TqSdk同样支持比如SSE.600000表示上交所的浦发银行。查合约代码最稳的方式是在他们的终端工具里直接搜别靠猜。如果上面这段代码能正常打印出行情时间说明环境OK可以进入下一步。3. 核心代码实现从订阅到落盘一次跑通3.1 TqSdk订阅逐笔tick并写入CSV这是整套方案里最核心的一小段代码作用是订阅一个合约的逐笔行情把每一笔成交写入CSV文件。代码不长但每一行都有讲究。from tqsdk import TqApi, TqAuth import csv # 登录模拟账号 api TqApi(authTqAuth(你的模拟账号, 你的模拟密码)) # 订阅螺纹钢主力合约示例请替换成实际合约代码 quote api.get_quote(SHFE.rb2510) # 准备CSV文件并写入表头 with open(tick_data.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([ datetime, last_price, last_volume, bid_price, bid_volume, ask_price, ask_volume ]) f.flush() try: while True: # 关键等待服务器推送行情更新 api.wait_update() # 判断这个合约的行情是否有更新 if quote.has_update(): row [ quote.datetime, quote.last_price, quote.last_volume, quote.bid_price[0], quote.bid_volume[0], quote.ask_price[0], quote.ask_volume[0], ] writer.writerow(row) f.flush() print(row) except KeyboardInterrupt: print(手动停止数据已保存) finally: api.close()逐段拆解一下api.wait_update()是整个程序的消息泵。每次调用TqSdk都会阻塞等待服务器推送更新过来。推一次返回一次你在返回之后读取各种行情字段拿到的就是最新的。这个机制比你自己写死循环去轮询靠谱得多这也是我选择它的核心理由。quote.has_update()用来判断当前这个合约的行情到底有没有刷新。如果你同时订阅多个合约每次wait_update()返回时可能只有其中一个有更新用这个方法过滤能避免重复写入。字段里的bid_price[0]和bid_volume[0]表示买一价和买一量[0]是下标代表档位0也就是最优档。如果你后来想看五档深度直接遍历bid_price这个数组就行。f.flush()很多人会忽略它的作用是把缓冲区里的数据立即写入磁盘。如果不调用这个程序意外崩溃时你可能会丢掉最近几十秒甚至几分钟的数据。抓tick这事数据就是资产丢一条都是损失。try/finally里调用api.close()是为了确保退出时连接被正常释放避免模拟账号的连接数被占用满。这段代码跑起来之后只要在交易时段内它就会持续不断地往CSV里写逐笔记录。我实测过螺纹钢这种活跃品种一天的逐笔记录轻松超过几十万行所以存储和后续清洗不能马虎后面会讲。3.2 备选方案新浪HTTP快照抓取器再来看备选方案用新浪接口做一个轻量级的快照抓取器。这个方案拿不到逐笔成交适合做价格提醒、盘中监控这些对数据精度要求不高的场景。import requests import time session requests.Session() headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://finance.sina.com.cn, } # 一次请求可以抓多个股票用逗号分隔sh600000,sz000001 url https://hq.sinajs.cn/listsh600000,sz000001 while True: try: resp session.get(url, headersheaders, timeout5) resp.encoding gbk # 新浪接口返回的是GBK编码必须手动指定 content resp.text.strip() for line in content.split(\n): # 形如var hq_str_sh600000浦发银行,今开,昨收,现价,...; name_part, data_part line.split(, 1) code name_part.split(_)[2] # 提取sh600000部分 fields data_part.strip().split(,) if len(fields) 10: continue # 索引3是现价索引8是成交量索引9是成交额 print(code, fields[0], 现价:, fields[3], 成交量:, fields[8]) time.sleep(3) # 轮询间隔实测3秒比较稳 except Exception as e: print(抓取异常:, e) time.sleep(5)这里提醒几个关键点Referer头必须带。新浪接口会校验请求来源直接裸请求经常返回空数据。加上Referer: https://finance.sina.com.cn之后基本就稳了。编码要指定为GBK。不指定的话中文名称会乱码而且某些字段的解析会错位。resp.encoding gbk这行千万不要省。轮询间隔控制在3秒以上。我实测过1秒一次能扛一会儿但时间一长接口响应会明显变慢甚至间歇性返回空串。3秒是稳定性和实时性的一个平衡点。严格说这个方案拿到的只是“最新快照”不是逐笔成交。但如果你只是要监控价格异动每秒甚至每3秒一次的采样精度已经够用。3.3 清洗与存储别让脏数据毁掉你的回测很多新手抓完数据就急着拿去做回测结果策略表现一塌糊涂。我先说结论tick级数据的清洗和存储直接决定你后面的策略研究靠不靠谱。我的清洗流程大概是这几步第一步时间戳对齐。TqSdk的datetime字段是行情服务器的时间不是你的本地时间。一定要以这个字段为准来排序和聚合不要用自己的系统时间。我从新浪接口抓数据时它返回的日期和时间是分开的两个字段清洗时要把它们拼成完整的YYYY-MM-DD HH:MM:SS格式否则排序和去重都会出问题。第二步过滤异常值。比如涨跌停的时候五档盘口里某些价位会出现0值或者极端值临时停牌期间接口可能返回上一交易日的残留数据。我处理的方式很简单价格小于等于0的直接丢弃成交量比前一笔突然大几十倍且价格没变的标记出来人工核查多半是数据源重传或合并成交。第三步去重。TqSdk偶尔会重复推送同一条逐笔记录尤其是网络抖动重连的时候。我是通过“时间戳最新价最新量”三字段组合来去重连续两条记录这三个值完全相同就直接丢掉。存储方面我的建议分两档研究探索阶段CSV完全够用方便用Excel或者pandas快速查看。但注意CSV对几十万行数据来说读写效率一般我一般按天分文件比如tick_20250610.csv避免一个文件过大。需要反复回测或数据量大建议导入SQLite或者直接用parquet列式存储。SQLite的好处是查询方便按时间范围提取秒级数据很快。parquet则胜在压缩率高几十G的tick数据压到几个Gpandas读取也快。我个人后面是切到了SQLite因为要频繁按日期和合约代码切片。4. 实操实录我踩过最多的四个坑及排查方案4.1 频率限制、断流和重连先说频率限制。新浪接口如果你把轮询频率拉到每秒一次以上刚开始一切正常十分钟后就会开始返回空数据再过一阵子直接超时。这个限制是隐性的不会给你明确的报错信息。遇到过几次之后我做了一个“自愈机制”连续三次请求返回空数据就自动降频把休眠时间从3秒加到5秒、再到10秒等恢复之后再慢慢调回来。TqSdk这边也有断流问题。我遇到过的情况是程序跑着跑着wait_update()长时间不返回最后抛出一个网络异常。后来我查了一下原因五花八门有网络瞬断、服务器端连接超时、模拟账号状态异常等。TqSdk的API文档里其实没有特别完善的自动重连机制我的方案是写一个外层守护循环捕获异常后等几秒重新初始化TqApi和get_quote继续写入同一个CSV文件。重启之后要注意续写模式别把之前的表头又写一遍。4.2 时间戳不一致导致回测失真这个坑我印象最深因为它的后果最隐蔽。一开始我用TqSdk抓数据时为了方便直接在本地时间戳外加了一个字段回测时如果某条记录的本地时间快了几秒或者慢了几秒成交顺序就会错乱。最典型的情况是一条本应排在后面的成交因为本地时间戳滞后排序后跑到前面去了。看起来只是毫秒级的差别但对日内策略来说进出场点差一秒盈利可能从正变负。解决方式其实一句话回测排序时永远只用行情服务器自带的时间戳也就是TqSdk返回的datetime字段。本地时间只用于文件命名、查询等管理用途不要参与策略逻辑。这个原则我后来也延续到了新浪接口方案里它返回的是交易所行情时间同样以它为准。4.3 常见问题速查表我把这段时间遇到过的问题整理成一张速查表出现同类现象时可以按表排查能省不少时间。现象可能原因处理办法新浪接口返回空字符串没带Referer头加上Referer: https://finance.sina.com.cn新浪返回的中文乱码编码没指定GBK设置resp.encoding gbk频繁请求后被限制轮询频率太高降到3秒以上做降频自愈TqApi连接失败模拟账号错误或未激活检查账号密码确认已注册模拟账号wait_update长时间不返回非交易时段或合约代码错误检查合约代码确认当前是交易时段行情数据突然重复网络抖动导致推送重传按“时间价格量”去重CSV同一时间出现大量记录正常每笔成交一条回测前按时间排序必要时聚合数据缺了一段程序崩溃或断线重连加日志重连后从断点续抓4.4 半年使用心得与合规提醒这套方案我用下来半年多最大的感受是免费方案能解决90%的学习研究需求但你得接受它“不够完美”。新浪接口偶尔字段会变动、TqSdk的模拟账号有时会在盘中断连一次。这些都是免费方案固有的不确定性。我的做法是永远保持一套备选方案主方案挂了立刻切换别把鸡蛋放一个篮子里。最后还有两句很重要的提醒。第一抓取公开行情数据仅限个人学习研究一定要遵守数据源的服务条款控制在合理频率内不要给免费服务造成太大压力。第二免费数据存在延迟和遗漏不适合直接用于实盘交易决策更不构成任何投资建议。把这些边界守住了这个项目才能长久地跑下去。我个人觉得这个项目最值钱的部分不是代码本身而是你通过亲手搭建理解了行情数据的产生、传输和落地过程。以后再遇到“策略回测和实盘表现不一致”的问题你至少能排查到数据层而不是一头扎进策略逻辑里瞎调参。