高性能Tick级别高频交易引擎设计与实现
Rustaceans终极保命符:
“编译通过不代表逻辑正确,逻辑正确不代表性能达标,性能达标不代表资金安全” 💸
(此时一位路过的量化交易员默默关闭了实盘账户…)
写在前面
去年遇到一个做量化的朋友,他跟我抱怨说用简单均线策略回测年化100%+,夏普3.0,结果上实盘一个月亏了50%。当时我就在想,这TM不就是经典的回测陷阱吗?
做了几年交易系统开发,深知高频交易这块水有多深。毫秒级的延迟、复杂的市场微观结构、动态变化的流动性,每一个细节都可能让你的策略从天堂掉到地狱。
所以我决定自己撸一个高频交易引擎,就叫Zilean(源自lol的时光老头,懂的都懂)。目标很简单:让回测更接近实盘,减少那些要命的偏差。
为什么回测和实盘差这么多?
说白了就是几个问题:
市场本身就很操蛋
- 信噪比低得离谱,1:100都算好的
- 微观结构复杂,不是你想的那么简单
- 流动性说变就变
- 交易成本各种非线性,坑你没商量
回测太理想化了
- 用未来数据回测(这不是作弊吗?)
- 拿OHLC数据当tick用
- 完全不考虑订单簿深度
- 把订单执行想得太简单
- 交易成本?什么是交易成本?
实盘各种现实问题
- 网络延迟、系统延迟
- 滑点成本(这个真的很痛)
- 市场冲击
- 流动性不够
- 交易所各种限制
系统设计思路
既然要做,就要做到极致。Zilean的设计原则:
- 性能第一:微秒级延迟,支持L3订单簿
- 真实模拟:尽可能接近真实交易环境
- 大数据支持:能跑大规模历史数据
- 稳定可靠:毕竟是要用真金白银测试的
架构设计
核心模块
经过几轮重构,最终确定了这几个核心模块:
引擎核心(Engine)
- 订单簿管理(这个是重点)
- 撮合逻辑(各种边界情况都要考虑)
- 价格计算
市场模拟(Market)
- 行情生成
- 订单提交模拟
- 延迟模拟(这个很关键)
数据加载器(DataLoader)
- 历史数据加载(异步,不然会卡死)
- 实时数据处理
- 数据预处理
回测服务器(Server)
- ZMQ通信(选择ZMQ是因为性能好)
- 回测任务管理
- 结果返回
通信方案
最开始想用HTTP,后来发现延迟太高,改用ZMQ+ipc:
let context = Context::new();
let responder = context.socket(zmq::REP).unwrap();
responder.bind(&zconfig.start_url).expect("Failed to bind socket");
用REP/REQ模式,简单可靠,延迟也低。
核心实现
订单簿设计
这块踩了不少坑。最开始用HashMap,后来发现排序是个问题,改用BTreeMap:
struct OrderBook {bids: BTreeMap<Price, Vec<Order>>, // 买单队列asks: BTreeMap<Price, Vec<Order>>, // 卖单队列order_map: HashMap<OrderId, Order>, // 订单索引
}
BTreeMap天然有序,查找也快,就是内存占用稍微大一点。
动态订单队列模拟
这是整个系统最核心的部分,也是最难的。要想真实模拟市场,必须模拟订单队列。
具体怎么做?
- 维护一个基于时间和价格的订单簿
- 根据orderbook变化动态管理新订单
- 同价位订单减少时,你的订单往前排
- 订单簿变深时,你的订单还是在前面(这很重要)
然后要根据trade数据和orderbook价格变化做最优模拟,防止"虚空成交"。还要设计合理的成交概率模型,离买一卖一越近,在队列前面时成交概率越高。
这块调试了很久,各种边界情况都要考虑。
撮合引擎
/// ZileanV1 - 核心引擎结构
pub struct ZileanV1 {pub config: BtConfig,order_list: OrderList,pub account: Account,data_loader: Arc<Mutex<DataLoader>>,data_cache: VecDeque<Depth>,trade_cache: VecDeque<Trade>,pub next_tick: String,latency: LatencyModel,fill_model: FillModel,state: BacktestState,depth: Depth,
}
撮合逻辑实现了以下几种情况:
完全成交
- 买单价格 >= 卖单价格时撮合
- 按队列顺序,先进先出
- 成交后从订单簿移除
价格不匹配
- 买单 < 卖单时进入等待队列
- 价格优先,时间优先
- 买单从高到低,卖单从低到高
特殊订单处理
- 冰山订单:大单分拆,避免冲击市场
- 市价单:立即成交
- 限价单:等价格
- 止损单:触发后转限价
每笔成交都有唯一ID,记录所有细节,更新账户状态。
数据缓存策略
这块也是重点。分两种缓存:
市场数据缓存
数据库查询太慢,必须缓存。但缓存太大会卡,所以要找平衡点。关键是异步读取,不能阻塞主流程。
返回缓存
交易员收到行情后,系统已经开始算下一个tick了。这样做有两个好处:
- 模拟真实延迟(现实中收到请求时已经是下一时刻了)
- 瞬间返回数据,客户端和回测端同时计算
async fn prepare_data(&mut self) -> Result<(), std::io::Error> {// 加载初始数据let mut loader = self.data_loader.lock().await;let data = loader.load_data().await?;// 填充缓存self.data_cache.extend(data);// 初始化深度if let Some(depth) = self.data_cache.front() {self.depth = depth.clone();}Ok(())
}
延迟模拟
这个很容易被忽视,但超级重要。真实交易中,网络延迟、交易所延迟、系统处理延迟都会影响执行。
实现了四种模式:
- 无延迟:理想情况测试
- 固定延迟:所有订单加相同延迟
- 随机延迟:在范围内随机
- 正态分布延迟:最接近真实情况
struct LatencySimulator {mean_latency: Duration,std_deviation: Duration,random: ThreadRng,
}
调试时发现,哪怕一毫秒的延迟差异,对高频策略的影响都很大。
性能优化
内存优化
- 用VecDeque做数据缓存,双端操作快
- 预分配内存,减少动态分配
- 0拷贝设计,减少克隆
- L2级缓存,利用空闲时间查数据
并发优化
用Tokio异步运行时,192个工作线程(根据CPU核数调整)。线程同步用Arc:
data_loader: Arc<Mutex<DataLoader>>
这里踩过坑,一开始用RwLock,后来发现在高并发下性能反而不如Mutex。
延迟优化
批量处理减少计算开销:
self.inner.retain_mut(|order| {// 批量处理订单
});
一些踩过的坑
- 内存泄漏:最开始没注意VecDeque的清理,跑长时间回测会OOM
- 精度问题:浮点数比较要用epsilon,不能直接==
- 时区问题:不同交易所时区不同,统一用UTC
- 数据质量:脏数据会导致奇怪的回测结果,必须做数据清洗
- 并发竞争:多线程访问共享数据时的各种竞争条件
翻车现场:高频撮合的血泪史
订单簿并发修改灾难
- 高频场景下,订单簿可能在撮合过程中被修改
- 一边在遍历BTreeMap撮合订单,另一边新订单插入导致数据不一致
- 解决方案:撮合时先快照订单簿状态,或者用读写锁分离
BTreeMap迭代器失效惨案
- BTreeMap的entry操作可能触发重新平衡,导致迭代器失效
- 在遍历过程中删除节点,迭代器直接panic
- 血的教训:先收集要删除的key,遍历完再统一删除
异步撮合的竞态条件
- 异步撮合过程中,新订单插入可能破坏撮合逻辑
- 订单A正在撮合,订单B插入了更优价格,结果A先成交了
- 现在用消息队列串行化所有订单操作,虽然慢点但稳定
教训:高频交易容不得半点马虎,每个细节都可能要命 🩸
总结
做了大半年,总算把这个引擎搞出来了。性能方面基本达到预期,微秒级延迟,能处理大规模数据。
最重要的收获是:高频交易不只是追求性能,稳定性和准确性更重要。再快的系统,如果不稳定或者模拟不准确,都是白搭。
现在用这个引擎跑回测,结果和实盘的差异明显小了很多。虽然还不能完全消除偏差(这也不现实),但至少不会出现回测100%实盘-50%这种离谱情况了。
下一步计划加入更多交易所的支持,以及更复杂的订单类型。如果有同行想交流,欢迎联系。
此文章内容由云梦量化科技Buttonwood的创作投稿。