2. 核心数据模型设计:订单簿、持仓、交易对与告警规则

做市系统跑起来,第一件事就是把数据模型定好。我见过不少团队,代码写了大半才发现数据结构设计不合理,回头重构,那叫一个痛苦。今天咱们就把订单簿、持仓、交易对、告警规则这四个核心模型聊透。

2.1 订单簿(OrderBook)数据结构

订单簿说白了就是买卖双方的挂单列表。做市商靠它判断市场深度,决定要不要吃单或者挂单。

核心字段设计:

字段名 类型 说明
symbol str 交易对,如 "BTCUSDT"
bids list[tuple] 买单列表,每个元素 (price, quantity)
asks list[tuple] 卖单列表,每个元素 (price, quantity)
timestamp int 快照时间戳(毫秒)
update_id int 更新序号,用于增量合并

我个人习惯用 SortedDict 来维护订单簿。为什么?因为买卖盘需要按价格排序,而且插入删除频繁。Python 的 dict 虽然快,但排序麻烦。用 SortedDict 可以 O(log n) 完成插入和查询。

避坑指南: 我曾经在某个项目中直接用 list 存订单簿,每次增量更新都全量排序。结果数据量一大,CPU 直接飙到 100%。后来换成 SortedDict,性能提升了 10 倍不止。

from sortedcontainers import SortedDict

class OrderBook:
    def __init__(self, symbol: str):
        self.symbol = symbol
        self.bids = SortedDict(lambda k: -k)  # 价格降序
        self.asks = SortedDict()              # 价格升序
        self.timestamp = 0
        self.update_id = 0

    def apply_snapshot(self, bids: list, asks: list, ts: int):
        """全量更新订单簿"""
        self.bids.clear()
        self.asks.clear()
        for price, qty in bids:
            if float(qty) > 0:
                self.bids[float(price)] = float(qty)
        for price, qty in asks:
            if float(qty) > 0:
                self.asks[float(price)] = float(qty)
        self.timestamp = ts

    def apply_update(self, bids: list, asks: list, update_id: int):
        """增量更新订单簿"""
        if update_id <= self.update_id:
            return  # 丢弃过期更新
        for price, qty in bids:
            price_f = float(price)
            qty_f = float(qty)
            if qty_f == 0:
                self.bids.pop(price_f, None)
            else:
                self.bids[price_f] = qty_f
        for price, qty in asks:
            price_f = float(price)
            qty_f = float(qty)
            if qty_f == 0:
                self.asks.pop(price_f, None)
            else:
                self.asks[price_f] = qty_f
        self.update_id = update_id

你想想看,如果增量更新时不做 update_id 校验,网络延迟导致旧数据覆盖新数据,那订单簿就乱套了。嗯,这里要注意。

2.2 持仓(Position)与风险敞口模型

持仓模型记录你当前持有哪些资产、数量多少、成本价多少。风险敞口则是告诉你,如果市场反向波动,你会亏多少钱。

持仓模型核心字段:

字段名 类型 说明
asset str 资产名称,如 "BTC"
total_qty float 总持仓数量
locked_qty float 冻结数量(挂单占用)
avg_cost float 平均成本价
unrealized_pnl float 未实现盈亏

风险敞口模型:

我个人习惯把风险敞口拆成两部分:

  • 方向性敞口: 净持仓数量 × 当前价格。做市商一般希望这个值接近 0。
  • 基差敞口: 现货与期货之间的价差风险。这个容易被忽略。

小技巧: 我建议在风险敞口模型里加一个 max_exposure 字段,用来限制单边最大敞口。比如你设定 BTC 敞口不能超过 0.5 个,系统会自动拒绝超过阈值的挂单。

@dataclass
class Position:
    asset: str
    total_qty: float = 0.0
    locked_qty: float = 0.0
    avg_cost: float = 0.0

    @property
    def available_qty(self) -> float:
        """可用数量 = 总持仓 - 冻结数量"""
        return self.total_qty - self.locked_qty

    @property
    def unrealized_pnl(self, current_price: float) -> float:
        """未实现盈亏 = (当前价 - 成本价) × 持仓量"""
        return (current_price - self.avg_cost) * self.total_qty

@dataclass
class RiskExposure:
    symbol: str
    net_position: float       # 净持仓(多头 - 空头)
    current_price: float
    max_exposure: float       # 最大允许敞口
    var_95: float = 0.0       # 95% VaR 值

    @property
    def directional_exposure(self) -> float:
        """方向性敞口 = |净持仓| × 当前价"""
        return abs(self.net_position) * self.current_price

    def is_safe(self) -> bool:
        """检查是否在安全范围内"""
        return self.directional_exposure <= self.max_exposure

