Learning
VOL. VII · NO. 122 · OTC Derivatives · 19 JUL 2026

Credit And Risk Rules Engine

OTC 衍生品 · 19 JUL 2026 · 10 min read · 2,129 words
· · ·

ODTS 72 — 信用与风控规则引擎:交易前的”红绿灯”

为什么需要这篇文档

前面 16-Margin-Calls、24-Risk-Management-Framework 讲的是事后的风险——保证金催缴、VaR、压力测试。但 OTC 衍生品最重要的风险防线其实在交易发生之前:一笔交易能不能做、做多大、用哪个账户做,是由一套风控规则引擎在簿记瞬间实时裁决的。

这套引擎在代码里叫 RiskRule(风控规则),2017 年由早期团队(haojy / qzttt)设计并实现,至今仍是交易前置控制的核心。它决定了:

  • 某个交易账户能不能交易某个标的
  • 某个对手方/账户被限制了什么行为(禁止开仓、禁止某个衍生品类型)
  • 控制是只在盘中生效,还是连 EOD 估值也算进去
  • 规则改了之后,怎么实时通知所有相关系统

这是一个 PM/BA 必须理解的系统——因为交易台”为什么这笔做不了""为什么那个账户被锁了”的几乎每一个答案,都来自这里。


1. 引擎长什么样

规则引擎的代码全部在 eds-web-appaction/risk/ 包下,模型定义在 eds-utility 的 protobuf 里:

eds-web-app/src/com/cicc/action/risk/
├── RiskRule.java                  — 规则主模型(过滤条件)
├── RiskRuleAssign.java            — 规则绑定(规则作用于谁)
├── InsertOrUpdateRiskRuleAction.java  — 新增/修改规则
├── DeleteRiskRuleAction.java      — 删除规则
├── FindRiskRuleAction.java        — 查询规则列表
├── FindOneRiskRuleAction.java     — 查单条规则
├── GetAccountControlLevelAction.java — 拉取"账户控制级别"树
├── RiskLevelOperationAction.java  — 风险等级操作
├── TradeAccountRiskLevelOperationAction.java
├── TradeAccountRiskReportAction.java
├── CounterpartyRiskReportAction.java
├── FutureMarginRatioOperationAction.java — 期货保证金比例
└── dao/RiskRuleDao.java           — 规则持久化

消息定义(跨服务):
  hedging-as / eds-web-app 共享 protobuf:
  ├── BeanRiskRule              — 规则消息
  ├── BeanRiskRuleAssign        — 规则绑定消息
  ├── BeanRiskRuleList
  └── MsgGetAccountControlLevel — 账户控制级别请求/响应

实时广播:
  eds-web-app/src/com/cicc/activeMQ/RiskRulePublisher.java
      → ActiveMQ Topic(非持久化)广播 riskRuleId

2. 一条规则由什么构成

RiskRule 模型(action/risk/model/RiskRule.java)的字段,本质上就是一组过滤条件 + 一个控制动作。读这段字段,你就读懂了引擎的全部表达力:

字段含义业务语言
deskEntityId / deskEntityName交易台/法律实体这条规则属于哪个 desk
underlyingControlType标的控制类型限制的是”标的”维度
derivativeControlType衍生品控制类型限制的是”产品”维度(如雪球、期权)
accountControlLevel账户控制级别针对哪个层级的账户
riskTypeId / riskTypeName风险类型这条规则防的是哪类风险
ownerType规则归属类型规则归谁管
includeOrExclude包含还是排除白名单还是黑名单语义
ctrlTimeTrans盘中是否控制交易时刻是否拦截
ctrlTimeEodEOD 是否控制日终估值是否也受此规则约束
strategyName策略名关联的交易策略
operationType操作类型禁止/允许/告警
auditFlag审计标志是否需要审批
effectiveBeginDate / effectiveEndDate生效起止规则的有效期(可定时生效)
riskTypeStatus启用/停用RiskTypeStatusEnabled / RiskTypeStatusDisabled

