4、限价单处理逻辑:从定义到撮合全流程
限价单,说白了就是「我只愿意在这个价格买/卖,高了/低了我不干」。这是交易系统里最基础、最核心的订单类型。我个人习惯把限价单比作「有原则的订单」——它有自己的底线,不会像市价单那样为了成交不顾一切。
4.1 限价单的定义
限价单(Limit Order)是指定了最高买入价或最低卖出价的订单。举个例子:
- 买入限价单:我挂一个「买BTC,价格30000,数量1个」。意思就是:我只接受30000或更低的价格,超过30000我不买。
- 卖出限价单:我挂一个「卖ETH,价格2000,数量10个」。意思就是:我只接受2000或更高的价格,低于2000我不卖。
核心要点:限价单不保证立即成交,但保证成交价格不会比指定价格差。
我在项目中遇到过不少新手把限价单和市价单搞混。其实区分很简单——限价单是「我要这个价」,市价单是「我要马上成交」。你想想看,如果你挂一个远低于当前市价的买单,可能挂一天都成交不了,这就是限价单的代价。
4.2 限价单进入订单薄的流程
限价单进入订单薄,不是简单地把订单扔进去就完事了。它要经过一套完整的处理流程。我把它拆成几个关键步骤:
4.2.1 第一步:价格校验
系统收到限价单后,第一件事就是检查价格是否合法。比如:
- 价格不能为负数(这还用说?但我真见过有人传负价格)
- 价格精度不能超过系统设定的最小变动单位(比如最小变动是0.01,你传个0.001就不行)
- 价格不能超过涨跌停板限制(如果有的话)
4.2.2 第二步:价格档位映射
校验通过后,系统要把订单价格映射到对应的价格档位。每个交易所都有自己的价格档位规则。比如:
| 价格区间 | 最小变动单位 |
|---|---|
| 0.01 - 1.00 | 0.01 |
| 1.00 - 100.00 | 0.05 |
| 100.00 - 10000.00 | 0.10 |
嗯,这里要注意:价格档位映射不是简单的四舍五入。比如最小变动是0.05,你传个10.03进来,系统会把它调整到10.00或10.05,具体规则看交易所设计。
4.2.3 第三步:买卖方向判断
这一步决定了订单要进入买单队列还是卖单队列。买单进买盘(Bid),卖单进卖盘(Ask)。
4.2.4 第四步:价格优先排队
这是订单薄的核心规则:
- 买单:价格高的排在前面(出价高的人优先成交)
- 卖单:价格低的排在前面(要价低的人优先成交)
如果价格相同,那就看时间优先——谁先挂单谁先成交。
4.2.5 第五步:尝试撮合
订单进入订单薄后,系统会立即尝试撮合。如果新进来的买单价格 >= 卖一价,或者新进来的卖单价格 <= 买一价,就会触发撮合逻辑。
下面这张图展示了限价单从进入系统到完成处理的完整流程:
4.3 部分成交与完全成交
限价单进入订单薄后,命运有两种:要么完全成交,要么部分成交(甚至完全不成交)。
4.3.1 完全成交
如果新进来的限价单能和订单薄里的对手单完全匹配,那就是完全成交。比如:
- 卖一队列有10个ETH,价格2000
- 你挂一个买单:买10个ETH,价格2000
- 结果:你的买单和卖一队列的10个ETH完全匹配,全部成交
完全成交后,你的订单就从系统里消失了,不会留在订单薄里。
4.3.2 部分成交
部分成交更常见,也更考验系统设计。举个例子:
- 卖一队列只有5个ETH,价格2000
- 你挂一个买单:买10个ETH,价格2000
- 结果:先成交5个(和卖一匹配),剩下5个留在订单薄里,成为新的买一
实战经验:部分成交后,剩余数量要重新进入订单薄排队。我曾经见过一个系统,部分成交后剩余订单的时间戳被重置了,导致它排到了后面——这其实是bug,正确的做法是保留原始时间戳。
4.3.3 部分成交的几种场景
在实际交易中,部分成交可能发生在多个价格档位:
| 场景 | 说明 | 示例 |
|---|---|---|
| 单档位部分成交 | 只在一个价格档位成交了一部分 | 买10个,卖一只有5个,成交5个 |
| 多档位部分成交 | 跨越多个价格档位成交 | 买10个,卖一有3个,卖二有4个,卖三有3个,全部吃掉 |
| 完全不成交 | 没有对手单匹配 | 买单价格低于所有卖单 |
4.3.4 部分成交的数据结构
在代码层面,部分成交需要维护好订单的「已成交数量」和「未成交数量」。我习惯用这样的结构:
// 限价单数据结构
struct LimitOrder {
string orderId; // 订单ID
string symbol; // 交易对
Side side; // 买卖方向
double price; // 限价价格
double totalQty; // 原始数量
double filledQty; // 已成交数量
double remainingQty; // 未成交数量
int64_t timestamp; // 时间戳
OrderStatus status; // 订单状态
};
// 部分成交处理逻辑
void handlePartialFill(LimitOrder& order, double matchQty) {
order.filledQty += matchQty;
order.remainingQty = order.totalQty - order.filledQty;
if (order.remainingQty == 0) {
order.status = OrderStatus::FILLED; // 完全成交
} else {
order.status = OrderStatus::PARTIAL_FILLED; // 部分成交
}
}
避坑指南:我曾经在项目中遇到过一个严重的bug——部分成交后,剩余订单的价格被错误地修改了。原因是代码里不小心把「已成交均价」赋值给了「订单价格」。这导致后续撮合时价格错乱,差点造成生产事故。记住:限价单的价格一旦确定,就不能修改。
4.4 限价单处理的核心原则
总结一下,限价单处理有几个铁律:
- 价格不可变:限价单的价格是用户指定的,系统不能擅自修改
- 数量可拆分:部分成交后,剩余数量继续排队
- 时间戳保留:部分成交后,剩余订单的排队顺序不变
- 先价格后时间:撮合时先看价格,价格相同再看时间
嗯,这些原则说起来简单,但真正实现起来细节很多。我建议你在写代码时,先把这些原则写进单元测试里,确保每个边界情况都覆盖到。
一句话总结:限价单处理的核心就是「价格优先、时间优先」这八个字。所有逻辑都是围绕这八个字展开的。