118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""RW-007 研判必须复核第十五条豁免额度(`docs/25` 第七节 #2)。
|
||||
|
|
|
|||
|
|
政策原文(`knowledge/policy/个人投资者适当性管理指南.md:334-341`):
|
|||
|
|
|
|||
|
|
- C3 买 R4:签署《产品风险超越投资者风险承受能力揭示书》后**可买**,但单只 R4 持仓
|
|||
|
|
不超过总资产 **20%**;
|
|||
|
|
- C4 买 R5:同理,上限 **10%**。
|
|||
|
|
|
|||
|
|
也就是说越级购买本身不是违规,**超出额度**才是。原先 `_assess_rw007` 只看留痕:
|
|||
|
|
只要揭示书、二次确认、录音都齐,就判"疑似误报" —— 而这三样恰恰是豁免的**前提条件**,
|
|||
|
|
把前提当成结论,于是"签了字但买超了额度"被静默放过。
|
|||
|
|
|
|||
|
|
数据缺失的情况单独处理:额度要靠总资产与持仓快照才能核算,本项目的画像与持仓由上游
|
|||
|
|
流程写入(实测 `total_asset` 全为 0、`fin_holding` 为空),既不能默认"没超"判误报,
|
|||
|
|
也不能默认"超了"判风险 —— 那会把豁免规则变成新的误报源。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
from app.service.risk_judgement_service import (
|
|||
|
|
VERDICT_CONTINUE_REVIEW,
|
|||
|
|
VERDICT_RISK_SUPPORTED,
|
|||
|
|
VERDICT_SUSPECTED_FALSE_POSITIVE,
|
|||
|
|
_assess_rw007,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
COMPLETE_WORK_ORDER: dict[str, Any] = {
|
|||
|
|
"risk_disclosure_ack_at": "2026-09-01T00:00:00",
|
|||
|
|
"second_confirmation_at": "2026-09-01T00:00:00",
|
|||
|
|
"recording_reference": "REC-1",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
REQUIRING_PRODUCT: dict[str, Any] = {
|
|||
|
|
"risk_level": "R4",
|
|||
|
|
"risk_disclosure_required": True,
|
|||
|
|
"second_confirmation_required": True,
|
|||
|
|
"recording_required": True,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _detail(
|
|||
|
|
snapshot: dict[str, Any],
|
|||
|
|
*,
|
|||
|
|
work_order: dict[str, Any] | None = None,
|
|||
|
|
investor_type: str = "C3",
|
|||
|
|
) -> dict[str, Any]:
|
|||
|
|
"""一条 C3→R4(相差 1 级、属豁免情形)的 RW-007 详情。"""
|
|||
|
|
return {
|
|||
|
|
"customer": {"investor_type": investor_type},
|
|||
|
|
"product": dict(REQUIRING_PRODUCT),
|
|||
|
|
"work_order": dict(COMPLETE_WORK_ORDER if work_order is None else work_order),
|
|||
|
|
"evidence_snapshot": snapshot,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_limit_breach_is_risk_supported_even_with_complete_trace() -> None:
|
|||
|
|
"""这是本次补上的判断:留痕齐全 ≠ 豁免成立,额度超了就是违规。"""
|
|||
|
|
result = _assess_rw007(
|
|||
|
|
_detail({"exemption_limit": "0.20", "exemption_ratio": "0.35"})
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert VERDICT_RISK_SUPPORTED in str(result)
|
|||
|
|
assert "35.00%" in str(result)
|
|||
|
|
assert "20%" in str(result)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_within_limit_with_complete_trace_is_a_false_positive() -> None:
|
|||
|
|
result = _assess_rw007(
|
|||
|
|
_detail({"exemption_limit": "0.20", "exemption_ratio": "0.10"})
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert VERDICT_SUSPECTED_FALSE_POSITIVE in str(result)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_exact_limit_is_not_a_breach() -> None:
|
|||
|
|
"""政策写的是"不超过",恰好等于上限属于合规。"""
|
|||
|
|
result = _assess_rw007(
|
|||
|
|
_detail({"exemption_limit": "0.20", "exemption_ratio": "0.2"})
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert VERDICT_SUSPECTED_FALSE_POSITIVE in str(result)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_missing_snapshot_data_asks_for_manual_review() -> None:
|
|||
|
|
"""额度核算不出结果时,既不判误报也不判风险。"""
|
|||
|
|
result = _assess_rw007(
|
|||
|
|
_detail(
|
|||
|
|
{
|
|||
|
|
"exemption_limit": "0.20",
|
|||
|
|
"exemption_ratio": None,
|
|||
|
|
"exemption_data_missing": True,
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert VERDICT_CONTINUE_REVIEW in str(result)
|
|||
|
|
assert "总资产" in str(result)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_missing_trace_still_outranks_the_exemption_path() -> None:
|
|||
|
|
"""留痕不完整仍然直接判风险 —— 豁免的**前提**没满足,谈额度没有意义。"""
|
|||
|
|
result = _assess_rw007(
|
|||
|
|
_detail(
|
|||
|
|
{"exemption_limit": "0.20", "exemption_ratio": "0.01"},
|
|||
|
|
work_order={},
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert VERDICT_RISK_SUPPORTED in str(result)
|
|||
|
|
assert "缺少交易留痕" in str(result)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_pairs_outside_the_exemption_table_keep_the_old_behaviour() -> None:
|
|||
|
|
"""C2→R5 不属豁免情形,快照里也不会有额度字段:行为必须与改动前一致。"""
|
|||
|
|
result = _assess_rw007(_detail({}, investor_type="C2"))
|
|||
|
|
|
|||
|
|
assert VERDICT_SUSPECTED_FALSE_POSITIVE in str(result)
|