数据模型设计(下):订单与持仓模型 & 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())
小提示:为什么用 UUID 做主键?因为做市系统订单量巨大,自增 ID 在分布式场景下容易冲突。UUID 虽然占空间大一点,但省心。

持仓模型:实时盯住你的头寸

持仓模型比订单模型更「动态」。订单是历史记录,持仓是当前快照。做市商最怕什么?持仓方向暴露。你想想看,如果只记录了总数量,没区分多头和空头,那风险敞口根本算不清。

我建议持仓模型至少包含以下字段:

  • 净持仓数量:多头 - 空头后的净额。做市商通常希望这个值接近 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.pyversions/ 目录。你需要修改 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

搞定。数据库里就会自动创建 orderspositions 两张表。

避坑指南:我曾经在生成迁移脚本后,直接 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" 表示当账户被删除时,关联的订单和持仓也会被自动删除。做市系统里,账户注销时确实需要清理所有关联数据,这个设置是合理的。

知识体系总览

下面这张图,把订单和持仓模型的核心逻辑串起来了:

Account(账户) Order(订单) 方向 · 状态 · 数量 · 价格 Position(持仓) 净量 · 均价 · 浮动盈亏 一对多 一对多 Alembic 迁移引擎 PostgreSQL / MySQL

从图上可以清楚看到:账户是核心,它关联着订单和持仓。订单是「动作记录」,持仓是「状态快照」。两者通过 Alembic 迁移引擎同步到数据库。

一些实战中的小细节

最后分享几个我在项目中遇到的坑:

  • 数值精度:做市交易涉及小数点后 8 位甚至更多,用 Numeric(18, 8) 是底线。别用 Float,否则你会被精度问题折磨死。
  • 索引设计:订单表按 account_id + created_at 建联合索引,持仓表按 account_id + symbol 建唯一索引。查询性能差很多。
  • 迁移回滚:每次上线前,先在测试环境跑一遍 alembic upgradealembic downgrade -1,确保回滚没问题。我吃过一次亏,上线后才发现降级脚本写错了,差点回不去。

好了,订单和持仓模型就聊到这。这两个模型是整套做市系统的「账本」,设计得越扎实,后面写策略逻辑就越顺手。


专注资料整理