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