基金转换 T-11:工具与 SQL 汇总去重(FR-C15 / R-d)
convert 在 core_trade 落两条流水(转出 redeem + 转入 subscribe,共享 convert_group_id), 所有金额聚合读取方必须只计一次,否则同笔转换金额翻倍。 落地(改码 3 处 + 新增 1 脚本 + 测试 3 文件) - rules.py:_amount_view → 公开 amount_view(提升而非复制别名,全仓唯一金额聚合口径); docstring 补「跨模块共用」说明 - core_tools.py:query_recent_trades 的 sum_amount 改走 amount_view(FR-C15); items / total_count 保持全量不变——两条流水是真实的两笔权益变动,读到两条是对的 - core_ro.py:list_holdings 加 h.qty > 0(convert 转出全部份额留下的 qty=0 归零行不是持仓); sum_trades_on_date 加 convert 去重条件(R-d),组内只计转出端 - 新增 tests/test_core_tools.py(3 条:汇总不翻倍 / 跨口径一致性 / 持仓不返回 qty=0 行) - tests/test_core_ro_sum.py 追加 1 条 convert 去重用例(既有断言零改动) - 新增 scripts/dev/verify_convert_tools.py:真库验证脚本(MySQL 8.0.46) 执行期裁定 2 条(已留痕) - _amount_view 提升为公开而非复制:一个函数两个名字会漂移(自检第 13 问) - SQL 条件从 IS NULL 扩为 IS NULL OR = '':Python 的 if not gid 把空串当无组, 而 SQL 里 '' IS NULL 恒假。真库实证:只写 IS NULL → 合计数 400000(正确 450000,漏算 50000)。 不加则 RISK-002 漏算,且 sqlite 单测若只造 NULL 数据永远发现不了 - 等价性边界:组内无 redeem 时 SQL 丢整组、amount_view 保留首条;由 R-b 保证不可达 验证 - pytest -q → 718 passed / 3 skipped(基线 714 加 4,零回归) - 突变验证 3 组均精准命中:去掉去重条件(2 红)/ 去掉 qty>0(1 红)/ 汇总不走 amount_view(2 红) - 真库 verify_convert_tools.py 14/14,隔离数据零残留 - 复跑受影响真库脚本零回归:T-8 31/31、T-7 35/35、T-10 20/20
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""T-8 规则引擎改造单测(开发计划 §7.1 DoD / 验收 5·6·7)。
|
||||
|
||||
覆盖三层:
|
||||
1. `rules._amount_view` 纯函数 —— 同组只留转出端 / 无 gid 恒等 / 组内无 redeem 防御 / 顺序保持
|
||||
1. `rules.amount_view` 纯函数 —— 同组只留转出端 / 无 gid 恒等 / 组内无 redeem 防御 / 顺序保持
|
||||
2. `run_rules` 视图分流 —— RISK-002 与 RISK-005 走金额视图(**不翻倍**),
|
||||
RISK-001 / RISK-003 仍看**全量**(证明去重未删行,验收 6)
|
||||
3. `engine.process_convert_event` —— **一张单 + `payload.events` 两条**(验收 7)、
|
||||
@@ -22,7 +22,7 @@ from sqlalchemy import text
|
||||
from app.repository.core_ro import CoreReadOnlyRepository
|
||||
from app.repository.risk_repository import RiskRepository
|
||||
from app.service.risk.engine import process_convert_event, process_trade_event
|
||||
from app.service.risk.rules import RiskThresholds, _amount_view, run_rules
|
||||
from app.service.risk.rules import RiskThresholds, amount_view, run_rules
|
||||
|
||||
CUST = "CUST-T8"
|
||||
PROD_A = "PROD-T8A" # 转出方
|
||||
@@ -84,27 +84,27 @@ def _ids(hits) -> set[str]:
|
||||
return {h.rule_id for h in hits}
|
||||
|
||||
|
||||
# ── 1. `_amount_view` 纯函数 ─────────────────────────────────────────
|
||||
# ── 1. `amount_view` 纯函数 ─────────────────────────────────────────
|
||||
def test_amount_view_identity_without_group():
|
||||
"""无 convert_group_id 的交易**原样通过**(非 convert 场景恒等 → 既有断言零影响)。"""
|
||||
raw = [
|
||||
_trade("T-1", "redeem", "1000"),
|
||||
_trade("T-2", "subscribe", "900", NOW + timedelta(seconds=1), product=PROD_B),
|
||||
]
|
||||
view = _amount_view(raw)
|
||||
view = amount_view(raw)
|
||||
assert view == raw, "无 gid 输入必须恒等(内容与顺序都不变)"
|
||||
assert view is not raw, "返回独立列表,不得共享可变状态"
|
||||
|
||||
|
||||
def test_amount_view_keeps_redeem_when_subscribe_comes_first():
|
||||
out, inn = _convert_legs()
|
||||
view = _amount_view([inn, out])
|
||||
view = amount_view([inn, out])
|
||||
assert [t["trade_id"] for t in view] == ["TRD-T8-OUT"], "同组只留转出端,且位置不变"
|
||||
|
||||
|
||||
def test_amount_view_keeps_redeem_when_redeem_comes_first():
|
||||
out, inn = _convert_legs()
|
||||
view = _amount_view([out, inn])
|
||||
view = amount_view([out, inn])
|
||||
assert [t["trade_id"] for t in view] == ["TRD-T8-OUT"], "转入端不得覆盖已占位的转出端"
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ def test_amount_view_group_without_redeem_keeps_first_row():
|
||||
a = _trade("T-IN-1", "subscribe", "100", gid="CNV-T8-X", product=PROD_B)
|
||||
b = _trade("T-IN-2", "subscribe", "200", NOW + timedelta(seconds=1), gid="CNV-T8-X",
|
||||
product=PROD_B)
|
||||
assert _amount_view([a, b]) == [a]
|
||||
assert amount_view([a, b]) == [a]
|
||||
|
||||
|
||||
def test_amount_view_mixes_groups_and_plain_trades_in_order():
|
||||
@@ -124,7 +124,7 @@ def test_amount_view_mixes_groups_and_plain_trades_in_order():
|
||||
out2 = _trade("TRD-T8-OUT2", "redeem", "500", NOW + timedelta(seconds=4), gid="CNV-T8-0002")
|
||||
in2 = _trade("TRD-T8-IN2", "subscribe", "480", NOW + timedelta(seconds=5),
|
||||
gid="CNV-T8-0002", product=PROD_B)
|
||||
view = _amount_view([n1, in1, out1, n2, in2, out2])
|
||||
view = amount_view([n1, in1, out1, n2, in2, out2])
|
||||
assert [t["trade_id"] for t in view] == ["T-N1", "TRD-T8-OUT", "T-N2", "TRD-T8-OUT2"]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user