小方的提示:
交易系统中负责匹配和实现订单成交的服务叫做“撮合服务”,“撮合”一词对应英文“match”,即“匹配”的意思,因此为了方便读者理解,下文中部分“match”一次直接使用成“匹配”字眼,而不是用“撮合”一词。

撮合引擎是整个系统的首个组件。在本文中,我们将重点构建交易所撮合引擎的订单簿,该订单簿基于客户输入的订单来创建。我们会实现各种数据结构和算法,用于跟踪这些订单、在订单相互匹配时执行撮合操作,以及更新订单簿。所谓“撮合”,指的是当买单的价格等于或高于卖单时,二者能够相互成交。我们将在本文更详细地讨论这一概念。由于拥有最佳基础设施的交易所往往能开展最多的业务,也更受参与者青睐,因此我们会着重使这些操作的延迟尽可能低。目前,我们先不考虑交易交易所中市场数据发布者和订单网关服务器组件的细节。

在本文中,我们将涵盖以下主题:

  • 定义撮合引擎中的操作和交互
  • 构建撮合引擎并与外部数据交互
  • 构建订单簿并进行订单撮合

在本文开篇,我们先阐明一些假设,以便简化撮合引擎的设计,并限定本文的讨论范围。在第一节中,我们还将定义一些类型、常量和基本结构。

定义撮合引擎中的操作和交互

在这里,我们将声明和定义在本文构建撮合引擎时所需的类型、常量和结构。

定义一些类型和常量

让我们定义一些常用的类型定义(typedef),以记录本文后续使用的类型。我们还将定义一些常量,这些常量代表了一些假设,纯粹是为了简化我们的撮合引擎设计。请注意,您不一定需要这些限制/常量,对此感兴趣的读者可以自行进行扩展。

定义一些基本类型

我们将定义一些类型,用于存储电子交易系统中的不同属性,如下所示:

  • OrderId(订单ID):用于识别订单。
  • TickerId(证券代码ID):用于识别交易工具。
  • ClientId(客户ID):供交易所识别不同客户。
  • Price(价格):用于存储交易工具的价格。
  • Qty(数量):用于存储订单的数量值。
  • Priority(优先级):用于确定订单在某一价格水平下先进先出(FIFO,First In First Out)队列中的位置,这在“设计交易生态系统”一章中有所讨论。
  • Side(方向):用于表示订单的买卖方向。

我们还将提供一些基本方法,将这些类型转换为字符串,纯粹用于日志记录目的。接下来,让我们逐个查看这些代码块,了解相关声明:

#pragma once
#include <cstdint>
#include <limits>
#include "common/macros.h"

首先,我们定义OrderId类型来识别订单,它只是uint64_t类型,并提供一个相应的orderIdToString()方法用于记录日志。我们还添加了一个OrderId_INVALID标记方法,用于表示无效值:

namespace Common {
    typedef uint64_t OrderId;
    constexpr auto OrderId_INVALID =
        std::numeric_limits<OrderId>::max();
    inline auto orderIdToString(OrderId order_id) ->
        std::string {
        if (UNLIKELY(order_id == OrderId_INVALID)) {
            return "INVALID";
        }
        return std::to_string(order_id);
    }

我们定义TickerId类型来识别交易工具,它只是uint32_t类型,并为其添加了相应的tickerIdToString()方法。我们有一个TickerId_INVALID标记值,用于表示无效的交易工具:

typedef uint32_t TickerId;
constexpr auto TickerId_INVALID =
    std::numeric_limits<TickerId>::max();
inline auto tickerIdToString(TickerId ticker_id) ->
    std::string {
    if (UNLIKELY(ticker_id == TickerId_INVALID)) {
        return "INVALID";
    }
    return std::to_string(ticker_id);
}

ClientId类型用于区分不同的交易参与者。ClientId_INVALID值代表无效标记。clientIdToString()方法用于日志记录:

typedef uint32_t ClientId;
constexpr auto ClientId_INVALID =
    std::numeric_limits<ClientId>::max();
inline auto clientIdToString(ClientId client_id) ->
    std::string {
    if (UNLIKELY(client_id == ClientId_INVALID)) {
        return "INVALID";
    }
    return std::to_string(client_id);
}

下一个类型是Price,用于记录订单价格。我们还添加了一个Price_INVALID常量,用于表示无效价格。最后,还有一个priceToString()方法,用于将这些值转换为字符串:

typedef int64_t Price;
constexpr auto Price_INVALID =
    std::numeric_limits<Price>::max();
inline auto priceToString(Price price) -> std::string {
    if (UNLIKELY(price == Price_INVALID)) {
        return "INVALID";
    }
    return std::to_string(price);
}

Qty类型是uint32_t的类型定义,代表订单数量。我们还提供了常用的Qty_INVALID标记和qtyToString()方法,用于将其转换为字符串:

typedef uint32_t Qty;
constexpr auto Qty_INVALID =
    std::numeric_limits<Qty>::max();
inline auto qtyToString(Qty qty) -> std::string {
    if (UNLIKELY(qty == Qty_INVALID)) {
        return "INVALID";
    }
    return std::to_string(qty);
}

Priority类型只是uint64_t类型的队列位置。我们分配了Priority_INVALID标记值和priorityToString()方法:

typedef uint64_t Priority;
constexpr auto Priority_INVALID =
    std::numeric_limits<Priority>::max();
inline auto priorityToString(Priority priority) ->
    std::string {
    if (UNLIKELY(priority == Priority_INVALID)) {
        return "INVALID";
    }
    return std::to_string(priority);
}

Side类型是一个枚举类型,包含两个有效值,如以下代码块所示。我们也像之前对其他类型所做的那样,定义了一个sideToString()方法:

enum class Side : int8_t {
    INVALID = 0,
    BUY = 1,
    SELL = -1
};
inline auto sideToString(Side side) -> std::string {
    switch (side) {
        case Side::BUY:
            return "BUY";
        case Side::SELL:
            return "SELL";
        case Side::INVALID:
            return "INVALID";
    }
    return "UNKNOWN";
}
}

这些就是本文所需的所有基本类型。接下来,我们将定义一些限制条件,以简化系统设计。

定义一些限制和约束

我们将定义以下常量限制:

  • LOG_QUEUE_SIZE表示日志记录器使用的无锁队列(lock-free queue)的大小。它代表在日志记录器队列未填满的情况下,内存中能够容纳的最大字符数。
  • ME_MAX_TICKERS表示交易所支持的交易工具数量。
  • ME_MAX_CLIENT_UPDATES表示撮合引擎(matching engine)尚未处理的来自所有客户端的未处理订单请求的最大数量。这也代表订单服务器尚未发布的撮合引擎的订单响应的最大数量。
  • ME_MAX_MARKET_UPDATES表示撮合引擎生成但尚未由市场数据发布者发布的市场更新的最大数量。
  • ME_MAX_NUM_CLIENTS表示我们的交易生态系统中最多可以同时存在的市场参与者数量。
  • ME_MAX_ORDER_IDS表示单个交易工具可能的最大订单数量。
  • ME_MAX_PRICE_LEVELS表示撮合引擎维护的限价订单簿(limit order book)的最大价格深度。

请注意,这里的这些值是随意选择的;根据运行电子交易生态系统的系统容量,可以增加或减少这些值。我们选择2的幂次方是为了在计算地址时能够使用移位操作代替乘法操作;然而,在现代处理器上,这种效果微不足道,我们不建议对此过于担心。前面描述的常量的源代码如下:

namespace Common {
    constexpr size_t LOG_QUEUE_SIZE = 8 * 1024 * 1024;
    constexpr size_t ME_MAX_TICKERS = 8;
    constexpr size_t ME_MAX_CLIENT_UPDATES = 256 * 1024;
    constexpr size_t ME_MAX_MARKET_UPDATES = 256 * 1024;
    constexpr size_t ME_MAX_NUM_CLIENTS = 256;
    constexpr size_t ME_MAX_ORDER_IDS = 1024 * 1024;
    constexpr size_t ME_MAX_PRICE_LEVELS = 256;
}

这些就是我们目前所需的所有常量。现在,我们可以将注意力转移到撮合引擎内部需要的更复杂的结构上。

设计撮合引擎

我们的撮合引擎需要一些结构来与市场数据发布者和订单服务器组件进行通信。

定义MEClientRequestClientRequestLFQueue类型

MEClientRequest结构体由订单服务器用于将客户端的订单请求转发给撮合引擎。请记住,订单服务器与撮合引擎之间的通信是通过我们之前构建的无锁队列组件建立的。ClientRequestLFQueueMEClientRequest对象的无锁队列的类型定义(typedef)。

#pragma once
#include <sstream>
#include "common/types.h"
#include "common/lf_queue.h"
using namespace Common;
namespace Exchange {

这里需要注意两点:我们使用#pragma pack()指令确保这些结构体是紧凑排列的,不包含任何额外的填充。这很重要,因为在后面的章节中,这些结构体将作为扁平二进制结构通过网络发送和接收。我们还定义了一个ClientRequestType枚举(enumeration)来定义订单请求的类型——是新订单还是对现有订单的取消请求。我们还定义了一个无效(INVALID)的哨兵值(sentinel value)和一个clientRequestTypeToString()方法,用于将这个枚举转换为人类可读的字符串:

#pragma pack(push, 1)
enum class ClientRequestType : uint8_t {
    INVALID = 0,
    NEW = 1,
    CANCEL = 2
};
inline std::string
clientRequestTypeToString(ClientRequestType type) {
    switch (type) {
        case ClientRequestType::NEW:
            return "NEW";
        case ClientRequestType::CANCEL:
            return "CANCEL";
        case ClientRequestType::INVALID:
            return "INVALID";
    }
    return "UNKNOWN";
}

现在,我们可以定义MEClientRequest结构体,它将包含交易参与者向交易所发出的单个订单请求的信息。请注意,这是撮合引擎使用的内部表示形式,不一定是客户端发送的精确格式。我们将在下一篇“与市场参与者通信”中探讨客户端发送的格式。这个结构体的重要成员如下:

