去年有个量化团队找我复盘,他们的策略回测年化收益38%,实盘却跑不出正收益。排查了三周,问题出在数据接口——行情从交易所到策略执行,平均延迟47毫秒,刚好错过最佳入场点。
这不是个例。我见过太多团队把精力全砸在算法优化上,却对市场数据API的 latency(延迟)来源一无所知。今天把我自己踩过的坑和验证过的方案摊开讲,全是能直接落地的代码级细节。
延迟从哪来:一张图理清瓶颈
动手优化前,我先画了一张 latency 热力图。网络延迟只是冰山一角,真正拖后腿的往往是这三个隐形杀手。
JSON 序列化/反序列化在低频场景下可以忽略,但高频场景下会吃掉 5-15 毫秒。路由抖动更隐蔽——同一条专线,早高峰和午盘的延迟能差出 3 倍。最坑的是请求模式:拆成 100 个单条请求,比批量请求慢 4-7 倍,因为 TCP 握手和头部开销在反复叠加。
有个细节很多人没注意:交易所的行情网关通常有「流量整形」机制。你的请求如果踩到阈值,会被直接丢进慢队列。这不是网络问题,是策略层面的设计缺陷。
异步化改造:一行代码砍掉 40% 延迟
我最早用的同步请求模式,Python 的 requests 库 sequential 执行,10 个 ticker 串下来要 200 毫秒以上。切到 asyncio + aiohttp 后,同样的数据量压到 80 毫秒以内。
核心逻辑很简单:把 IO 等待期释放出来,让 CPU 去干别的。代码结构长这样:
async with aiohttp.ClientSession() as session: results = await asyncio.gather(*(fetch(session, url) for url in urls))
关键点在 ClientSession 的复用。很多人每次请求新建 session,SSL 握手又吃掉 20-30 毫秒。session 池化 + 连接保活,这一步 alone 就能再省 15%。
但异步化有个陷阱:并发数不是越大越好。我测试过,aiohttp 默认的 100 并发在千兆环境下是甜点,超过 300 反而因为内核调度开销导致延迟抖动。需要用 semaphore 做背压控制,把并发钉死在环境能消化的水位。
Delta 更新:带宽和算力的双重套利
全量快照是最浪费的取数方式。以沪深 300 为例,每秒推送 300 条完整记录,但 90% 以上的字段根本没变化。Delta 更新只传变更字段,带宽直接砍到 1/10,解析开销同步下降。
实现上需要本地维护一个 last_snapshot 字典,每次更新做增量 merge:
for symbol, value in update.items(): last_snapshot[symbol] = value # 只覆盖变更项
这个模式对 orderbook 特别友好。L2 十档行情里,买一卖一的变动频率是中间档位的 50 倍以上,delta 推送能把 CPU 占用率从 60% 压到 15%。
有个 edge case 要处理:重连后的首次同步必须是全量快照,否则本地缓存和交易所状态对不上。我习惯在连接建立时发一个「reset」标记,强制刷新全量,之后切回 delta 模式。
数据结构瘦身:别让 pandas 毁了你的延迟
我见过最离谱的代码:每个 tick 进来都转成 DataFrame,做一行计算再销毁。pandas 的内存分配和索引开销,在万级 tps 下能把延迟堆到 100 毫秒以上。
我的做法是 raw data 阶段只用原生 dict,计算环节再按需转 numpy array。dict 的哈希查找 O(1),插入也是 O(1),比 DataFrame 的 iloc 快一个数量级。
只有到策略层需要做向量化运算时,才批量转 DataFrame。这个边界要卡清楚:数据清洗用 dict,信号生成用 numpy,组合优化才上 pandas。三层结构各司其职,内存池化复用,GC 压力也能控住。
监控这块我搭了一套实时看板,采集三个核心指标:请求发送时间戳、API 响应时间戳、本地处理完成时间戳。三者的差值分别对应网络延迟、交易所处理延迟、本地解析延迟。
有个反直觉的发现:本地解析延迟的波动,80% 来自日志打印。把 logging 级别调到 WARNING 以上,或者改用异步日志队列,这一项能再压 3-5 毫秒。
调完这套组合拳,那个量化团队的实盘延迟从 47 毫秒压到 12 毫秒,策略夏普比率从 0.8 爬到 1.6。他们 CTO 的原话是:「早半年知道这些,去年能多赚 800 万。」
现在我的监控看板上还挂着一个指标:delta 更新覆盖率。你的行情接口,delta 模式能覆盖多少品种?
热门跟贴