feat(convert): 基金转换 T-0/T-0b 门禁 + T-1 数据层 + T-2/T-2b 纯函数与实算回填
T-0 / T-0b(门禁 · 2026-09-10) - T-0:sqlite 与 MySQL 结构对齐 —— core_holding 统一为 qty/cost_amount/as_of/pnl_pct + PK + UNIQUE(customer_id, product_id);补 core_product_nav;新增建库自校验 _assert_ddl_aligned()(R-g);test_db.py 增 3 条门禁用例(含反向验证门禁失效) - T-0b:DB 账号分离(D20)—— 新增 scripts/core/00-grant.sql(三账号逐表授权); settings.py 增 3 组账号;db.py 改 get_engine(db, role),缓存键改为 (库名, 角色), 账号未配置回退单账号;core_ro→ro / gateway_repository→rw / risk·session_repository→rw; tests/conftest.py 四处显式 role="admin"(R-e) T-1(数据层) - scripts/core/01-ddl.sql:新建 core_fee_rule / core_share_lot / core_convert_lot_detail; core_trade 加 convert_group_id + idx_convert_group;core_product 加 8 列 + fee_rate 补 COMMENT - 新增 07-seed-fee-rule.sql(赎回费 5 档 × 14 产品,按 22 号文 §10)/ 08-seed-share-lot.sql (58 行持仓 → 61 行批次,Σ remain_qty 恒等于 qty)/ 09-seed-org.sql(管理人 + TA + 申购费率 + 最低持有余额,v1.1 按「管理人全产品线」重排) - reset.ps1 追加 07/08/09;02-mysql-agent专用.sql 追加 risk_convert_detail - tests/_ddl.py 同步 4 表 + 新增 REQUIRED_CONVERT_TABLES 建库门禁 - 新增 scripts/dev/verify_convert_seed.py(pymysql 等价 reset 流程 + 8 条 DoD 断言, 含断言 ⑧「费率档 ↔ product_type 匹配」,越档即 FAIL) T-2 / T-2b(纯函数包 + 示例实算回填) - 新增 app/service/convert/ 7 文件:__init__ / types / calc / fee / nav / lot_bootstrap / errors (纯函数,不查库、不碰 SQL;所有量化显式 ROUND_HALF_UP;lot_bootstrap 用 zlib.crc32 保证 D18 跨进程同源) - 新增 tests/test_convert_calc.py 93 用例(12 类:HALF_UP 反向自证 / 分档边界 / FIFO 含同 confirmed_at 兜底 / 双口径 / 强制全转与强制赎回 / PRD §5.3 全链自证 / 纯函数零 IO 依赖断言) - 重写 scripts/dev/calc_convert_demo.py:去掉脚本内公式副本,改为调用生产 calc.py, 末尾与 PRD §5.3 逐项比对(不一致即退出码 1),兼作一致性门禁 验证 - pytest 609 passed / 3 skipped(516 → +93,零回归) - verify_convert_seed.py 8/8 PASS;calc_convert_demo.py 15/15 与 PRD §5.3 一致 文档:PRD v0.9.1(费率分类修正)· 架构 §7 签名回填 / §8.3 错误码注 / §15 T-2 完成 · 开发计划 §1.5 新增 R-h + §4.2·§4.3 执行记录 · AGENTS.md · docs/memory
This commit is contained in:
+41
-10
@@ -1,6 +1,6 @@
|
||||
# PRD · 基金转换(convert)交易
|
||||
|
||||
> 版本:**v0.9(架构评审增补 · 定稿)** · 日期:2026-09-10
|
||||
> 版本:**v0.9.1(费率分类修正 · 定稿)** · 日期:2026-09-10
|
||||
> 分支:`risk-control-agent`
|
||||
> 状态:第 2 步产出。**v0.7 已冻结**(架构 v0.2 §0 查证发现 4 项合规硬伤,外审判定「❌ 不建议进入编码」)
|
||||
> → 回退修订出 **v0.8**(按处置表全量修订,33 条闭环)→ 架构 v1.0 独立评审**通过**后,回填 2 处契约出 **v0.9**。
|
||||
@@ -20,6 +20,21 @@
|
||||
> | 2 | 响应增 `confirm_basis: "natural_day_approx"`(T+1 用自然日近似,真实 Core 接入后为工作日) | 契约(响应) | 架构评审 R2 |
|
||||
> | 3 | `cleanup_pending_convert.py` 措辞由「清理超 24h 的 `pending` 孤儿」统一为「置 `status='expired'`(**标记不硬删**,留痕供对账)」 | **措辞统一**(同 S2 语义,非新增契约) | 架构评审 S2 |
|
||||
>
|
||||
> **v0.9 → v0.9.1 变更(费率分类修正 · 用户裁定,2026-09-10)**
|
||||
>
|
||||
> | # | 变更 | 性质 | 依据 |
|
||||
> | --- | --- | --- | --- |
|
||||
> | 1 | 申购费率档**严格按 `product_type` 取**:`PROD-005827` 由「主动偏股 **0.80%**」更正为「**其他混合型 0.50%**」—— `mixed` 只能套 ≤0.5% 档 | 契约(种子口径) | 22 号文 §8 的分类维度 + 用户裁定 |
|
||||
> | 2 | 主示例**转入方**由 `PROD-005827` 改为 `PROD-003095`(`stock` / 主动偏股型 / R4 / 净值 **0.9500**);费率仍 0.80%、**§5.3 全部派生数字一个都不用改** | 契约(示例) | 同上(两者净值恰好同为 0.9500) |
|
||||
> | 3 | 主示例两端主体明确为 **华夏模拟基金 / TA-CN-001**(同管理人 + 同 TA —— 转换的前置硬约束) | 契约(示例) | 架构:同销售机构 + 同管理人 + 同 TA |
|
||||
> | 4 | 新增「**分类必须与 `product_type` 匹配**」说明 + 连带约束核对(同主体 / 净值 / 适当性三分支) | 说明(非契约) | 本次「外审级漏网」复盘 |
|
||||
>
|
||||
> **根因(必须记住)**:v0.8 按 22 号文上限重定费率时,为凑出 0.50% 的补差幅度,
|
||||
> 把 `mixed` 的 `PROD-005827` 标成「主动偏股」并直接取到 0.8% 上限 —— **这是分类错误**。
|
||||
> 五轮外审与我方逐条核对**只验证了「数值 ≤ 上限」,从未验证「该产品是否属于这一档」**。
|
||||
> → 新增**自检第 12 问:类型与口径必须匹配**,并把该约束固化为**可执行断言 ⑧**
|
||||
> (`scripts/dev/verify_convert_seed.py`,越档即 FAIL),不再依赖人工比对。
|
||||
>
|
||||
> **变更门规(防版本循环)**:只有「**对外契约 / 表结构 / 枚举**」变更才回改本 PRD;
|
||||
> 算法、补偿方式、索引、类型定义等**内部实现由架构自主决定**,本 PRD 不动。
|
||||
>
|
||||
@@ -117,7 +132,7 @@ B 亦为新近公告的主流写法。
|
||||
|
||||
> ⚠️ `diff_fee` 一期由两端产品的 `subscribe_fee_rate` 决定(**独立列,详见 §4.2 / §10 Q8,
|
||||
> 不得取 `fee_rate`**——后者是管理费率,语义不同)。
|
||||
> **主示例取不同费率**(债基 **0.30%** → 主动偏股 **0.80%**)→ `diff_fee = 252.40`(§5.3 示例自证块)。
|
||||
> **主示例取不同费率**(`PROD-110022` 债基 **0.30%** → `PROD-003095` 主动偏股 **0.80%**)→ `diff_fee = 252.40`(§5.3 示例自证块)。
|
||||
> 同费率场景(补差 0)由 §5.3 同费率对照 + §9 第 19 条用例覆盖。
|
||||
|
||||
### 2.1.3 赎回费分档(v0.8 按 22 号文重排)
|
||||
@@ -376,11 +391,26 @@ ALTER TABLE core_product
|
||||
> | 产品 | 类型 | `subscribe_fee_rate` | 依据(22 号文 §8 上限) |
|
||||
> | --- | --- | --- | --- |
|
||||
> | `PROD-110022`(示例转出) | bond | **0.0030** | 债券型 ≤0.3% |
|
||||
> | `PROD-005827`(示例转入) | 主动偏股 | **0.0080** | 主动偏股型 ≤0.8% |
|
||||
> | `PROD-005827` | mixed(其他混合型) | **0.0050** | 其他混合型 ≤0.5% |
|
||||
> | `PROD-003095`(示例转入) | stock(主动偏股型) | **0.0080** | 主动偏股型 ≤0.8% |
|
||||
> | `PROD-000001`(同费率对照组) | money | 0.0000 | 货基 0 |
|
||||
> | `PROD-510300`(同费率对照组) | index | 0.0030 | 指数型 ≤0.3% |
|
||||
>
|
||||
> §5.3 主示例即取自前两行(**补差 0.50%**);§9 第 19 条两条用例(非零 / 同费率)均需可跑。
|
||||
> §5.3 主示例即取自 `PROD-110022`(0.0030)→ `PROD-003095`(0.0080)两行(**补差 0.50%**);
|
||||
> §9 第 19 条两条用例(非零 / 同费率)均需可跑。
|
||||
>
|
||||
> ⚠️ **分类必须与 `product_type` 匹配**(自检第 12 问):费率档由**产品类型**决定,
|
||||
> **不得为凑出数值差把 `mixed` 产品按「主动偏股」取 0.8%**。v0.8 曾把
|
||||
> `PROD-005827`(`mixed`,平衡混合一号)标为「主动偏股」并取到 0.8% 上限——
|
||||
> 属分类错误,v0.9.1 已修正:`PROD-005827` 归「其他混合型 0.5%」,
|
||||
> 示例转入方改用真正的主动偏股产品 `PROD-003095`(`stock` / R4)。
|
||||
>
|
||||
> **连带约束核对**(同一次修正必须一起看):
|
||||
> ① `PROD-003095` 归 **华夏模拟基金 / TA-CN-001**,与转出方 `PROD-110022`
|
||||
> **同管理人、同 TA** —— 满足转换的前置硬约束(否则跨主体不可转换);
|
||||
> ② `PROD-003095` 净值 **0.9500**,与原示例除数的 0.9500 相同 → **§5.3 派生数字全部不变**;
|
||||
> ③ 客户 CUST-9527 为 **C3**、`PROD-003095` 为 **R4** → 匹配结果
|
||||
> `allowed_with_disclosure`(需签风险揭示书后**放行**,非阻断;`forbidden` 才阻断,§7.2)。
|
||||
>
|
||||
> **赎回费种子同步(B-3)**:`07-seed-fee-rule.sql` 的 `core_fee_rule.rate` 按 §2.1.3 新档写
|
||||
> `0.0150 / 0.0100 / 0.0050 / 0.0025 / 0`(**v0.7 的 7–30 日 0.0075 低于法定下限,违规**)。
|
||||
@@ -448,7 +478,7 @@ POST /api/simulate/trade
|
||||
"customer_id": "CUST-9527",
|
||||
"trade_type": "convert",
|
||||
"from_product_id": "PROD-110022",
|
||||
"to_product_id": "PROD-005827",
|
||||
"to_product_id": "PROD-003095",
|
||||
"qty": 50000.0000,
|
||||
"client_request_id": "CLI-20260909-0001" // 幂等键,选填
|
||||
}
|
||||
@@ -468,8 +498,9 @@ POST /api/simulate/trade
|
||||
|
||||
> **示例自证:每个派生值都由本块数据算出(v0.8 · 口径 B · 脚本实算)**
|
||||
>
|
||||
> 数值来源:`scripts/dev/calc_convert_demo.py`(与 §2.1 / §2.5 同一套公式),
|
||||
> **禁止手算**——v0.7 曾两次因手算与口径不符被外部审查质疑。
|
||||
> 数值来源:`scripts/dev/calc_convert_demo.py`,**禁止手算**——v0.7 曾两次因手算与口径不符被外部审查质疑。
|
||||
> **T-2b 落地(2026-09-10)**:该脚本已改为**直接调用生产 `app/service/convert/calc.py` 与 `fee.py`**
|
||||
> (不再自带公式副本),并在末尾与本节表格**逐项比对,不一致即退出码 1**;实算 15/15 一致。
|
||||
>
|
||||
> | 步 | 计算 | 结果 |
|
||||
> | --- | --- | --- |
|
||||
@@ -485,7 +516,7 @@ POST /api/simulate/trade
|
||||
> | `in_qty` | 50784.10 ÷ 0.9500 = 53456.9474 | q2 **53456.95**(2 位四舍五入) |
|
||||
> | `rounding_diff` | 理论 53456.9474 − 实际 53456.95 | **-0.0026**(负 = 客户多得) |
|
||||
>
|
||||
> **两端申购费率**:转出 `PROD-110022` 债基 **0.30%**、转入 `PROD-005827` 主动偏股 **0.80%**
|
||||
> **两端申购费率**:转出 `PROD-110022` 稳健债基 A(bond)**0.30%**、转入 `PROD-003095` 医药健康精选(stock,主动偏股型)**0.80%**
|
||||
> (均 §4.3 种子显式给定,且在 22 号文 §8 上限内)。
|
||||
>
|
||||
> **补差费口径差异对照**:同输入下口径 A(费率差法)`diff_fee = 253.91`、`in_qty = 53455.36`
|
||||
@@ -525,7 +556,7 @@ POST /api/simulate/trade
|
||||
|
||||
// ── 转入端(视同申购)──
|
||||
"in_trade_id": "TRD-20260909-YYYYYYYY",
|
||||
"in_nav": "0.9500", // PROD-005827 成交净值
|
||||
"in_nav": "0.9500", // PROD-003095 成交净值
|
||||
"convert_amount": "51036.50", // 扣赎回费后可用于转入的金额 = 51500.00 − 463.50
|
||||
"diff_fee": "252.40", // 口径B:405.05 − 152.65(价外法两端差,§2.1.1)
|
||||
"in_amount": "50784.10", // 扣补差费后实际买入金额 = 51036.50 − 252.40
|
||||
@@ -834,7 +865,7 @@ SELECT 1 FROM core_trade WHERE convert_group_id = :gid LIMIT 1;
|
||||
第 6 步集成测试**实测补录真实耗时**;若实测超阈值,需优化索引 / 锁策略后再定阈值,
|
||||
**不得反向修改实测数据迁就指标**)
|
||||
19. **补差费非零场景有覆盖**(v0.7 自查问题 A 衍生 · **Q8 已定稿**):
|
||||
存在两端 `subscribe_fee_rate` 不同的产品对(`PROD-110022` 0.30% / `PROD-005827` 0.80%),且有用例验证
|
||||
存在两端 `subscribe_fee_rate` 不同的产品对(`PROD-110022` 0.30% / `PROD-003095` 0.80%,且**同属一个管理人 + 同一 TA**),且有用例验证
|
||||
`diff_fee > 0` 时 §2.1 公式成立(`in_amount = convert_amount − diff_fee`);
|
||||
另需一条**同费率对照**用例(`diff_fee = "0.00"`)。
|
||||
否则补差费链路在种子数据下**恒为 0、永不触发**(同第 14 条教训:无覆盖 = 校验能力漏测)
|
||||
|
||||
Reference in New Issue
Block a user