  • 一个ClientRequestType类型的type_变量。
  • 发送此请求的交易客户端的ClientId类型的client_id_变量。
  • 此请求所针对的交易工具的TickerId类型的ticker_id_变量。
  • 此请求所涉及的订单的订单ID(order_id_),可以是新订单的ID,也可以是引用现有订单的ID。
  • side_变量中保存的订单方向(Side)。
  • price_变量中保存的订单价格(Price)。
  • qty_变量中保存的订单数量(Qty)。

此外,我们还将添加一个简单的toString()方法,以便后续用于日志记录,如下所示:

struct MEClientRequest {
    ClientRequestType type_ = ClientRequestType::INVALID;
    ClientId client_id_ = ClientId_INVALID;
    TickerId ticker_id_ = TickerId_INVALID;
    OrderId order_id_ = OrderId_INVALID;
    Side side_ = Side::INVALID;
    Price price_ = Price_INVALID;
    Qty qty_ = Qty_INVALID;
    auto toString() const {
        std::stringstream ss;
        ss << "MEClientRequest"
           << " ["
           << "type:" << clientRequestTypeToString(type_)
           << " client:" << clientIdToString(client_id_)
           << " ticker:" << tickerIdToString(ticker_id_)
           << " oid:" << orderIdToString(order_id_)
           << " side:" << sideToString(side_)
           << " qty:" << qtyToString(qty_)
           << " price:" << priceToString(price_)
           << "]";
        return ss.str();
    }
};

如前所述,我们还定义了ClientRequestLFQueue类型定义,以表示这些结构体的无锁队列,如下代码片段所示。#pragma pack(pop)只是将对齐设置恢复为默认值——即不紧密排列(我们之前通过指定#pragma pack(push, 1)指令进行了设置)。这是因为我们只希望对通过网络发送的结构体进行紧密排列,而其他结构体不需要:

#pragma pack(pop)
typedef LFQueue<MEClientRequest> ClientRequestLFQueue;
}

我们将定义一个类似的结构体,由撮合引擎用于向订单服务器组件发送订单响应。让我们在下一小节中了解一下。

定义MEClientResponseClientResponseLFQueue类型

接下来介绍撮合引擎使用的结构实现,该结构用于发送订单响应,供订单服务器组件分发给客户端。与上一节类似,我们还将定义ClientResponseLFQueue,它是MEClientResponse对象的无锁队列(lock-free queue)。

#pragma once
#include <sstream>
#include "common/types.h"
#include "common/lf_queue.h"
using namespace Common;

namespace Exchange {

首先,我们定义一个ClientResponseType枚举,用于表示客户端订单的响应类型。除了无效标记值INVALID外,它还包含代表新订单请求被接受、订单被取消、订单被执行,以及取消请求被撮合引擎拒绝等情况的值。我们还添加了clientResponseTypeToString()方法,用于将ClientResponseType值转换为字符串:

#pragma pack(push, 1)
enum class ClientResponseType : uint8_t {
    INVALID = 0,
    ACCEPTED = 1,
    CANCELED = 2,
    FILLED = 3,
    CANCEL_REJECTED = 4
};
inline std::string
clientResponseTypeToString(ClientResponseType type) {
    switch (type) {
        case ClientResponseType::ACCEPTED:
            return "ACCEPTED";
        case ClientResponseType::CANCELED:
            return "CANCELED";
        case ClientResponseType::FILLED:
            return "FILLED";
        case ClientResponseType::CANCEL_REJECTED:
            return "CANCEL_REJECTED";
        case ClientResponseType::INVALID:
            return "INVALID";
    }
    return "UNKNOWN";
}

最后,我们定义MEClientResponse消息,当客户端订单有更新时,撮合引擎内部使用该消息与交易客户端进行订单响应消息的通信。在查看源代码之前,该结构体中的重要数据成员列举如下:

  • ClientResponseType type_变量:用于表示客户端响应的类型。
  • ClientId client_id_变量:用于表示响应消息针对的是哪个市场参与者。
  • TickerId ticker_id_变量:用于表示此响应的交易工具。
  • client_order_id_变量:类型为OrderId,用于标识此响应消息所涉及订单的订单ID。这个OrderId是客户端在原始的订单MEClientRequest消息中发送的ID。
  • market_order_id_变量:同样为OrderId类型,但它用于在公开市场数据流中标识此订单。这个OrderId在所有市场参与者中是唯一的,因为不同市场参与者有可能发送具有相同client_order_id_值的订单。即便如此,两个client_order_id_相同的订单在响应中也会有不同的market_order_id_值。在为此订单生成市场更新时也会用到这个market_order_id_值。
  • Side side_变量:用于表示此订单响应的方向(买卖方向)。
  • price_变量:表示此客户端响应更新中的价格,以及订单是被接受、取消还是执行。
  • Qty exec_qty_变量:仅在订单执行的情况下使用。该变量用于保存此MEClientResponse消息中执行的数量。这个值不是累计的,意味着当一个订单被多次部分执行时,每次执行都会生成一个MEClientResponse消息,且该消息仅包含此次执行的数量,而非所有执行的累计数量。
  • Qty leaves_qty_变量:用于表示原始订单数量在撮合引擎订单簿中仍有效的部分。这用于表明订单簿中该特定订单的规模,即仍可用于进一步执行的部分。

最后,为了便于记录日志,我们还定义了常用的toString()方法。如前所述,MEClientResponse结构的定义如下:

struct MEClientResponse {
    ClientResponseType type_ = ClientResponseType::INVALID;
    ClientId client_id_ = ClientId_INVALID;
    TickerId ticker_id_ = TickerId_INVALID;
    OrderId client_order_id_ = OrderId_INVALID;
    OrderId market_order_id_ = OrderId_INVALID;
    Side side_ = Side::INVALID;
    Price price_ = Price_INVALID;
    Qty exec_qty_ = Qty_INVALID;
    Qty leaves_qty_ = Qty_INVALID;
    auto toString() const {
        std::stringstream ss;
        ss << "MEClientResponse"
           << " ["
           << "type:" << clientResponseTypeToString(type_)
           << " client:" << clientIdToString(client_id_)
           << " ticker:" << tickerIdToString(ticker_id_)
           << " coid:" << orderIdToString(client_order_id_)
           << " moid:" << orderIdToString(market_order_id_)
           << " side:" << sideToString(side_)
           << " exec_qty:" << qtyToString(exec_qty_)
           << " leaves_qty:" << qtyToString(leaves_qty_)
           << " price:" << priceToString(price_)
           << "]";
        return ss.str();
    }
};
#pragma pack(pop)

ClientResponseLFQueue类型定义如下,它表示我们前面讨论的结构的无锁队列:

typedef LFQueue<MEClientResponse> ClientResponseLFQueue;
}

关于表示客户端与撮合引擎之间请求和响应所需的结构讨论到此结束。接下来,让我们进入下一小节,讨论市场更新结构。

定义MEMarketUpdateMEMarketUpdateLFQueue类型

市场更新结构用于撮合引擎向市场数据发布组件提供市场数据更新。我们还有MEMarketUpdateLFQueue类型,用于表示MEMarketUpdate对象的无锁队列。

#pragma once
#include <sstream>
#include "common/types.h"
using namespace Common;
namespace Exchange {

MEMarketUpdate结构体也需要是一个紧凑结构体(packed structure),因为它将作为通过网络发送和接收的消息的一部分;因此,我们再次使用#pragma pack()指令。在定义结构体之前,我们需要定义MarketUpdateType枚举,该枚举表示订单市场更新中的更新操作。除了无效标记值INVALID外,它还可用于表示订单簿中订单被添加、修改、取消等事件,以及市场中的交易事件:

#pragma pack(push, 1)
enum class MarketUpdateType : uint8_t {
    INVALID = 0,
    ADD = 1,
    MODIFY = 2,
    CANCEL = 3,
    TRADE = 4
};
inline std::string
marketUpdateTypeToString(MarketUpdateType type) {
    switch (type) {
        case MarketUpdateType::ADD:
            return "ADD";
        case MarketUpdateType::MODIFY:
            return "MODIFY";
        case MarketUpdateType::CANCEL:
            return "CANCEL";
        case MarketUpdateType::TRADE:
            return "TRADE";
        case MarketUpdateType::INVALID:
            return "INVALID";
    }
    return "UNKNOWN";
}

最后,我们定义MEMarketUpdate结构体,它包含以下重要数据成员:

  • MarketUpdateType type_变量:用于表示市场更新的类型。
  • OrderId order_id_变量:用于表示限价订单簿(limit order book)中适用此订单更新的特定订单。
  • TickerId ticker_id_变量:用于表示此更新所适用的交易工具。
  • Side side_变量:用于表示此订单的方向。
  • Price price_变量:用于表示此市场订单更新中订单的具体价格。
  • Priority priority_变量:如前所述,它将用于指定此订单在先进先出(FIFO)队列中的具体位置。我们会构建一个相同价格的所有订单的先进先出队列,该字段用于指定此订单在该队列中的位置。

下面的代码块展示了完整的MEMarketUpdate结构体,以及MEMarketUpdateLFQueue类型定义,它表示MEMarketUpdate结构体消息的无锁队列:

struct MEMarketUpdate {
    MarketUpdateType type_ = MarketUpdateType::INVALID;
    OrderId order_id_ = OrderId_INVALID;
    TickerId ticker_id_ = TickerId_INVALID;
    Side side_ = Side::INVALID;
    Price price_ = Price_INVALID;
    Qty qty_ = Qty_INVALID;
    Priority priority_ = Priority_INVALID;
    auto toString() const {
        std::stringstream ss;
        ss << "MEMarketUpdate"
           << " ["
           << " type:" << marketUpdateTypeToString(type_)
           << " ticker:" << tickerIdToString(ticker_id_)
           << " oid:" << orderIdToString(order_id_)
           << " side:" << sideToString(side_)
           << " qty:" << qtyToString(qty_)
           << " price:" << priceToString(price_)
           << " priority:" << priorityToString(priority_)
           << "]";
        return ss.str();
    }
};
#pragma pack(pop)

typedef Common::LFQueue<Exchange::MEMarketUpdate>
MEMarketUpdateLFQueue;
}

