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 第五步:尝试撮合

订单进入订单薄后,系统会立即尝试撮合。如果新进来的买单价格 >= 卖一价,或者新进来的卖单价格 <= 买一价,就会触发撮合逻辑。

下面这张图展示了限价单从进入系统到完成处理的完整流程:

限价单处理流程图 接收限价单 价格校验 价格档位映射 判断买卖方向 价格优先排队 尝试撮合 关键说明 1. 价格不能为负 2. 精度不能超限 3. 不能超涨跌停 4. 映射到标准档位 5. 买单进Bid队列 6. 卖单进Ask队列 7. 价格高/低优先 8. 同价时间优先 9. 触发撮合条件 10. 执行成交逻辑 注:每一步都可能 返回错误或拒绝订单

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 限价单处理的核心原则

总结一下,限价单处理有几个铁律:

  1. 价格不可变:限价单的价格是用户指定的,系统不能擅自修改
  2. 数量可拆分:部分成交后,剩余数量继续排队
  3. 时间戳保留:部分成交后,剩余订单的排队顺序不变
  4. 先价格后时间:撮合时先看价格,价格相同再看时间

嗯,这些原则说起来简单,但真正实现起来细节很多。我建议你在写代码时,先把这些原则写进单元测试里,确保每个边界情况都覆盖到。

一句话总结:限价单处理的核心就是「价格优先、时间优先」这八个字。所有逻辑都是围绕这八个字展开的。


专注资料整理