Caloogy 文章
English

监控、风控闸门与"熔断开关"

日期
2025 年 4 月 3 日
系列
量化
阅读
4 分钟
主题
QuantQuant infraPre trade riskKill switch

一句话版本

自动交易系统的可怕之处在于:它出错的速度和它赚钱的速度一样快。2012 年,美国做市商 Knight Capital 因为一次部署失误,旧代码在开盘后 45 分钟内疯狂下单,亏损约 4.6 亿美元,公司几乎当天就失去了独立生存能力。 每一笔订单发出之前,都应当经过一道独立于策略的交易前风控闸门:检查标的、价格、数量、仓位、频率是否在允许范围内;同时要有一个能一键停止所有交易的熔断开关(kill switch)。这些检查简单得近乎无聊,但正是它们决定了一个 bug 是损失几百块还是损失整个公司。

免责

本文仅为技术科普,不构成任何投资建议。


第一层:通俗理解

1.1 银行柜台的复核

银行柜员办理一笔大额转账,系统会自动检查:账户余额够不够、金额是否超过单笔限额、收款账户是否在黑名单上、今天是否已经转了很多笔。任何一项不通过,转账就办不了,柜员本人也绕不过去。

交易系统里的风控闸门就是这个"柜台复核"。它和策略是分开的:策略可能有 bug、可能被错误的数据误导,但风控闸门只做简单、确定、可审查的检查。

1.2 Knight Capital 事件(2012 年 8 月 1 日)

时间 事件
部署前 新代码需要部署到 8 台服务器上;其中 1 台被漏掉了,仍然运行着一段多年未用的旧逻辑
开盘 新代码复用了一个原本属于旧功能的标志位。在那台漏部署的服务器上,这个标志位唤醒了旧逻辑,开始不断发送订单
45 分钟内 系统在约 150 只股票上发出了数百万笔订单,形成了数十亿美元的意外头寸
结果 亏损约 4.6 亿美元;几天后公司接受紧急注资,次年被收购
监管 美国证监会随后认定其违反了市场准入规则中关于风控措施的要求,并处以罚款

事后分析中被反复强调的几点:部署流程没有自动校验、没有对异常订单量的自动熔断、系统发出的告警邮件没有引起及时行动。


第二层:风控闸门检查什么

2.1 交易前检查清单

检查 防什么
标的白名单 交易了不该交易的品种(代码写错、数据错位)
数量和价格合法 零、负数、极大值
单笔金额上限 "胖手指":多打了几个零
价格偏离 下单价格偏离当前参考价太多(比如超过 5%),通常是数据错误或单位错误
持仓上限 成交后的总头寸超过允许范围
下单频率 短时间内大量下单,往往是循环 bug
每日亏损上限 当日亏损达到阈值后停止开新仓
自成交检查 自己的买单和卖单撮合(可能违反规则)

2.2 熔断开关

一个任何时候都能用、不依赖策略状态的开关:

它应该有多个触发方式:人工按钮、自动规则(亏损、订单量、错误率超阈值)、甚至外部的"死人开关"(监控系统失联就自动触发,见《安全监控与告警疲劳》)。

2.3 设计原则

原则 说明
独立 风控代码和策略代码分开部署、分开维护,策略无法关闭风控
简单 只做确定性的检查,便于审查和测试
fail-closed 风控服务不可用、参考价拿不到时,拒绝订单而不是放行(见《Fail-closed》)
双层 本地风控 + 券商或交易所侧的风控
可审计 每次拒绝都记录原因

第三层:代码:一个最小的风控闸门

3.1 实现

from dataclasses import dataclass, field
import time

@dataclass
class Order:
    symbol: str
    side: str        # "buy" / "sell"
    qty: float
    price: float

@dataclass
class RiskLimits:
    max_order_notional: float = 50_000
    max_position_notional: float = 180_000
    max_price_deviation: float = 0.05      # 偏离参考价 5% 以上拒绝(防"胖手指")
    max_orders_per_sec: int = 5
    allowed_symbols: set = field(default_factory=lambda: {"BTC-USD", "ETH-USD"})

