数据模型设计(下):订单与持仓模型 & Alembic 迁移实战
好,我们接着往下聊。上一节我们把账户和资产模型给定了,这节轮到核心中的核心——订单(Order)和持仓(Position)。这两个模型要是设计不好,后面做市策略跑起来,你连自己赚了亏了都算不清。
我个人习惯,先把最关键的字段列出来,再考虑怎么用代码实现。别一上来就写 ORM,先想清楚业务逻辑。
订单模型:记录每一次交易行为
订单模型说白了,就是记录「谁在什么时间、以什么价格、买了/卖了什么」。但做市系统里的订单,跟普通交易系统的订单有个本质区别——高频、双向、部分成交。
我见过不少新手,把订单模型设计得跟电商订单一样,结果跑回测时发现数据对不上。嗯,这里要注意几个关键点:
- 订单方向:买还是卖?用枚举,别用 0/1 硬编码。
- 订单状态:新建、部分成交、全部成交、已撤销、已过期。做市单经常被部分吃掉,状态流转要清晰。
- 成交数量 vs 委托数量:这两个字段必须分开。我曾经踩过坑,只存了委托数量,结果部分成交后根本不知道还剩多少没成交。
- 订单来源:是策略自动下单,还是手动干预?这个字段在复盘时特别有用。
来看一个简化版的订单模型:
class Order(Base):
__tablename__ = "orders"
id = Column(UUID, primary_key=True, default=uuid4)
account_id = Column(UUID, ForeignKey("accounts.id"), nullable=False)
symbol = Column(String(20), nullable=False)
side = Column(Enum(OrderSide), nullable=False) # BUY / SELL
order_type = Column(Enum(OrderType), default=OrderType.LIMIT)
status = Column(Enum(OrderStatus), default=OrderStatus.NEW)
price = Column(Numeric(18, 8), nullable=False)
quantity = Column(Numeric(18, 8), nullable=False) # 委托数量
filled_quantity = Column(Numeric(18, 8), default=0) # 已成交数量
avg_fill_price = Column(Numeric(18, 8), nullable=True)
source = Column(String(50), default="strategy") # 订单来源
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, onupdate=func.now())
持仓模型:实时盯住你的头寸
持仓模型比订单模型更「动态」。订单是历史记录,持仓是当前快照。做市商最怕什么?持仓方向暴露。你想想看,如果只记录了总数量,没区分多头和空头,那风险敞口根本算不清。
我建议持仓模型至少包含以下字段:
- 净持仓数量:多头 - 空头后的净额。做市商通常希望这个值接近 0。
- 持仓均价:加权平均入场价。注意,这个价格要随着每次成交动态更新。
- 浮动盈亏:当前市价 vs 持仓均价的差值 × 数量。实时计算,别等收盘再算。
- 冻结数量:已经挂单但未成交的部分。这部分不能用于新的开仓。
来看代码:
class Position(Base):
__tablename__ = "positions"
id = Column(UUID, primary_key=True, default=uuid4)
account_id = Column(UUID, ForeignKey("accounts.id"), nullable=False)
symbol = Column(String(20), nullable=False)
net_quantity = Column(Numeric(18, 8), default=0) # 净持仓
avg_cost = Column(Numeric(18, 8), default=0) # 持仓均价
unrealized_pnl = Column(Numeric(18, 8), default=0) # 浮动盈亏
frozen_quantity = Column(Numeric(18, 8), default=0) # 冻结数量
updated_at = Column(DateTime, onupdate=func.now())
数据库迁移实战:Alembic 从零配置
模型定义好了,怎么同步到数据库?手动建表?别闹了。做市系统迭代快,模型一天改三次,手动建表会疯掉。
我用的是 Alembic,SQLAlchemy 官方推荐的迁移工具。说白了,它就是帮你自动生成 SQL 脚本,然后按顺序执行。
第一步,初始化 Alembic:
alembic init alembic
这会在项目根目录生成一个 alembic/ 文件夹,里面有个 env.py 和 versions/ 目录。你需要修改 env.py,告诉 Alembic 你的数据库连接和模型元数据:
# 在 env.py 中
from my_project.models import Base
target_metadata = Base.metadata
# 修改数据库 URL
config.set_main_option("sqlalchemy.url", "postgresql://user:pass@localhost/mydb")
第二步,生成迁移脚本:
alembic revision --autogenerate -m "add orders and positions tables"
Alembic 会自动对比当前模型和数据库状态,生成一个迁移脚本。你打开 versions/ 下的文件看看,里面是 upgrade() 和 downgrade() 两个函数。
第三步,执行迁移:
alembic upgrade head
搞定。数据库里就会自动创建 orders 和 positions 两张表。
alembic upgrade head,结果发现字段类型不对。为什么?因为 --autogenerate 不是万能的,它可能漏掉某些约束或索引。所以每次生成后,一定要手动检查生成的脚本,别偷懒。
模型关系与级联操作
订单和持仓之间是什么关系?一对多。一个账户可以有多个订单,但一个订单只属于一个账户。持仓也是,一个账户对每个交易对只有一个持仓记录。
在 SQLAlchemy 中,我们可以这样定义关系:
class Account(Base):
# ... 其他字段
orders = relationship("Order", back_populates="account", cascade="all, delete-orphan")
positions = relationship("Position", back_populates="account", cascade="all, delete-orphan")
class Order(Base):
account = relationship("Account", back_populates="orders")
class Position(Base):
account = relationship("Account", back_populates="positions")
这里有个细节:cascade="all, delete-orphan" 表示当账户被删除时,关联的订单和持仓也会被自动删除。做市系统里,账户注销时确实需要清理所有关联数据,这个设置是合理的。
知识体系总览
下面这张图,把订单和持仓模型的核心逻辑串起来了:
从图上可以清楚看到:账户是核心,它关联着订单和持仓。订单是「动作记录」,持仓是「状态快照」。两者通过 Alembic 迁移引擎同步到数据库。
一些实战中的小细节
最后分享几个我在项目中遇到的坑:
- 数值精度:做市交易涉及小数点后 8 位甚至更多,用
Numeric(18, 8)是底线。别用 Float,否则你会被精度问题折磨死。 - 索引设计:订单表按
account_id + created_at建联合索引,持仓表按account_id + symbol建唯一索引。查询性能差很多。 - 迁移回滚:每次上线前,先在测试环境跑一遍
alembic upgrade和alembic downgrade -1,确保回滚没问题。我吃过一次亏,上线后才发现降级脚本写错了,差点回不去。
好了,订单和持仓模型就聊到这。这两个模型是整套做市系统的「账本」,设计得越扎实,后面写策略逻辑就越顺手。