2026-09-10 11:29:55 +08:00
|
|
|
|
# 交接文档 · 基金转换(convert)交易线
|
|
|
|
|
|
|
2026-09-10 15:04:13 +08:00
|
|
|
|
> # ⛔ 本文件已废弃(2026-09-10)—— 请勿据此开工
|
|
|
|
|
|
> **内容已全部并入项目根 `交接文档.md` §B(三线合并版唯一交接入口)**,且本文件停留在 **v1.0(设计阶段完结)**,
|
|
|
|
|
|
> **不含** T-0 / T-0b / T-1 / T-2 / T-2b 的落地进度与 **609 passed** 现状。
|
|
|
|
|
|
> ⚠️ 下方「下一步是第 4 步开发计划」等表述**早已过期**(第 4 步、第 5 步前两批均已完成)。
|
|
|
|
|
|
> **正确入口:`交接文档.md` §B** —— 本文件仅作历史留档,内容**已过期,勿读**。
|
|
|
|
|
|
|
2026-09-10 11:29:55 +08:00
|
|
|
|
> **版本**:2026-09-10 **v1.0(设计阶段完结,交接给开发会话)**
|
|
|
|
|
|
> **用途**:**给接手本线的 AI / 人**。读完本文即可开工,**不需要重读全部代码**。
|
|
|
|
|
|
> **代码基线**:分支 `risk-control-agent`(HEAD 以 `git log -1` 为准);pytest **510 绿**。
|
|
|
|
|
|
> ⚠️ **本线代码尚未改动一行** —— 当前只完成了「讨论需求 → PRD → 架构设计」三步(AIcoding 流程第 1~3 步),**下一步是第 4 步开发计划**。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ⚡ 当前状态(一句话)
|
|
|
|
|
|
|
|
|
|
|
|
| 项 | 状态 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| 第 1 步 讨论需求 | ✅ 完成(多轮讨论 + 外部事实核验) |
|
|
|
|
|
|
| 第 2 步 PRD | ✅ **v0.9 定稿**(33 条外审闭环 + 2 处架构回填) |
|
|
|
|
|
|
| 第 3 步 架构设计 | ✅ **v1.0 定稿**(独立评审通过,13 条建议 0 悬空) |
|
|
|
|
|
|
| 门控 M-7(PRD 未定稿不得进第 4 步) | ✅ **已满足** |
|
|
|
|
|
|
| 第 4 步 开发计划 | ⬜ **← 下一步做这个** |
|
|
|
|
|
|
| 第 5 步 todo 开发 / 第 6 步 集成测试 | ⬜ 未开始 |
|
|
|
|
|
|
| **代码改动** | ⬜ **零**(架构里的 T-1 之后才有代码) |
|
|
|
|
|
|
|
|
|
|
|
|
**开工前必须先做的两件阻断前置**:**T-0**(sqlite/MySQL 列名统一)与 **T-0b**(DB 账号分离)。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 五分钟背景
|
|
|
|
|
|
|
|
|
|
|
|
### 1.1 项目是什么
|
|
|
|
|
|
|
|
|
|
|
|
XingHuo 智能财富管家:金融四 Agent(客户财富 / 代理人 / 数据分析 / 风控)共用数据层与合规底座,**四 Agent 不互调 LLM**。
|
|
|
|
|
|
|
|
|
|
|
|
技术栈:Python 3.13 + FastAPI + SQLAlchemy Core(**text SQL,无 ORM**)+ MySQL 双库(`jinrong_core` 模拟 Core / `jinrong_agent` 业务)+ Redis + Milvus + Neo4j。
|
|
|
|
|
|
|
|
|
|
|
|
### 1.2 本线要做什么
|
|
|
|
|
|
|
|
|
|
|
|
`docs/PRD/PRD-风控监测Agent.md` 的 **FR-1 一期显式拒收 `convert`**(`trade_gateway` 里直接 400 拒绝)。
|
|
|
|
|
|
本线把它**放开为可交易**:新增基金转换(转出 A 基金 → 转入 B 基金)的完整链路。
|
|
|
|
|
|
|
|
|
|
|
|
### 1.3 业务本质(看懂这段才不会写错)
|
|
|
|
|
|
|
|
|
|
|
|
**一次基金转换 = 一次赎回(转出)+ 一次申购(转入)**,但**不是两笔独立交易**:
|
|
|
|
|
|
|
|
|
|
|
|
- **未知价法**:按申请当日净值计价,T 日申请 → **T+1 权益登记**(扣转出、登转入)→ T+2 可用
|
|
|
|
|
|
- **逐批次计费**:按 `core_share_lot` 的批次,**每批按各自持有期**查赎回费率(<7 日 1.5% / 7–30 日 1.0% / 30–180 日 0.5% …)
|
|
|
|
|
|
- **先进先出(FIFO)**:注册日期在前的批次先转出
|
|
|
|
|
|
- **单笔计算法**:当日多笔转换**不合并**计费
|
|
|
|
|
|
- **补差费**:转入费率高于转出费率时补差额(**口径 B 为默认**,见 §3)
|
|
|
|
|
|
- **份额/金额一律 2 位四舍五入**,误差「在基金资产列支」(`rounding_diff` 可正可负)
|
|
|
|
|
|
|
|
|
|
|
|
**合规基准**:证监会公告〔2025〕22 号《公开募集证券投资基金销售费用管理规定》(2026-01-01 施行)
|
|
|
|
|
|
—— 申购费率上限(主动偏股 ≤0.8% / 其他混合 ≤0.5% / 指数·债券 ≤0.3% / 货基 0)、赎回费下限(<7 日 ≥1.5% …)、赎回费**全额计入基金财产**。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 设计资产(读这四份就够,不用翻代码)
|
|
|
|
|
|
|
|
|
|
|
|
| 顺序 | 文档 | 看什么 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| **1** | 本文 | 全局、状态、坑、禁止事项 |
|
|
|
|
|
|
| **2** | `docs/项目框架设计/架构设计-基金转换交易.md`(**v1.0**) | **怎么实现**:§2 目录 / §3 时序 / §4 决策 D1~D20 / §5 事务补偿 / §7 计算口径 / §8 契约 / §9 DDL 落点 / §10 测试 / §11 配置 / §12 风险门禁 / §15 任务映射 |
|
|
|
|
|
|
| **3** | `docs/PRD/PRD-基金转换交易.md`(**v0.9**) | 需求权威:场景 FR-C1~C16 / §4 表结构 / §5 接口 / §7 流程与事务 / §9 验收 |
|
|
|
|
|
|
| 4 | `docs/项目框架设计/评审待办-风控主架构与基金转换.md` | 评审逐条判定与核实证据(§二)+ 交叉点(§三)+ 执行期风险(§四) |
|
|
|
|
|
|
| 5 | `docs/项目框架设计/基金转换-审查意见处置表.md` | 33 条外审处置 + **§九 用户拍板记录** |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 已拍板的关键决策(**不能改**,改了会破坏已验证方案)
|
|
|
|
|
|
|
|
|
|
|
|
| # | 决策 | 口径 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| 补差费 | **口径 B(价外法两端差)为默认**,A(费率差法)以 `convert_diff_fee_mode='rate_diff'` 保留 |
|
|
|
|
|
|
| 份额精度 | **2 位 `ROUND_HALF_UP`**(不是 4 位、不是向下舍);金额同 |
|
|
|
|
|
|
| T+1 确认 | **建模**:`confirmed_at = T+1 自然日`(近似,注释标明真实为工作日);持有期自确认日起算 |
|
|
|
|
|
|
| 最低持有余额 | **两种都做**:`min_hold_action ∈ {force_transfer, force_redeem}`,默认 `force_transfer` |
|
|
|
|
|
|
| 普通申赎无批次 | **不跳过** → 按 `core_holding.as_of` **兜底补建**初始批次(与 `rebuild_lots.py` **同调** `lot_bootstrap.bootstrap_lots`,D18) |
|
|
|
|
|
|
| 转出归零 | **保留 `qty=0` 行**,不删;持仓查询过滤 `qty <= 0` |
|
|
|
|
|
|
| Core 侧明细 | **仅补偿,不对外查询**(`core_convert_lot_detail`) |
|
|
|
|
|
|
| 引擎时序 | **阶段 1.5**:阶段一提交后、阶段二之前**同步**跑 `process_convert_event`;失败 **不阻断交易**(D17) |
|
|
|
|
|
|
| 引擎异常钩子 | `on_error_hook=None` 预留(D19),二期注入 Redis 事件/补偿队列;hook 异常不得反噬主流程 |
|
|
|
|
|
|
| **DB 账号分离** | **D20**:`xh_core_ro`(SELECT)/ `xh_core_rw`(4 表写,无 DELETE/DDL)/ `xh_agent_rw`(`audit_log` 只授 INSERT);见 §11.1 |
|
|
|
|
|
|
| 批次上限 | `convert_batch_max_lots=200`,超限 400 `TOO_MANY_LOTS`,**不自动分拆**;**单笔 = 单事务 = 单 `convert_group_id`** |
|
|
|
|
|
|
| 交易发起主体 | **仅客户本人或 `risk_demo`**;**代理人只能查、不能交易**(`simulate.py:42` 现状即如此,convert 沿用) |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 4. 开工前置(⛔ 阻断,两个都要绿)
|
|
|
|
|
|
|
|
|
|
|
|
| 任务 | 内容 | 门禁 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| **T-0** | 以 MySQL 为准统一 sqlite DDL:`tests/_ddl.py` 的 `core_holding` 改成 `qty`/`cost_amount`/`as_of`/`pnl_pct` **+ 主键**(现状 `tests/_ddl.py:50-54` 只有 `market_value`/`quantity` 且**无 PK**);`conftest.py` 加启动期列名断言 | `pytest tests/test_db.py::test_core_holding_columns` **必绿**才准跑 T-1 之后 |
|
|
|
|
|
|
| **T-0b** | DB 账号分离(D20):`scripts/core/00-grant.sql` 建 3 账号授权;`settings.py` 加 3 组账号;`db.py` 改 `get_engine(database, role="rw")`(缓存键改 `(库名, 角色)`);`core_ro` 走 `ro`、**gateway 写路径走 `rw`(读仍 `ro`)** | 3 个真 MySQL 权限断言:ro 账号 `INSERT` 被拒 · `audit_log` 改/删被拒 · rw 账号改第 5 张表被拒 |
|
|
|
|
|
|
|
|
|
|
|
|
> **账号未配置时回退单账号**(`settings.mysql_user`)——本地开发不被阻塞,但**真 MySQL 集成测试必须跑在拆分账号下**。
|
|
|
|
|
|
> **sqlite 测试路径零影响**(无账号概念),现有 510 用例**一行都不用改**。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 5. 任务清单(架构 §15 已定稿,可直接作为开发计划输入)
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
T-0 (⛔ 列名统一 + 门禁)─┐
|
|
|
|
|
|
T-0b(⛔ DB 账号分离 D20)─┴─► T-1(DDL/种子 00-grant·01-ddl·07·08·09·02-agent + reset.ps1)
|
|
|
|
|
|
└─► T-2(纯函数 types/calc/fee/nav/lot_bootstrap)─► T-2b(calc_convert_demo 实算回填)
|
|
|
|
|
|
├─► T-3 ─┐
|
|
|
|
|
|
├─► T-4 ─┼─► T-6(阶段一事务)─► T-7(编排 · 关键路径)
|
|
|
|
|
|
└─► T-5 ─┘ ├─► T-9(API + gateway 分派)
|
|
|
|
|
|
└─► T-12(补偿脚本)
|
|
|
|
|
|
T-8(引擎改造 · 可全程并行)─► T-11(工具汇总去重)
|
|
|
|
|
|
T-10(批次维护 · 回归风险最大:先跑 510 基线再动)
|
|
|
|
|
|
└─► T-13(全量回归 + 50 并发压测 + 性能补录)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- **并行组 A**:T-3 / T-4 / T-5(只依赖 T-1,可与 T-2 同时开工)
|
|
|
|
|
|
- **并行组 B**:T-8 与 T-2 之后任意阶段并行
|
|
|
|
|
|
- **关键路径**:T-0 → T-1 → T-2 → T-6 → T-7 → T-13
|
|
|
|
|
|
- **高风险任务**:**T-10**(改 `trade_gateway` 主流程,直接影响现有 510 用例)
|
|
|
|
|
|
- **基线**:510 → 预计 **580~600** 用例
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 6. 执行期风险(用户 2026-09-10 提供,架构 §12 已并入三列表)
|
|
|
|
|
|
|
|
|
|
|
|
| # | 风险 | 必须守住的验收点 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| 1 | T-0 未完成就跑 T-1~ | CI 门禁 `test_core_holding_columns` 必绿 |
|
|
|
|
|
|
| 2 | 批次补建规则写两处 → 漂移 | D18 抽 `lot_bootstrap` 纯函数,两侧同调;单测断言同源 |
|
|
|
|
|
|
| 3 | `Decimal.quantize` 默认 `ROUND_HALF_EVEN` | `calc/fee/nav` **所有量化处**显式传 `ROUND_HALF_UP`;`.5` 边界断言 |
|
|
|
|
|
|
| 4 | 阶段 1.5 引擎异常丢预警 | D19 预留 `on_error_hook`;hook 抛异常不得反噬主流程 |
|
|
|
|
|
|
| 5 | `convert_batch_max_lots=200` 超限 | 400 错误体带 `batch_count`/`max_lots`;前端提示「请拆分多笔申请」 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 本线特有的坑
|
|
|
|
|
|
|
|
|
|
|
|
1. **sqlite 与 MySQL 列名不同名**(T-0 根因):`tests/_ddl.py:50-54` 的 `core_holding` 用 `market_value`/`quantity`,MySQL 用 `qty`/`cost_amount`/`as_of`/`pnl_pct`,且**无 PK**。手工同步无迁移工具,**不先统一一定在集成测试期才炸**。
|
|
|
|
|
|
2. **`Decimal.quantize()` 默认是银行家舍入**(`ROUND_HALF_EVEN`),必须显式传 `ROUND_HALF_UP`,否则 `.5` 边界错。
|
|
|
|
|
|
3. **`core_ro.py:390` / `:432` 写死了 `AND trade_type IN ('subscribe','redeem')`** —— 不改的话 convert 在 RISK-001/002/003 聚合里**完全隐形且不报错**(架构 D7 `_amount_view` 就是为此)。
|
|
|
|
|
|
4. **改路由必须同步** `tests/test_main.py::test_all_routers_mounted` 的路径清单,否则必红。
|
|
|
|
|
|
5. **测试/演示库重灌两步缺一不可**:`scripts/core/reset.ps1` + `scripts/demo/prepare_risk_demo.sql`(只跑第一步 → 27/33 客户风评过期,是种子设计不是 bug)。
|
|
|
|
|
|
6. **用系统 Python 3.13.14** 跑测试:`C:/Users/YUAN/AppData/Local/Programs/Python/Python313/python.exe`(managed 3.13.12 未装依赖)。
|
|
|
|
|
|
7. **sqlite 不支持绑定 `Decimal`**,测试插 `core_trade.amount` 须转 float。
|
|
|
|
|
|
8. **双库无跨库事务**(`jinrong_core` / `jinrong_agent` 各自 engine)→ 所以有「三阶段 + 补偿」,不要试图写一个跨库事务。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 8. 禁止事项
|
|
|
|
|
|
|
|
|
|
|
|
**项目五条红线**(碰了就是事故):Core 表只读 · 审计表只 INSERT · 代理人草稿不外发 · **仅 R-02 可阻断交易** · 四 Agent 不互调 LLM。
|
|
|
|
|
|
|
|
|
|
|
|
**本线纪律**:
|
|
|
|
|
|
|
|
|
|
|
|
- ❌ 不改表结构 / 接口协议**而不经用户确认**(改表需确认是项目硬规则)
|
|
|
|
|
|
- ❌ 不做清单外的改动(看到别的问题**记下来**,别顺手改)
|
|
|
|
|
|
- ❌ 不动 `tests/_ddl.py` 之外的第二份 sqlite DDL(单一事实源)
|
|
|
|
|
|
- ❌ 不新增第三方依赖
|
|
|
|
|
|
- ❌ 所有阈值**不得硬编码**(走 `settings.py`,架构 §11)
|
|
|
|
|
|
- ❌ 提交前必须 `pytest` 全绿;**推送前须用户目视验收**
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 9. 另一条线的遗留(**别混淆,不在本线范围**)
|
|
|
|
|
|
|
|
|
|
|
|
**架构改进与稳定性加固线**(T-101~T-202)**2026-09-10 已全部收尾闭环**:
|
|
|
|
|
|
|
|
|
|
|
|
1. **§7.2 七项手工冒烟已补跑完毕,7/7 PASS**(含「停 Redis → 退回进程内锁」「调换装饰器 → T-202 守卫变红」两项)
|
|
|
|
|
|
2. ~~`037ce7e` 已 commit 未 push~~ → **本文原表述已过期**:实测 `git ls-remote`,远程 `risk-control-agent` = `fffb78a` = 本地 HEAD,`037ce7e` 早在祖先链上,**该线全部提交已推送,无待推内容**
|
|
|
|
|
|
3. 冒烟产生的演示库残留已按 SOP §2 重灌清除,全量 **510 passed** 复绿
|
|
|
|
|
|
|
2026-09-10 15:04:13 +08:00
|
|
|
|
明细与入口见 **`交接文档.md` §C**(原 `docs/交接文档-架构改进.md` 已废弃为历史留档)。本线 **T-0b 与其无关**(那是基金转换自己的前置)。
|
2026-09-10 11:29:55 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 10. 完成后要回写的文档
|
|
|
|
|
|
|
|
|
|
|
|
1. `docs/memory/TODO.md` —— 勾选进度
|
|
|
|
|
|
2. `docs/memory/MEMORY.md` §0 —— 进度与仓库地图
|
|
|
|
|
|
3. `docs/项目框架设计/开发计划-基金转换交易.md` —— 第 4 步产出(**待创建**)
|
|
|
|
|
|
4. `docs/memory/YYYY-MM-DD.md` —— 当日工作日志
|
|
|
|
|
|
5. 本文件 —— 状态块与任务清单勾选
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 11. 有问题怎么办
|
|
|
|
|
|
|
|
|
|
|
|
- **发现文档与代码不符** → 以代码为准,并在 TODO 备注记下差异(先核实,别急着改文档)
|
|
|
|
|
|
- **对设计有疑问** → 查架构对应章节;仍不明确则**停下问用户**,不要猜
|
|
|
|
|
|
- **评审意见与自己的判断冲突** → 先核实证据(文件行号),**不盲从也不护短**;驳回要给出核实过的证据
|