class PreTradeRisk:
    def __init__(self, limits, ref_price, positions):
        self.lim, self.ref, self.pos = limits, ref_price, positions
        self.recent, self.killed = [], False

    def kill(self):            # 熔断开关:立即停止一切新订单
        self.killed = True

    def check(self, o: Order):
        if self.killed:
            return False, "熔断开关已触发"
        if o.symbol not in self.lim.allowed_symbols:
            return False, "标的不在白名单"
        if o.qty <= 0 or o.price <= 0:
            return False, "数量或价格非法"
        notional = o.qty * o.price
        if notional > self.lim.max_order_notional:
            return False, f"单笔金额 {notional:,.0f} 超限"
        ref = self.ref[o.symbol]
        if abs(o.price / ref - 1) > self.lim.max_price_deviation:
            return False, f"价格偏离参考价 {o.price/ref-1:+.1%}"
        signed = o.qty if o.side == "buy" else -o.qty
        new_pos = (self.pos.get(o.symbol, 0) + signed) * ref
        if abs(new_pos) > self.lim.max_position_notional:
            return False, f"成交后持仓 {new_pos:,.0f} 超限"
        now = time.monotonic()
        self.recent = [t for t in self.recent if now - t < 1.0]
        if len(self.recent) >= self.lim.max_orders_per_sec:
            return False, "下单频率超限"
        self.recent.append(now)
        return True, "通过"

3.2 测试

risk = PreTradeRisk(RiskLimits(), {"BTC-USD": 60_000, "ETH-USD": 3_000}, {"BTC-USD": 2.5})
tests = [
    Order("BTC-USD", "buy", 0.5, 60_100),
    Order("BTC-USD", "buy", 0.5, 66_000),    # 价格偏离
    Order("ETH-USD", "buy", 20, 3_000),      # 单笔超限
    Order("BTC-USD", "buy", 0.8, 60_000),    # 持仓超限
    Order("DOGE-USD", "buy", 100, 0.1),      # 白名单
]
for o in tests:
    print(o.symbol, o.side, o.qty, o.price, "->", risk.check(o))
for i in range(6):
    ok, why = risk.check(Order("ETH-USD", "sell", 1, 3_000))
print("连续下单第 6 笔:", ok, why)
risk.kill(); print("熔断后:", risk.check(Order("ETH-USD", "sell", 1, 3_000)))

输出:

BTC-USD buy 0.5 60100 -> (True, '通过')
BTC-USD buy 0.5 66000 -> (False, '价格偏离参考价 +10.0%')
ETH-USD buy 20 3000 -> (False, '单笔金额 60,000 超限')
BTC-USD buy 0.8 60000 -> (False, '成交后持仓 198,000 超限')
DOGE-USD buy 100 0.1 -> (False, '标的不在白名单')
连续下单第 6 笔: False 下单频率超限
熔断后: (False, '熔断开关已触发')

3.3 生产环境还需要什么

这段代码只是骨架,真实系统还需要:

需要 说明
持仓实时更新 每笔成交回报后更新 positions;挂单也要计入"潜在持仓"
参考价的可靠性 参考价本身可能出错或过时;过时就拒绝(fail-closed)
并发安全 多个策略同时下单时,检查和更新要原子化
限额的权限管理 修改限额需要审批和记录,不能由策略自己改
自动熔断规则 当日亏损、拒单率、错误率超阈值时自动调用 kill()
告警 每次拒绝和熔断都要通知人(见《安全监控与告警疲劳》)
部署校验 Knight Capital 的教训:部署后自动核对所有服务器的版本一致
演练 定期在模拟环境里触发熔断,确认它真的有效(见《混沌工程与故障注入》)

总结

层次 要点
教训 Knight Capital:45 分钟、4.6 亿美元;部署失误 + 没有自动熔断
检查 白名单、单笔金额、价格偏离、持仓、频率、日亏损
原则 独立于策略、简单、fail-closed、双层、可审计
熔断 任何时候可用;人工、自动、死人开关多种触发方式