3、价格时间优先原则

做交易撮合,最核心的规则就是「价格时间优先」。

说白了,就是谁出的价好,谁先成交。价格一样,那就看谁先到。这个规则听起来简单,但落地到代码里,坑不少。我当年第一次写撮合引擎时,就因为在时间精度上吃了亏,导致回测数据和实盘对不上。嗯,今天咱们就把这块彻底讲透。

3.1 价格优先规则详解

价格优先,指的是:买单价格高的优先,卖单价格低的优先

你想想看,买方愿意出更高的价,说明他更想买,理应优先成交。卖方愿意卖更低的价,说明他更想卖,也应该优先成交。这是市场的基本逻辑。

核心原则:

  • 买单:价格从高到低排序
  • 卖单:价格从低到高排序
  • 价格相同,再按时间排序

我在项目中遇到过一个问题:有些新手会把买单和卖单混在一起排序。千万别这么做。买单和卖单是两条独立的队列,各自按自己的规则排。撮合时,从买一和卖一取出来对比,能成交就成交,不能成交就等着。

举个例子:

方向 价格 数量 时间
100.5 10 09:30:01.001
100.3 5 09:30:01.002
100.2 8 09:30:01.003
100.4 6 09:30:01.004

你看,买单里100.5最高,卖单里100.2最低。两者能成交吗?能,因为买价≥卖价。成交价取哪个?这个后面章节会讲,这里先记住:价格优先决定了谁先被拿出来撮合。

3.2 时间优先规则详解

价格相同怎么办?那就看谁先到。

时间优先,说白了就是「先来后到」。同一个价格上,先挂单的先成交。这个规则在低延迟场景下特别重要。

注意:时间精度问题是个大坑。

我曾经在回测时发现,两个订单的时间戳居然一样。后来一查,是因为用了毫秒级时间戳,而系统并发量太大,同一毫秒内来了多个订单。解决办法是加一个自增序列号,或者用纳秒级时间戳。

时间优先的实现,通常有两种方式:

  1. 全局时间戳:每个订单到达时,记录系统当前时间。精度越高越好。
  2. 队列序号:每个订单进入队列时,分配一个递增的序号。序号小的优先。

我个人习惯用「时间戳+序号」的组合。时间戳做主键,序号做辅键。这样即使时间戳相同,也能通过序号区分先后。

小技巧:在分布式系统中,时间戳可能不同步。这时候可以用逻辑时钟(Lamport clock)或者全局序号生成器。我在一个跨机房的项目里就吃过这个亏,后来改用Redis生成全局递增序号,问题才解决。

3.3 优先级队列的实现思路

价格时间优先,本质上就是一个优先级队列的问题。买单按价格降序、时间升序排列。卖单按价格升序、时间升序排列。

实现方式主要有三种:

方式 优点 缺点 适用场景
有序数组 实现简单,查询快 插入慢(O(n)) 订单量小,低频场景
二叉堆 插入和删除都是O(log n) 不支持按价格范围查询 通用场景,大部分撮合引擎用这个
跳表 插入、删除、查询都是O(log n) 实现复杂,内存占用稍高 需要按价格范围查询的场景

我建议初学者先用二叉堆。为什么呢?因为它的性能足够好,而且实现起来不复杂。我在第一个生产级别的撮合引擎里用的就是二叉堆,跑了两年没出过问题。

下面是一个简单的二叉堆实现思路:

// 伪代码:买单优先级队列
class BuyPriorityQueue {
    // 内部用数组存储,下标从1开始
    vector<Order> heap;
    
    // 比较规则:价格高的优先,价格相同时间早的优先
    bool compare(Order a, Order b) {
        if (a.price != b.price) 
            return a.price > b.price;  // 价格高的在上
        return a.time < b.time;        // 时间早的在上
    }
    
    void push(Order order) {
        heap.push_back(order);
        // 上浮操作
        int i = heap.size() - 1;
        while (i > 1 && compare(heap[i], heap[i/2])) {
            swap(heap[i], heap[i/2]);
            i /= 2;
        }
    }
    
    Order pop() {
        Order top = heap[1];
        heap[1] = heap.back();
        heap.pop_back();
        // 下沉操作
        int i = 1;
        while (i * 2 < heap.size()) {
            // 找到左右子节点中更优的那个
            int j = i * 2;
            if (j + 1 < heap.size() && compare(heap[j+1], heap[j]))
                j = j + 1;
            if (compare(heap[i], heap[j]))
                break;
            swap(heap[i], heap[j]);
            i = j;
        }
        return top;
    }
}

关键点:

  • 比较函数决定了队列的排序规则
  • 上浮和下沉操作保证了堆的性质
  • 每次取top就是当前最优的订单

你可能会问:为什么不用红黑树?红黑树也能实现优先级队列啊。没错,但红黑树的实现复杂度比二叉堆高得多。除非你需要频繁地删除中间节点(比如撤单操作),否则二叉堆完全够用。

说到撤单,这里有个坑。二叉堆不支持直接删除任意节点。如果你需要撤单,得用「懒删除」策略:给每个订单加一个状态标记,出队时检查状态,如果已撤销就跳过。我一开始没注意这个,结果撤单功能上线后,堆里全是无效订单,性能直线下降。

避坑指南:我曾经在撮合引擎里直接用std::priority_queue,结果发现它不支持自定义比较器的动态更新。后来自己手写了一个二叉堆,才彻底解决问题。所以,别偷懒,自己实现一个吧。

最后,咱们用一张图来总结价格时间优先的核心逻辑:

价格时间优先原则 - 核心逻辑 买单队列(价格降序) 卖单队列(价格升序) 买一:100.5 买二:100.3 卖一:100.2 卖二:100.4 比较 成交条件:买一价格 ≥ 卖一价格 价格相同时,按时间先后排序 时间戳 + 自增序号 = 唯一确定顺序

这张图展示了整个流程:买单和卖单各自按价格排序,然后从最优价格开始比较。如果能成交,就撮合;不能成交,就等着新订单进来。价格相同时,时间戳说了算。

好了,价格时间优先原则就讲到这里。记住:价格优先是骨架,时间优先是血肉。两者结合,才能构建出一个公平、高效的撮合引擎。

总结一下:

  • 买单用最大堆(价格降序),卖单用最小堆(价格升序)
  • 时间戳精度要足够高,最好配合自增序号
  • 二叉堆是性价比最高的实现方式
  • 撤单用懒删除,别直接删堆里的元素

下一节,咱们聊聊成交价的确定规则。这个在实盘中也有不少讲究。

专注资料整理