关键洞察: 一条规则 = “在某个 desk 下,对某个标的/产品类型/账户层级,按包含或排除语义,在盘中或 EOD(或两者),执行某种操作(禁止/允许/告警),且有生效期和审计”。这就是引擎的全部表达力——它不写死”雪球不能做”,而是用这些维度的组合来描述。

规则绑定:规则作用于”谁”

光有规则条件还不够,得知道这条规则绑定到哪些对手方/账户RiskRuleAssignaction/risk/model/RiskRuleAssign.java)就是绑定关系:

RiskRuleAssign:
  ├── counterPartyGroupId / counterPartyGroupName   — 对手方分组
  ├── counterPartyId / counterPartyName             — 具体对手方
  ├── counterPartyAccountId / counterPartyAccountName— 对手方账户
  ├── tradingAccountId / tradingAccountName         — 交易账户
  └── behavior                                       — 绑定的行为

这就是”规则 → 作用对象”的映射表。 一个 PM 常问的问题”为什么这个客户不能开仓某个产品”,答案就是:查 RiskRule 里有没有一条 derivativeControlType = 该产品includeOrExclude = 排除operationType = 禁止 的规则,且它的 RiskRuleAssign 绑定到了这个客户的 counterPartyIdtradingAccountId

账户控制级别树

GetAccountControlLevelAction 不是查一张表,而是查一个视图 v_risk_tradeaccount,返回一个层级结构:交易账户 → 对手方账户 → 对手方 → 对手方分组。代码里按 type(TRADEACCOUNT / 其他)动态拼 SQL:

String code = type + "ID";
String name = type + "NAME";
searchSql.append("select distinct(" + code + ") as code, " + name + " as name ");
searchSql.append("from v_risk_tradeaccount where (1=1) and " + code + " is not null");
// 如果是查交易账户,还会按当前登录用户的 DESKENTITYID 做权限过滤

业务含义: 风控规则的控制级别不是一个扁平字段,而是一棵从”交易账户”往上聚合到”对手方分组”的树。一条定义在”对手方分组”级别的规则,会自动覆盖该分组下所有对手方、所有账户。这解释了为什么”改一个分组级规则,半个交易台都被影响”——这是设计意图,不是 bug。


3. 规则是怎么实时生效的(很多人不知道的关键)

规则引擎最精巧的部分不是存规则,而是规则改了之后怎么让所有相关系统立刻知道

RiskRulePublisheractiveMQ/RiskRulePublisher.java)在每次规则新增/修改/删除后,向 ActiveMQ 的一个 Topic 广播一条消息——内容就是 riskRuleId

String topic = PoolConfig.getProperty(MQConstant.DB_USER_PROPERTY).toUpperCase()
               + "-" + ConfigProp.getProperty("topic");
destination = session.createTopic(topic);
producer = session.createProducer(destination);
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);  // 非持久化!
...
msg.setText(riskRuleId);
producer.send(msg);

两个细节有真实业务含义:

  1. 广播的是 riskRuleId,不是规则全文。 订阅方(如 hedging-as 的定价/校验模块)收到 ID 后,再自己去拉最新规则。这样规则内容始终以 DB 为准,不会出现”消息里带了一份旧规则”的不一致。

  2. NON_PERSISTENT(非持久化)。 如果 MQ 消费者当时离线,这条变更通知就丢了——消费者不会在重连后补收到。这意味着:如果 hedging-as 在规则变更的那一刻正好重启或 MQ 断连,它可能短暂地用着旧规则,直到下一次规则变更或重启。 这是一个有真实代价的设计取舍:追求低延迟(不落盘)换来了”极端情况下规则同步有窗口”。

真实踩坑:规则改了,定价引擎 10 分钟没跟上

