落地第十五条豁免额度校验;登记三处业务裁定

一、第十五条豁免规则(docs/25 第七节 #2,业务裁定:实现)

政策原文:C3→R4 签署风险揭示书后**可买**,但单只 R4 持仓不超过总资产 20%;
C4→R5 同理,上限 10%。越级购买本身不是违规,超出额度才是 —— 原先扫描侧只看
"留痕是否齐全",于是"签了字但买超额度"这种明确违规没有预警;研判侧也把豁免的
前提条件(留痕齐全)当成了结论,直接判"疑似误报"。

- 扫描侧:新增 EXEMPTION_LIMITS 与 RiskRuleEngine._exemption_state,核算
  "单只持仓 / 总资产"并写进证据快照;触发条件改为
  gap > 0 and (missing_trace or 超出额度)。
- 研判侧:_assess_rw007 先判额度再判留痕。超限 → 证据支持风险;留痕齐全且在额度
  内 → 疑似误报;留痕齐全但快照缺总资产/持仓 → 继续复核。

数据前提(tools/probe_exemption_data.py,证据见 docs/evidence/exemption-data-probe.json):
库内 fin_customer_profile 仅 1 行且 total_asset = 0.00、fin_holding 0 行、无任何申购
交易 —— 这条规则当前不会被触发,与 behavior_score 同源(画像与持仓由本项目之外的
流程写入)。因此刻意不把"算不出来"当成"超限":拿 0 去算会让每一笔 C3→R4 都变成违规,
豁免规则反倒成了误报源。上游把数据写入后无需再改代码即可生效。

二、三处业务裁定(此前挂在"待裁定")

- 模型网关 chat + tools 入口:本轮不补,按基座能力缺口记录。它要贯穿
  ModelGateway → … → BaseAgent 整条链路,属公共契约变更,演示联调期影响面大于收益。
- exclude(关闭误报)是否必须先"调查中":保持现状,不加门禁。
- 政策冲突:以第十四条 C ≥ R 为准;客服侧 check_suitability 复核后确认本来就按
  C ≥ R 实现,无需改动。

三、其他

- 新增 tests/unit/service/test_risk_judgement_rw007.py(6 例)与扫描侧 4 例。
- 风控文档 03/05 同步 RW-007 的豁免额度条件与研判口径。
This commit is contained in:
2026-09-11 14:24:45 +08:00
parent df96275ab5
commit ff681df143
9 changed files with 518 additions and 16 deletions
+57 -3
View File
@@ -97,6 +97,18 @@ agent_intent_config 中 agent_type='risk':0 条
`ModelGenerationService` → `BaseAgent`),再让风控改用它。因为**只是新增方法、不改现有行为**,
对已有 Agent 零影响,但涉及基座核心链路,**是否做需要项目方定**(本轮未实施)。
**业务裁定(2026-09-11):本轮不补,按基座能力缺口记录。**
理由:风控已经跑通,收益只在风控一侧;而 `chat(messages, tools)` 要贯穿
`ModelGateway` → `OpenAICompatibleGateway` → `ModelDispatchService` →
`ModelGenerationService` → `BaseAgent` **整条链路**,属于公共契约变更 —— 在演示与联调期间
改它,影响面大于收益。留待基座排期时一并做,届时风控改用它是替换
`RiskAgentModelClient` 一个类的事。
上面"仍然成立的两点"里,第 1 点(`bootstrap.py:262-265` 的 lambda 让注入点形同虚设)本轮
同样**不改**:它只在注入替身时才看得出差别,不影响任何行为,等基座补 `chat` 入口时一起处理。
第 2 点(HTTP 逻辑重复一份)随第 1 点一起消失。
### 3. 能力过滤失效,当前能工作只是巧合 🔁 ✅
`risk_agent_model_client.py:51-54` 传 `task_type="risk_agent_chat"`,不在 `TASK_CAPABILITY` 里 →
@@ -186,6 +198,10 @@ Asia/Shanghai 展示。北京 08:00 前生成时,统计窗口是"前一日 08:
规则**:把明显误报(如系统重复触发)也强制走一遍调查,未必是想要的效果。代码里两道门是有意
设置的,看不出实现偏差。**需要业务方裁定**,不由技术侧单方面加门禁。
**业务裁定(2026-09-11):保持现状,不加门禁。** `exclude` 继续只要求"已确认接收 +
未闭环"。理由与复核时的判断一致:强制"调查中"会让明显误报(如系统重复触发)也必须走
一遍调查流程,代价大于收益;`exclude` 仍会写审计、仍要求已确认接收,不是无声关闭。
**b. `min(20, score_before)` —— 误报。**
`BEHAVIOR_SCORE_INITIAL = 20` 是**满分**(`:18`),扣分表 `{"低":3, "中":5, "高":20}`(`:19`)
@@ -287,10 +303,48 @@ C1(1) 与 R2(2) 相比 `1 < 2`:**按矩阵可以买,按第十四条不能买
**这不是代码问题,是制度文本冲突**,需要业务方定一条为准。客服侧的适当性裁决走的是
`check_suitability`(按档案等级与匹配规则),两边口径也需要对齐。
### 2. 第十五条豁免规则未落地 ⚠️
**业务裁定(2026-09-11):以第十四条 `C ≥ R` 为准。**
C3→R4(单只 ≤ 总资产 20%)、C4→R5(≤10%)的**持仓占比校验完全没有实现**;
`risk_judgement_service.py:157` 只要留痕齐全就判"疑似误报"。是暂不实现还是漏了,需确认。
**客服侧口径复核(同日):本来就一致,无需改动。** 复核 `SuitabilityService._decide`
(`suitability_service.py:195`)后确认,它的判定是
`if profile.customer_risk_level < request.product_risk_level: 拒绝` —— **同样是第十四条的
`C ≥ R`**,并没有使用第十二条的匹配矩阵。原文"按档案等级与匹配规则"是评审时的推测,
不成立。
两侧看上去的差异只有两点,且都不构成口径冲突:
1. **专业投资者**:客服侧豁免等级匹配,但强制 `required_disclosure` /
`requires_confirmation` / `requires_recording`(`suitability_service.py:183-194`);
风控扫描不做等级豁免,而是直接检查"该有的揭示、二次确认、录音留痕有没有"。
两者合起来是同一句话:豁免等级不等于豁免留痕。
2. **触发条件**:客服是**事前拦截**(`C < R` 直接不许买),风控是**事后发现**
(`risk_scan_service.py:184` 的 `gap > 0 and missing_trace`)。这是职责差异,不是口径
差异 —— RW-007 的语义是"错配**且**留痕不全",不是"所有错配"。留痕完整却仍然成交,
那是客服没能拦住,属另一个问题。
### 2. 第十五条豁免规则未落地 ✅(业务裁定:实现,2026-09-11 已完成)
C3→R4(单只 ≤ 总资产 20%)、C4→R5(≤ 10%)的**持仓占比校验原先完全没有实现**;
`risk_judgement_service.py` 只要留痕齐全就判"疑似误报" —— 把豁免的**前提条件**当成了
结论。业务方裁定实现,两侧一起改:
- **扫描侧**:新增 `EXEMPTION_LIMITS` 与 `RiskRuleEngine._exemption_state`,核算
"单只持仓 / 总资产"并写进证据快照(`exemption_limit`、`exemption_ratio`、
`exemption_data_missing`、`holding_value`、`total_asset`);触发条件由
`gap > 0 and missing_trace` 改为 `gap > 0 and (missing_trace or 超出额度)`。
- **研判侧**:`_assess_rw007` 先判额度、再判留痕 —— 超限 → "证据支持风险";
留痕齐全且在额度内 → "疑似误报";留痕齐全但快照缺总资产/持仓 → "继续复核"。
**数据前提**(探查脚本 `tools/probe_exemption_data.py`,证据留档
`docs/evidence/exemption-data-probe.json`):实测库内 `fin_customer_profile` 只有 1 行、
`total_asset = 0.00`,`fin_holding` 为 0 行,且没有任何 `申购` 交易 —— 这条规则**当前不会
被触发**,与 `behavior_score` 同源:画像与持仓由本项目之外的流程写入。
因此刻意**不**把"算不出来"当成"超限"。拿 `total_asset = 0` 去算,每一笔 C3→R4 都会变成
违规,豁免规则反而成了新的误报源。数据缺失时扫描侧不产生预警,研判侧返回"继续复核"并
要求补查总资产与持仓快照 —— 由人工定案,而不是用缺失数据假装有结论。
上游把总资产与持仓写入之后,这条链路**无需再改代码**即可生效。
### 3. `docs/24` 需要同步更新
+28
View File
@@ -0,0 +1,28 @@
{
"profile_total_asset": [
{
"rows_count": 1,
"positive_assets": "0",
"zero_assets": "1",
"min_asset": "0.00",
"max_asset": "0.00"
}
],
"profile_investor_type": [
{
"investor_type": "C2",
"rows_count": 1
}
],
"holding_shape": [
{
"rows_count": 0,
"null_market_value": null,
"positive_current_value": null,
"min_current_value": null,
"max_current_value": null
}
],
"subscription_pairs": [],
"exemptible_pairs_detail": []
}
@@ -27,12 +27,17 @@
| 项目 | 说明 |
|---|---|
| 场景 | 客户风险承受等级低于产品风险等级,且交易留痕不完整 |
| 核心条件 | 产品风险等级高于客户等级;缺少风险揭示、二次确认或录音留痕 |
| 场景 | 客户风险承受等级低于产品风险等级,且交易留痕不完整或超出豁免额度 |
| 核心条件 | 产品风险等级高于客户等级;且(缺少风险揭示、二次确认或录音留痕,**或**属第十五条可豁免情形但单只持仓占比超过额度) |
| 风险等级 | 等级差大于等于 2 时高风险;等级差为 1 时中风险 |
| 关键证据 | 客户等级、产品等级、风险揭示、二次确认和录音编号 |
| 风险结论 | 存在等级差且留痕缺失时支持风险判断 |
| 误报关注 | 当前等级不再错配,或要求的留痕已完整存在 |
| 关键证据 | 客户等级、产品等级、风险揭示、二次确认、录音编号、豁免额度与单只持仓占比 |
| 风险结论 | 存在等级差且(留痕缺失或超出豁免额度)时支持风险判断 |
| 误报关注 | 当前等级不再错配;或留痕完整**且**单只持仓在豁免额度内 |
**第十五条豁免额度**(《个人投资者适当性管理指南》):C3 买 R4 单只持仓不超过总资产
20%,C4 买 R5 不超过 10%。越级购买本身不是违规,**超出额度**才是。额度需要总资产与
单只持仓快照才能核算;数据未落地时扫描侧不判超限,研判侧返回"继续复核"提示人工补查,
不会凭缺失数据直接定案。
## RW-012 老年客户异常大额赎回
@@ -24,10 +24,14 @@
## RW-007 研判
- 风险成立:客户风险等级低于产品风险等级,且所需留痕存在缺失。
- 疑似误报:当前客户等级与产品等级不再错配,或风险揭示、二次确认和录音留痕完整。
- 继续复核:缺少客户等级或产品等级。
- 复核动作:核对最新风险测评、留痕时间和录音编号真实性。
- 风险成立:客户风险等级低于产品风险等级,且所需留痕存在缺失;或留痕完整但单只持仓
占比超过第十五条豁免额度(C3→R4 为 20%,C4→R5 为 10%)。
- 疑似误报:当前客户等级与产品等级不再错配,或风险揭示、二次确认和录音留痕完整**且**
单只持仓在豁免额度内。
- 继续复核:缺少客户等级或产品等级;或属可豁免情形、留痕完整但缺少总资产/持仓快照,
无法核算豁免额度。
- 复核动作:核对最新风险测评、留痕时间和录音编号真实性;必要时补查客户总资产与单只
产品持仓市值。
## RW-012 研判