交易风控与实时限额

交易风控与实时限额的工程实践:事前事中事后三层风控体系、资金与持仓限额设计、实时风控的计算模型与低延迟实现、自成交与异常报单防范、熔断与降级策略、风控规则引擎的建模方式、与 OMS 的集成点以及风控指标的监控告警体系,回答如何在不拖慢交易的前提下拦住异常订单。

风控是量化系统里唯一「不产生收益但决定生死」的模块。它不创造 alpha,但一次失效可能让一年的收益归零。2012 年骑士资本(Knight Capital)因为部署失误在 45 分钟内亏损 4.4 亿美元,直接导致公司被收购——根因就是缺少一个能在异常报单量下自动熔断的风控。

真正难的地方在于风控必须在关键路径上、但又不能拖慢关键路径。每一笔订单都要过风控,所以风控的延迟直接加到端到端延迟上;但风控规则又要足够复杂才能拦住异常。这个矛盾决定了风控的实现方式:规则预编译 + 状态预加载 + 无锁读取。

本文按「层次 → 限额 → 计算模型 → 校验 → 异常防范 → 熔断 → 规则引擎 → 集成 → 监控」的顺序展开。订单侧的状态管理在 订单管理与执行算法 中讨论,本文聚焦风控逻辑。

目录

  1. 风控的三个层次
  2. 限额体系设计
  3. 实时风控的计算模型
  4. 资金与持仓校验
  5. 自成交与异常报单防范
  6. 熔断与降级
  7. 风控规则引擎
  8. 与 OMS 的集成点
  9. 风控监控与告警

1. 风控的三个层次

风控按介入时机分三层,每层的定位完全不同:

层次时机目标延迟预算
事前报单前拦截不该发的单< 5 μs
事中持仓中限制风险敞口毫秒级
事后收盘后复盘与归因分钟级
策略信号 ──► [事前风控] ──► OMS ──► 交易所
                │
                ├─ 资金/持仓限额
                ├─ 单笔/单日限额
                ├─ 价格偏离校验
                └─ 自成交防范

持仓变化 ──► [事中风控] ──► 告警/强制平仓
                ├─ 实时敞口监控
                ├─ 回撤监控
                └─ 集中度监控

收盘 ──► [事后风控] ──► 报告
            ├─ 交易行为分析
            ├─ 限额使用率
            └─ 异常交易识别

事前风控必须在关键路径上,因为它是唯一能真正阻止损失的环节。事后风控只能发现问题,无法挽回。很多团队把重心放在事后报表上,这是本末倒置。

2. 限额体系设计

限额是多维度的,每个维度都有「软限」和「硬限」两档:

维度软限(告警)硬限(拦截)粒度
单笔金额50 万100 万每笔
单标的持仓流通股 3%5%每标的
单日成交量日均量 10%20%每日
总持仓市值资金 80%95%全局
单日亏损2%5%每日
换手率200%300%每日
LIMITS = {
    'max_order_amount':    {'soft':  5e5, 'hard': 1e6},   # 单笔金额
    'max_symbol_position': {'soft': 0.03, 'hard': 0.05},  # 单标的占流通股比例
    'max_daily_volume':    {'soft': 0.10, 'hard': 0.20},  # 单日成交量占市场比例
    'max_gross_exposure':  {'soft': 0.80, 'hard': 0.95},  # 总仓位占资金比例
    'max_daily_loss':      {'soft': 0.02, 'hard': 0.05},  # 单日亏损比例
    'max_turnover':        {'soft': 2.0,  'hard': 3.0},   # 单日换手率
}

软限触发告警,硬限直接拒单。两档设计的价值在于:软限让交易员有时间反应(降低仓位),硬限防止灾难。只有硬限会导致频繁的意外拒单,只有软限则可能来不及反应。

限额不是静态的,要随策略表现动态调整:

def dynamic_limit(base_limit, strategy_sharpe, days_live):   # 表现差时收紧限额
    if days_live < 20:
        return base_limit * 0.3          # 新策略只用 30% 限额
    if strategy_sharpe < 0:
        return base_limit * 0.5          # 表现差时减半
    return base_limit

3. 实时风控的计算模型

风控要在微秒级完成校验,所以不能有锁、不能有分配、不能有系统调用。核心是「状态常驻内存 + 无锁读取」:

// 风控状态:所有计数器的原子快照
struct RiskState {
    alignas(64) std::atomic<int64_t> gross_exposure{0};    // 总敞口(分)
    alignas(64) std::atomic<int64_t> daily_pnl{0};         // 当日盈亏(分)
    alignas(64) std::atomic<int64_t> daily_volume{0};      // 当日成交量
    alignas(64) std::atomic<int64_t> order_count{0};       // 当日报单数
    alignas(64) std::atomic<int64_t> reject_count{0};      // 当日拒单数
};

