一、核心数据结构设计:库存台账、订单簿、成交记录与资金流水

做市系统的核心,说白了就是管好两样东西:

钱是资金,货是库存。中间穿插着订单、成交、流水这些记录。我做了这么多年做市系统,发现很多新手一上来就写业务逻辑,结果数据结构没设计好,后面改得想哭。

今天我们就来聊聊,这四张核心表该怎么设计。

1. 库存台账设计

库存台账,就是记录你手里有多少货。听起来简单,但坑不少。

举个例子:你在币安上有10个BTC,在OKX上也有5个BTC。总库存是15个。但如果你只记一个总数,哪天币安上的BTC被借出去了,你都不知道。

所以我个人习惯,库存台账要分三层:

  • 总库存:所有交易所加起来的总量
  • 分交易所库存:每个交易所的独立库存
  • 可用/冻结库存:哪些能卖,哪些被订单锁住了

来看一个实际的数据结构:

// 库存台账核心字段
struct InventoryLedger {
    string symbol;          // 交易对,比如 BTC/USDT
    string exchange;        // 交易所标识
    double total_qty;       // 总数量
    double available_qty;   // 可用数量
    double frozen_qty;      // 冻结数量(挂单占用)
    double locked_qty;      // 锁仓数量(风控锁定)
    int64_t update_time;    // 更新时间戳
};

这里有个关键点:可用 + 冻结 + 锁仓 = 总数量。这个等式必须时刻成立。我在项目中遇到过有人忘了更新冻结数量,结果系统以为还有货,继续卖空,最后爆仓了。

⚠️ 我曾经踩过的坑: 库存更新一定要用乐观锁或版本号。并发场景下,两个线程同时扣减库存,会导致数据不一致。我建议用 CAS (Compare And Swap) 方式更新。

2. 订单簿数据结构

订单簿就是买卖双方的排队列表。做市策略的核心就是盯着订单簿,找机会吃单或挂单。

订单簿的数据结构,我推荐用跳表(Skip List)或者红黑树。为什么?因为订单簿需要频繁的插入、删除、查询操作,而且价格是有序的。

来看一个简化版的设计:

// 订单簿中的单个档位
struct OrderBookLevel {
    double price;           // 价格
    double qty;            // 数量
    int order_count;       // 该价格上的订单数量
};

// 订单簿整体结构
struct OrderBook {
    string symbol;
    vector<OrderBookLevel> bids;  // 买盘,按价格降序
    vector<OrderBookLevel> asks;  // 卖盘,按价格升序
    int64_t timestamp;
};

你可能会问:为什么不用数组?

嗯,这里要注意。订单簿的深度是动态变化的。用数组的话,中间插入一个档位,后面的全要移动,性能扛不住。我见过有人用Python的list硬扛,结果一到行情剧烈波动,CPU直接飙到100%。

💡 小技巧: 在实际生产中,我通常用 std::map(红黑树)来存储订单簿。价格作为key,数量作为value。插入、删除、查询都是O(log n),够用了。

3. 成交记录数据结构

成交记录,就是每一笔买卖的明细。这东西看似简单,但设计不好,复盘的时候会疯掉。

我见过最糟糕的设计:只记录了成交价格和数量,没记录订单ID。结果对账的时候,根本不知道这笔成交对应的是哪个挂单。

来看一个靠谱的设计:

struct TradeRecord {
    string trade_id;        // 成交ID,全局唯一
    string order_id;        // 关联的订单ID
    string symbol;
    string side;            // BUY 或 SELL
    double price;
    double qty;
    double fee;             // 手续费
    string fee_currency;    // 手续费币种
    int64_t trade_time;     // 成交时间戳
    string exchange;        // 交易所
};

这里有个细节:手续费一定要单独记录。很多交易所的手续费是用平台币抵扣的,如果你不记清楚,算利润的时候会差一大截。

我记得有一次,一个同事没记录手续费币种,结果月底算账,发现利润少了2%。查了半天,原来是手续费用BNB抵扣了,但系统默认按USDT算的。

4. 资金流水数据结构

资金流水,就是记录你的钱怎么进怎么出的。做市系统里,资金流水比成交记录更重要。为什么?因为成交记录只反映买卖,而资金流水能反映所有资金变动,包括充提、转账、利息、手续费返还等。

我设计的资金流水表,核心字段如下:

struct FundFlow {
    string flow_id;         // 流水ID
    string account_id;      // 账户ID
    string currency;        // 币种
    double amount;          // 变动金额,正数表示增加,负数表示减少
    double balance_before;  // 变动前余额
    double balance_after;   // 变动后余额
    string flow_type;       // 流水类型:TRADE, DEPOSIT, WITHDRAW, FEE, INTEREST
    string ref_id;          // 关联ID,比如订单ID或充值ID
    int64_t create_time;    // 创建时间
};

这里有个原则:每一笔资金变动,都必须有对应的流水记录。而且流水记录一旦生成,就不能修改,只能追加。

🔑 核心原则: 资金流水是只追加的(append-only)。如果你发现某笔流水错了,不要修改它,而是新增一笔冲正流水。这样审计的时候才能追溯。

5. 四张表的关系

这四张表不是孤立的。它们之间的关系,我用一张图来说明:

库存台账 Inventory Ledger 订单簿 Order Book 成交记录 Trade Record 资金流水 Fund Flow 撮合 更新库存 生成流水 核心数据结构关系图:订单簿撮合产生成交,成交更新库存并生成资金流水

从图上可以看得很清楚:

  • 订单簿是输入,它决定了你能以什么价格买卖
  • 成交记录是结果,每一笔成交都来自订单簿的撮合
  • 库存台账资金流水是状态,成交后要同时更新这两者

这里有个容易忽略的点:库存和资金必须同时更新。不能先更新库存再更新资金,万一中间系统挂了,数据就不一致了。我建议用数据库事务,或者用分布式事务框架。

💡 实战建议: 如果你用C++开发,可以考虑用 std::atomic 做库存的并发控制。如果是Python,用 threading.Lock 或者 asyncio.Lock。但说实话,高频场景下,我建议用C++,Python的GIL在极端行情下会拖后腿。

6. 总结一下

这四张表,是做市系统的地基。地基没打好,上面的策略再牛也白搭。

我个人习惯,在项目一开始就把这些数据结构定义好,用单元测试把增删改查都测一遍。别等到上线了才发现字段不够用,那时候改起来成本就高了。

嗯,今天就聊到这里。记住一句话:数据结构决定上层建筑。设计好了,后面写策略就像搭积木一样轻松。

专注资料整理