核心报价引擎设计:订单簿数据结构与报价策略
做市系统的核心,说白了就是报价引擎。你想想看,交易所每秒几万笔订单砸过来,你的系统得在几微秒内做出反应——报什么价、报多少量、要不要撤单重报。这背后,订单簿数据结构和报价生成策略就是地基。
我个人习惯把报价引擎拆成两层来看:底层是订单簿,负责“看清战场”;上层是报价策略,负责“决定怎么打”。今天咱们就一层层剥开。
一、订单簿数据结构设计:Level 1/2/3
订单簿的数据结构,直接决定了你的报价引擎能跑多快。我见过不少团队,一开始图省事用Python的dict存订单簿,结果回测时跑得飞起,一上实盘就崩——延迟直接飙到毫秒级。嗯,这里要注意,选对数据结构比选对语言更重要。
Level 1:最优报价
Level 1只存买一卖一的价格和数量。数据结构最简单,一个结构体搞定:
struct Level1 {
double bid_price;
double bid_size;
double ask_price;
double ask_size;
int64_t timestamp; // 纳秒级时间戳
};
这种结构适合做高频行情的“快照”。我在项目中遇到过,有些交易所的行情推送就是Level 1级别,你只能看到最优价,看不到深度。这时候报价策略就受限了——你没法判断“如果我现在吃单,会滑几个tick”。
Level 2:深度行情
Level 2存多个档位的买卖盘口,通常是前5档或前10档。数据结构用跳表(Skip List)或红黑树最合适。为什么?因为订单簿的增删改查操作频繁,而且需要按价格排序。
// 用跳表实现Level 2订单簿
class OrderBookLevel2 {
SkipList<double, PriceLevel> bids; // 买盘,降序
SkipList<double, PriceLevel> asks; // 卖盘,升序
void onNewOrder(Order& order);
void onCancelOrder(Order& order);
void onTrade(Trade& trade);
};
避坑指南:我曾经用std::map实现过Level 2,结果发现每次插入删除都要O(log n)的平衡操作。后来换成跳表,虽然理论复杂度一样,但实际吞吐量提升了30%。原因很简单——跳表的缓存局部性更好,而且并发控制更容易。
Level 3:全量订单簿
Level 3存的是所有未成交订单,包括每个订单的ID、价格、数量、时间戳。数据结构用哈希表+双向链表组合:哈希表按订单ID快速查找,双向链表按价格排序。
class OrderBookLevel3 {
unordered_map<OrderID, OrderNode*> order_map;
map<double, list<OrderNode*>> price_levels; // 按价格分组
void addOrder(Order& order);
void cancelOrder(OrderID id);
void executeOrder(OrderID id, double price, int64_t qty);
};
说实话,Level 3在实盘中用得不多,因为数据量太大了。比特币永续合约的订单簿,每秒可能有几万笔订单变动,全量维护成本太高。我建议只在回测或风控场景用Level 3,实盘用Level 2就够了。
二、报价生成策略
数据结构搭好了,接下来就是“怎么报价”。报价策略千千万,但核心就三种:基于价差、基于波动率、基于库存。咱们一个个说。
1. 基于价差的报价策略
这是最基础的策略。你盯着买卖价差,如果价差大于某个阈值,你就挂单吃这个价差。说白了就是“低买高卖”。
double spread = ask_price - bid_price;
if (spread > min_spread_threshold) {
// 在买一价+1 tick处挂买单
placeOrder(bid_price + tick_size, order_size, BUY);
// 在卖一价-1 tick处挂卖单
placeOrder(ask_price - tick_size, order_size, SELL);
}
我在项目中遇到过,这种策略在流动性好的市场很稳,但遇到极端行情就惨了——价差突然拉大,你以为是机会,结果一挂单就被吃,然后价格继续往一个方向跑,你直接变成“接盘侠”。
注意:基于价差的策略一定要加“价差回归”判断。如果价差持续扩大,说明市场在剧烈波动,这时候应该暂停报价,而不是冲进去。
2. 基于波动率的报价策略
波动率越高,报价越保守。这个逻辑很直观——市场越乱,你越要留足安全边际。
double volatility = calculateVolatility(price_history, window=100);
double spread_multiplier = 1.0 + volatility * 2.0; // 波动率越高,价差越大
double bid_price = mid_price - spread_multiplier * tick_size;
double ask_price = mid_price + spread_multiplier * tick_size;
你想想看,波动率低的时候,市场很平稳,你可以把价差报小一点,多赚点手续费。波动率高的时候,价格可能瞬间跳几个tick,你报得太近容易被“打脸”。
3. 基于库存的报价策略
这个策略我最喜欢,因为它直接关系到你的资金安全。库存多了,你就得降价卖;库存少了,你就得提价买。
double inventory_ratio = current_inventory / max_inventory;
double inventory_skew = (inventory_ratio - 0.5) * 2.0; // [-1, 1]
double bid_offset = -inventory_skew * tick_size; // 库存多,买价压低
double ask_offset = inventory_skew * tick_size; // 库存多,卖价压低
double bid_price = mid_price + bid_offset;
double ask_price = mid_price + ask_offset;
核心要点:库存管理是做市商的生命线。我曾经见过一个团队,策略回测年化50%,结果实盘第一天就爆仓——因为他们没做库存限制,价格一路跌,他们一路接,最后库存爆了。嗯,从那以后,我所有策略都加了库存硬上限。
三、报价更新频率与性能权衡
报价更新频率,这是个经典的trade-off。更新太快,系统压力大;更新太慢,报价容易被吃。
| 更新频率 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 每笔行情更新 | 报价最精准 | CPU/网络开销大 | 高频做市 |
| 固定间隔(如1ms) | 性能可控 | 可能错过机会 | 中低频做市 |
| 阈值触发 | 平衡性能和精度 | 实现复杂 | 大多数场景 |
我个人习惯用阈值触发:当订单簿变化超过一定阈值(比如价格变动超过1个tick,或深度变化超过10%)才重新报价。这样既保证了报价的时效性,又不会让CPU跑满。
四、对称与非对称报价模型
对称报价,就是买卖价差一样,报价在中间价两侧对称分布。非对称报价,就是故意让买价和卖价不一样,偏向某一侧。
对称报价的好处是简单,适合流动性好的市场。非对称报价则更灵活——比如你库存偏多,就可以让卖价比买价更激进,加速去库存。
// 对称报价
double symmetric_bid = mid_price - spread / 2;
double symmetric_ask = mid_price + spread / 2;
// 非对称报价(库存偏多时)
double asymmetric_bid = mid_price - spread / 2 - inventory_skew * tick_size;
double asymmetric_ask = mid_price + spread / 2 - inventory_skew * tick_size;
小技巧:非对称报价的“非对称程度”应该动态调整。市场平稳时,可以大胆偏向;市场剧烈波动时,还是回归对称更安全。我曾经在ETH永续合约上试过,非对称系数设得太高,结果价格一反弹,两边都被吃,直接亏了3个BTC。
知识体系总览
下面这张图,把报价引擎的核心模块和它们之间的关系画出来了。你可以看到,订单簿是数据基础,报价策略是决策核心,更新频率和报价模型是调优手段。
做市系统的报价引擎,说到底就是“数据+策略+调优”三个环节。订单簿给你数据,策略帮你决策,更新频率和报价模型让你在性能和精度之间找到平衡点。没有银弹,只有根据市场特征不断调整。
我记得刚入行时,总觉得报价引擎越复杂越好,恨不得把机器学习、深度学习全塞进去。后来被市场教育了几次才明白——简单、稳定、可解释,才是做市系统的王道。你想想看,一个复杂的神经网络模型,你都不知道它为什么报这个价,怎么敢让它实盘跑?
一句话总结:订单簿选对数据结构,报价策略盯住库存和波动率,更新频率用阈值触发,报价模型根据市场状态动态切换。做到这四点,你的报价引擎就稳了。
公众号:蓝海资料掘金营,微信deep3321