// 校验:只读原子变量,无锁
bool RiskEngine::check(const Order& o) {
    int64_t new_exposure = state_.gross_exposure.load(std::memory_order_relaxed)
                         + o.price * o.qty;
    if (new_exposure > limits_.max_gross_exposure) {
        state_.reject_count.fetch_add(1, std::memory_order_relaxed);
        return false;
    }
    if (o.price * o.qty > limits_.max_order_amount) return false;
    return true;
}

关键设计是读写分离:校验路径只读原子变量(无锁、无竞争),状态更新由成交回报驱动(低频)。这样校验路径的延迟稳定在百纳秒级。

限额的原子性问题:多笔订单并发校验时,可能同时通过导致总量超限。解法是在校验时预占(reserve):

bool RiskEngine::check_and_reserve(const Order& o) {
    int64_t amount = o.price * o.qty;
    int64_t cur = state_.gross_exposure.load(std::memory_order_relaxed);
    while (true) {
        if (cur + amount > limits_.max_gross_exposure) return false;
        // CAS 预占,失败则重试
        if (state_.gross_exposure.compare_exchange_weak(
                cur, cur + amount, std::memory_order_relaxed)) {
            return true;
        }
    }
}

成交或撤单后释放预占的额度。这个模式与 分布式幂等与可靠性 中的额度扣减是同一个问题。

4. 资金与持仓校验

资金校验的复杂性在于可用量与总量的区分:

总资金 = 可用资金 + 冻结资金 + 持仓市值
可用资金 = 总资金 − 冻结(挂单占用)− 已用保证金

期货还有保证金制度,占用随价格波动:

def available_cash(account, order):
    if order.is_futures:
        margin = order.price * order.qty * order.multiplier * margin_rate  # 保证金
        return account.cash - account.frozen - margin   # 盘中用最新价估算占用
    else:
        return account.cash - account.frozen - order.price * order.qty  # 股票全额

冻结/解冻的时序是资金对不上的常见原因:

报单 → 冻结资金 → 部分成交 → 按成交量解冻 → 撤单 → 解冻剩余

任何一个环节漏掉都会导致资金「凭空消失」。必须以柜台回报为准,本地计算只作预估。

持仓校验还要考虑 T+1 制度:当天买入的股票当天不能卖,所以「可用持仓」不等于「总持仓」:

def available_position(account, symbol):
    pos = account.positions[symbol]
    return pos.yesterday_qty - pos.frozen_sell_qty   # 可用 = 昨仓 − 卖出冻结

5. 自成交与异常报单防范

自成交(wash trade) 是监管红线:同一账户或关联账户的买卖单成交,构成「洗售」。事前防范的方法是在报单前检查:

def check_self_trade(order, open_orders):
    for o in open_orders:
        if o.symbol != order.symbol:
            continue
        if o.account == order.account and o.side != order.side:
            if can_cross(o, order):   # 同账户反向挂单,可能自成交
                return False
    return True

异常报单的常见模式:

模式特征监管关注
幌骗(Spoofing)大量挂单后迅速撤单是
塞单(Quote Stuffing)超高频率报撤单是
分层(Layering)多价位挂单制造假象是
尾随(Front Running)利用客户单信息抢先是
频繁撤单撤单率 > 90%是

撤单率是最重要的监控指标。交易所对高撤单率账户会重点关注,很多交易所对「报撤单比」有硬性要求(如不超过 100:1):

def check_cancel_ratio(state, limit=100):
    if state.order_count == 0:
        return True
    ratio = state.cancel_count / state.order_count
    return state.cancel_count <= state.order_count * limit

6. 熔断与降级

熔断是在检测到异常时自动停止交易。触发条件必须是明确可量化的:

class CircuitBreaker:
    def __init__(self):
        self.trips = []

    def check(self, state):
        if state.daily_pnl < -state.capital * 0.05:              # 单日亏损超限
            return self.trip('daily_loss')
        if state.order_count > 100 and state.reject_count / state.order_count > 0.3:
            return self.trip('high_reject_rate')                 # 拒单率异常
        if state.orders_per_second > 1000:
            return self.trip('order_rate')                       # 报单频率异常
        if state.last_tick_age_ms > 5000:
            return self.trip('stale_market_data')                # 行情长时间无更新
        return None

    def trip(self, reason):
        self.trips.append((time.time(), reason))
        return reason

熔断后的降级策略要分级:

一级降级:停止新开仓,允许平仓(防止风险扩大)
二级降级:全部停止,撤销所有挂单
三级降级:断开通道,人工介入

降级必须能自动执行且可恢复。最常见的设计是「熔断自动、恢复手动」——熔断可以自动触发,但重新开始交易必须人工确认,避免在异常未排除时反复触发。

熔断的告警必须走独立的通道(短信、电话),不能依赖可能已经故障的交易系统,这与 告警设计与事故响应 中的原则一致。

7. 风控规则引擎

当风控规则增长到几十条时,硬编码会变得难以维护。规则引擎的价值是规则与代码分离:

rules:
  - name: max_order_amount
    priority: 10
    condition: "order.price * order.qty > 1000000"
    action: reject
    message: "单笔金额超限"

  - name: position_limit
    priority: 20
    condition: "position.ratio > 0.05"
    action: reject
    message: "单标的持仓超限"

  - name: high_turnover
    priority: 30
    condition: "daily.turnover > 3.0"
    action: reject
    message: "换手率超限"