至此,我们完成了表示和发布来自撮合引擎的市场数据更新所需的结构定义。在下一小节中,我们将构建一些结构并定义一些类型,用于构建限价订单簿。

设计订单簿

在本节中,我们将定义一些基础模块,用于高效地构建、维护和更新限价订单簿(limit order book)。在讨论所需的各个结构和对象之前,我们先展示一个图表,帮助你从视觉上理解限价订单簿的实现。

限价订单簿由买单(称为“买盘” bids)和卖单(称为“卖盘” asks)组成。在我们的撮合引擎(matching engine)中,相同价格的订单按先进先出(FIFO,First In First Out)的顺序排列。我们在 “设计交易生态系统” 一章的 “设计交易交易所的C++撮合引擎” 一节中讨论过这些细节。

对于我们在撮合引擎内部构建的订单簿,我们有一个包含活跃订单的买盘价格和卖盘价格列表。每个价格水平由MEOrdersAtPrice结构体表示,如下图所示。买盘按价格从高到低排序,卖盘按价格从低到高排序。每个MEOrdersAtPrice结构体将单个订单按优先级从高到低存储在一个双向链表中。每个订单的信息包含在MEOrder结构体中。我们将在一个OrdersAtPriceHashMap类型的哈希表中跟踪每个价格水平,该哈希表以价格水平的价格作为索引。我们还将通过market_order_id_值在一个OrderHashMap类型的哈希表中跟踪每个MEOrder对象。表示我们撮合引擎订单簿设计的图表如下。

撮合引擎内部限价订单簿的设计

既然我们已经讨论了限价订单簿数据结构的整体设计以及构成它的组件,就可以开始定义实现该设计所需的基本结构体了。在下一小节中,我们将首先设计基本模块——用于保存单个订单信息的MEOrder结构体。

定义MEOrder、OrderHashMap和ClientOrderHashMap类型

第一个结构体用于在订单簿中保存单个限价订单的信息,我们称之为MEOrder。以下代码块展示了该结构体。

MEOrder结构体包含以下重要数据成员,用于保存表示限价订单簿中单个订单所需的属性:

  • 一个TickerId类型的ticker_id_变量,用于表示该订单对应的金融工具。
  • 一个ClientId类型的client_id_变量,用于记录下该订单的市场参与者。
  • 如前所述,有两个OrderId集合——client_order_id_,即客户在订单请求中发送的订单ID;以及market_order_id_,由撮合引擎生成,在所有客户中是唯一的。
  • Side类型的side_,用于表示订单是买单还是卖单。
  • 一个Price类型的price_变量,用于表示订单的价格。
  • Qty类型的qty_,用于表示订单簿中仍处于活跃状态的订单数量。
  • 一个Priority类型的priority_变量,如前所述,它将表示此订单在具有相同side_price_值的其他MEOrder实例队列中的准确位置。
  • MEOrder结构体还有两个指向其他MEOrder对象的指针。这是因为,如前一节所述,MEOrder对象在MEOrdersAtPrice结构体中也作为按价格水平排列的订单双向链表进行维护:
#pragma once
#include <array>
#include <sstream>
#include "common/types.h"
using namespace Common;
namespace Exchange {

struct MEOrder {
    TickerId ticker_id_ = TickerId_INVALID;
    ClientId client_id_ = ClientId_INVALID;
    OrderId client_order_id_ = OrderId_INVALID;
    OrderId market_order_id_ = OrderId_INVALID;
    Side side_ = Side::INVALID;
    Price price_ = Price_INVALID;
    Qty qty_ = Qty_INVALID;
    Priority priority_ = Priority_INVALID;
    MEOrder *prev_order_ = nullptr;
    MEOrder *next_order_ = nullptr;
    // only needed for use with MemPool.
    MEOrder() = default;
    MEOrder(TickerId ticker_id, ClientId client_id, OrderId
            client_order_id, OrderId market_order_id, Side side,
            Price price, Qty qty, Priority priority, MEOrder
            *prev_order, MEOrder *next_order) noexcept
        :     ticker_id_ (ticker_id),
              client_id_ (client_id),
              client_order_id_ (client_order_id),
              market_order_id_ (market_order_id),
              side_ (side),
              price_ (price),
              qty_ (qty),
              priority_ (priority),
              prev_order_ (prev_order),
              next_order_ (next_order) {}
    auto toString() const -> std::string;
};

此外,OrderHashMap类型用于表示一个哈希表,使用std::array实现,其中OrderId作为键,MEOrder作为值。我们还将定义另一种类型ClientOrderHashMap,它也是一个哈希表,使用std::array实现,用于表示从ClientIdOrderHashMap对象的映射:

typedef std::array<MEOrder *, ME_MAX_ORDER_IDS> OrderHashMap;
typedef std::array<OrderHashMap, ME_MAX_NUM_CLIENTS> ClientOrderHashMap;
}

我们展示MEOrder结构体的toString()方法,该方法非常简单,可在ch6/exchange/matcher/me_order.cpp文件中找到:

#include "me_order.h"
namespace Exchange {
auto MEOrder::toString() const -> std::string {
    std::stringstream ss;
    ss << "MEOrder" << "["
       << "ticker:" << tickerIdToString(ticker_id_) << " "
       << "cid:" << clientIdToString(client_id_) << " "
       << "oid:" << orderIdToString(client_order_id_) << " "
       << "moid:" << orderIdToString(market_order_id_) << " "
       << "side:" << sideToString(side_) << " "
       << "price:" << priceToString(price_) << " "
       << "qty:" << qtyToString(qty_) << " "
       << "prio:" << priorityToString(priority_) << " "
       << "prev:" << orderIdToString(prev_order_ ?
                                     prev_order_->market_order_id_ :
                                     OrderId_INVALID) << " "
       << "next:" << orderIdToString(next_order_ ?
                                     next_order_->market_order_id_ :
                                     OrderId_INVALID) << "]";
    return ss.str();
}
}

接下来,我们将构建一些其他结构来包含和管理订单对象。

定义MEOrdersAtPrice和OrdersAtPriceHashMap类型

如图6.1所示,我们定义了另一个结构体MEOrdersAtPrice,用于维护MEOrder对象列表。以下代码块展示的这个结构体,将用于保存所有以相同价格输入的订单,并按先进先出的优先级顺序排列。这是通过创建一个MEOrder对象的单向链表来实现的,该链表按优先级从高到低排列。为此,我们创建了一个MEOrder类型指针first_me_order_变量,它将表示这个价格水平的第一个订单,后续订单按先进先出的顺序链接在一起。

MEOrdersAtPrice结构体还有两个指向MEOrdersAtPrice对象的指针,一个指向前一个(prev_entry_),一个指向后一个(next_entry_)。这是因为该结构体本身是MEOrdersAtPrice对象双向链表中的一个节点。MEOrdersAtPrice的双向链表按买卖双方从最激进价格到最不激进价格排列。

这个结构体包含的另外两个变量分别是Side类型的side_变量和Price类型的price_变量,它们分别表示这个价格水平的交易方向和价格:

namespace Exchange {
struct MEOrdersAtPrice {
    Side side_ = Side::INVALID;
    Price price_ = Price_INVALID;
    MEOrder *first_me_order_ = nullptr;
    MEOrdersAtPrice *prev_entry_ = nullptr;
    MEOrdersAtPrice *next_entry_ = nullptr;

我们添加一个默认构造函数和一个简单的自定义构造函数来初始化这个结构体的对象:

    MEOrdersAtPrice() = default;
    MEOrdersAtPrice(Side side, Price price, MEOrder
    *first_me_order, MEOrdersAtPrice *prev_entry,
    MEOrdersAtPrice *next_entry)
        : side_(side), price_(price),
          first_me_order_(first_me_order),
          prev_entry_(prev_entry), next_entry_(next_entry) {}

我们还添加了一个简单的toString()方法用于日志记录,如下所示:

    auto toString() const {
        std::stringstream ss;
        ss << "MEOrdersAtPrice["
           << "side:" << sideToString(side_) << " "
           << "price:" << priceToString(price_) << " "
           << "first_me_order:" << (first_me_order_ ?
                                   first_me_order_->toString() : "null") << " "
           << "prev:" << priceToString(prev_entry_ ?
                                      prev_entry_->price_ : Price_INVALID) << " "
           << "next:" << priceToString(next_entry_ ?
                                      next_entry_->price_ : Price_INVALID) << "]";
        return ss.str();
    }
};

OrdersAtPriceHashMap类型表示一个哈希映射(hash map),通过std::array实现,用于表示从PriceMEOrdersAtPrice的映射:

typedef std::array<MEOrdersAtPrice *,
                  ME_MAX_PRICE_LEVELS> OrdersAtPriceHashMap;
}

关于为撮合引擎和限价订单簿设置初始类型、定义和基本结构的内容到此结束。接下来,我们可以看看撮合引擎框架是如何构建的。

