Apache Doris 4.x 在量化交易中的完整应用实践
这两年量化交易的战场越来越卷,从最初靠几台 MySQL 加 Python 脚本就能跑的 MVP 策略,到如今 tick 级行情、高频因子、实时信号推送成为标配,数据架构的迭代速度几乎决定了策略团队的迭代速度。今年上半年我们把核心数据底座从一套“MySQL + ClickHouse”的混合架构整体迁移到了 Apache Doris 4.x,到现在已经稳定运行了接近半年。这期间踩过的坑、优化过的模型、处理过的线上事故,不少经验在官方文档里根本找不到现成答案,干脆整理出来,希望能给正在做同类选型或者已经入坑 Doris 的同行一些参考。
这篇文章不是什么架构推广大赛的 PPT 文案,就是一套真实跑在量化交易环境里的 Doris 应用实践。适合三类人看:一是正在做量化数据架构选型的技术负责人,二是已经在用 Doris 但想优化行情存储和因子计算性能的开发者,三是想搞明白量化交易数据链路到底长什么样子的后端工程师。文中涉及的表结构、写入链路、查询优化、故障排查都是我们线上环境的真实操作记录,参数和思路可以直接抄作业,但在抄之前最好先理解后面的设计逻辑,因为量化数据有一个和其他行业很不一样的特点——它同时要求高吞吐写入、极低延迟点查、复杂分析计算,这三者放在一起,对任何数据库都是地狱级考验。
1. 量化交易场景下数据架构的痛点分析
1.1 量化交易对数据系统的四类核心诉求
如果只用一个词概括量化交易对数据系统的要求,那就是“全都要”。传统业务数据库通常只需要把某一类诉求做到极致,比如 MySQL 擅长事务、ClickHouse 擅长分析,但量化策略的整个生命周期横跨数据接入、因子计算、策略回测、实盘交易、绩效归因五个阶段,每个阶段对数据库的诉求完全不一样,这就是大多数团队最终被迫混合部署多套存储系统的根本原因。
具体拆开来看,量化环境一般有四个逃不掉的核心诉求。第一是实时写入能力,行情数据是典型的流式数据,A 股每分钟约产生数万条逐笔成交,美股全市场 tick 级行情一天的原始数据量动辄几十 GB,这意味着数据库必须支持持续不断的高吞吐写入,而且写入峰值出现在开盘瞬间,属于典型的脉冲式流量。第二是低延迟点查能力,实盘交易系统里,下单前需要查持仓、查最新价格、查账户资金,这些点查操作的 p99 延迟如果超过 50 毫秒,高频策略基本就废了。第三是复杂分析能力,因子计算和回测本质上就是把几年甚至十几年的历史行情拉出来做窗口计算、聚合统计、回归分析,这要求数据库具备强大的 SQL 分析能力,最好能支持窗口函数、自定义聚合、数组处理这些高级特性。第四是存储成本可控,行情数据是典型的时序型数据,价值随时间递减,但策略回测又需要长期历史数据,所以存储方案必须在热数据性能和冷数据成本之间找到平衡。
这四类诉求放在一起,就导致了一个很有意思的现象:量化团队的数据架构往往比一般互联网业务复杂得多,因为我们不是在选一个“最好的数据库”,而是在找一套能同时兼顾这些极端诉求的“最小可行组合”。
1.2 传统组合方案:“MySQL + ClickHouse”的拆东墙补西墙
我们团队在迁移到 Doris 之前,用的是一套非常流行的组合:MySQL 负责交易系统的事务数据和实时点查,ClickHouse 负责历史行情存储和因子计算。这套架构在最早期策略还没那么多时跑得挺顺畅,毕竟每个组件都只需要做好一件事。但到了策略数量增加到几十个、因子数量超过上千个之后,问题就一个接一个地暴露出来。
MySQL 这边首先扛不住的是行情写入。开盘期间每分钟数万条的写入请求,加上事务主从同步的延迟,经常导致从库查询延迟达到秒级,而量化策略对数据新鲜度极其敏感——如果你算出来的因子用的还是 3 秒前的数据,在高频场景下这个信号已经失效了。更难受的是 MySQL 对大查询的支持非常弱,一个稍微复杂一点的多表 join 加上窗口函数,就能把实例的 CPU 打满,影响线上交易系统的稳定性。ClickHouse 这边虽然分析能力强,但它的主键更新机制和点查能力实在不行,每次做持仓查询或者按标的精确拉取最新行情,响应时间都让人抓狂。
还有存储层的割裂问题。行情数据放在 ClickHouse,交易数据放在 MySQL,因子结果又放在 Redis 或者文件里,三套系统之间的数据同步靠一堆定时脚本串起来。某一天某个同步脚本出了问题,因子结果和行情数据对不上,策略跑出了完全错误的信号——这种事故我们至少出过三次。最终让我们下定决心整体迁移的导火索,是有一个日内策略需要同时关联 tick 行情、分钟线、订单流和持仓数据做实时计算,每次要跨三套系统取数,查询逻辑复杂到几乎没法维护。那时候我意识到,我们缺的不是更强性能的数据库,而是一套能统一承载这些数据的架构。
1.3 为什么最终选择了 Apache Doris 4.x
说实话,市面上具备“实时数仓”概念的数据库产品不少,StarRocks、Doris、ClickHouse、甚至 TiDB 我都认真测过。最终选择 Apache Doris 4.x,不是因为它某个单项性能最强,而是它在量化场景的综合匹配度最好。这几个理由我可以直接摆出来。
第一,Doris 天然支持高并发点查和复杂分析两种模式,这得益于它的 MPP 架构和智能物化视图机制。实测下来,在百万级分区的行情表上做“按标的加时间范围”的点查,p99 能稳定在 20 毫秒左右,这已经能满足绝大多数中频策略的实时取数需求。第二,Doris 的 Unique 模型支持主键更新且性能不错,这对持仓表、订单表这类需要频繁变更状态的交易数据来说极其关键。第三,Doris 的 Stream Load 和 Routine Load 对高吞吐流式写入支持非常好,单批次百万行写入只需要秒级完成,还支持原子生效,不需要像 ClickHouse 那样做复杂的 partition 替换。第四,Doris 4.x 对标准 SQL 的支持广度已经非常完善,窗口函数、CASE WHEN、高阶聚合、甚至 array 函数和 bitmap 函数都有,这意味着大量因子计算可以直接用 SQL 完成,无需把数据拉到 Python 层再算。
当然还有一层考虑是社区生态。Apache Doris 的社区活跃度这几年有目共睹,中文文档完善,遇到问题基本能找到解决方案,这对我们这种技术团队规模不大、又不希望被商业产品绑死的团队来说,是很实在的安全感。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Doris 4.x 关键特性在量化场景的落地
2.1 写入性能:从 Kafka 到 Doris 的实时管道
量化数据的写入链路标配是 Kafka 接行情源,然后通过消费端写入数据库。Doris 提供三种常见的接入方式:Stream Load(主动推)、Routine Load(消费 Kafka)、Insert Into(批量写入)。我们在实践中最终形成了两套链路:一套是实时行情通过 Routine Load 直接消费 Kafka 写入明细表;另一套是离线批量计算的因子结果通过 Stream Load 批量导入。
Routine Load 的使用有几个关键参数值得注意。第一个是 max_batch_interval,这个参数控制每次写入的最大时间间隔,默认是 10 秒,如果对数据新鲜度要求高,可以适当调小到 3 到 5 秒。第二个是 max_batch_rows,控制单批次最大行数,默认 10 万行。对于 A 股全市场逐笔成交这种数据量,我们设置的是 5 万行一批,因为行情数据每秒产生的量级已经足够大,不需要靠累积时间凑批次。第三个是 max_error_number,这个参数很容易被忽略,它控制的是容忍的最大解析错误行数。行情数据源偶尔会出现脏数据,如果不设置这个参数,默认容忍 0 错误,任何一个坏消息进来都会让整个消费任务暂停,而这个暂停是不可自动恢复的——我们最开始就是没注意这个参数,导致凌晨行情波动时消费任务挂了,第二天早上开盘才发现数据断档。
Stream Load 我们主要用于批量写入计算结果和回测数据。它的一个非常实用的特性是支持 CSV 和 JSON 两种格式,而且可以通过 -H "columns: col1,col2,temp=now()" 这样的方式在导入时做简单的列变换。比如我们在导入因子数据时,原始文件里只有日期和因子值,目标表里需要额外的 create_time 字段,直接在 Stream Load 里用表达式加一列就行,完全不需要写临时 Python 脚本做预处理。
2.2 查询性能:让策略实时取数成为可能
性能调优是 Doris