规则引擎的两种实现方式:

方式实现延迟灵活性
解释执行解析表达式树微秒~毫秒高
预编译编译成函数指针数组纳秒中
位图/位运算把规则编码成位极低低

关键路径上的风控应该用预编译:启动时把规则编译成有序的函数指针数组,运行时顺序调用,短路求值。

class CompiledRiskEngine {
    std::vector<bool(*)(const Order&, const RiskState&)> checks_;
public:
    void compile(const std::vector<Rule>& rules) {
        for (auto& r : rules) {
            checks_.push_back(compiler_.compile(r));   // 启动时编译
        }
    }
    bool check(const Order& o) {
        for (auto fn : checks_) {
            if (!fn(o, state_)) return false;          // 短路
        }
        return true;
    }
};

8. 与 OMS 的集成点

风控与 OMS 的集成有三个位置,各有取舍:

位置 A:策略 → [风控] → OMS → 交易所     (风控在前,OMS 无感)
位置 B:策略 → OMS → [风控] → 交易所     (OMS 内部,紧密耦合)
位置 C:策略 → OMS → 交易所 → [旁路风控]  (旁路,不阻塞)
位置延迟影响拦截能力复杂度
A中强低
B低强中
C无弱(只能事后撤单)高

实践中的常见组合是 A + C:主风控在 OMS 之前(强拦截),旁路风控实时监控异常(补充检测)。旁路风控不阻塞交易,但能发现主风控规则未覆盖的模式。

风控的旁路不能用来拦截,只能告警和触发熔断。如果旁路风控能拦截,它就变成了关键路径的一部分,失去了「不阻塞」的意义。

9. 风控监控与告警

风控本身也需要被监控。核心指标:

指标含义阈值
拒单率被拒订单/总订单> 10% 告警
限额使用率当前/限额> 80% 告警
风控延迟校验耗时 P99> 10 μs 告警
熔断次数当日触发次数> 0 立即告警
撤单率撤单/报单> 80% 告警

限额使用率是最有预警价值的指标。当某个限额用到 80% 时,说明策略的行为正在逼近边界,应该提前介入。这些指标需要纳入统一的监控面板,与交易系统的其他指标一起看。

风控日志必须完整留存,它是事后审计和监管检查的依据:

def log_risk_decision(order, decision, reason, latency_ns):
    logger.info({
        'ts': time.time_ns(),
        'order_id': order.id,
        'symbol': order.symbol,
        'decision': decision,        # PASS / REJECT
        'reason': reason,
        'latency_ns': latency_ns,
        'limits_snapshot': current_limits(),
    })

权衡取舍

维度严格风控宽松风控
安全性高低
误杀率高低
策略自由度低高
监管风险低高

风控的宽严取舍本质是误杀与漏放的权衡。过严会把正常的策略行为当成异常,导致频繁拒单影响收益;过松则可能放过真正的风险。实践中应该分级:新策略严格、成熟策略宽松;小资金宽松、大资金严格。

规则引擎与硬编码的取舍:规则引擎灵活但延迟高,硬编码快但难维护。折中方案是核心规则硬编码(延迟敏感)+ 边缘规则用引擎(可热更新)。核心规则包括金额、持仓、价格这些每笔都要查的;边缘规则包括撤单率、行为模式这些可以低频计算的。

常见坑清单

  1. 风控放在报单之后:事后风控无法阻止损失,必须在报单前。
  2. 限额用非原子变量:并发校验时多笔订单同时通过,总量超限。
  3. 冻结/解冻不对等:部分成交后忘记按量解冻,资金凭空消失。
  4. T+1 未区分可用持仓:当天买入当天卖出,报单被交易所拒绝。
  5. 自成交未防范:同账户对敲,构成违规。
  6. 熔断无自动恢复限制:异常未排除就自动恢复,反复触发。
  7. 告警依赖交易系统:系统故障时告警也发不出,用独立通道。
  8. 风控延迟未监控:风控本身成为瓶颈,拖慢所有报单。
  9. 限额静态不变:策略表现恶化时未收紧,风险持续累积。
  10. 风控日志不完整:监管检查时无法提供决策依据。

小结

风控的核心是用确定性的规则限制不确定的市场。它不产生收益,但决定了你能不能在市场里活下来。工程上的关键是三点:在关键路径上(能真正拦截)、足够快(不拖慢交易)、可审计(能回溯每个决策)。

判断风控是否合格的最快方式是故障演练:人为制造异常报单、行情中断、资金不足等场景,看风控是否能正确拦截并告警。没有演练过的风控,在真实故障时大概率会失效。

下一步建议阅读 清结算与对账体系 ,看成交之后资金与持仓如何被正确记账。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「量化交易」更多文章

  1. 风险模型与因子归因
  2. 回测偏差与过拟合防范
  3. 市场微结构与流动性