Credit And Risk Rules Engine
ODTS 72 — 信用与风控规则引擎:交易前的”红绿灯”
为什么需要这篇文档
前面 16-Margin-Calls、24-Risk-Management-Framework 讲的是事后的风险——保证金催缴、VaR、压力测试。但 OTC 衍生品最重要的风险防线其实在交易发生之前:一笔交易能不能做、做多大、用哪个账户做,是由一套风控规则引擎在簿记瞬间实时裁决的。
这套引擎在代码里叫 RiskRule(风控规则),2017 年由早期团队(haojy / qzttt)设计并实现,至今仍是交易前置控制的核心。它决定了:
- 某个交易账户能不能交易某个标的
- 某个对手方/账户被限制了什么行为(禁止开仓、禁止某个衍生品类型)
- 控制是只在盘中生效,还是连 EOD 估值也算进去
- 规则改了之后,怎么实时通知所有相关系统
这是一个 PM/BA 必须理解的系统——因为交易台”为什么这笔做不了""为什么那个账户被锁了”的几乎每一个答案,都来自这里。
1. 引擎长什么样
规则引擎的代码全部在 eds-web-app 的 action/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 | 盘中是否控制 | 交易时刻是否拦截 |
ctrlTimeEod | EOD 是否控制 | 日终估值是否也受此规则约束 |
strategyName | 策略名 | 关联的交易策略 |
operationType | 操作类型 | 禁止/允许/告警 |
auditFlag | 审计标志 | 是否需要审批 |
effectiveBeginDate / effectiveEndDate | 生效起止 | 规则的有效期(可定时生效) |
riskTypeStatus | 启用/停用 | RiskTypeStatusEnabled / RiskTypeStatusDisabled |
关键洞察: 一条规则 = “在某个 desk 下,对某个标的/产品类型/账户层级,按包含或排除语义,在盘中或 EOD(或两者),执行某种操作(禁止/允许/告警),且有生效期和审计”。这就是引擎的全部表达力——它不写死”雪球不能做”,而是用这些维度的组合来描述。
规则绑定:规则作用于”谁”
光有规则条件还不够,得知道这条规则绑定到哪些对手方/账户。RiskRuleAssign(action/risk/model/RiskRuleAssign.java)就是绑定关系:
RiskRuleAssign:
├── counterPartyGroupId / counterPartyGroupName — 对手方分组
├── counterPartyId / counterPartyName — 具体对手方
├── counterPartyAccountId / counterPartyAccountName— 对手方账户
├── tradingAccountId / tradingAccountName — 交易账户
└── behavior — 绑定的行为
这就是”规则 → 作用对象”的映射表。 一个 PM 常问的问题”为什么这个客户不能开仓某个产品”,答案就是:查 RiskRule 里有没有一条 derivativeControlType = 该产品、includeOrExclude = 排除、operationType = 禁止 的规则,且它的 RiskRuleAssign 绑定到了这个客户的 counterPartyId 或 tradingAccountId。
账户控制级别树
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. 规则是怎么实时生效的(很多人不知道的关键)
规则引擎最精巧的部分不是存规则,而是规则改了之后怎么让所有相关系统立刻知道。
RiskRulePublisher(activeMQ/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);
两个细节有真实业务含义:
-
广播的是
riskRuleId,不是规则全文。 订阅方(如 hedging-as 的定价/校验模块)收到 ID 后,再自己去拉最新规则。这样规则内容始终以 DB 为准,不会出现”消息里带了一份旧规则”的不一致。 -
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=OPTION、includeOrExclude=包含 的白名单规则 |
| 规则只在工作时间拦,EOD 不算 | ctrlTimeTrans=true、ctrlTimeEod=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)