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 时间优先规则详解
价格相同怎么办?那就看谁先到。
时间优先,说白了就是「先来后到」。同一个价格上,先挂单的先成交。这个规则在低延迟场景下特别重要。
注意:时间精度问题是个大坑。
我曾经在回测时发现,两个订单的时间戳居然一样。后来一查,是因为用了毫秒级时间戳,而系统并发量太大,同一毫秒内来了多个订单。解决办法是加一个自增序列号,或者用纳秒级时间戳。
时间优先的实现,通常有两种方式:
- 全局时间戳:每个订单到达时,记录系统当前时间。精度越高越好。
- 队列序号:每个订单进入队列时,分配一个递增的序号。序号小的优先。
我个人习惯用「时间戳+序号」的组合。时间戳做主键,序号做辅键。这样即使时间戳相同,也能通过序号区分先后。
小技巧:在分布式系统中,时间戳可能不同步。这时候可以用逻辑时钟(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,结果发现它不支持自定义比较器的动态更新。后来自己手写了一个二叉堆,才彻底解决问题。所以,别偷懒,自己实现一个吧。
最后,咱们用一张图来总结价格时间优先的核心逻辑:
这张图展示了整个流程:买单和卖单各自按价格排序,然后从最优价格开始比较。如果能成交,就撮合;不能成交,就等着新订单进来。价格相同时,时间戳说了算。
好了,价格时间优先原则就讲到这里。记住:价格优先是骨架,时间优先是血肉。两者结合,才能构建出一个公平、高效的撮合引擎。
总结一下:
- 买单用最大堆(价格降序),卖单用最小堆(价格升序)
- 时间戳精度要足够高,最好配合自增序号
- 二叉堆是性价比最高的实现方式
- 撤单用懒删除,别直接删堆里的元素
下一节,咱们聊聊成交价的确定规则。这个在实盘中也有不少讲究。