构建撮合引擎并交换外部数据

在本节中,我们将构建撮合引擎类的各个部分。处理客户端请求、构建和更新限价订单簿,以及生成订单响应和市场更新等许多繁重的工作将交给订单簿类来完成,我们将在下一节讨论订单簿类。请重读上一章“设计我们的交易生态系统”中的“设计交易交易所中的C++撮合引擎”部分,回顾一下我们将在本节构建的组件以及其背后的设计原则。我们在此展示该章中的图表,以便于参考,该图表展示了撮合引擎的设计。

撮合引擎是一个独立的执行线程,它从ClientRequestLFQueue中获取订单请求,将订单响应发布到ClientResponseLFQueue,并将市场更新发布到MEMarketUpdateLFQueue。让我们首先为撮合引擎声明并定义一些用于构造、析构、线程管理和通用功能的代码。

构建撮合引擎

MatchingEngine类包含几个重要的数据成员 - 首先,一个OrderBookHashMap对象用于跟踪每个交易工具的限价订单簿。该类还包含指向以下对象的指针 - ClientRequestLFQueueClientResponseLFQueueMEMarketUpdateLFQueue,所有这些对象都将在构造函数中传递给它。让我们首先为撮合引擎声明并定义一些用于构造、析构、线程管理和通用功能的代码。我们还将有一个布尔变量run_来跟踪线程状态,一个time_str_字符串和一个Logger对象用于输出一些日志。

首先,构建撮合引擎所需包含的头文件如下:

#pragma once
#include "common/thread_utils.h"
#include "common/lf_queue.h"
#include "common/macros.h"
#include "order_server/client_request.h"
#include "order_server/client_response.h"
#include "market_data/market_update.h"
#include "me_order_book.h"

接下来,我们声明构造函数和析构函数,并添加start()stop()方法,分别用于启动和停止我们即将构建的主撮合引擎循环的执行:

namespace Exchange {
class MatchingEngine final {
public:
    MatchingEngine(ClientRequestLFQueue *client_requests,
                   ClientResponseLFQueue *client_responses,
                   MEMarketUpdateLFQueue *market_updates);

    ~MatchingEngine();
    auto start() -> void;
    auto stop() -> void;

我们添加常用的构造函数和赋值运算符的样板代码,以防止意外复制:

    // 删除默认、复制和移动构造函数以及赋值运算符。
    MatchingEngine() = delete;
    MatchingEngine(const MatchingEngine &) = delete;
    MatchingEngine(const MatchingEngine &&) = delete;
    MatchingEngine &operator=(const MatchingEngine &) =
        delete;
    MatchingEngine &operator=(const MatchingEngine &&) =
        delete;

最后,我们添加如前所述的MatchingEngine类的数据成员。OrderBookHashMap类型的ticker_order_book_变量用于存储每个交易工具的MEOrderBook。我们分别存储ClientRequestLFQueueClientResponseLFQueueMEMarketUpdateLFQueue类型的incoming_requests_outgoing_ogw_responses_outgoing_md_updates_指针,以便与其他线程进行通信。然后,我们有一个run_布尔变量,我们将其标记为volatile,因为它将从不同的线程访问:

private:
    OrderBookHashMap ticker_order_book_;
    ClientRequestLFQueue *incoming_requests_ = nullptr;
    ClientResponseLFQueue *outgoing_ogw_responses_ =
        nullptr;
    MEMarketUpdateLFQueue *outgoing_md_updates_ = nullptr;
    volatile bool run_ = false;
    std::string time_str_;
    Logger logger_;
};
}

让我们看看构造函数、析构函数和start()方法的实现,start()方法创建并启动一个线程来执行run()方法(我们很快会讲到)。这段代码在ch6/exchange/matcher/matching_engine.cpp源文件中。

构造函数本身很简单 - 它初始化内部数据成员,并为每个支持的交易工具创建一个MEOrderBook实例:

#include "matching_engine.h"
namespace Exchange {
MatchingEngine::MatchingEngine(ClientRequestLFQueue
                               *client_requests, ClientResponseLFQueue
                               *client_responses, MEMarketUpdateLFQueue
                               *market_updates)
    : incoming_requests_(client_requests),
      outgoing_ogw_responses_(client_responses),
      outgoing_md_updates_(market_updates),
      logger_("exchange_matching_engine.log") {
    for (size_t i = 0; i < ticker_order_book_.size(); ++i) {
        ticker_order_book_[i] = new MEOrderBook(i, &logger_,
                                               this);
    }
}

析构函数的操作与构造函数相反,它重置内部数据成员变量。它还会删除在构造函数中创建的MEOrderBook对象:

MatchingEngine::~MatchingEngine() {
    run_ = false;
    using namespace std::literals::chrono_literals;
    std::this_thread::sleep_for(1s);
    incoming_requests_ = nullptr;
    outgoing_ogw_responses_ = nullptr;
    outgoing_md_updates_ = nullptr;
    for (auto& order_book : ticker_order_book_) {
        delete order_book;
        order_book = nullptr;
    }
}

start()方法创建并启动一个新线程,并为其分配MatchingEngine::run()方法。在这之前,它启用run_标志,因为它控制着run()方法的执行:

auto MatchingEngine::start() -> void {
    run_ = true;

    ASSERT(Common::createAndStartThread(-1,
                                       "Exchange/MatchingEngine", [this]() { run(); }) !=
               nullptr, "Failed to start MatchingEngine thread.");
}

stop()方法只是将run_标志设置为false,这反过来会导致run()方法退出其主循环,不过这一点很快就会清楚:

auto MatchingEngine::stop() -> void {
    run_ = false;
}
}

接下来,我们将研究处理撮合引擎如何消费订单请求以及发布订单响应和市场更新的源代码。但首先,让我们展示撮合引擎线程执行的主要run()循环。这段代码非常简单 - 它只是从incoming_requests_无锁队列中消费MEClientRequest对象,并将它们转发到processClientRequest()方法。为了实现这一点,它只需检查LFQueue::getNextToRead()方法,查看是否有有效的条目可供读取,如果有,则将该条目的对象转发进行处理,并使用LFQueue::updateReadIndex()方法更新无锁队列中的读取索引。这段代码在ch6/exchange/matcher/matching_engine.h源文件中:

auto run() noexcept {
    logger_.log("%:% %() %\n", __FILE__, __LINE__,
                __FUNCTION__, Common::getCurrentTimeStr(&time_str_));
    while (run_) {
        const auto me_client_request =
            incoming_requests_->getNextToRead();
        if (LIKELY(me_client_request)) {
            logger_.log("%:% %() % Processing %\n", __FILE__,
                        __LINE__, __FUNCTION__,
                        Common::getCurrentTimeStr(&time_str_),
                        me_client_request->toString());
            processClientRequest(me_client_request);
            incoming_requests_->updateReadIndex();
        }
    }
}

现在,让我们看看处理客户端请求的源代码。

从订单网关队列消费数据并向其发布数据

首先,我们从matching_engine.h头文件中MatchingEngine类的processClientRequest()实现开始。这个实现只是检查MEClientRequest的类型,并将其转发到相应交易工具的限价订单簿。它通过访问ticker_order_book_容器,使用MEClientRequest中的ticker_id_字段,找到这个MEClientRequest对应的正确订单簿实例:

auto processClientRequest(const MEClientRequest *client_request)
noexcept {
    auto order_book = ticker_order_book_[client_request
                                       ->ticker_id_];

对于尝试添加新订单(ClientRequestType::NEW)的客户端请求,我们调用MEOrderBook::add()方法并让它处理该请求:

    switch (client_request->type_) {
    case ClientRequestType::NEW: {
        order_book->add(client_request->client_id_,
                        client_request->order_id_,
                        client_request->ticker_id_,
                        client_request->side_, client_request->price_,
                        client_request->qty_);
    }
    break;

类似地,尝试取消现有订单(ClientRequestType::CANCEL)的客户端请求会被转发到MEOrderBook::cancel()方法:

    case ClientRequestType::CANCEL: {
        order_book->cancel(client_request->client_id_,
                           client_request->order_id_,
                           client_request->ticker_id_);
    }
    break;
    default: {
        FATAL("Received invalid client-request-type:" +
              clientRequestTypeToString(client_request->type_));
    }
    break;
    }
}

我们还将在同一个类中定义一个方法,限价订单簿将使用这个方法通过MEClientResponse消息发布订单响应。这个方法只是将响应写入outgoing_ogw_responses_无锁队列并推进写入索引。它通过调用LFQueue::getNextToWriteTo()方法找到下一个有效的写入MEClientResponse消息的索引,将数据移动到该位置,并通过调用LFQueue::updateWriteIndex()方法更新下一个写入索引来实现:

auto sendClientResponse(const MEClientResponse *client_response)
noexcept {
    logger_.log("%:% %() % Sending %\n", __FILE__, __LINE__,
                __FUNCTION__, Common::getCurrentTimeStr(&time_str_),
                client_response->toString());
    auto next_write = outgoing_ogw_responses_
                      ->getNextToWriteTo();
    *next_write = std::move(*client_response);
    outgoing_ogw_responses_->updateWriteIndex();
}

现在,我们来看一些与刚才类似的代码,不过它是用于发布市场数据更新的。

向市场数据发布者队列发布数据

ch6/exchange/matcher/matching_engine.h中的sendMarketUpdate()方法由限价订单簿用于通过MEMarketUpdate结构体发布市场数据更新。它只是写入outgoing_md_updates_无锁队列并推进写入指针。它的实现方式与我们之前看到的完全一样——通过调用getNextToWriteTo()方法,将MEMarketUpdate消息写入该位置,并使用updateWriteIndex()更新下一个写入索引:

auto sendMarketUpdate(const MEMarketUpdate *market_update) noexcept {
    logger_.log("%:% %() % Sending %\n", __FILE__, __LINE__,
                __FUNCTION__, Common::getCurrentTimeStr(&time_str_),
                market_update->toString());
    auto next_write = outgoing_md_updates_
                      ->getNextToWriteTo();
    *next_write = *market_update;
    outgoing_md_updates_->updateWriteIndex();
}

本节内容到此结束,现在我们已经完成了撮合引擎的实现。在下一小节中,我们将把这些部分整合到交易交易所的二进制文件中,不过限价订单簿的实现除外,这是我们将在本文最后一节讨论的内容。

构建交易所应用程序二进制文件

现在我们可以构建交易交易所的二进制文件了。我们将实例化撮合引擎对象所需的三个无锁队列,分别用于订单请求、订单响应和市场更新。我们还将创建MatchingEngine对象并启动线程,然后这个二进制文件会一直处于休眠状态。由于应用程序进入了一个无限循环,我们还将为这个应用程序安装一个信号处理程序,以捕获外部信号并优雅地退出。请注意,在本文后面的章节中,当我们在交易交易所端构建需要添加到这里的订单服务器和市场数据发布者组件时,这段代码将得到扩展。让我们分解这个源文件,理解每个代码块。

首先,我们添加一些变量,它们将是Logger对象和MatchingEngine对象的指针。我们还将添加一个signal_handler()方法,在终止交易所应用程序时调用。信号处理程序只是删除这些对象并退出:

#include <csignal>
#include "matcher/matching_engine.h"
Common::Logger* logger = nullptr;
Exchange::MatchingEngine* matching_engine = nullptr;
void signal_handler(int) {
    using namespace std::literals::chrono_literals;
    std::this_thread::sleep_for(10s);
    delete logger; 
    logger = nullptr;
    delete matching_engine; 
    matching_engine = nullptr;
    std::this_thread::sleep_for(10s);
    exit(EXIT_SUCCESS);
}

目前main()方法还比较简单,直到我们在下一篇添加其他组件。它使用std::signal()函数安装signal_handler()方法,以捕获外部的SIGINT信号。SIGINT信号的值是2,在Linux中按下Ctrl + C,或者向进程ID(PID)发送kill –2 PID时,就会将这个信号发送给正在运行的进程。这是优雅终止进程的常用方法。然后,它初始化ClientRequestLFQueue变量client_requestsClientResponseLFQueue变量client_responses,大小都为ME_MAX_CLIENT_UPDATES。我们还初始化MEMarketUpdateLFQueue类型的无锁队列变量market_updates,容量为ME_MAX_MARKET_UPDATESmain()方法还使用Logger类的一个实例初始化logger变量:

int main(int, char **) {
    logger = new Common::Logger("exchange_main.log");
    std::signal(SIGINT, signal_handler);
    const int sleep_time = 100 * 1000;
    Exchange::ClientRequestLFQueue
        client_requests(ME_MAX_CLIENT_UPDATES);
    Exchange::ClientResponseLFQueue
        client_responses(ME_MAX_CLIENT_UPDATES);
    Exchange::MEMarketUpdateLFQueue
        market_updates(ME_MAX_MARKET_UPDATES);

最后,main()方法使用我们创建的MatchingEngine类的一个实例初始化matching_engine变量,并将前面代码块中它需要的三个无锁队列传递给它。然后调用start()方法,以便主撮合引擎线程可以开始执行。此时,main()方法完成了任务,所以它进入一个无限循环,大部分时间都在休眠,等待会终止这个进程的外部信号:

    std::string time_str;
    logger->log("%:% %() % Starting Matching Engine...\n",
                __FILE__, __LINE__, __FUNCTION__,
                Common::getCurrentTimeStr(&time_str));
    matching_engine = new
        Exchange::MatchingEngine(&client_requests,
                                 &client_responses, &market_updates);
    matching_engine->start();
    while (true) {
        logger->log("%:% %() % Sleeping for a few
                     milliseconds..\n", __FILE__, __LINE__, __FUNCTION__,
                     Common::getCurrentTimeStr(&time_str));
        usleep(sleep_time * 1000);
    }
}

为了便于构建主二进制文件,我们提供了一个脚本ch6/build.sh,它使用CMakeNinja来构建这个二进制文件。你需要更新这个脚本,使其指向你系统上正确的二进制文件,或者如果你愿意,也可以使用不同的构建系统。下一节将提供一些关于如何运行这个exchange_main应用程序的信息。

运行交易应用程序二进制文件

此时,运行exchange_main应用程序很简单,只需调用exchange_main二进制文件即可,如下代码块所示。我们还展示了你在终端上应该能看到的输出:

root@mydevserver:~/building-trading-system-with-cpp20/ch6$ ./cmake-build-release/exchange_main
Set core affinity for Common/Logger exchange_main.log 139685103920704 to -1
Set core affinity for Common/Logger exchange_matching_engine.log 139684933506624 to -1
Set core affinity for Exchange/MatchingEngine 139684925113920 to -1

如前文所述,通过向该进程发送SIGINT信号可以停止它。此时,它会生成三个日志文件,类似于以下代码片段中展示的内容。不过需要注意,目前日志文件里没什么有价值的信息,因为在构建完整交易生态系统所需的所有组件中,我们仅构建了撮合引擎组件。在下一篇“与市场参与者通信”结束时,我们会使用更多组件再次运行这个应用程序,届时会有更有意思的输出:

exchange_main.log
exchange_matching_engine.log

下一节将探讨订单簿(order book)的内部工作机制,以及它如何处理客户订单请求、生成订单响应和市场更新信息。

构建订单簿与订单匹配

本节将实现订单簿功能。请记住,订单簿负责处理从撮合引擎转发过来的客户订单请求。它会检查订单请求类型、更新订单簿、为客户生成订单响应,并为公开市场数据馈送生成市场数据更新。撮合引擎中限价订单簿的所有代码都在me_order_book.hme_order_book.cpp源文件中。

构建内部数据结构

首先,我们要声明限价订单簿的数据成员。之前在图6.1中展示了构成限价订单簿的数据结构示意图。限价订单簿包含以下重要数据成员:

  • 一个matching_engine_指针变量,指向订单簿所属的MatchingEngine(撮合引擎)父对象,订单簿通过它发布订单响应和市场数据更新。
  • ClientOrderHashMap类型的变量cid_oid_to_order_,用于通过客户ID(ClientId)键来追踪OrderHashMap对象。提醒一下,OrderHashMap通过订单ID(OrderId)键来追踪MEOrder对象。
  • MEOrdersAtPrice对象的内存池变量orders_at_price_pool_,用于创建新对象,并将不再使用的对象返还到内存池。
  • 买盘(bids_by_price_)和卖盘(asks_by_price_)双向链表的头节点,因为我们将价格水平上的订单记录为MEOrdersAtPrice对象的列表。
  • 一个哈希映射(hash map)OrdersAtPriceHashMap,使用价格水平作为键,来追踪对应价格水平的MEOrdersAtPrice对象。
  • MEOrder对象的内存池order_pool_,在这个内存池中创建和回收MEOrder对象,无需进行动态内存分配。
  • 一些辅助成员,例如该订单簿所对应的交易工具的交易代码(TickerId)、用于追踪下一个市场数据订单ID的OrderIdMEClientResponse变量(client_response_)、MEMarketUpdate对象(market_update_)、用于记录时间的字符串,以及用于日志记录的Logger对象。

首先,我们包含一些相关的头文件,并前置声明MatchingEngine类,因为我们会引用该类型,但目前不会完全定义它:

#pragma once
#include "common/types.h"
#include "common/mem_pool.h"
#include "common/logging.h"
#include "order_server/client_response.h"
#include "market_data/market_update.h"
#include "me_order.h"
using namespace Common;
namespace Exchange {

现在,我们按照之前讨论的内容来定义数据成员变量:

class MatchingEngine;
class MEOrderBook final {
private:
    TickerId ticker_id_ = TickerId_INVALID;
    MatchingEngine *matching_engine_ = nullptr;
    ClientOrderHashMap cid_oid_to_order_;
    MemPool<MEOrdersAtPrice> orders_at_price_pool_;
    MEOrdersAtPrice *bids_by_price_ = nullptr;
    MEOrdersAtPrice *asks_by_price_ = nullptr;
    OrdersAtPriceHashMap price_orders_at_price_;
    MemPool<MEOrder> order_pool_;
    MEClientResponse client_response_;
    MEMarketUpdate market_update_;
    OrderId next_market_order_id_ = 1;
    std::string time_str_;
    Logger *logger_ = nullptr;

此时,我们还将定义之前提到过的OrderBookHashMap类型,它是一个std::array,其中的MEOrderBook对象通过TickerId进行索引:

typedef std::array<MEOrderBook *, ME_MAX_TICKERS>
OrderBookHashMap;
};
}

接下来,我们给出构造函数和析构函数的简单实现,以及默认构造函数和赋值运算符的样板代码:

#include "me_order_book.h"
#include "matcher/matching_engine.h"
MEOrderBook::MEOrderBook(TickerId ticker_id, Logger *logger,
    MatchingEngine *matching_engine)
    : ticker_id_(ticker_id),
    matching_engine_(matching_engine),
    orders_at_price_pool_(ME_MAX_PRICE_LEVELS),
    order_pool_(ME_MAX_ORDER_IDS),
    logger_(logger) {
}
MEOrderBook::~MEOrderBook() {
    logger_->log("%:% %() % OrderBook\n%\n",    FILE   ,
        LINE   ,   FUNCTION  ,
        Common::getCurrentTimeStr(&time_str_),
        toString(false, true));
    matching_engine_ = nullptr;
    bids_by_price_ = asks_by_price_ = nullptr;
    for (auto &itr: cid_oid_to_order_) {
        itr.fill(nullptr);
    }
}

