4. 数据库模型设计:设计库存、订单、交易对等核心数据表结构,使用SQLAlchemy ORM
好,咱们进入数据库模型设计这一章。说实话,做市商系统里最核心的资产就是数据——库存、订单、交易对,这三样东西要是没设计好,后面所有策略都是空中楼阁。我个人习惯是先画清楚业务关系,再动手写代码,这样不容易跑偏。
4.1 先理清业务关系
你想想看,做市商每天都在干什么?说白了就是两件事:报价和成交。报价基于交易对,成交产生订单,订单影响库存。这三者之间是环环相扣的。
我画了一张关系图,帮你快速建立整体认知:
嗯,这张图你看明白了吗?交易对是「舞台」,订单是「演员」,库存是「后台的物资」。三者缺一不可。
4.2 交易对表(Pair)—— 最基础的配置
交易对表是所有报价的起点。我在项目中遇到过一个问题:一开始只存了交易对名称,后来发现不同交易所的命名规则不一样,比如币安叫 BTCUSDT,火币叫 btcusdt,差点搞出大乌龙。所以,我建议你从一开始就把基础信息存全。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | Integer (PK) | 主键,自增 |
| base_asset | String(16) | 基础币种,如 BTC |
| quote_asset | String(16) | 计价币种,如 USDT |
| symbol | String(32) | 统一交易对标识,如 BTC/USDT |
| exchange | String(32) | 交易所名称 |
| min_qty | Numeric(18,8) | 最小交易数量 |
| tick_size | Numeric(18,8) | 最小价格变动 |
| is_active | Boolean | 是否启用 |
| created_at | DateTime | 创建时间 |
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)—— 记录每一次心跳
订单表是数据量最大的表。做市商一天可能产生几万甚至几十万笔订单。我刚开始做的时候,把所有订单都塞一张表里,结果查询慢得要命。后来学乖了——按时间分区,或者至少把「当前活跃订单」和「历史订单」分开。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | Integer (PK) | 主键 |
| pair_id | Integer (FK) | 关联交易对 |
| order_id | String(64) | 交易所订单ID |
| side | Enum('buy','sell') | 买卖方向 |
| order_type | Enum('limit','market') | 订单类型 |
| price | Numeric(18,8) | 委托价格 |
| quantity | Numeric(18,8) | 委托数量 |
| filled_qty | Numeric(18,8) | 已成交数量 |
| status | Enum('pending','partial','filled','canceled') | 订单状态 |
| created_at | DateTime | 创建时间 |
| updated_at | DateTime | 最后更新时间 |
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)—— 你的家底
库存表记录每个币种、每个交易对上的持仓情况。做市商最怕什么?最怕库存管理混乱,明明账户里没钱了还在下买单,或者币不够了还在卖空。所以,库存表的设计要「实时、准确、可追溯」。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | Integer (PK) | 主键 |
| pair_id | Integer (FK) | 关联交易对 |
| asset | String(16) | 币种,如 BTC、USDT |
| total_balance | Numeric(18,8) | 总余额 |
| locked_balance | Numeric(18,8) | 冻结余额(挂单占用) |
| available_balance | Numeric(18,8) | 可用余额 |
| updated_at | DateTime | 更新时间 |
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)
好了,数据库模型设计这块就讲完了。这三张表是后面所有功能的基础——报价引擎要读交易对和库存,风控模块要查订单状态,报表系统要聚合历史订单。把地基打牢,后面盖楼才稳当。