第四章:订单管理与路由系统

订单管理与路由系统,说白了就是交易系统的「交通指挥中心」。我个人习惯把它看作三个核心模块:订单状态机、订单簿管理、智能路由。再加上一道风控前置检查,这四块构成了整个交易执行的核心骨架。

嗯,咱们一个一个来拆解。

4.1 订单状态机设计

订单状态机,是订单从生到死的完整生命周期管理。我见过不少新手直接用一个字符串字段存状态,结果上线后各种状态混乱,对账对到崩溃。

⚠️ 我曾经踩过的坑: 状态机设计时漏掉了「部分成交」状态,导致系统在极端行情下出现订单状态死锁。从那以后,我设计状态机必画完整的状态转换图。

一个标准的订单状态机,至少包含以下状态:

状态 含义 触发条件
PENDING_NEW 待提交 用户下单
NEW 已提交 交易所确认接收
PARTIALLY_FILLED 部分成交 部分订单被撮合
FILLED 全部成交 订单完全撮合
CANCELED 已撤销 用户主动撤单
REJECTED 已拒绝 风控或交易所拒绝

你想想看,如果状态机设计得不够严谨,会出现什么情况?比如订单已经全部成交了,还能收到撤单指令,那系统就乱套了。

我个人习惯用有限状态机(FSM)来实现,代码结构清晰,也方便后续扩展。下面是一个简单的状态机核心逻辑:

public enum OrderStatus {
    PENDING_NEW, NEW, PARTIALLY_FILLED, 
    FILLED, CANCELED, REJECTED
}

public class OrderStateMachine {
    private OrderStatus currentStatus;
    
    // 状态转换规则表
    private static final Map<OrderStatus, Set<OrderStatus>> transitions = 
        Map.of(
            OrderStatus.PENDING_NEW, Set.of(OrderStatus.NEW, OrderStatus.REJECTED),
            OrderStatus.NEW, Set.of(OrderStatus.PARTIALLY_FILLED, OrderStatus.FILLED, OrderStatus.CANCELED),
            OrderStatus.PARTIALLY_FILLED, Set.of(OrderStatus.FILLED, OrderStatus.CANCELED)
        );
    
    public boolean canTransitionTo(OrderStatus target) {
        return transitions.getOrDefault(currentStatus, Set.of()).contains(target);
    }
}
💡 小技巧: 状态转换一定要做幂等处理。同一个成交回报重复推送,状态机不能报错,也不能重复修改状态。

4.2 订单簿管理

订单簿,就是买卖双方的挂单队列。说白了,它就是个价格优先、时间优先的排序结构。

我在项目中遇到过一个问题:订单簿深度更新不及时,导致策略看到的盘口和实际盘口差了几个tick。结果策略以为能成交,实际根本吃不到单子。

订单簿的核心数据结构其实不复杂:

  • 买盘(Bid): 按价格从高到低排序
  • 卖盘(Ask): 按价格从低到高排序
  • 每个价格档位: 记录总数量和订单列表

这里我画了一张订单簿的结构图,方便你理解:

订单簿结构示意图 卖盘 (Ask) 价格: 100.50 数量: 200 价格: 100.45 数量: 150 价格: 100.40 数量: 300 最新成交价: 100.35 买盘 (Bid) 价格: 100.30 数量: 500 价格: 100.25 数量: 250 关键说明 • 卖盘:价格从低到高排列 • 买盘:价格从高到低排列 • 最优卖价(Ask1): 100.40 • 最优买价(Bid1): 100.30 • 买卖价差: 0.10 更新频率要求: • 行情快照: 每100ms • 增量更新: 实时推送 • 延迟超过500ms需告警

订单簿的更新逻辑,我建议用增量更新+全量快照的双重机制。平时只处理增量数据,每隔一段时间做一次全量快照做校验。这样既能保证实时性,又能防止数据累积误差。

4.3 智能路由算法

智能路由,就是帮你的订单找到最优的交易所。现在很多品种都在多个交易所同时交易,同一个价格在不同交易所可能差好几个tick。

为什么会这样?因为每个交易所的流动性深度不一样,撮合速度也不一样。你想想看,如果一个大单子全部砸在一个交易所,可能直接把盘口打穿好几个档位。但拆成小单子分散到多个交易所,成交成本就低很多。

我常用的路由策略有几种:

  • 价格优先路由: 哪个交易所价格好,就往哪送
  • 流动性路由: 按各交易所的深度比例分配订单
  • 延迟敏感路由: 优先选择网络延迟最低的交易所

核心算法示例: 基于流动性的加权路由

public class SmartRouter {
    // 计算各交易所的流动性权重
    public Map<Exchange, Double> calculateWeights(OrderBookSnapshot snapshot) {
        double totalLiquidity = snapshot.getExchanges().stream()
            .mapToDouble(e -> e.getBidDepth(5) + e.getAskDepth(5))
            .sum();
        
        Map<Exchange, Double> weights = new HashMap<>();
        for (Exchange ex : snapshot.getExchanges()) {
            double depth = ex.getBidDepth(5) + ex.getAskDepth(5);
            weights.put(ex, depth / totalLiquidity);
        }
        return weights;
    }
}

嗯,这里要注意一点:路由算法不能只看当前状态,还要考虑历史表现。我记得有一次某个交易所突然网络抖动,但流动性数据还没更新,结果路由算法继续往那送单,白白损失了好几笔。

4.4 风控前置检查

风控前置检查,是订单进入交易系统前的最后一道防线。说白了,就是「先检查,再执行」。我见过太多因为风控后置导致爆仓的案例了。

我个人习惯把风控检查分成三个层级:

层级 检查项 说明
L1 基础检查 订单参数合法性 价格、数量、方向是否合规
L2 资金检查 可用资金、持仓限额 防止超仓、透支交易
L3 策略检查 频率限制、自成交防护 防止高频报单、自成交
⚠️ 我曾经踩过的坑: 风控检查放在订单路由之后,结果路由已经发出去了,风控才拦截。虽然最终撤单了,但已经造成了市场冲击。从那以后,我坚持风控前置,路由之前必须过风控。

风控检查的代码实现,我建议用责任链模式。每个检查项是一个独立的处理器,可以灵活组合和扩展:

public interface RiskCheckHandler {
    boolean check(Order order);
    void setNext(RiskCheckHandler next);
}

public class RiskCheckChain {
    private List<RiskCheckHandler> handlers = new ArrayList<>();
    
    public boolean execute(Order order) {
        for (RiskCheckHandler handler : handlers) {
            if (!handler.check(order)) {
                // 记录风控日志
                log.warn("风控拦截: {} 未通过 {}", order.getId(), handler.getClass().getSimpleName());
                return false;
            }
        }
        return true;
    }
}
💡 小技巧: 风控检查的日志一定要详细记录。出了问题复盘时,风控日志就是你的「黑匣子」。我一般会记录:订单ID、检查项、检查结果、耗时、当前系统状态快照。

好了,订单管理与路由系统的核心内容就这些。状态机管生命周期,订单簿管市场数据,路由算法管执行路径,风控检查管安全底线。四者配合好了,交易系统才能跑得稳、跑得快。


公众号:蓝海资料掘金营,微信deep3321