4. 数据库模型设计:设计库存、订单、交易对等核心数据表结构,使用SQLAlchemy ORM

好,咱们进入数据库模型设计这一章。说实话,做市商系统里最核心的资产就是数据——库存、订单、交易对,这三样东西要是没设计好,后面所有策略都是空中楼阁。我个人习惯是先画清楚业务关系,再动手写代码,这样不容易跑偏。

4.1 先理清业务关系

你想想看,做市商每天都在干什么?说白了就是两件事:报价成交。报价基于交易对,成交产生订单,订单影响库存。这三者之间是环环相扣的。

我画了一张关系图,帮你快速建立整体认知:

交易对 (Pair) BTC/USDT, ETH/BTC ... 订单 (Order) 买单 / 卖单 / 成交记录 库存 (Inventory) 各币种持仓 & 可用量 报价依据 计价基准 成交后更新持仓 核心关系:交易对驱动报价 → 报价产生订单 → 订单改变库存 库存又反过来影响报价策略(库存高则降价,库存低则提价)

嗯,这张图你看明白了吗?交易对是「舞台」,订单是「演员」,库存是「后台的物资」。三者缺一不可。

4.2 交易对表(Pair)—— 最基础的配置

交易对表是所有报价的起点。我在项目中遇到过一个问题:一开始只存了交易对名称,后来发现不同交易所的命名规则不一样,比如币安叫 BTCUSDT,火币叫 btcusdt,差点搞出大乌龙。所以,我建议你从一开始就把基础信息存全。

字段名类型说明
idInteger (PK)主键,自增
base_assetString(16)基础币种,如 BTC
quote_assetString(16)计价币种,如 USDT
symbolString(32)统一交易对标识,如 BTC/USDT
exchangeString(32)交易所名称
min_qtyNumeric(18,8)最小交易数量
tick_sizeNumeric(18,8)最小价格变动
is_activeBoolean是否启用
created_atDateTime创建时间
class Pair(Base):
    __tablename__ = 'pairs'

    id = Column(Integer, primary_key=True, autoincrement=True)
    base_asset = Column(String(16), nullable=False)
    quote_asset = Column(String(16), nullable=False)
    symbol = Column(String(32), unique=True, nullable=False)
    exchange = Column(String(32), nullable=False)
    min_qty = Column(Numeric(18, 8), default=0.0001)
    tick_size = Column(Numeric(18, 8), default=0.01)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)

    orders = relationship('Order', back_populates='pair')
    inventories = relationship('Inventory', back_populates='pair')
💡 我的小技巧:symbol 设为 unique 索引,查询时直接用 symbol 查,不用每次都拼接 base_asset 和 quote_asset。性能提升很明显。

4.3 订单表(Order)—— 记录每一次心跳

订单表是数据量最大的表。做市商一天可能产生几万甚至几十万笔订单。我刚开始做的时候,把所有订单都塞一张表里,结果查询慢得要命。后来学乖了——按时间分区,或者至少把「当前活跃订单」和「历史订单」分开。

字段名类型说明
idInteger (PK)主键
pair_idInteger (FK)关联交易对
order_idString(64)交易所订单ID
sideEnum('buy','sell')买卖方向
order_typeEnum('limit','market')订单类型
priceNumeric(18,8)委托价格
quantityNumeric(18,8)委托数量
filled_qtyNumeric(18,8)已成交数量
statusEnum('pending','partial','filled','canceled')订单状态
created_atDateTime创建时间
updated_atDateTime最后更新时间
class Order(Base):
    __tablename__ = 'orders'

    id = Column(Integer, primary_key=True, autoincrement=True)
    pair_id = Column(Integer, ForeignKey('pairs.id'), nullable=False)
    order_id = Column(String(64), unique=True, nullable=False)
    side = Column(Enum('buy', 'sell', name='order_side'), nullable=False)
    order_type = Column(Enum('limit', 'market', name='order_type'), nullable=False)
    price = Column(Numeric(18, 8), nullable=False)
    quantity = Column(Numeric(18, 8), nullable=False)
    filled_qty = Column(Numeric(18, 8), default=0)
    status = Column(Enum('pending', 'partial', 'filled', 'canceled', name='order_status'), default='pending')
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    pair = relationship('Pair', back_populates='orders')
⚠️ 避坑指南: 我曾经把 order_id 设成 Integer 自增,结果交易所返回的订单ID是字符串,还带字母!后来改成 String(64) 才解决。记住:永远不要假设外部系统的ID格式

4.4 库存表(Inventory)—— 你的家底

库存表记录每个币种、每个交易对上的持仓情况。做市商最怕什么?最怕库存管理混乱,明明账户里没钱了还在下买单,或者币不够了还在卖空。所以,库存表的设计要「实时、准确、可追溯」。

字段名类型说明
idInteger (PK)主键
pair_idInteger (FK)关联交易对
assetString(16)币种,如 BTC、USDT
total_balanceNumeric(18,8)总余额
locked_balanceNumeric(18,8)冻结余额(挂单占用)
available_balanceNumeric(18,8)可用余额
updated_atDateTime更新时间
class Inventory(Base):
    __tablename__ = 'inventories'

    id = Column(Integer, primary_key=True, autoincrement=True)
    pair_id = Column(Integer, ForeignKey('pairs.id'), nullable=False)
    asset = Column(String(16), nullable=False)
    total_balance = Column(Numeric(18, 8), default=0)
    locked_balance = Column(Numeric(18, 8), default=0)
    available_balance = Column(Numeric(18, 8), default=0)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    pair = relationship('Pair', back_populates='inventories')

    @property
    def available(self):
        """计算可用余额,其实就是总余额减去冻结部分"""
        return self.total_balance - self.locked_balance
