2. 系统架构设计:做市系统整体架构、核心模块划分、数据流与订单流设计
做市系统的架构设计,说白了就是搭骨架。骨架搭歪了,后面填再多肉也跑不稳。我这些年见过不少团队,一上来就怼代码,结果联调时发现数据流打结了,订单飞了,回测数据对不上——嗯,那感觉就像你炒菜发现盐和糖放反了,锅都烧红了。
今天咱们就聊聊,一个能扛住实盘压力的做市系统,架构上到底该怎么切。我个人习惯,先画一张大图,把模块、数据流、订单流理清楚,再动手写一行代码。
2.1 整体架构:三层两总线
我参与过的几个做市项目,最后沉淀下来的架构模式都差不多。我管它叫「三层两总线」。
- 接入层:对接交易所API,处理行情、交易、账户信息。说白了就是跟交易所打交道的「翻译官」。
- 核心层:策略引擎、风控引擎、订单管理。这是做市系统的大脑和心脏。
- 服务层:数据存储、监控告警、UI界面、运维工具。给人和机器看的。
两总线呢?一条是行情总线,一条是订单总线。所有模块都挂在总线上,解耦通信。我在项目中遇到过,有人把行情处理和策略逻辑写在一个进程里,结果行情一爆发,策略卡死,订单全挂。从那以后,我坚持用消息总线隔离开。
核心原则:行情流和订单流必须物理隔离。行情可以丢,订单不能乱。
2.2 核心模块划分:各司其职
模块怎么切?我建议按职责边界来,别按功能大小来。你想想看,一个「订单管理」模块如果既管下单又管风控又管资金,那改一个地方就得动全身,线上出问题你都不知道锅在谁头上。
2.2.1 行情模块
- 行情订阅:支持多交易所、多币对、多深度。我习惯用WebSocket,稳定且实时。
- 行情清洗:去重、排序、校验时间戳。曾经有一次,交易所推送了重复的tick,策略按两倍量报价,差点出事。
- 行情分发:通过行情总线推给策略引擎和风控引擎。注意,这里要支持「快照+增量」模式。
2.2.2 策略引擎
- 报价策略:根据当前盘口、库存、波动率,计算最优买卖报价。
- 对冲策略:在多个交易所或币对之间做Delta对冲。
- 库存管理:设定目标库存区间,超限则触发rebalance。
小技巧:策略引擎最好支持热加载。我在项目中遇到过,改一个参数要重启整个进程,那几分钟的停机时间,做市商根本扛不住。后来我们用了动态配置中心,参数秒级生效。
2.2.3 风控引擎
风控是做市系统的底线。我见过太多团队,策略赚钱时觉得风控碍事,结果一次黑天鹅就归零了。
- 价格风控:偏离市场中间价超过X%的报价,直接拦截。
- 数量风控:单笔订单数量、累计持仓量、日交易量上限。
- 自成交风控:同一策略或不同策略之间,禁止自我成交。
- 频率风控:每秒下单次数限制,防止程序失控。
警告:风控规则一定要写在策略引擎之前执行。我曾经见过一个架构,风控在订单发出后才检查,结果策略发了1000个废单,交易所直接封了API。
2.2.4 订单管理器
订单管理器负责订单的完整生命周期:创建、发送、确认、撤销、成交、失败。每个订单都有一个状态机,状态转换必须严格校验。
// 订单状态机示例(伪代码)
enum OrderStatus {
PENDING, // 待发送
SENT, // 已发送
PARTIAL_FILLED,// 部分成交
FILLED, // 全部成交
CANCELLED, // 已撤销
REJECTED // 被拒
}
function transition(order, newStatus) {
// 校验状态转换合法性
if (!isValidTransition(order.status, newStatus)) {
throw new Error("非法状态转换: " + order.status + " -> " + newStatus);
}
order.status = newStatus;
// 记录状态变更日志
logStateChange(order);
}
2.3 数据流设计:行情怎么跑,资金怎么算
数据流设计,说白了就是回答三个问题:数据从哪来?到哪去?中间怎么处理?
2.3.1 行情数据流
- 交易所推送行情 → 行情网关接收并解码
- 行情网关做初步清洗(去重、排序)
- 推送到行情总线(Kafka topic: market.raw)
- 策略引擎订阅行情总线,计算报价
- 风控引擎订阅行情总线,做实时风控检查
- 数据存储模块订阅行情总线,落盘到时序数据库(如InfluxDB)
2.3.2 资金数据流
- 交易网关收到成交回报 → 更新本地订单状态
- 资金核算模块计算:成交金额、手续费、保证金变化
- 更新账户余额和持仓(写入Redis + MySQL)
- 推送资金变更事件到监控告警模块
注意:资金数据必须保证强一致性。我建议用「本地事务+补偿机制」,别依赖分布式事务,那玩意在低延迟场景下根本跑不动。
2.4 订单流设计:从策略到交易所的完整链路
订单流是做市系统的生命线。一个订单从策略产生到交易所确认,中间经过的每一步都要可追踪、可回放。
2.4.1 订单生成与发送
- 策略引擎计算报价 → 生成订单对象(包含价格、数量、方向、策略ID)
- 订单对象发送到订单总线(RabbitMQ queue: order.outgoing)
- 订单管理器从队列消费,做本地校验(风控检查、资金检查)
- 校验通过 → 发送到交易网关 → 交易网关调用交易所API
- 校验不通过 → 记录拒绝原因,推送到告警模块
2.4.2 订单回报处理
- 交易所返回成交/撤销/拒绝回报 → 交易网关接收
- 回报推送到订单总线(queue: order.incoming)
- 订单管理器更新本地订单状态
- 策略引擎收到状态变更事件,调整报价
- 资金核算模块更新账户余额
// 订单流关键路径(伪代码)
function placeOrder(order) {
// 1. 风控检查
if (!riskEngine.check(order)) {
logRejection(order, "风控拦截");
return;
}
// 2. 发送到交易所
exchangeOrder = await tradeGateway.send(order);
// 3. 更新本地状态
orderManager.updateStatus(exchangeOrder.id, "SENT");
// 4. 等待回报(异步)
waitForFill(exchangeOrder.id);
}
2.5 架构设计中的几个坑
最后聊几个我踩过的坑,希望能帮你省点时间。
- 坑一:行情和订单共用一条总线。行情量大,订单要求低延迟,混在一起互相影响。我后来强制分了两条总线,世界清净了。
- 坑二:风控放在策略内部。策略出bug时,风控也跟着失效。风控必须独立部署,独立进程,甚至独立机器。
- 坑三:没有订单重放机制。系统重启后,订单状态丢了,资金对不上。我建议所有订单事件都写WAL(Write-Ahead Log),重启后重放恢复。
- 坑四:忽略行情延迟。行情从交易所到策略引擎,延迟超过50ms,报价就失去意义了。我习惯用UDP multicast或者共享内存做行情分发,延迟能压到1ms以内。
个人经验:架构设计没有银弹。每个交易所的API风格、限频规则、数据格式都不一样。我建议先做一套「适配器模式」,把交易所差异封装在接入层,核心层只跟标准接口打交道。这样换交易所时,改一行配置就行。
好了,架构设计这块就聊到这儿。记住一句话:好的架构是改出来的,不是设计出来的。先跑通,再优化,别一开始就追求完美。
公众号:蓝海资料掘金营,微信deep3321