AIcoding 第 5 步 todo 开发(开发计划 v2.0)前半段: - T-1 3 张新表 DDL(core_convert_request 6 态 ENUM / core_trade_calendar / core_share_rule)+ 种子 + sqlite 单一事实源同步 + 列清单断言 - T-2/T-2b calc 扩展(product_round/redeem_amount/partial_qty)+ 真实净值实算回填 - T-3 convert_request_repository(6 态 + 条件 UPDATE 守卫)+ core_ro 三读方法 + share_lot_repository.available_qty_with_inflight(R-3 在途占用推导) - T-4 convert_repository.sync_mirror 成为 risk_convert_detail 唯一进度镜像写入口 (旧三方法标 Deprecated,T-7 后删) - T-5 locks.py 锁键构造器 convert_req_lock_key / convert_confirm_lock_key - T-6 convert_service.accept_convert 受理事务(八步:锁→幂等→校验→受理日顺延 →在途占用校验→落单→镜像+审计→202;不扣份额/不折算/不写流水) + tests/test_convert_accept.py(16 用例) + scripts/dev/verify_convert_accept.py(真库 36/36 一致) + trading_calendar.py 纯函数包(R-5)+ 21 用例 T-6 真库实测暴露并修复:confirm_eta 在日历数据边界抛 ValueError,会让已落库 的受理单在调用方眼里变 500;改为展示性字段容错 + 单测守护。 基线:798 passed / 10 skipped,零回归。
46 KiB
架构设计说明书 · 基金转换(convert)交易
版本:v2.1(精简最终态 · T+1 受理/确认分离模型) · 2026-09-11 · 已通过独立 AI 联网审查(0 阻断级),已定稿(2026-09-11 用户批准) 上游:
docs/PRD/PRD-基金转换交易.md(v1.1) 分支:risk-control-agent· 关联:docs/项目框架设计/架构设计-风控模块.md技术选型不变:FastAPI + SQLAlchemy Core(text SQL) + MySQL 双库 + Redis + LangGraph + DeepSeek。 本文定义目录划分、模块职责、核心时序、技术决策、事务与并发、测试策略、任务映射(第 4 步输入)。
0. 合规与真实业务基准
来源:证监会公告〔2025〕22 号《公开募集证券投资基金销售费用管理规定》(2026-01-01 施行)+ 汇添富/中欧/鹏华/国投瑞银/东海/华商/金鹰/财通/中银/东方证券资管 10 家转换业务公告。清单见 §0.5。
0.1 法规硬约束
| 项 | 22 号文规定 | 本项目取值 | 落点 |
|---|---|---|---|
| 申购费率上限 | 主动偏股 ≤0.8% / 其他混合 ≤0.5% / 指数·债券 ≤0.3% / 货基 0(22号文 §8) | 种子 0.0080 / 0.0050 / 0.0030 / 0 | PRD §4.3 |
| 赎回费下限 | <7 日 ≥1.5% / 7–30 日 ≥1.0% / 30–180 日 ≥0.5%(22号文 §10) | 档表见 §7(+180–365 0.25% / ≥365 0,合同约定) | PRD §2.1.3 |
| 赎回费归属 | 全额计入基金财产(22号文 §10,旧规 75%/50%/25% 分级已废止) | core_fee_rule.to_fund_ratio = 1.0(D16,留痕、不参与计算) |
PRD §4.1 |
豁免不启用:22 号文 §10 允许个人持满 7 日的指数/债基、机构持满 30 日的债基另约赎回费。本项目不走豁免,保留完整费率梯度以便演示与测试。
0.2 行业惯例
| 项 | 真实做法(多份公告一致) | 本项目 |
|---|---|---|
| 份额精度 | 转入份额四舍五入保留 2 位(主流);个别差异:易方达 ETF 场外整数位、南方截断 | 主流 2 位 HALF_UP;按产品 core_share_rule(product_id+business_type) 配置(D27) |
| 金额精度 | 转出金额/赎回费/补差费 2 位四舍五入(主流);个别差异:东方赎回费 2 位后截断 | 主流 HALF_UP;按产品 rounding_mode 配置(D27) |
| T+1 确认 | T 申请(未知价法)→ T+1 权益登记 → T+2 可用;持有期自确认日起算 | 转入新批次 confirmed_at = T+1(交易日历推算) |
| 先进先出 / 逐批计费 | 注册日期在前先转出;不同持有期分别计费 | FIFO + 逐批计费 |
| 单笔计算法 | 当日多笔转换单笔计费,不合并 | 逐笔 |
| 未知价法 | 以申请受理当日净值为基准 | 确认时取 T 日净值 |
| 转出可赎回 / 转入可申购 | 双方状态校验 | FR-C9a |
| 同机构限制 | 同一销售机构 + 同一管理人 + 同一 TA | CROSS_ENTITY_NOT_SUPPORTED |
| 冻结份额 / 撤单 | 冻结不可转(司法冻结为人工柜台);T 日结束前可撤 | 在途占用由受理单推导;撤单已实现 |
| 赎回申报方向 | 份额赎回——投资者以份额申报,登记机构按 T 日净值算金额 | redeem 份额申报(D26) |
| 巨额赎回 | 转出与赎回同优先级、可全额或部分转出;真实条款为部分延期赎回(未确认部分顺延至下一开放日) | 投资者层部分成交(可用份额不足时实际=min(申请,可用))+ 占用释放(不顺延,属本项目投资者层建模取舍,与基金层顺延不同)(D28) |
| 资金流 | 赎回款经注册登记账户中转,一次转换一次资金划转 | Core 侧建 core_cash_flow 中转建模(D29) |
| A/C 份额互转 | 同一基金 A/C 类可互转(由管理人公告决定,非所有基金支持) | core_product 加 share_class + per-product 开关 allow_ac_convert(D30) |
0.3 已拍板的多口径决策
| 项 | 拍板结论 | 落点 |
|---|---|---|
| 补差费口径 | B(价外法两端差)为默认;A 以 convert_diff_fee_mode='rate_diff' 保留 |
D13 / §7 / §11 |
| 最低持有余额处置 | 两种都做:min_hold_action ∈ {force_transfer, force_redeem} |
D15 / §7 |
| P1 普通申赎无批次 | 不跳过(批次表覆盖全部交易类型;遇历史持仓无批次兜底补建) | D8 / §7 |
| P2 转出归零 | 保留(remain_qty/qty = 0),不删行 |
D9 / §7 |
| P3 Core 侧明细 | 仅补偿,不对外查询 | D6 / §7 |
0.4 与真实业务全量对齐(D26~D30)
所有与真实业务不符的简化本期全部收敛:redeem 份额申报、按产品舍入、投资者层部分成交、资金流中转、A/C 互转。唯一硬限制 = 模拟库不是真实登记机构底座;但以下差异在模拟层内均可建模对齐真实语义。
| # | 原简化 | 真实对齐落地 | 决策 |
|---|---|---|---|
| ① | redeem 金额反算份额 | 份额申报;赎回金额 = qty × T日净值 − 赎回费 |
D26 |
| ② | redeem 折算份额估算 | 随①收敛(份额来自申报,T+1 取 T 日真实净值) | D26 |
| ③ | 赎回费统一 HALF_UP | 按产品 rounding_mode(东方截断等) |
D27 |
| ④ | 转入份额统一 2 位 | 按产品 share_digits(易方达整数/南方截断等) |
D27 |
| ⑤ | 巨额赎回不做部分成交 | 确认段部分成交(投资者可用份额不足时实际=min(申请,可用))+ 占用释放(不顺延,投资者层取舍;基金层巨额赎回为顺延) | D28 |
| ⑥ | 资金流不建模中转 | Core 侧 core_cash_flow 中转建模 |
D29 |
| ⑦ | 无 share_class |
core_product 加 share_class + seed 标注,放开 A/C 互转 |
D30 |
| ⑧ | 赎回费归属不参与 | 22 号文全额计入(to_fund_ratio=1.0)数值已对齐 |
D16 |
0.5 来源清单
| # | 来源 | 关键结论 |
|---|---|---|
| 1 | 证监会公告〔2025〕22 号(2026-01-01 施行) | 申购费上限 22号文 §8、赎回费下限与全额计入基金财产 22号文 §10 |
| 2 | 上交所 501048 号公告(汇添富) | 口径 A 补差费公式 + 完整算例;0.1 份阈值强制全转 |
| 3 | 中国证券报 2015-11-24(中欧) | 口径 A + T+1 权益登记、T+2 可查询 |
| 4 | 中国证券报 2026-08-19(鹏华) | 口径 B 外扣法 + 同销售机构 + 持有期自确认日重算 |
| 5 | 中国证券报 2026-07-08(国投瑞银) | 口径 B 价外法两端差 + 单笔计算法 + 逐批计费 |
| 6 | 中国金融信息网(泰信/东海) | 未知价法、先进先出、2 位四舍五入、误差在基金资产列支 |
| 7 | 上海证券报 2026-07-30 | 巨额赎回时转出与赎回同优先级;余额不足最低持有 → 强制赎回 |
| 8 | 中国证券报 2026-09-01(中银) | 申请份额精确到 2 位、单笔 ≥1000 份、余额低于 1000 份须全部转出 |
| 9 | 深交所 43da8375(东方证券资管) | 口径 A + 赎回费部分计入基金财产(旧规,已被 22 号文取代) |
| 10 | 睿远 §65 / 华泰保兴 §69 / 东方 §57 / 国投瑞银 §33 | 赎回 = 份额赎回(投资者以份额申报) |
| 11 | 农银汇理 / 泰信 / 华宝兴业「巨额赎回」条款 | 转出与赎回同优先级、可全额或部分转出、未确认部分顺延至下一开放日 |
| 12 | 东方 §59 | 个别管理人赎回费 2 位后截断(非四舍五入) |
1. 设计原则
继承既有(不可违反):
- 分层
api(薄)→ service(业务)→ repository/tool;唯一例外app/gateway/(模拟外部 Core 交易系统,仅此层可写jinrong_core) - 规则即纯函数(
rules.py),便于单测与阈值调整 - 双库无跨库事务(
jinrong_core/jinrong_agent各自 engine) - 审计表只 INSERT;trace_id 用 contextvars 贯通
- 不新增 ORM(
app/model/entities.py不动),沿用 text SQL + 返回 dict
本期新增:
- 计算与编排分离:折算/费率/舍入做成无 IO 纯函数(
service/convert/calc.py),编排层只取数、调纯函数、管事务 - 受理/确认/撤单三段编排:受理段(Core 单库落单)、确认段(Core 单库批量串行扣减)、撤单段(Core 单库),各自独立事务
- 批次表必存 + 兜底补建:
core_share_lot覆盖全部交易类型;普通申赎有批次则 FIFO 扣/增,无批次则兜底补建(按core_holding.as_of建初始批次,lot_bootstrap单点) - 锁原语:统一
locks.run_locked(等待 + 降级双层锁) Decimal一律显式quantize,禁止裸浮点:金额/份额 2 位ROUND_HALF_UP(默认),按产品core_share_rule的rounding_mode/share_digits可覆盖(D27);舍入误差在基金资产列支,响应可选回传rounding_diff- ⚠️ Python
Decimal.quantize()默认ROUND_HALF_EVEN(银行家舍入),必须显式传舍入方式
- ⚠️ Python
- 响应金额全部
str()化 + 单点量化:库内一律DECIMAL(18,4),对外必经convert_service._q()单点量化(金额/份额 2 位、净值/费率/尾差 4 位),响应、审计 summary、日志共用同一出口 - 单次转换最多跨
convert_batch_max_lots个批次(默认 200):超限返回 HTTP 400TOO_MANY_LOTS,防事务膨胀 - Core 读写账号物理分离:
jinrong_core拆只读账号与最小写权限账号;get_engine(database, role)缓存键由「库名」改为「(库名, 角色)」(D20) - 全差异按真实业务走:所有与真实不符的简化本期全部收敛(①
⑧,§0.4 / D26D30)
2. 目录与文件划分
app/
├── api/
│ ├── simulate.py # 【改】TradeRequest 按 trade_type 分支校验;
│ │ # subscribe=金额申购 / redeem=份额申报(D26) / convert=份额申报;
│ │ # convert 调 convert_service;错误码映射 ApiError
│ └── convert_admin.py # 【新增】运维接口:确认触发 FR-C23 / 受理单查询 / 撤单
│
├── gateway/ # 【core 库写侧唯一出口】
│ ├── trade_gateway.py # 【改】convert 分支:校验 → 受理(落 core_convert_request,202),折算移到确认段;
│ │ # redeem 分支:份额申报 qty 入参,赎回金额=qty×净值−费;
│ │ # subscribe/redeem 分支:增补批次维护(FR-C16)+ T+2 可赎回校验(FR-C25)
│ ├── gateway_repository.py # 【改】insert_trade 增 qty / convert_group_id / cash_flow 两列写入
│ └── convert_core_repository.py # 【改】仅 confirm_convert(确认事务:扣批次+写流水+lot_detail+holding+中转+cash_flow+置confirmed);
│ # accept/cancel 归 convert_request_repository(受理单权威状态机)
│
├── service/
│ ├── convert/ # 【新增包】
│ │ ├── types.py # 【类型】Lot / FeeRule / PlanResult / ShareRule,@dataclass(frozen=True)
│ │ ├── calc.py # 【纯函数】FIFO 分配 / 逐批计费 / 补差费(A/B)/ 份额折算 / 舍入(按产品)
│ │ ├── nav.py # 【纯函数】净值口径:确认时取 T 日净值(不回退)
│ │ ├── fee.py # 【纯函数】持有期→赎回费率查表(输入规则行,不查库)
│ │ ├── lot_bootstrap.py # 【纯函数】批次补建规则(D8):gateway 兜底补建 与 rebuild_lots.py 同源
│ │ ├── errors.py # 【新增】convert 专属异常(继承 ApiError,§8.3)
│ │ ├── convert_service.py # 【编排】受理段 + 确认段(含部分确认 D28)+ 撤单段 + 幂等 + 执行权 + 补偿日志
│ │ └── confirm_service.py # 【新增】T+1 批处理确认编排(串行捞受理单 → 逐单 confirm_convert)
│ └── risk/
│ ├── rules.py # 【改】_amount_view();RISK-002/005 走金额视图
│ ├── engine.py # 【改】process_convert_event(out, in),确认事务提交后调用
│ ├── alert_service.py # 【改】record_trade_alerts 增 events 可选参数
│ └── locks.py # 【改】仅 run_locked(双层锁)
│
├── repository/
│ ├── core_ro.py # 【改】增 get_nav_as_of / get_redeem_fee_rules / list_share_lots /
│ │ # sum_remain_qty / get_holding / get_trade_calendar / get_share_rule
│ ├── share_lot_repository.py # 【新增】core_share_lot 读:FIFO 选批 + 汇总
│ ├── convert_request_repository.py # 【新增】core_convert_request 权威状态机仓储:accept_convert(受理事务 INSERT)/
│ │ # cancel_convert(撤单事务置 cancelled)/ query_request(状态查询)
│ ├── convert_repository.py # 【改】agent 库 risk_convert_detail:仅写「agent 侧详情写入进度」
│ └── risk_repository.py # 【不改】复用 insert_audit_log
│
├── tool/
│ └── core_tools.py # 【改】query_recent_trades 汇总走 _amount_view;持仓查询过滤 qty <= 0
│
├── config/
│ └── settings.py # 【改】§11 全部 convert_* 配置项 + §11.1 四个 DB 账号(D20)
└── utils/
└── db.py # 【改】get_engine(database, role="rw")(D20)
scripts/
├── core/
│ ├── 00-grant.sql # 【新增】3 应用账号 + 最小权限(D20)
│ ├── 01-ddl.sql # 【改】core_fee_rule / core_share_lot / core_convert_lot_detail /
│ │ # core_convert_request(新增) / core_trade_calendar(新增) / core_cash_flow(新增 D29)
│ │ # + core_product 加 share_class(D30) / rounding_mode / share_digits(D27) 等 8 列
│ ├── 07-seed-fee-rule.sql # 【新增】赎回费持有期分档(5 档)
│ ├── 08-seed-share-lot.sql # 【新增】按持仓反推批次
│ ├── 09-seed-org.sql # 【新增】fund_company / ta_code / subscribe_fee_rate
│ ├── 11-seed-share-rule.sql # 【新增】真实产品舍入/位数规则,逐产品列 (share_digits, rounding_mode) 组合(D27)
│ ├── rebuild_lots.py # 【新增】按 core_holding 重建批次(快照重建,非交易回滚)
│ └── reset.ps1 # 【改】$files 追加 07/08/09/10/11
├── agent/
│ └── cleanup_pending_convert.py # 【改】扫描 core_convert_request 超 SLA 的非终态单(accepted/nav_pending)置 expired
├── dev/
│ └── calc_convert_demo.py # 【改】与生产同一套 calc.py 实算主示例/同费率对照,回填 PRD §5.3 与验收断言
└── demo/
└── rebuild_alerts.py # 【改】支持 --convert-group 按 Core 侧数据补写详情(D6)
不新增:ORM 实体、app/service/risk/ 新规则(RISK-001~006 口径不变,仅金额视图去重)。
3. 核心时序(受理段 + 确认段 + 撤单段)
POST /api/simulate/trade {trade_type: "convert", from_product_id, to_product_id, qty, client_request_id?}
│
├─ ① 参数与产品校验(不落库):from != to · can_redeem / can_subscribe · fund_company+ta_code 同机构(含 share_class 同基金判定 D30)
├─ ② 受理日判定(15:00 截点 + 交易日历 → T)
├─ ③ 可用份额校验(不落库):Σ remain_qty − Σ(未终态受理单 requested_qty) + 最低份额(含全额豁免)
├─ ④ 适当性校验(转入端,受理时校验一次留痕含测评版本号)
├─ ⑤ 受理事务(Core 单库):INSERT core_convert_request(status='accepted',含 accept_date/confirm_date/available_date)
│ 撞 uk_idem → 返回首次受理回执(幂等);其他失败 → 整体回滚
└─ ⑥ 返回 202 + 受理回执(不含任何金额/净值/折算字段)
POST /api/simulate/trade/convert/{gid}/cancel (T 日 15:00 前)
│
├─ ① 校验受理单存在 + 状态 accepted/nav_pending
├─ ② 当前时间 ≤ T 日 convert_cutoff_time(15:00)
├─ ③ 撤单事务(Core 单库):置 cancelled + cancelled_at;占用自然释放
└─ ④ 落审计 decision='convert_cancelled'
POST /api/simulate/trade/convert/confirm?accept_date=T (运维/批处理 FR-C23,指定业务日)
│
├─ run_locked("convert:confirm:{T}") # 批处理串行锁(D22)
├─ 捞 core_convert_request WHERE accept_date=T AND status∈(accepted, nav_pending) ORDER BY accepted_at
└─ 逐单 confirm_convert(见下)
confirm_convert(单笔确认事务,Core 单库):
├─ 1 取两端 T 日净值;缺失 → 置 nav_pending,本轮跳过(不 reject、不降级)
├─ 2 复核产品可申赎状态 + 适当性(转入端,D25)→ 不满足 → rejected + 释放占用
├─ 3 plan_lots(FIFO + 最低持有处置)+ 折算(calc.py,口径 B)
├─ 4 【可用份额不足 D28】若申请份额 > 可用份额(部分成交场景):实际成交 = min(申请, 可用);
│ 未确认部分占用释放(不顺延至后续批处理,属投资者层建模取舍;基金层巨额赎回为顺延);记录 partial 标记
│ (部分成交 = 受理段③占用校验之外的二次兜底,覆盖 T 日~T+1 间其它在途单抢占致可用份额变少的场景)
├─ 5 确认事务(Core 单库):扣批次(条件 UPDATE 哨兵)+ 写两条流水 + core_convert_lot_detail
│ + 更新两端 core_holding + 写 core_cash_flow(中转,D29)+ 置 confirmed
│ 失败 → 整体回滚 → 保持 accepted,下轮重试(幂等)
├─ 6 确认后同步跑规则引擎(一次):process_convert_event(out, in);异常落 engine_error + 本地日志,不阻断
└─ 7 agent 侧写详情 + 主审计(附加写入):失败不回滚 Core,落待补偿(SLA 24h)
引擎时机:受理时无金额(净值未公告),规则引擎金额类规则无输入;确认后跑,读到已更新持仓,集中度规则(RISK-006)连带效应仍被覆盖。引擎只调用一次,传入两条流水。
4. 关键技术决策(D1~D30)
| # | 决策点 | 选型 | 备选 | 理由 |
|---|---|---|---|---|
| D1 | 折算计算放哪 | service/convert/calc.py 纯函数 |
内联在 service | 金额逻辑最易错,必须可穷举单测 |
| D2 | 批次读写分工 | 读 → share_lot_repository;写 → convert_core_repository(确认段同事务) |
全塞一个 repository | 写必须与两条流水同事务;读需在事务外预检 |
| D3 | 两段事务实现 | 受理事务 convert_request_repository.accept_convert + 确认事务 convert_core_repository.confirm_convert,各 with engine.begin() 单库单方法 |
单事务实时扣减 | 受理单下沉 Core 后两段各自同库,单事务闭合(PRD §7.1) |
| D4 | 锁原语 | 统一 run_locked(双层锁,抢不到返回 202+已有 group_id) |
一次性 try_lock | 语义与「抢不到立即 202」一致 |
| D5 | 引擎只跑一次 | process_convert_event(out, in),确认后调用 |
给现有函数加参数 | 语义清晰 |
| D6 | 补偿数据来源 | core_convert_lot_detail 含 nav/nav_date,rebuild_alerts 仅凭 Core 侧补写 |
靠 agent 库残留 | 附加写入失败 agent 库可能为空,Core 侧必须自包含 |
| D7 | 金额去重 | rules._amount_view():同 convert_group_id 组内保留转出端 |
删行/求和 | 组内无 redeem 时取第一条(防御) |
| D8 | 普通申赎批次维护 | 兜底补建:无批次 → 按 core_holding.as_of 补建 | 跳过/报错 | P1 已拍板 |
| D9 | core_holding 同步 |
UPSERT;转出后 qty=0 保留行 | 删除行 | P2 已拍板 |
| D10 | 净值查询 | core_ro.get_nav_as_of(pid, trade_date)(nav_date <= :d 降序取 1) |
改 get_latest_nav | 现有方法无条件取最新,改它动既有调用方 |
| D11 | 费率来源 | 运行时读 core_fee_rule + core_product.subscribe_fee_rate |
硬编码 | 数据驱动 |
| D12 | 请求模型 | 扩展 TradeRequest(trade_type 分支校验) |
新增 ConvertRequest | 端点契约不变 |
| D13 | 补差费公式 | 口径 B(价外法两端差)为默认,A 配置保留 | 只用 A | 已拍板 |
| D14 | T+1 确认 | confirmed_at = T+1 交易日(core_trade_calendar 推算),不用自然日近似 |
自然日近似 | 真实业务 T+1 为工作日 |
| D15 | 最低持有余额处置 | 两种都实现(force_transfer/force_redeem),默认 force_transfer | 只做强制全转 | 已拍板 |
| D16 | 赎回费归属 | to_fund_ratio = 1.0(22 号文 §10 全额计入),不参与计算 |
不建模 | 字段留痕 |
| D17 | 规则引擎时机 | 确认事务提交后同步跑(受理时无金额) | 受理后跑 | 受理段无金额,引擎无输入(§3) |
| D18 | 批次补建规则复用 | 纯函数 lot_bootstrap.bootstrap_lots(),gateway 与 rebuild_lots 同调 |
两处各写 | 防漂移 |
| D19 | 引擎异常钩子预留 | process_convert_event(..., on_error_hook=None) |
写死 | 二期可插拔 |
| D20 | Core 读写账号分离 | xh_core_ro/xh_core_rw/xh_agent_rw + get_engine(db, role) |
单 root | 主架构 C1 已拍板 |
| D21 | 受理单落 Core 库 | core_convert_request(权威状态机)落 jinrong_core |
落 agent 库 | 在途占用须与 core_share_lot 同库单 SQL 闭环;幂等锚点 uk_idem 与流水同库消除「重跑第二组流水」 |
| D22 | 批处理串行锁 | run_locked("convert:confirm:{accept_date}") |
无锁/各单并发 | 防两个批处理同时跑同一受理日;锁 + 状态二次校验双保险 |
| D23 | 全量重写 | 删「T 日实时扣减」路径,无双模型开关 | 双模型并存 | 用户拍板:全量重写 T+1,口径统一无漂移 |
| D24 | 确认触发 | 运维接口 FR-C23 + 脚本(手动指定业务日),语义批量串行;不引 apscheduler(模拟库无真实时钟,二期可接) | 引入调度器 | 用户拍板「按真实业务」:登记机构语义=批量串行自动确认;落地为手动接口+脚本模拟批处理 |
| D25 | 适当性 T+1 复核 | 确认段第 2 步加 suitability_check(转入端),不通过 → rejected(份额不变) |
不复核 | 用户拍板「T+1 复核」 |
| D26 | redeem 份额申报 | redeem 入参改 qty(份额);赎回金额 = qty × T日净值 − 赎回费;加 T+2 可赎回校验(FR-C25) |
金额反算份额(与真实相反) | 用户拍板「按真实业务走」:真实=份额赎回(睿远 §65 等) |
| D27 | 按产品舍入/位数 | core_share_rule(product_id + business_type) 配置(易方达 FOF 转换转入 2 位、ETF 场外整数位,须产品级区分);rounding_mode ∈ {HALF_UP, TRUNCATE(=ROUND_DOWN)}(东方/南方截断=ROUND_DOWN,须与种子一致);11-seed-share-rule.sql 逐产品列 (share_digits, rounding_mode) 组合,禁错配 |
统一 HALF_UP/2 位 | 用户拍板「都按真实业务走」:真实不统一,主流口径不算「对齐真实」 |
| D28 | 可用份额不足部分确认 | 确认段支持部分成交(实际=min(申请,可用))+ 未确认部分占用释放(不顺延至后续批处理,属投资者层建模取舍;真实基金层巨额赎回为「部分延期赎回/顺延」,不可混淆) | 不做(无份额池) | 用户拍板:真实规则(可用份额不足部分成交)不可因模拟简化「就算了」;基金层同比例确认需另引现金头寸,本期不做 |
| D29 | 资金流中转建模 | Core 侧建 core_cash_flow(注册登记账户中转),一次转换 1 笔资金划转 |
不建模(core_trade 记两份金额) | 用户拍板:真实资金流经中转账户,模拟层须还原语义 |
| D30 | A/C 份额互转 | core_product 加 share_class 列 + allow_ac_convert TINYINT DEFAULT 0 + seed 真实产品标注;放开同基金 A/C 互转(须 allow_ac_convert=1,易方达 FOF 等公告暂不开通) |
无 share_class 不做 | 用户拍板:真实产品有 A/C 份额,是 seed 未建列,非真实业务没有 |
5. 事务、并发与补偿
5.1 三段 + 引擎 + SQL 清单
| 段 | 库 | 入口 | SQL / 动作 |
|---|---|---|---|
| 受理 | core | convert_request_repository.accept_convert |
1×INSERT core_convert_request(uk_idem 兜底) |
| 确认 | core | convert_core_repository.confirm_convert(单事务) |
扣批次(条件 UPDATE)+ 2×INSERT core_trade + core_convert_lot_detail + 2×UPSERT core_holding + core_cash_flow(中转)+ SET confirmed |
| 撤单 | core | convert_request_repository.cancel_convert |
1×UPDATE core_convert_request SET cancelled |
| 引擎 | — | engine.process_convert_event(out, in) |
确认事务提交后;异常落 engine_error,不阻断 |
受理/确认/撤单任一 SQL 失败 → 整体回滚(with engine.begin() 异常即 rollback)。
5.2 重试判定(仅在取得执行权后)
-- 受理段:查受理单状态
SELECT status FROM core_convert_request WHERE convert_group_id = :gid;
-- 确认段:查是否已有流水(幂等,不产生第二组)
SELECT 1 FROM core_trade WHERE convert_group_id = :gid LIMIT 1;
走 core_ro(只读)。依赖索引(idx_confirm_queue / core_trade.idx_convert_group),否则全表扫。
5.3 锁原语(统一 run_locked)
run_locked(f"convert:avail:{cid}:{out_pid}", lambda acquired: ...) # 受理段可用份额短临界区
run_locked(f"convert:idem:{client_request_id}", lambda acquired: ...) # 幂等执行权(抢不到返回 202)
run_locked(f"convert:confirm:{accept_date}", lambda acquired: ...) # 批处理串行
run_locked(f"convert:rerun:{gid}", lambda acquired: ...) # agent 详情补写串行
确认段扣批次哨兵(并发安全):必须用条件 UPDATE + rowcount 断言,禁止先 SELECT 后 UPDATE:
UPDATE core_share_lot SET remain_qty = remain_qty - :q WHERE lot_id = :id AND remain_qty >= :q; -- rowcount == 1 才成功;否则 LOT_CONFLICT(409) 让路,不重扣
5.4 补偿(一期:监控告警 + 人工脚本,SLA 24h)
| 项 | 口径 |
|---|---|
| 触发者 | risk_convert_detail.status='failed' 或 core_convert_request 超 SLA 非终态的巡检/告警 |
| 补偿工具 | scripts/demo/rebuild_alerts.py --convert-group(Core 侧自包含补写) |
| SLA | convert_confirm_sla_days=2(交易日,T+1 必确认 + 净值延迟宽容) |
| 兜底清理 | cleanup_pending_convert.py 把超 SLA 的 accepted/nav_pending 置 expired(标记不硬删,释放占用) |
幂等锚点:convert 补偿以转出端
out_trade_id为锚点(复用现有find_alerts_by_trade)。
6. 规则引擎改造(FR-C11 / FR-C15)
6.1 _amount_view
def _amount_view(trades: list[dict]) -> list[dict]:
"""同 convert_group_id 组内只保留转出端(trade_type='redeem')。无 gid 原样通过。"""
run_rules 内部:eligible 供 RISK-001/003/004;amount_view(eligible) 供 RISK-002/005。RISK-006 读持仓快照(确认后已更新)。
6.2 一次转换 = 一条预警事件
def process_convert_event(out_trade, in_trade, *, core_ro=None, risk_repo=None,
thresholds=None, on_error_hook=None) -> dict:
- 确认事务提交后调用,传入两条流水
record_trade_alerts(primary=out_trade, hits, events=[event_of(out), event_of(in)])→ 一张单、payload.events 两条
7. 计算口径落地(纯函数签名)
类型定义:
Lot/FeeRule/PlanResult/ShareRule统一放service/convert/types.py,@dataclass(frozen=True)。
# service/convert/calc.py
def plan_lots(lots, requested_qty, min_hold_qty, min_hold_action) -> PlanResult: ...
def lot_amount(qty, nav) -> Decimal: # 2 位(默认,可由 share_digits 覆盖)
def lot_fee(amount, rate, rounding_mode="HALF_UP") -> Decimal: # 按产品 rounding_mode(D27)
def pick_fee_rate(rules, hold_days) -> Decimal: # 分档匹配(左闭右开)
def convert_amount(out_amount, redeem_fee) -> Decimal: ...
def diff_fee(conv_amount, out_rate, in_rate, mode="amount_diff") -> Decimal: # 口径 B 默认(D13)
def in_qty(in_amount, in_nav, share_digits=2) -> Decimal: # 按产品 share_digits(D27)
def hold_days(redeem_confirm_date, confirmed_at) -> int: ... # 持有期按自然日计(赎回确认日−申购确认日),非交易日;
# 第一参须传本次转换受理单的确认日 T+1(即赎回 leg 被确认之日),严禁传受理日 T,亦严禁传被赎批次的
# 申购 confirmed_at——否则边界少算 1 天(如真实满 7 日算成 6 日→误收 1.5%);口径详见 PRD §2.1.3;
# 单测须覆盖满 7/30/180 日边界
def rounding_diff(in_amount, in_nav, actual_in_qty, places=4) -> Decimal: ...
def ensure_batch_limit(plan, max_lots) -> None: ...
def redeem_amount(qty, nav, fee_rate, rounding_mode="HALF_UP") -> Decimal: # D26:份额赎回金额
def partial_confirm(requested, available) -> tuple[Decimal, bool]: # D28:部分确认
赎回费分档(区间左闭右开 [min_hold_days, max_hold_days))
| 档 | min | max | rate | 依据 |
|---|---|---|---|---|
| < 7 日 | 0 | 7 | 0.0150 | 22 号文 §10 下限 |
| 7–30 日 | 7 | 30 | 0.0100 | 22 号文 §10 下限 |
| 30–180 日 | 30 | 180 | 0.0050 | 22 号文 §10 下限 |
| 180–365 日 | 180 | 365 | 0.0025 | 合同约定 |
| ≥ 365 日 | 365 | NULL | 0.0000 | 合同约定 |
费率档说明:<7/7–30/30–180 三档为 22 号文 §10 下限(法定最低),真实产品可高于下限;本 seed 取下限演示(
to_fund_ratio=1.0全额计入)。180 日以上为合同约定档。
core_holding 更新口径(PRD FR-C13)
| 端 | qty | cost_amount | market_value | as_of | pnl_pct |
|---|---|---|---|---|---|
| 转出 | qty − actual_qty |
cost × (1 − actual/原qty) |
新qty × out_nav |
nav_date | (mv−cost)/cost |
| 转入 | qty + in_qty |
cost + in_amount |
新qty × in_nav |
nav_date | 同上 |
FIFO 选批 SQL(确定性 tiebreaker)
SELECT lot_id, confirmed_at, remain_qty
FROM core_share_lot
WHERE customer_id = :cid AND product_id = :pid AND remain_qty > 0
ORDER BY confirmed_at ASC, lot_id ASC
LIMIT :max_lots;
8. 接口契约落地
8.1 请求模型(api/simulate.py)
class TradeRequest(BaseModel):
customer_id: str
product_id: str | None = None # subscribe 必填
trade_type: str # subscribe | redeem | convert
amount: Decimal | None = None # subscribe 必填(金额申购)
from_product_id: str | None = None # convert 必填
to_product_id: str | None = None # convert 必填
qty: Decimal | None = None # convert 必填(份额申报);redeem 必填(份额赎回,D26)
client_request_id: str | None = None # 幂等键(选填)
@model_validator(mode="after")
subscribe = 金额申购;redeem = 份额申报(D26);convert = 份额申报。
8.2 响应
convert 受理成功返回 HTTP 202 Accepted(受理回执,不含金额/净值);确认结果 T+1 后由查询接口返回。redeem 成功返回确认结果(份额申报,金额由 qty × 净值 − 费 算出)。
8.3 错误码映射
| 异常 | HTTP | error_code |
|---|---|---|
ProductNotRedeemable |
400 | PRODUCT_NOT_REDEEMABLE |
ProductNotSubscribable |
400 | PRODUCT_NOT_SUBSCRIBABLE |
InsufficientShares |
400 | INSUFFICIENT_SHARES(含在途占用) |
BelowMinQty |
400 | BELOW_MIN_QTY |
SameProduct |
400 | SAME_PRODUCT |
CrossEntityNotSupported |
400 | CROSS_ENTITY_NOT_SUPPORTED(含跨 share_class 同基金判定 D30) |
TooManyLots |
400 | TOO_MANY_LOTS |
NoTradingDay |
400 | NO_TRADING_DAY |
CancelNotAllowed |
409 | CANCEL_NOT_ALLOWED(撤单超窗口/已确认) |
LotConflict |
409 | LOT_CONFLICT(确认段条件 UPDATE 哨兵) |
IdempotencyUnavailable |
503 | IDEMPOTENCY_UNAVAILABLE |
| 未抢到执行权 | 202 | 返回 {convert_group_id, status: "processing"} |
| 幂等命中 completed | 200 | 返回首次结果 |
异常类定义在 app/utils/exceptions.py + service/convert/errors.py。
reject_reason取值(SUITABILITY_FAILED等,确认段失败原因)见 PRD §5.6;本节error_code为 HTTP 层错误码,两层不混用。
8.4 确认触发接口(FR-C23,运维/批处理)
POST /api/simulate/trade/convert/confirm?accept_date=2026-09-09
指定业务日,串行捞 accepted/nav_pending 受理单逐单确认。手动触发(D24),模拟登记机构批处理。
8.5 撤单接口(FR-C22)
POST /api/simulate/trade/convert/{convert_group_id}/cancel
T 日 15:00 前 + 状态 accepted/nav_pending → 200 置 cancelled 释放占用;否则 409 CANCEL_NOT_ALLOWED。
9. 数据模型落点清单(DDL 与种子)
| 对象 | 落点文件 | 动作 |
|---|---|---|
core_fee_rule |
01-ddl.sql |
新建(to_fund_ratio) |
core_share_lot |
01-ddl.sql |
新建 |
core_convert_lot_detail |
01-ddl.sql |
新建(nav/nav_date;完整列集含 hold_days/fee_rate/lot_count 等,详见 PRD §4.1) |
core_convert_request |
01-ddl.sql |
新建(权威状态机,6 态 ENUM) |
core_trade_calendar |
01-ddl.sql |
新建(交易日历) |
core_cash_flow |
01-ddl.sql |
新建(D29,注册登记账户中转);列:convert_group_id / from_account / to_account / amount DECIMAL(18,4) / direction(枚举 OUT_TO_TA/TA_TO_IN/NET,落地单笔 NET 行 amount=in_amount)/ trade_date;「一次转换 1 笔资金划转」为建模假设(非法规明文);写入方=confirm_convert,读取方=报表/重建(core_ro) |
core_trade.convert_group_id + KEY |
01-ddl.sql |
改表 |
core_product |
01-ddl.sql |
加列(合计 8 列,含 share_class(D30) + allow_ac_convert TINYINT DEFAULT 0(D30 开关) + rounding_mode/share_digits(D27) + can_subscribe/can_redeem + min_hold_qty/min_redeem_qty + subscribe_fee_rate + fund_company/ta_code + fee_rate COMMENT) |
risk_convert_detail |
02-mysql-agent专用.sql |
新建(status ENUM 5 值,语义收窄为 agent 写入进度) |
core_share_rule |
11-seed-share-rule.sql |
新建(D27,key=product_id+business_type,逐产品 (share_digits, rounding_mode) 组合) |
| 赎回费分档种子 | 07-seed-fee-rule.sql |
新建 |
| 份额批次种子 | 08-seed-share-lot.sql |
新建 |
| 机构 + 申购费率种子 | 09-seed-org.sql |
新建(含 share_class 标注 D30) |
| reset 流程 | reset.ps1 |
$files 追加 07/08/09/10/11 |
D25 数据落点(适当性 T+1 复核):投资者风险等级 + 测评版本号取自既有投资者信息表(非新建表);落地前须确认该表已含 risk_level + assess_version 列,否则在 01-ddl.sql 补建并写入本清单。
种子文件版本注释约定(PRD L-6):新增种子 SQL 文件头带版本注释块。
sqlite 测试 DDL 同步(tests/_ddl.py):5 新表 + core_product 新列;test_db.py::test_core_holding_columns 启动期列名断言。
10. 测试策略
| 层 | 文件 | 覆盖点 | 预计用例 |
|---|---|---|---|
| 纯函数 | test_convert_calc.py |
FIFO / 分档费率 / 补差费双口径 / 舍入(含按产品 rounding_mode/share_digits D27)/ 部分确认 D28 / T+1 起算 | ~40 |
| 受理编排 | test_convert_accept.py |
受理段 6 步 / 可用份额校验(含在途占用)/ 幂等 / 202 回执 | ~15 |
| 确认编排 | test_convert_confirm.py |
确认段 7 步 / 批量串行 / 部分确认 D28 / nav_pending 挂起 / 适当性 T+1 复核 D25 / 资金流中转 D29 | ~25 |
| 撤单 | test_convert_cancel.py |
15:00 前撤单 / 超窗口 409 / 占用释放 | ~8 |
| 赎回份额申报 | test_redeem_qty.py |
redeem 份额申报(D26)+ T+2 校验 FR-C25 | ~10 |
| 并发 | test_convert_concurrency.py |
可用份额短临界区 / 批处理串行 / 同键 202 / 50 并发压测 | ~10 |
| 批次 | test_share_lot.py |
普通申赎批次维护 + 兜底补建 + rebuild_lots | ~12 |
| 集成 | test_convert_integration.py |
真 MySQL,端到端折算与 §7 calc.py 纯函数签名 + calc_convert_demo 实算回填吻合 | ~6 |
| 回归 | test_trade_gateway.py |
补批次维护 + redeem 份额申报断言;现有用例全绿 | +10 |
基线:现有全量 → 预计 650~700。
50 并发压测:同一 (customer, product) 上 50 并发争抢可用份额(短临界区),断言不超卖 + confirm 串行无重复流水。
11. 配置新增(app/config/settings.py)
| 项 | 默认 | 说明 |
|---|---|---|
convert_cutoff_time |
"15:00" |
受理截点(不得硬编码) |
convert_confirm_sla_days |
2 |
超 SLA 置 expired(交易日) |
convert_batch_max_lots |
200 |
超 200 触发;返回 HTTP 400 TOO_MANY_LOTS(400 为 HTTP 状态码、非阈值,阈值=200) |
convert_diff_fee_mode |
"amount_diff" |
补差费口径 B 默认(D13) |
convert_lock_ttl_seconds |
30 |
锁 TTL |
convert_nav_stale_days |
3 |
退役(不再回退取旧净值) |
mysql_core_ro_user / mysql_core_rw_user / mysql_agent_user |
"" |
D20 三账号(空回退 root) |
11.1 DB 账号与最小权限(D20)
xh_core_ro(SELECT)/ xh_core_rw(4 表写,扩展至 core_convert_request/core_cash_flow)/ xh_agent_rw(audit 仅 INSERT);get_engine(db, role)。
12. 风险与应对
| # | 风险 | 应对 |
|---|---|---|
| 1 | sqlite/MySQL DDL 列名失配 | T-0 先统一 + 启动断言 |
| 2 | 批次补建规则写两处 → 漂移 | D18 抽纯函数同调 |
| 3 | Decimal 默认 HALF_EVEN |
所有 quantize 显式传舍入(按产品 rounding_mode 覆盖) |
| 4 | 引擎异常不丢预警 | 不阻断、落 engine_error + 本地日志(D19 钩子预留) |
| 5 | 批次数超限 | 400 不自动分拆 |
| 6 | 双库不一致 | 两段 Core 单库事务 + agent 附加 + 补偿 |
| 7 | 锁 TTL 被确认段超时突破 | 锁 + 状态二次校验双保险;批处理串行 |
| 8 | redeem 改份额申报打穿现有用例 | D26:redeem 接口/网关/折算全改,现有申赎用例重跑基线 |
| 9 | 部分确认语义复杂 | D28:确认段部分成交 + 占用释放 + 不顺延,单测覆盖 |
| 10 | 按产品舍入/位数配置 | D27:core_share_rule(product_id+business_type) 表 + calc 按产品取,老产品缺规则回退主流 2 位 HALF_UP(显式标注) |
13. 拍板结论与溯源
13.1 关键拍板(2026-09-11)
| # | 事项 | 拍板 | 依据 |
|---|---|---|---|
| 1 | 模型策略 | 全量重写 T+1(T 日受理/确认分离) | 用户拍板(D23) |
| 2 | 确认触发 | 运维接口 + 脚本(不引调度器) | 用户拍板「按真实业务」(D24) |
| 3 | 适当性 T+1 复核 | 复核 | 用户拍板(D25) |
| 4 | SLA | 2 交易日 | 用户拍板「按真实业务」(D24 派生) |
| 5 | redeem 份额申报 | 本期收敛(份额赎回) | 用户拍板「按真实业务走」(D26) |
| 6 | 全差异收敛 | ①~⑧ 全部本期对齐真实 | 用户拍板「都按真实业务走」(D26~D30) |
13.2 独立评审结论
架构 v2.1 精简重写版已通过独立 AI 联网审查(0 阻断级,5 项外部事实联网核验全部与文档相符),已定稿(2026-09-11 用户批准)。第 4 步开发计划 v2.0 已按本版 §15 任务映射落地(docs/项目框架设计/开发计划-基金转换交易.md,经三轮独立审核闭环:挑毛病 / 验证型 / 白纸重审 + 终验)。
14. 设计自检十三问复核
| # | 问 | 本期答案 |
|---|---|---|
| 1 | 新增表/字段谁写? | core_convert_request(convert_request_repository)/ core_cash_flow(confirm_convert)/ core_share_lot(确认段+普通申赎)/ risk_convert_detail(agent 写入进度)/ share_class/rounding_mode/share_digits(seed 写入,代码只读) |
| 2 | 谁读? | core_ro(批次/净值/费率/持仓/日历/产品规则)/ rules._amount_view / core_tools / rebuild_alerts / cleanup_pending_convert |
| 3 | 枚举/常量 DDL? | core_convert_request.status ENUM 6 值(建表即全量);risk_convert_detail.status ENUM 5 值(agent 进度);audit_log.decision VARCHAR |
| 4 | 事务跨库? | 两段 Core 单库事务 + agent 附加(补偿) |
| 5 | 种子数据? | 06~11 种子(含真实产品舍入规则 D27、share_class D30) |
| 6 | 并发安全? | 可用份额短临界区(run_locked)+ 确认条件 UPDATE 哨兵 + 批处理串行锁 + 幂等锚点同库 |
| 7 | 汇总语义唯一? | _amount_view 取转出端;out/convert/in_amount 语义分立 |
| 8 | 向后兼容? | core_trade 加可空列;core_product 新列有 DEFAULT;get_engine(role) 默认回退 |
| 9 | 示例自证? | §7 纯函数签名(calc_convert_demo 实算回填,禁手算) |
| 10 | 外部事实核验? | §0.1/§0.2/§0.5 列 22 号文 + 12 个来源条目;份额赎回/巨额赎回/资金流/舍入差异均有出处 |
| 11 | 重置类伴随数据? | cleanup_pending_convert 扫描 core_convert_request;reset.ps1 显式清单 |
| 12 | 类型与口径匹配? | 申购费率按 product_type 取档(PRD §4.3);share_class 同基金判定(D30) |
| 13 | 同一规则/公式单副本? | 折算/费率/舍入抽 calc.py 纯函数;lot_bootstrap 单点;格式出口 _q 单点 |
二补 · 格式契约三问
| # | 问 | 答案 |
|---|---|---|
| 补-1 | 展示位数/精度写在哪? | §1 设计原则#10/#11 + §7 calc.py + convert_service._q():金额/份额 2 位(按产品 share_digits 覆盖 D27)、净值/费率/尾差 4 位 |
| 补-2 | 同一逻辑数据产出路径几条? | 首次公式算 / 库 DECIMAL(18,4) 重建 → 均经 _q() 单点,逐字节一致 |
| 补-3 | 格式出口单点? | 响应/审计/日志共用 _q();禁止裸 str(Decimal) 出网 |
新持久对象谁清理:core_convert_request 生命周期 = accepted → confirmed/rejected/cancelled/nav_pending → expired;非终态超 SLA 由 cleanup_pending_convert.py 置 expired 释放占用(功能正确性要求,非可选运维)。
15. 与第 4 步的衔接(任务映射)
| 任务 | 内容 | 依赖 | 说明 |
|---|---|---|---|
| T-0 ✅ | 列名统一 + 启动断言 | 无 | 已完成 |
| T-0b ✅ | DB 账号分离(D20) | 无 | 已完成 |
| T-1 | DDL + 种子(含 core_convert_request / core_trade_calendar / core_cash_flow / share_class / rounding_mode / share_digits / 11-seed-share-rule) | T-0+T-0b | 新增 5 表 + 8 列 + 2 种子 |
| T-2 | service/convert/ 纯函数(含 D27 按产品舍入/位数、D28 partial_confirm、D26 redeem_amount) | 无 | 扩展 calc.py |
| T-2b | calc_convert_demo 实算回填 | T-2 | 示例随真实净值重算 |
| T-3 | core_ro 新方法 + share_lot_repository + convert_request_repository | T-1 | 新增受理单仓储 |
| T-4 | convert_repository(agent 进度) | T-1 | 语义收窄 |
| T-5 | run_locked 复用 | 无 | 统一锁原语 |
| T-6 | accept_convert(受理事务) | T-1/T-3 | 受理单 INSERT |
| T-7 | convert_service 编排(受理/确认/撤单三段)+ confirm_service 批处理 | T-2~T-6 | 核心编排 |
| T-8 | rules._amount_view + engine.process_convert_event(确认后跑) | 无 | 引擎时机改 |
| T-9 | api/simulate.py 模型(redeem 份额申报 D26)+ 撤单/确认接口 + trade_gateway 分派 | T-7 | redeem 改份额申报 |
| T-10 | 普通申赎批次维护 + T+2 校验(FR-C25)+ rebuild_lots | T-3 | 扩展 |
| T-11 | core_tools 汇总去重 + 持仓过滤 | T-8 | 沿用 |
| T-12 | 补偿脚本 + cleanup_pending_convert(扫 core_convert_request) | T-4/T-7 | 改写扫描对象 |
| T-13 | 全量回归 + 集成 + 50 并发 + 性能补录 | 全部 | 基线重估 |
| T-14 | D27 按产品 core_share_rule(product_id+business_type) 舍入/位数 seed + calc 接入 |
T-1/T-2 | 新增 |
| T-15 | D28 投资者层部分确认 | T-7 | 新增 |
| T-16 | D29 资金流中转 core_cash_flow 建模 | T-6 | 新增 |
| T-17 | D30 share_class + A/C 互转 | T-1/T-9 | 新增 |
收尾说明:本版落实 PRD v1.1「下游同步令」+ 用户四项拍板(全量重写 / 确认触发 / T+1 适当性复核 / SLA)+「全差异按真实业务走」硬指令(①~⑧ 收敛)。已落地且复用的资产(D20 / T-0 / T-0b / 纯函数包)继续生效。第 4 步开发计划 v2.0 已按本表 §15 落地(
docs/项目框架设计/开发计划-基金转换交易.md,三轮独立审核闭环);本架构 v2.1 经独立 AI 联网审查通过,已定稿(2026-09-11 用户批准)。