核心数据结构设计:订单簿、持仓与账户

做市商系统里,数据结构就是骨架。骨架歪了,后面再怎么填肉都跑不起来。今天咱们聊聊三个最核心的数据结构:订单簿、持仓和账户。这三个东西,说白了就是做市商的「三件套」——少了哪个都不行。

订单簿(OrderBook)数据结构

订单簿是什么?就是记录所有买卖挂单的账本。做市商靠它赚钱,也靠它亏钱。我见过不少团队,订单簿设计得乱七八糟,最后回测数据跟实盘数据对不上,查了半天发现是数据结构的问题。

我个人习惯用「价格-数量」的映射关系来组织订单簿。为什么?因为做市商最关心的是某个价格上有多少单子,而不是单子是谁挂的。

// 订单簿核心结构
struct OrderBook {
    // 买盘,按价格降序排列
    map<double, double, greater<double>> bids;
    // 卖盘,按价格升序排列
    map<double, double> asks;
    // 最新更新时间
    int64_t timestamp;
    // 交易所标识
    string exchange;
    // 交易对
    string symbol;
};

这里有个细节:买盘用降序,卖盘用升序。为什么?因为买盘最高价在最前面,卖盘最低价在最前面。这样取最优买卖价就是 O(1) 的操作。我在项目中遇到过有人用数组存订单簿,每次更新都要排序,结果延迟直接飙到毫秒级——做市商哪受得了这个?

核心要点:订单簿的更新频率极高,每秒可能几百次。数据结构必须支持快速查询、插入和删除。红黑树(C++ map)或者跳表都是不错的选择。

还有一个容易被忽略的点:订单簿的深度。做市商不仅要看最优价,还要看整个深度。比如你想吃进100个BTC,最优价只有10个,剩下的就得往深处吃。所以订单簿结构里最好能支持「按价格区间聚合」的查询。

// 获取指定深度的订单簿
struct DepthLevel {
    double price;
    double quantity;
    int orderCount;  // 该价格上的订单数量
};

// 深度查询接口
vector<DepthLevel> getDepth(int level = 10);
避坑指南:我曾经在订单簿里只存了价格和数量,没存订单ID。结果后来要做订单撤销跟踪,发现根本对不上。建议至少保留订单ID、价格、数量、时间戳四个字段。

持仓(Position)数据结构

持仓结构,说白了就是记录你手里有什么、多少钱买的、现在赚了多少。但做市商的持仓跟普通交易者不一样——我们同时持有多头和空头,而且持仓变化极快。

我建议把持仓分成两部分:总持仓明细持仓。总持仓用来快速计算风险敞口,明细持仓用来做盈亏分析。

// 总持仓结构
struct Position {
    string symbol;           // 交易对
    double netPosition;      // 净持仓(正数多头,负数空头)
    double avgEntryPrice;    // 平均入场价
    double unrealizedPnL;    // 未实现盈亏
    double realizedPnL;      // 已实现盈亏
    double marginUsed;       // 占用保证金
    int64_t updateTime;      // 更新时间
};

// 明细持仓结构
struct PositionDetail {
    string tradeId;          // 交易ID
    double quantity;         // 数量
    double entryPrice;       // 入场价
    double currentPrice;     // 当前价
    double pnl;              // 盈亏
    int64_t openTime;        // 开仓时间
    bool isLong;             // 是否多头
};

你想想看,为什么要有明细持仓?因为做市商经常做「网格交易」——在某个价格区间内反复买卖。如果没有明细,你根本算不清哪笔单子赚了、哪笔亏了。我记得有一次,系统显示总持仓是0,但实际盈亏却是负的。查了半天,发现是明细持仓和总持仓没同步更新。

注意:持仓数据必须实时更新,而且更新操作必须是原子性的。做市商系统里,持仓更新和订单簿更新往往是并发的,不加锁的话数据就乱了。我建议用读写锁,读多写少的情况下性能更好。

账户(Account)数据结构

账户结构,这个就简单了——记录你有多少钱。但做市商的账户跟普通用户不一样,我们关心的是「可用余额」、「冻结余额」、「总资产」这三个值。

// 账户结构
struct Account {
    string accountId;        // 账户ID
    string exchange;         // 交易所
    double totalBalance;     // 总余额
    double availableBalance; // 可用余额
    double frozenBalance;    // 冻结余额
    double marginBalance;    // 保证金余额
    double unrealizedPnL;    // 未实现盈亏
    int64_t updateTime;      // 更新时间
    
    // 各币种余额
    map<string, CurrencyBalance> currencies;
};

struct CurrencyBalance {
    string currency;         // 币种
    double total;            // 总数量
    double available;        // 可用数量
    double frozen;           // 冻结数量
};

这里有个坑:不同交易所的账户结构不一样。有的交易所把「冻结余额」和「可用余额」分开,有的合在一起。我建议在底层做一层抽象,把不同交易所的账户数据统一成上面的结构。这样上层逻辑就不用关心具体交易所的差异了。

设计原则:账户数据是「权威数据源」。订单簿和持仓都可以从交易所重新拉取,但账户数据必须保证绝对准确。每次交易完成后,都要重新计算账户余额。

还有一个细节:账户的更新频率。订单簿每秒更新几百次,但账户可能几秒才更新一次。为什么?因为账户数据涉及资金,更新太频繁反而容易出错。我一般设置账户更新间隔为1秒,或者每次交易完成后立即更新。

三者之间的关系

这三个数据结构不是孤立的。订单簿的变化会影响持仓,持仓的变化会影响账户。举个例子:

  1. 订单簿上出现一个低价卖单
  2. 你的做市策略决定吃掉这个卖单
  3. 持仓增加(多头)
  4. 账户余额减少(资金被占用)
  5. 订单簿更新(这个卖单被移除)

这个流程看起来简单,但实际实现时要注意数据一致性。我见过最离谱的bug是:订单簿已经更新了,但持仓还是旧的,结果风控系统以为还有仓位,继续开仓,最后爆仓了。

我的建议:用事件驱动的方式更新这三个数据结构。订单簿变化触发事件,持仓监听事件并更新,账户再监听持仓事件。这样数据流是单向的,不容易出错。

SVG 结构图

做市商核心数据结构关系图 订单簿 Bids / Asks 价格-数量映射 深度查询 持仓 净持仓 / 明细持仓 平均入场价 未实现盈亏 账户 总余额 / 可用余额 冻结余额 各币种余额 成交事件 盈亏更新 风控引擎 读取三个数据结构,计算风险指标 订单簿 持仓 账户 风控引擎

这张图展示了三个核心数据结构的关系。订单簿、持仓、账户各自独立,但通过事件机制相互关联。风控引擎定期读取这三个数据,计算风险指标。说白了,这就是做市商系统的「三驾马车」——缺一不可。

总结一下:
  • 订单簿:关注速度和深度,用红黑树或跳表实现
  • 持仓:区分总持仓和明细持仓,注意数据一致性
  • 账户:保证绝对准确,用事件驱动更新

这三个数据结构设计好了,后面的风控逻辑、策略实现都会顺畅很多。我见过太多团队在数据结构上偷懒,结果后面返工的成本比一开始好好设计高十倍。嗯,这个坑我踩过,你们就别再踩了。