2.3 交易对(TradingPair)配置模型

交易对配置是系统的「交通规则」。每个交易对都有不同的最小交易量、价格精度、费率等。我建议把这些配置集中管理,不要硬编码。

字段名 类型 说明
symbol str 交易对名称
base_asset str 基础资产,如 "BTC"
quote_asset str 计价资产,如 "USDT"
min_qty float 最小交易数量
price_precision int 价格小数位数
qty_precision int 数量小数位数
taker_fee float 吃单费率
maker_fee float 挂单费率
spread_bps float 目标价差(基点)

注意: 价格精度和数量精度搞错了,会导致下单被交易所拒绝。我曾经因为 price_precision 少了一位小数,挂单一直失败,排查了半天才发现是精度问题。

@dataclass
class TradingPairConfig:
    symbol: str
    base_asset: str
    quote_asset: str
    min_qty: float
    price_precision: int
    qty_precision: int
    taker_fee: float
    maker_fee: float
    spread_bps: float

    def round_price(self, price: float) -> float:
        """按精度舍入价格"""
        return round(price, self.price_precision)

    def round_qty(self, qty: float) -> float:
        """按精度舍入数量"""
        return round(qty, self.qty_precision)

    def calculate_spread(self, mid_price: float) -> tuple:
        """计算买卖价差"""
        spread = mid_price * self.spread_bps / 10000
        bid = self.round_price(mid_price - spread / 2)
        ask = self.round_price(mid_price + spread / 2)
        return bid, ask

2.4 告警规则(AlertRule)模型设计

告警规则是做市系统的「哨兵」。市场异常、持仓超限、网络断开,都需要及时告警。我习惯把告警规则设计成可配置的,这样运营人员可以随时调整,不用改代码。

告警规则核心字段:

字段名 类型 说明
rule_id str 规则唯一标识
rule_type str 规则类型:price_spike / position_limit / network_down / pnl_loss
symbol str 关联交易对(可选)
condition dict 触发条件,如 {"threshold": 0.05, "operator": "gt"}
severity str 告警级别:info / warning / critical
enabled bool 是否启用
cooldown_seconds int 冷却时间,防止重复告警

避坑指南: 我曾经没加 cooldown_seconds,结果网络抖动时,告警短信一分钟发了 200 条,运营同事差点把我拉黑。后来我强制每个规则至少 60 秒冷却时间。

@dataclass
class AlertRule:
    rule_id: str
    rule_type: str
    symbol: Optional[str] = None
    condition: dict = field(default_factory=dict)
    severity: str = "warning"
    enabled: bool = True
    cooldown_seconds: int = 60
    last_triggered: float = 0.0  # 上次触发时间戳

    def should_trigger(self, current_value: float) -> bool:
        """判断是否应该触发告警"""
        if not self.enabled:
            return False
        # 检查冷却时间
        if time.time() - self.last_triggered < self.cooldown_seconds:
            return False
        threshold = self.condition.get("threshold", 0)
        operator = self.condition.get("operator", "gt")
        if operator == "gt":
            return current_value > threshold
        elif operator == "lt":
            return current_value < threshold
        elif operator == "abs_gt":
            return abs(current_value) > threshold
        return False

    def trigger(self):
        """触发告警,更新上次触发时间"""
        self.last_triggered = time.time()
        # 这里可以调用发送邮件、短信、钉钉等通知

2.5 数据模型关系图

这四个模型不是孤立的。订单簿提供市场数据,持仓模型记录你的状态,交易对配置决定交易参数,告警规则监控一切异常。我画了一张图,帮你理清它们之间的关系。

订单簿 OrderBook 持仓模型 Position 交易对配置 TradingPair 告警规则 AlertRule 提供深度 配置参数 监控异常 价格异常 费率异常 核心逻辑:订单簿提供市场数据 → 持仓模型计算风险敞口 → 交易对配置约束交易行为 → 告警规则监控一切异常

你看这张图,订单簿和持仓模型是数据源,交易对配置是规则引擎,告警规则是最后的防线。四者配合,才能构建一个健壮的做市系统。

个人建议: 刚开始做的时候,别想着把所有字段都设计完美。先跑通核心流程,再根据实际需求迭代。我见过太多人花两周设计模型,结果上线后发现一半字段用不上。

好了,数据模型就聊到这儿。记住一句话:好的数据模型,能让你的系统跑得又快又稳;差的数据模型,会让你天天加班修 Bug。希望今天的分享对你有帮助。


公众号:蓝海数据掘金营,微信deep3321