然后,我们在大多数类中添加样板代码,以防止意外复制和赋值MEOrderBook对象:

// 禁用默认、复制和移动构造函数以及赋值运算符。
MEOrderBook() = delete;
MEOrderBook(const MEOrderBook &) = delete;
MEOrderBook(const MEOrderBook &&) = delete;
MEOrderBook &operator=(const MEOrderBook &) = delete;
MEOrderBook &operator=(const MEOrderBook &&) = delete;

在继续实现订单簿上的各种操作之前,我们先介绍几个简单的方法,用于生成新的市场订单ID、将价格(Price)转换为OrdersAtPriceHashMap中的索引,以及在给定价格时访问OrdersAtPriceHashMap类型的price_orders_at_price_映射:

namespace Exchange {
class MatchingEngine;
class MEOrderBook final {
private:

generateNewMarketOrderId()方法很简单,它返回next_market_order_id_的值,并在下次调用该方法时将其递增:

auto generateNewMarketOrderId() noexcept -> OrderId {
    return next_market_order_id_++;
}

priceToIndex()方法将Price参数转换为一个介于0到ME_MAX_PRICE_LEVELS - 1之间的索引,该索引随后用于对价格水平的std::array进行索引:

auto priceToIndex(Price price) const noexcept {
    return (price % ME_MAX_PRICE_LEVELS);
}

最后,getOrdersAtPrice()实用方法通过使用priceToIndex()方法将传入的价格转换为索引,对price_orders_at_price_std::array进行索引,从而返回MEOrdersAtPrice对象:

auto getOrdersAtPrice(Price price) const noexcept ->
MEOrdersAtPrice * {
    return price_orders_at_price_.at(priceToIndex(price));
}
};
}

接下来的几个小节将详细介绍处理新订单请求、现有订单的取消请求,以及匹配与订单簿另一侧现有被动订单交叉的主动订单等重要操作。我们还将生成订单响应和市场数据更新,并将其发布回撮合引擎。

处理新的被动订单

在订单簿(order book)中,我们需要执行的首要重要任务是处理客户端想要在市场中下达新订单的请求。我们将实现MEOrderBook::add()方法,撮合引擎(matching engine)首先会调用这个方法。它生成并发送MEClientResponse,表示接受新订单,并将其发送回撮合引擎(再由撮合引擎发送给下达新订单的客户)。然后,它还会通过调用checkForMatch()方法,检查这个新订单是否与另一边已有的被动订单交叉,以及是否完全匹配或部分匹配。如果新订单完全不匹配,或者只是部分成交且订单簿中还有剩余数量,那么MEOrder会被添加到订单簿中。在这种情况下,它还会为公开市场数据馈送生成MEMarketUpdate,并将其发送回撮合引擎(由市场数据发布者组件发布)。

我们将在本节稍后讨论getNextPriority()checkForMatch()addOrder()方法,但让我们先研究一下MEOrderBook::add()方法:

auto MEOrderBook::add(ClientId client_id, OrderId client_order_id,
                      TickerId ticker_id, Side side, Price price, Qty qty) noexcept -> void
{

它做的第一件事是生成new_market_order_id,用于MEClientResponseMEMarketUpdate。它使用此请求中的属性更新client_response_data成员,并调用MatchingEngine::sendClientResponse()方法,将该响应发布回撮合引擎:

const auto new_market_order_id =
    generateNewMarketOrderId();
client_response_ = {ClientResponseType::ACCEPTED,
                    client_id, ticker_id, client_order_id,
                    new_market_order_id, side, price, 0, qty};
matching_engine_->sendClientResponse(&client_response_);

接下来,MEOrderBook::add()方法调用MEOrderBook::checkForMatch()方法,该方法根据刚收到的新客户请求检查订单簿的当前状态,查看是否可以进行部分或完全匹配。checkForMatch()方法(我们很快会构建它)返回匹配事件后剩余的订单数量(如果有的话)。对于完全未执行的订单,返回的leaves_qty与订单的原始数量相同。对于部分执行的订单,它是匹配后剩余的数量。对于完全执行的订单,此方法将返回0值,并将其赋给leaves_qty。我们很快会看到checkForMatch()的完整实现,现在,让我们先使用它:

const auto leaves_qty = checkForMatch(client_id,
                                      client_order_id, ticker_id, side, price, qty,
                                      new_market_order_id);

如果匹配事件后有剩余数量,我们需要为这个将加入订单簿的新订单生成一个市场数据更新。为此,MEOrderBook::add()方法通过调用MEOrderBook::getNextPriority()方法,找出此订单的正确优先级值。它从order_pool_内存池中分配一个新的MEOrder对象,并为其分配此订单的属性。然后,它调用MEOrderBook::addOrder()方法,将其实际添加到MEOrdersAtPrice数据结构中正确的价格水平和优先级位置。最后,它用市场更新的值填充market_update_对象,并调用MatchingEngine::sendMarketUpdate()方法,将其发布到撮合引擎:

if (LIKELY(leaves_qty)) {
    const auto priority = getNextPriority(ticker_id,
                                          price);

    auto order = order_pool_.allocate(ticker_id, client_id,
                                      client_order_id, new_market_order_id, side, price,
                                      leaves_qty, priority, nullptr, nullptr);
    addOrder(order);
    market_update_ = {MarketUpdateType::ADD,
                      new_market_order_id, ticker_id, side, price,
                      leaves_qty, priority};
    matching_engine_->sendMarketUpdate(&market_update_);
}

getNextPriority()方法非常简单直接。如果某个价格水平已经存在,那么它只返回比该价格上最后一个订单的优先级高1的值。如果某个价格水平不存在,那么对于该价格水平的第一个订单,它返回1:

auto getNextPriority(Price price) noexcept {
    const auto orders_at_price = getOrdersAtPrice(price);
    if (!orders_at_price)
        return 1lu;
    return orders_at_price->first_me_order_->prev_order_->priority_ + 1;
}

接下来,我们将详细介绍如何将新订单添加到限价订单簿中。该方法将传入的MEOrder对象追加到该订单价格对应的MEOrdersAtPrice条目的末尾。如果MEOrdersAtPrice条目尚不存在(新的价格水平),它首先分配一个新条目,使用addOrdersAtPrice()方法将新的价格水平添加到订单簿中,然后追加订单。此外,它会在ClientOrderHashMap类型的id_oid_to_order_映射中跟踪MEOrder对象,该映射将ClientIdOrderId映射到MEOrder对象:

auto addOrder(MEOrder *order) noexcept {

首先,我们尝试通过调用getOrdersAtPrice()方法检查并获取MEOrdersAtPrice(如果存在),并将其保存在orders_at_price变量中。然后,我们检查是否存在有效的MEOrdersAtPrice,这意味着具有此订单价格和方向的价格水平已经存在。如果这样的价格水平不存在,且这是构成该水平的第一个订单,我们从orders_at_price_pool_中创建一个新的MEOrdersAtPrice,对其进行初始化,并调用addOrdersAtPrice()方法:

const auto orders_at_price = getOrdersAtPrice(order
                                             ->price_);

if (!orders_at_price) {
    order->next_order_ = order->prev_order_ = order;
    auto new_orders_at_price =
        orders_at_price_pool_.allocate(order->side_,
                                       order->price_, order, nullptr, nullptr);
    addOrdersAtPrice(new_orders_at_price);
}

如果存在有效的价格水平,我们将新订单追加到MEOrder对象双向链表的末尾,该链表可通过MEOrdersAtPricefirst_me_order_成员访问。然后,我们更新正在添加的MEOrder上的prev_order_next_order_指针,以及链表上的最后一个元素,之后再追加MEOrder对象:

else {
    auto first_order = (orders_at_price ?
                        orders_at_price->first_me_order_ : nullptr);
    first_order->prev_order_->next_order_ = order;
    order->prev_order_ = first_order->prev_order_;
    order->next_order_ = first_order;
    first_order->prev_order_ = order;
}

最后,我们将这个MEOrder指针添加到cid_oid_to_order_容器中,cid_oid_to_order_std::arraystd::array实例,首先按订单的client_id_索引,然后按订单的client_order_id_索引:

cid_oid_to_order_.at(order->client_id_)
                  .at(order->client_order_id_) = order;

最后,为了完成对向订单簿添加新订单的讨论,我们需要实现addOrdersAtPrice()方法,用于向订单簿添加新的价格水平。此方法首先将新的MEOrdersAtPrice条目添加到OrdersAtPriceHashMap类型的price_orders_at_price_映射中,该映射将Price映射到MEOrdersAtPrice

auto addOrdersAtPrice(MEOrdersAtPrice *new_orders_at_price) noexcept {
    price_orders_at_price_.at(priceToIndex(
        new_orders_at_price->price_)) = new_orders_at_price;

然后,我们需要将其插入按价格排列的买盘/卖盘的正确位置。我们首先将best_orders_by_price变量赋值为按价格排序的买盘或卖盘的开头:

const auto best_orders_by_price = (new_orders_at_price->
                                  side_ == Side::BUY ? bids_by_price_ : asks_by_price_);

我们需要处理一种边界情况,即没有买盘或没有卖盘——也就是说,订单簿的一侧为空。在这种情况下,我们设置bids_by_price_asks_by_price_成员,它们分别指向该侧排序列表的头部:

if (UNLIKELY(!best_orders_by_price)) {
    (new_orders_at_price->side_ == Side::BUY ?
     bids_by_price_ : asks_by_price_) =
        new_orders_at_price;
    new_orders_at_price->prev_entry_ =
        new_orders_at_price->next_entry_ =
        new_orders_at_price;
}

否则,我们需要在价格水平的双向链表中找到正确的条目。我们通过遍历买盘或卖盘,直到找到正确的价格水平,在该价格水平之前或之后插入新的价格水平。我们在下面的target变量中跟踪新价格水平之前或之后的价格水平,并使用add_after布尔标志跟踪我们需要在target变量之前还是之后插入:

else {
    auto target = best_orders_by_price;
    bool add_after = ((new_orders_at_price->side_ ==
                       Side::SELL && new_orders_at_price->price_ >
                       target->price_) ||  (new_orders_at_price->side_ ==
                                          Side::BUY && new_orders_at_price->price_ <
                                          target->price_));
    if (add_after) {
        target = target->next_entry_;
        add_after = ((new_orders_at_price->side_ ==
                      Side::SELL && new_orders_at_price->price_ >
                      target->price_) ||  (new_orders_at_price->side_ ==
                                         Side::BUY && new_orders_at_price->price_ <
                                         target->price_));
    }
    while (add_after && target != best_orders_by_price) {
        add_after = ((new_orders_at_price->side_ ==
                      Side::SELL && new_orders_at_price->price_ >
                      target->price_) ||  (new_orders_at_price->side_ ==
                                         Side::BUY && new_orders_at_price->price_ <
                                         target->price_));
        if (add_after)
            target = target->next_entry_;
    }

一旦我们找到新MEOrdersAtPrice条目的正确位置,我们通过更新目标MEOrdersAtPrice结构中的prev_entry_next_entry_变量,以及正在追加的新MEOrdersAtPrice,来追加新的价格水平,如下所示:

if (add_after) { // 在target之后添加new_orders_at_price
    if (target == best_orders_by_price) {
        target = best_orders_by_price->prev_entry_;
    }
    new_orders_at_price->prev_entry_ = target;
    target->next_entry_->prev_entry_ =
        new_orders_at_price;
    new_orders_at_price->next_entry_ =
        target->next_entry_;
    target->next_entry_ = new_orders_at_price;
} else { // 在target之前添加new_orders_at_price
    new_orders_at_price->prev_entry_ =
        target->prev_entry_;
    new_orders_at_price->next_entry_ = target;
    target->prev_entry_->next_entry_ =
        new_orders_at_price;
    target->prev_entry_ = new_orders_at_price;

最后,如果我们在现有价格水平之前添加新的价格水平,我们需要检查前置此价格水平是否会改变bids_by_price_asks_by_price_变量。请记住,这些变量分别跟踪买盘或卖盘的起始位置——即最高买价和最低卖价。如果我们有了新的最佳买价/卖价水平,我们分别更新bids_by_price_asks_by_price_变量:

if ((new_orders_at_price->side_ == Side::BUY &&
     new_orders_at_price->price_ > best_orders_by_price
     ->price_) || new_orders_at_price->side_ ==
                 Side::SELL && new_orders_at_price->price_ <
                 best_orders_by_price->price_)) {
    target->next_entry_ = (target->next_entry_ ==
                          best_orders_by_price ? new_orders_at_price :
                                                target->next_entry_);
    (new_orders_at_price->side_ == Side::BUY ?
     bids_by_price_ : asks_by_price_) =
        new_orders_at_price;
}
}

接下来,我们将讨论处理订单取消请求的源代码。

处理订单取消请求

处理订单取消请求的代码由撮合引擎转发。首先,它会检查取消请求是否有效,即ClientId有效且取消请求中的OrderId与订单簿中的活跃订单相对应。如果订单无法取消,它会生成并发布一条MEClientResponse消息,向撮合引擎表明取消请求被拒绝。如果订单可以取消,它会生成MEClientResponse消息表明取消尝试成功,并调用removeOrder()方法从限价订单簿中移除该订单。接下来,我们马上会讨论removeOrder()的详细内容。

我们会跟踪一个is_cancelable布尔变量,用于判断是否成功找到并取消了客户的订单。如果client_id大于最大可能的客户ID值,那么我们无法取消该订单。如果客户ID有效,那么我们会在cid_oid_to_order_容器中查找提供的client_idorder_id值。如果不存在有效的订单,那么我们确认该订单无法取消:

auto MEOrderBook::cancel(ClientId client_id, OrderId order_id,
                         TickerId ticker_id) noexcept -> void {
    auto is_cancelable = (client_id < cid_oid_to_order_.size());
    MEOrder *exchange_order = nullptr;
    if (LIKELY(is_cancelable)) {
        auto &co_itr = cid_oid_to_order_.at(client_id);
        exchange_order = co_itr.at(order_id);
        is_cancelable = (exchange_order != nullptr);
    }

如果我们确定订单无法取消,就会生成一条ClientResponseType::CANCEL_REJECTED类型的MEClientResponse消息,通知撮合引擎:

if (UNLIKELY(!is_cancelable)) {
    client_response_ =
        {ClientResponseType::CANCEL_REJECTED, client_id,
         ticker_id, order_id, OrderId_INVALID,
         Side::INVALID, Price_INVALID, Qty_INVALID,
         Qty_INVALID};
}

如果我们能够成功取消订单,就会更新client_response_成员变量和market_update_成员变量中的属性。然后,我们调用removeOrder()方法来更新订单簿并从中删除该订单。最后,我们使用sendMarketUpdate()方法将市场更新信息发送给撮合引擎,并使用sendClientResponse()方法将客户响应发送给撮合引擎:

else {
    client_response_ = {ClientResponseType::CANCELED,
                        client_id, ticker_id, order_id,
                        exchange_order->market_order_id_,
                        exchange_order->side_, exchange_order->price_,
                        Qty_INVALID, exchange_order->qty_};
    market_update_ = {MarketUpdateType::CANCEL,
                      exchange_order->market_order_id_, ticker_id,
                      exchange_order->side_, exchange_order->price_, 0,
                      exchange_order->priority_};
    removeOrder(exchange_order);
    matching_engine_->sendMarketUpdate(&market_update_);
}
matching_engine_->sendClientResponse(&client_response_);
}

接下来,让我们实现removeOrder()方法。它首先找到要删除的订单所属的MEOrdersAtPrice,然后在MEOrdersAtPrice包含的订单列表中找到并删除MEOrder。如果要删除的订单是该价格水平上的唯一订单,该方法还会调用removeOrdersAtPrice()来删除整个价格水平,因为删除该订单后,该价格水平不再存在。最后,它从cid_oid_to_order_哈希表中删除该MEOrder的条目,并将释放的MEOrder对象返回给order_pool_内存池:

auto removeOrder(MEOrder *order) noexcept {
    auto orders_at_price = getOrdersAtPrice(order->price_);
    if (order->prev_order_ == order) { // 只有一个元素。
        removeOrdersAtPrice(order->side_, order->price_);
    } else { // 删除链接。
        const auto order_before = order->prev_order_;
        const auto order_after = order->next_order_;
        order_before->next_order_ = order_after;
        order_after->prev_order_ = order_before;
        if (orders_at_price->first_me_order_ == order) {
            orders_at_price->first_me_order_ = order_after;
        }
        order->prev_order_ = order->next_order_ = nullptr;
    }
    cid_oid_to_order_.at(order->client_id_).at(order
                                             ->client_order_id_) = nullptr;
    order_pool_.deallocate(order);
}

为了完成对处理订单取消请求相关任务的讨论,我们将实现removeOrdersAtPrice()方法。它会从买盘或卖盘的MEOrdersAtPrice双向链表中找到并删除MEOrdersAtPrice。如果要删除的这个价格条目恰好是订单簿该侧的唯一MEOrdersAtPrice条目,它会将双向链表的头设置为nullptr,表示订单簿该侧为空。最后,该方法从price_orders_at_price_hash哈希表中删除该价格的条目,并将释放的MEOrdersAtPrice返回给orders_at_price_pool_内存池:

auto removeOrdersAtPrice(Side side, Price price) noexcept {
    const auto best_orders_by_price = (side == Side::BUY ?
                                      bids_by_price_ : asks_by_price_);
    auto orders_at_price = getOrdersAtPrice(price);
    if (UNLIKELY(orders_at_price->next_entry_ ==
                 orders_at_price)) { // 订单簿一侧为空。
        (side == Side::BUY ? bids_by_price_ : asks_by_price_) =
            nullptr;
    } else {
        orders_at_price->prev_entry_->next_entry_ =
            orders_at_price->next_entry_;
        orders_at_price->next_entry_->prev_entry_ =
            orders_at_price->prev_entry_;
        if (orders_at_price == best_orders_by_price) {
            (side == Side::BUY ? bids_by_price_ : asks_by_price_)
                = orders_at_price->next_entry_;
        }
        orders_at_price->prev_entry_ = orders_at_price
                                      ->next_entry_ = nullptr;
    }
    price_orders_at_price_.at(priceToIndex(price)) = nullptr;
    orders_at_price_pool_.deallocate(orders_at_price);
}

我们需要处理的最后一个操作很重要——将主动订单与订单簿另一侧的被动订单进行撮合。接下来我们看看这个操作的实现。

撮合主动订单并更新订单簿

在本小节中,我们将通过展示之前提到的MEOrderBook::checkForMatch()方法,来实现限价订单簿中的撮合功能。图6.3展示了在限价订单簿的一个假设状态下会发生什么。这里展示了卖盘的状态,由MEOrdersAtPrice表示的被动卖盘价格依次为117、118、119等。在最佳卖盘价格117处,显示了前两个MEOrder对象,第一个的优先级为11,市场订单ID为1200,数量为20。在先进先出队列中排在其后的MEOrder优先级为13,市场订单ID为1400,数量为10。在这种情况下,一个数量为25、价格为117的新买单(以蓝色表示)将与市场订单ID为1200的第一个订单(以黄色表示)完全匹配并执行。然后,它将剩余的5个数量部分地与市场订单ID为1400的订单(以品红色表示)执行匹配,撮合事件就此完成。以下图表之后的算法展示了这些步骤。

限价订单簿中撮合事件的示例

此方法会遍历订单簿中与新(可能是主动)订单相反一侧的MEOrdersAtPrice对象。它从最激进价格到最不激进价格遍历各个价格水平,对于每个价格水平,按先进先出顺序从第一个到最后一个匹配该价格水平包含的MEOrder对象。它通过调用match()方法,继续将新订单与另一侧的被动订单从最激进价格到最不激进价格、在每个价格水平从第一个到最后一个订单进行匹配。当新的主动订单没有剩余未匹配数量、另一侧剩余的价格水平不再与新订单的价格交叉,或者订单簿该侧为空时,它停止匹配并返回。此时,它将新订单上剩余的未匹配数量返回给调用者:

auto MEOrderBook::checkForMatch(ClientId client_id, OrderId client_order_id,
                                TickerId ticker_id, Side side, Price price, Qty qty, Qty
                                new_market_order_id) noexcept {
    auto leaves_qty = qty;

我们从asks_by_price_价格水平开始,持续遍历所有卖盘价格水平,这些价格水平按从最低到最高的顺序排列。对于asks_by_price_价格水平,我们从MEOrder类型指针的first_me_order_对象开始,按先进先出顺序,从最低优先级到最高优先级进行遍历。对于每个可以与新主动订单匹配的订单,我们调用MEOrder::match()方法进行实际匹配。我们持续这样做,直到leaves_qty没有剩余、asks_by_price_变量为nullptr(表示订单簿该侧为空),或者剩余的价格水平无法用于匹配新订单:

if (side == Side::BUY) {
    while (leaves_qty && asks_by_price_) {
        const auto ask_itr = asks_by_price_->first_me_order_;
        if (LIKELY(price < ask_itr->price_)) {
            break;
        }
        match(ticker_id, client_id, side, client_order_id,
              new_market_order_id, ask_itr, &leaves_qty);
    }
}

如果新订单是卖单,我们执行与上述相同的逻辑,只是我们遍历bids_by_price_价格水平,这些价格水平按从最高买盘价格到最低买盘价格的顺序排列,如下所示:

if (side == Side::SELL) {
    while (leaves_qty && bids_by_price_) {
        const auto bid_itr = bids_by_price_->first_me_order_;
        if (LIKELY(price > bid_itr->price_)) {
            break;
        }
        match(ticker_id, client_id, side, client_order_id,
              new_market_order_id, bid_itr, &leaves_qty);
    }
}
return leaves_qty;
}

当新的主动订单与订单簿另一侧的现有被动订单匹配时,会调用match()方法。它计算执行数量,即新订单数量和与之匹配的现有被动订单数量中的最小值。它从主动订单的剩余数量以及与之匹配的被动订单数量中减去这个执行数量。它生成两个执行订单响应并发送给撮合引擎——一个给发送主动订单的客户,另一个给其被动订单与新订单匹配的客户。它还会创建并发布一条MarketUpdateType::TRADE类型的市场更新信息,以在公共市场数据馈送中通知参与者此次执行情况。最后,它检查此次交易是否完全执行了被动订单,如果完全执行,它会生成另一条MarketUpdateType::CANCEL类型的市场更新信息,通知参与者该被动订单已被移除。如果被动订单只是部分匹配,它会生成一条MarketUpdateType::MODIFY类型的市场更新信息,包含被动限价订单的新剩余数量。

这意味着,选择忽略市场数据流中交易消息的参与者仍然可以准确地构建和维护限价订单簿。理论上,我们可以省去额外的取消或修改市场更新信息,但这将要求下游市场数据消费者将交易消息应用到他们的订单簿并进行更新。

MEOrderBook::match()方法带有几个参数来识别客户信息,但关键参数是MEOrder指针、迭代器itr,以及Qty指针leaves_qtyMEOrder指针代表订单簿中与新订单进行撮合的订单,而Qty代表新订单的剩余数量。这些参数通过指针传递,因为我们会在这个方法中直接修改它们,并期望这些修改能在调用方法中体现出来:

auto MEOrderBook::match(TickerId ticker_id, ClientId client_id, Side
side, OrderId client_order_id, OrderId new_market_order_id, MEOrder*
itr, Qty* leaves_qty) noexcept {

我们计算fill_qty变量,它是订单簿中已有被动订单数量和新订单数量两者中的最小值。然后,我们用fill_qty同时减少leaves_qtyMEOrder对象的qty_成员:

const auto order = itr;
const auto order_qty = order->qty_;
const auto fill_qty = std::min(*leaves_qty, order_qty);
*leaves_qty -= fill_qty;
order->qty_ -= fill_qty;

我们生成一个类型为ClientResponseType::FILLED的客户端响应消息,发送给提交新订单的客户,并使用sendClientResponse()方法将其发送到撮合引擎:

client_response_ = {ClientResponseType::FILLED,
    client_id, ticker_id, client_order_id,
    new_market_order_id, side, itr->price_, fill_qty,
    *leaves_qty};
matching_engine_->sendClientResponse(&client_response_);

我们还生成第二个类型为ClientResponseType::FILLED的客户端响应消息,这条消息是给订单簿中被撮合订单的客户的:

client_response_ = {ClientResponseType::FILLED, order
    ->client_id_, ticker_id, order->client_order_id_,
    order->market_order_id_, order->side_, itr->price_,
    fill_qty, order->qty_};
matching_engine_->sendClientResponse(&client_response_);

我们还会生成一个类型为MarketUpdateType::TRADE的市场更新消息,并使用sendMarketUpdate()发布,通知参与者发生的交易,并告知他们成交数量fill_qty

market_update_ = {MarketUpdateType::TRADE,
    OrderId_INVALID, ticker_id, side, itr->price_,
    fill_qty, Priority_INVALID};
matching_engine_->sendMarketUpdate(&market_update_);

最后,我们为订单簿中已有的被动客户订单生成一个市场更新消息。如果这个MEOrder还有剩余数量,那么我们生成一个MarketUpdateType::MODIFY消息,并传递该订单剩余的数量。如果订单已全部成交,那么我们生成一个MarketUpdateType::CANCEL更新消息,发布出去,同时调用MEOrderBook::removeOrder()方法,将这个MEOrder从订单簿中移除:

if (!order->qty_) {
    market_update_ = {MarketUpdateType::CANCEL,
        order->market_order_id_, ticker_id, order->side_,
        order->price_, order_qty, Priority_INVALID};
    matching_engine_->sendMarketUpdate(&market_update_);

    removeOrder(order);
} else {
    market_update_ = {MarketUpdateType::MODIFY,
        order->market_order_id_, ticker_id, order->side_,
        order->price_, order->qty_, order->priority_};
    matching_engine_->sendMarketUpdate(&market_update_);
}

关于处理客户订单请求、更新撮合引擎内的限价订单簿,以及生成和发布订单响应及市场更新所涉及的操作,我们的讨论到此结束。

总结

在本文中,我们开始用C++实现电子交易生态系统。我们构建的第一个组件是交易所撮合引擎,它负责接受和响应来自交易所基础设施中订单服务器组件的订单请求。这个组件还负责生成市场数据更新,并发布给交易所基础设施中的市场数据发布者组件。

首先,我们阐述了在撮合引擎和限价订单簿方面的一些假设。我们还定义了一些基本的普通旧数据(POD,Plain Old Data)风格的结构体,用于封装限价订单簿中单个订单、订单服务器发送的单个订单请求、发回给订单服务器的订单响应,以及单个市场数据更新的信息。我们展示了如何使用无锁队列(lock-free queue)来促进撮合引擎与订单服务器和市场数据发布者之间关于订单请求、订单响应和市场数据更新的通信。为了构建限价订单簿,我们还定义了一些哈希表(hash maps),以便通过订单ID跟踪订单,并在MEOrdersAtPrice结构中将相同价格的订单链接在一起。再次强调,这些价格水平本身是通过双向链表(doubly linked list)和按价格索引的哈希表来维护的。

然后,我们构建了撮合引擎组件,它是一个独立的执行线程,从订单服务器接收更新,并将响应和市场数据更新发布回订单服务器和市场数据发布者。我们还构建了电子交易交易所的主应用程序二进制文件,在下一篇中我们将对其进行增强。

最后,我们详细介绍了构建和更新限价订单簿数据结构所涉及的机制。我们讨论了处理新订单请求和订单取消请求所涉及的任务。我们还实现了撮合引擎的功能,使其能够对新的主动订单和现有价格匹配的被动订单进行实际撮合。撮合事件会为参与撮合事件的市场参与者生成私有执行消息。此外,该事件还会在公开市场数据馈送中生成交易消息和订单删除或修改信息。

在下一篇中,我们将构建市场数据发布者组件,这个组件负责接收撮合引擎生成的市场数据更新,并将其发送出去供参与者使用。此外,我们还将构建位于电子交易交易所内的订单服务器组件,该组件管理与不同市场参与者订单网关的通信,转发与撮合引擎之间的请求和响应。

如果你对交易系统的开发与设计感兴趣,可以阅读小方在知识星球的专栏:

使用 C++20 从零构建一个完整的低延迟交易系统

如果你想求职交易系统相关的岗位,可以阅读小方在知识星球的专栏:

交易系统开发岗位求职与面试指南

推荐阅读

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