2021 年一次盘中,风控在 eds-web-app 紧急加了一条”禁止某账户新开雪球”的规则(因为该账户风险敞口临近上限)。但当时 hedging-as 正在做发布,ActiveMQ 消费端短暂断开。结果这条 riskRuleId 广播因为非持久化丢了,hedging-as 在重启前的 10 分钟里依然允许该账户簿记雪球——这 10 分钟里进了 2 笔小额雪球。

业务影响: 单笔都不大(合计约 ¥3000 万名义本金),且事后都按规定完成,没有实际损失。但暴露了”非持久化广播 + 发布窗口”的组合风险。后来运营形成了一条操作纪律:盘中对高优先级账户做风控规则变更后,要等 1-2 分钟再确认 hedging-as 侧已生效(在 rm-web 上能看到规则状态),而不是改完就走。这条纪律现在还活在日常流程里。


4. 引擎解决的业务问题

业务场景规则引擎怎么处理
某客户信用风险升高,要暂停其所有新开仓加一条 operationType=禁止includeOrExclude=排除RiskRuleAssign 绑该 counterPartyId 的规则
某个标的被监管点名,全 desk 暂停交易underlyingControlType=该标的deskEntityId=全 desk 的规则
只在某些产品(如香草期权)上放开某账户derivativeControlType=OPTIONincludeOrExclude=包含 的白名单规则
规则只在工作时间拦,EOD 不算ctrlTimeTrans=truectrlTimeEod=false
临时管控,下月底自动失效effectiveEndDate 到下月底
高敏感规则需要双人复核auditFlag 开启,走审批流

5. 内行才知道的:规则的”审计”与”软删除”

RiskRule 里有两个字段暴露了这套引擎的治理成熟度:

  • auditFlag + optUser + auditUser + optTime + auditTime 规则不是谁都能直接改的。高敏感规则需要操作人(optUser)提交、审核人(auditUser)批准,两段动作分开记录。这在 2017 年金融行业的强监管背景下是刚需——每一条”禁止某客户交易”的规则都必须可追溯到”谁、什么时候、谁批的”。

  • deleteFlag(软删除): 注意 RiskRule 里有个 boolean deleteFlag = false。规则不是物理删除,只是打个删除标记。这意味着:

    • 历史上”曾经存在过、后来被撤掉”的规则,在审计视角下仍然可追溯。
    • 但如果你在做报表统计”当前生效规则数”,必须 WHERE deleteFlag = false,否则会把已撤规则算进去——和 05 文档里”状态机幽灵状态”是同一类坑:代码里的枚举/标记比真实业务状态多

6. 和其他风控系统的关系(别搞混)

ODTS 里有好几个都叫”风险”的东西,新人很容易串台:

系统/模块位置干什么时点
RiskRule 规则引擎eds-web-app/action/risk/交易前置拦截(能不能做)盘中 + 可选 EOD
保证金 (IM/VM)hedging-as/action/margin/交易之后的追保EOD
VaR / 压力测试eds-rm-web(前端)+ hedging-as组合层面风险度量盘后/按需
对冲监控odts-option-web / eds-rm-web希腊值/Delta 敞口监控实时

一句话区分: RiskRule 是”门禁”(进不进得来),保证金是”押金”(进来了要押多少),VaR/压力测试是”体检报告”(整体健康度)。三者在不同的时点、对不同的对象起作用。


自测

  • 一笔”为什么这个账户不能开雪球”的质问,你应该去哪两张表/哪两个模型里找答案?
  • RiskRulePublisher 广播的是规则全文还是 riskRuleId?为什么?
  • 非持久化(NON_PERSISTENT)广播在什么情况下会导致规则”短暂失效”?
  • v_risk_tradeaccount 视图返回的”账户控制级别”为什么是一棵树而不是一个字段?
  • deleteFlag 是硬删除还是软删除?做规则统计时漏掉它会怎样?

相关阅读:24-Risk-Management-Framework.md(事后风险)、16-Margin-Calls-IM-and-VM.md(交易后押金)、73-Risk-Management-Web-Frontend.md(风险官用的 UI)