核心数据结构设计:订单簿、持仓与账户
做市商系统里,数据结构就是骨架。骨架歪了,后面再怎么填肉都跑不起来。今天咱们聊聊三个最核心的数据结构:订单簿、持仓和账户。这三个东西,说白了就是做市商的「三件套」——少了哪个都不行。
订单簿(OrderBook)数据结构
订单簿是什么?就是记录所有买卖挂单的账本。做市商靠它赚钱,也靠它亏钱。我见过不少团队,订单簿设计得乱七八糟,最后回测数据跟实盘数据对不上,查了半天发现是数据结构的问题。
我个人习惯用「价格-数量」的映射关系来组织订单簿。为什么?因为做市商最关心的是某个价格上有多少单子,而不是单子是谁挂的。
// 订单簿核心结构
struct OrderBook {
// 买盘,按价格降序排列
map<double, double, greater<double>> bids;
// 卖盘,按价格升序排列
map<double, double> asks;
// 最新更新时间
int64_t timestamp;
// 交易所标识
string exchange;
// 交易对
string symbol;
};
这里有个细节:买盘用降序,卖盘用升序。为什么?因为买盘最高价在最前面,卖盘最低价在最前面。这样取最优买卖价就是 O(1) 的操作。我在项目中遇到过有人用数组存订单簿,每次更新都要排序,结果延迟直接飙到毫秒级——做市商哪受得了这个?
还有一个容易被忽略的点:订单簿的深度。做市商不仅要看最优价,还要看整个深度。比如你想吃进100个BTC,最优价只有10个,剩下的就得往深处吃。所以订单簿结构里最好能支持「按价格区间聚合」的查询。
// 获取指定深度的订单簿
struct DepthLevel {
double price;
double quantity;
int orderCount; // 该价格上的订单数量
};
// 深度查询接口
vector<DepthLevel> getDepth(int level = 10);
持仓(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秒,或者每次交易完成后立即更新。
三者之间的关系
这三个数据结构不是孤立的。订单簿的变化会影响持仓,持仓的变化会影响账户。举个例子:
- 订单簿上出现一个低价卖单
- 你的做市策略决定吃掉这个卖单
- 持仓增加(多头)
- 账户余额减少(资金被占用)
- 订单簿更新(这个卖单被移除)
这个流程看起来简单,但实际实现时要注意数据一致性。我见过最离谱的bug是:订单簿已经更新了,但持仓还是旧的,结果风控系统以为还有仓位,继续开仓,最后爆仓了。
SVG 结构图
这张图展示了三个核心数据结构的关系。订单簿、持仓、账户各自独立,但通过事件机制相互关联。风控引擎定期读取这三个数据,计算风险指标。说白了,这就是做市商系统的「三驾马车」——缺一不可。
- 订单簿:关注速度和深度,用红黑树或跳表实现
- 持仓:区分总持仓和明细持仓,注意数据一致性
- 账户:保证绝对准确,用事件驱动更新
这三个数据结构设计好了,后面的风控逻辑、策略实现都会顺畅很多。我见过太多团队在数据结构上偷懒,结果后面返工的成本比一开始好好设计高十倍。嗯,这个坑我踩过,你们就别再踩了。