一句话版本
自动交易系统的可怕之处在于:它出错的速度和它赚钱的速度一样快。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、双层、可审计 |
| 熔断 | 任何时候可用;人工、自动、死人开关多种触发方式 |