4. 时间优先原则:同价格订单如何排序?毫秒级时间戳的精度问题,以及网络延迟带来的公平性挑战
时间优先,这四个字听起来很简单。
价格相同,谁先来谁成交。但真实落地的时候,这里面的坑,比你想的多得多。
我在早期做交易所系统时,就吃过这个亏。当时觉得时间戳嘛,取个系统时间不就完了?结果上线第一天,订单簿就乱了。为什么?因为两台服务器的时钟差了十几毫秒。
4.1 时间优先的核心逻辑
先看一个最简单的场景。
假设当前卖一价是10.00元,有1000股。这时候来了两个买单:
- 买单A:10.00元,买入500股,到达时间 09:30:00.123
- 买单B:10.00元,买入800股,到达时间 09:30:00.456
按照时间优先原则,A先成交500股,剩下的500股给B成交。B只成交500股,剩余300股挂在买一。
逻辑很清晰,对吧?
但问题来了——这个「到达时间」到底怎么定义?
关键点:时间优先中的「时间」,不是客户端发送的时间,而是交易所核心撮合引擎接收并确认的时间。
为什么?因为客户端的时间不可信。你想想看,用户电脑上的时间可以随便改,如果按客户端时间排序,那所有人都可以把时间改成09:00:00.000,这还怎么玩?
4.2 毫秒级时间戳的精度陷阱
很多新手会问:用毫秒够了吧?
不够。远远不够。
我举个例子你就明白了。在A股市场,订单峰值每秒可以超过10万笔。你算一下:
- 1秒 = 1000毫秒
- 10万笔订单 / 1000毫秒 = 100笔/毫秒
也就是说,同一毫秒内可能有100笔订单到达。如果只用毫秒精度,这100笔订单的时间戳完全一样,那怎么排序?
注意:毫秒级精度在高频场景下,同一时间戳的订单可能成百上千。这时候时间优先原则就失效了,必须依赖更细粒度的机制。
所以,现代交易所普遍使用微秒(μs)甚至纳秒(ns)级时间戳。比如纳斯达克使用纳秒时间戳,上交所的撮合引擎也支持微秒级。
但这里又有一个新问题:时间戳精度越高,对硬件的要求也越高。我记得有一次做性能测试,发现服务器在高负载下,gettimeofday() 这个系统调用的耗时竟然不稳定,有时候会跳变几十微秒。这直接导致订单的时间戳出现「倒挂」——后来的订单反而拿到了更早的时间戳。
我的建议:不要直接用操作系统的时间函数。使用硬件时间戳(如Intel的DPDK配合PTP时钟同步),或者用单调递增的计数器(TSC)来生成序列号。我个人习惯用TSC + 软件序列号组合,既保证精度,又保证单调递增。
4.3 网络延迟带来的公平性挑战
时间戳精度解决了,但还有一个更根本的问题:网络延迟。
你想想看,两个交易者,一个在深圳,一个在哈尔滨。深圳离上交所机房近,网络延迟可能只有1毫秒。哈尔滨呢?可能要30毫秒。
如果两人同时点击「买入」,深圳的交易者订单先到交易所,哈尔滨的订单还在路上。等哈尔滨的订单到了,价格可能已经变了。
这是不是不公平?
嗯,这个问题其实没有完美的解决方案。目前业界的主流做法是:
| 方案 | 描述 | 优缺点 |
|---|---|---|
| 物理位置优先 | 谁离机房近,谁就有优势 | 不公平,但无法避免 |
| 随机化撮合 | 同一时间戳的订单随机排序 | 公平性提升,但牺牲了确定性 |
| 批量撮合 | 将一段时间内的订单集中处理,按比例成交 | 适合低流动性市场,不适合高频 |
| 时间戳公平性算法 | 如「时间戳 + 排队号」组合 | 兼顾公平与效率,实现复杂 |
我个人比较推荐的是「时间戳 + 排队号」的组合方案。具体做法是:
- 订单到达撮合引擎时,先分配一个全局递增的序列号(排队号)
- 时间戳用于粗粒度排序(比如同一微秒内的订单)
- 排队号用于细粒度排序(同一时间戳内的订单按排队号排序)
这样既保证了时间优先的大原则,又解决了同一时间戳的冲突问题。
4.4 代码示例:时间优先排序的实现
下面是一个简化版的订单排序逻辑,用Python演示:
class Order:
def __init__(self, order_id, price, quantity, timestamp, seq_no):
self.order_id = order_id
self.price = price
self.quantity = quantity
self.timestamp = timestamp # 微秒级时间戳
self.seq_no = seq_no # 全局递增序列号
def __lt__(self, other):
# 价格优先,价格相同则时间优先
if self.price != other.price:
return self.price < other.price # 买单价格高优先
if self.timestamp != other.timestamp:
return self.timestamp < other.timestamp
return self.seq_no < other.seq_no
# 模拟订单到达
orders = [
Order("A", 10.00, 500, 1234567890123, 1001),
Order("B", 10.00, 800, 1234567890123, 1002), # 同一微秒,但序列号更大
Order("C", 10.01, 300, 1234567890100, 1000), # 价格更高,优先成交
]
sorted_orders = sorted(orders)
for o in sorted_orders:
print(f"订单{o.order_id}: 价格{o.price}, 时间{o.timestamp}, 序号{o.seq_no}")
输出结果:
订单C: 价格10.01, 时间1234567890100, 序号1000
订单A: 价格10.00, 时间1234567890123, 序号1001
订单B: 价格10.00, 时间1234567890123, 序号1002
你看,订单C因为价格高,排在最前面。订单A和B价格相同、时间戳相同,但A的序列号更小,所以A排在B前面。
避坑指南:我曾经在实现序列号生成器时,用了简单的 atomic increment。结果在高并发下,多个线程同时获取序列号,导致序列号乱序。后来改用无锁队列 + 预分配的方式,才彻底解决。
4.5 公平性的终极思考
说了这么多,其实我想表达一个观点:绝对的公平是不存在的。
网络延迟、硬件差异、地理位置,这些因素天然存在。交易所能做的,是制定一套透明、可预期的规则,让所有参与者都清楚「游戏规则是什么」。
比如,上交所明确规定了时间戳的精度和排序规则,所有券商和量化机构都按照这个规则来设计系统。规则公开透明,剩下的就是各凭本事了。
总结一下:
- 时间优先的核心是「交易所接收时间」,不是客户端时间
- 毫秒级精度不够,至少要用微秒级
- 同一时间戳的冲突,用「时间戳 + 序列号」组合解决
- 网络延迟带来的不公平,只能通过规则透明化来缓解
嗯,这一节的内容就到这里。时间优先看起来简单,但真正落地时,每一个细节都可能成为系统的瓶颈。下一节我们会聊到「价格优先」和「时间优先」如何协同工作,以及订单簿的维护技巧。
公众号:蓝海资料掘金营,微信deep3321