🔑 关键设计思路: 我特意把 available_balance 单独存为一个字段,而不是每次都计算。为什么?因为查询频率太高了!每次报价都要查可用余额,如果每次都做减法,数据库压力会很大。用字段存着,更新时一次性算好,查询时直接取。

4.5 表之间的关系与约束

三张表的关系其实很清晰:

  • Pair → Order:一对多。一个交易对可以有无数笔订单。
  • Pair → Inventory:一对多。一个交易对涉及两个币种(基础币和计价币),所以至少两条库存记录。
  • Order → Inventory:间接关联。订单成交后,通过业务逻辑更新库存。

这里有个细节要注意:外键约束。我建议在数据库层面加上外键,虽然 ORM 层面也能维护关系,但数据库级别的约束能防止脏数据。比如,你删了一个交易对,如果还有订单引用它,数据库会直接报错,而不是留下一堆孤儿数据。

# 在创建表时,可以加上外键约束
Order.__table__.create(bind=engine, checkfirst=True)
# 或者手动添加
ALTER TABLE orders ADD CONSTRAINT fk_orders_pair FOREIGN KEY (pair_id) REFERENCES pairs(id);

4.6 索引策略——查询快不快,就看索引

做市商系统对查询性能要求极高。我见过一个项目,订单表几百万条数据,没有索引,查一笔订单要好几秒——这还怎么做高频交易?

我建议至少加这几个索引:

  • orders.order_id:唯一索引,按交易所订单ID查
  • orders.pair_id + orders.status:联合索引,查某个交易对的活跃订单
  • orders.created_at:普通索引,按时间范围查询
  • inventories.pair_id + inventories.asset:联合唯一索引,一个交易对一个币种只有一条记录
class Order(Base):
    __tablename__ = 'orders'
    # ... 字段定义 ...

    __table_args__ = (
        Index('idx_pair_status', 'pair_id', 'status'),
        Index('idx_created_at', 'created_at'),
    )
💡 经验之谈: 索引不是越多越好。写操作频繁的表,索引太多会拖慢插入速度。我的原则是:查询频率高的字段加索引,写多读少的表少加索引。订单表是读多写多,所以只加最必要的索引。

4.7 完整代码示例

最后,我把三张表的完整定义放在一起,方便你直接复制使用:

from sqlalchemy import create_engine, Column, Integer, String, Numeric, DateTime, Enum, Boolean, ForeignKey, Index
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from datetime import datetime

Base = declarative_base()

class Pair(Base):
    __tablename__ = 'pairs'
    id = Column(Integer, primary_key=True, autoincrement=True)
    base_asset = Column(String(16), nullable=False)
    quote_asset = Column(String(16), nullable=False)
    symbol = Column(String(32), unique=True, nullable=False)
    exchange = Column(String(32), nullable=False)
    min_qty = Column(Numeric(18, 8), default=0.0001)
    tick_size = Column(Numeric(18, 8), default=0.01)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    orders = relationship('Order', back_populates='pair')
    inventories = relationship('Inventory', back_populates='pair')

class Order(Base):
    __tablename__ = 'orders'
    id = Column(Integer, primary_key=True, autoincrement=True)
    pair_id = Column(Integer, ForeignKey('pairs.id'), nullable=False)
    order_id = Column(String(64), unique=True, nullable=False)
    side = Column(Enum('buy', 'sell', name='order_side'), nullable=False)
    order_type = Column(Enum('limit', 'market', name='order_type'), nullable=False)
    price = Column(Numeric(18, 8), nullable=False)
    quantity = Column(Numeric(18, 8), nullable=False)
    filled_qty = Column(Numeric(18, 8), default=0)
    status = Column(Enum('pending', 'partial', 'filled', 'canceled', name='order_status'), default='pending')
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    pair = relationship('Pair', back_populates='orders')
    __table_args__ = (
        Index('idx_pair_status', 'pair_id', 'status'),
        Index('idx_created_at', 'created_at'),
    )

class Inventory(Base):
    __tablename__ = 'inventories'
    id = Column(Integer, primary_key=True, autoincrement=True)
    pair_id = Column(Integer, ForeignKey('pairs.id'), nullable=False)
    asset = Column(String(16), nullable=False)
    total_balance = Column(Numeric(18, 8), default=0)
    locked_balance = Column(Numeric(18, 8), default=0)
    available_balance = Column(Numeric(18, 8), default=0)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    pair = relationship('Pair', back_populates='inventories')
    __table_args__ = (
        Index('idx_pair_asset', 'pair_id', 'asset', unique=True),
    )

# 初始化数据库
engine = create_engine('sqlite:///market_maker.db')
Base.metadata.create_all(engine)

好了,数据库模型设计这块就讲完了。这三张表是后面所有功能的基础——报价引擎要读交易对和库存,风控模块要查订单状态,报表系统要聚合历史订单。把地基打牢,后面盖楼才稳当。


专注资料整理