Files
group_xinghuo_jinrong/docs/memory/2026-09-12.md
T
GaoYiYuan_0626 cde0c226fe 基金转换 T+1 模型:T-15/T-16/T-17 收官(842 绿 / 真库 9/9 / 转换线第 5 步完成)
T-15 D28 部分成交专项:
- sqlite +1 终态单幂等 skipped 口径裁定(ConfirmConflict 仅限读后状态才变窗口)
- 真库 confirm +3 组 19 项(E2 占用释放双向 / G2 actual_qty=0 → rejected / H2 撤单后确认)→ 102/102
- CONVERT_STRESS Barrier(2) 窄窗争批:恰 1 confirmed + 1 rejected
- 突变实证防御纵深:④可用量复核 / ⑤plan_lots / 事务内哨兵三道防线拆任一道仍收敛 rejected

T-16 D29 资金流中转:
- _insert_cash_flows 全仓唯一写入点(out/redeem 费前 + in/subscribe 净转入,remark=convert:{gid} 同事务)
- test_confirm_writes_no_cash_flow 反转为恰 2 条 + 金额对账(T-7 登记闭环)
- 真库 apply A2 组 + B 组 0 残留 → 30/30;confirm 反转断言 → 103/103
- FK 坑(core_cash_flow.fk_cf_customer 挡客户删除):5 处清理段补删

T-17 D30 share_class + A/C 互转:
- _validate_products 纯新增拦截分支:两端 share_class 非空且不同才查 allow_ac_convert(任一端=1 两向放行)
- 口径裁定 2 条(R-12 字面偏差,待用户追认):NULL 不参与 A/C 判定 / 同类走一般转换规则
- 联网核实(用户铁律):A/C 互转需管理人开通(摩根 2025-11 公告)= 开关真实载体;持有期重新起算与本仓一致
- 种子 ⑥ 段(双开对/双关对照/同类对照);sqlite +4 → 842 passed / 8 skipped;真库 accept H 组 → 43/43
- 突变:分支失效 → closed 红,还原绿

文档:开发计划 T-15~T-17 DoD 全勾 + 执行记录 · 基线 842 · TODO/MEMORY/AGENTS/交接文档 v4.5 终态(第 5 步收官 → 下一步第 6 步集成测试)
2026-09-12 13:42:49 +08:00

35 KiB
Raw Blame History

2026-09-12 工作日志

本日:基金转换线(T+1 受理/确认分离模型)第 5 步 T-11、T-12、T-13 完成 —— T-11 core_tools 汇总去重 + 持仓过滤(确认段场景补用例,生产代码零改动);T-12 补偿 + SLA 清理切 T+1 链路 + confirm_one 引擎异常留痕修复(真库脚本暴露)。用户新增铁律:不确定的外部真实业务事实必须联网核实,禁止凭训练记忆假设。


一、接手与状态核对

读入:项目根 交接文档.md(v4.0,§0 + §B.11)→ 开发计划 v2.0 §1.4 / §1.5 / §5 / T-11 节 → 现状代码(core_tools.py / rules.py / core_ro.py)→ 既有测试(test_core_tools.py / test_concentration_c4.py / test_convert_confirm.py)。

基线复核:全量 pytest -q(系统 Python 3.13.14)沿用户上次实测 844 passed / 10 skipped(当日收尾实测 846,见下)。

二、T-11 · core_tools 汇总去重 + 持仓过滤 ✅

任务定位(与 v0.x T-11 的区别)

开发计划 v2.0 T-11 的改法两条都是「已实现 → 确认不回归 → 补用例」:

  • 口径三处同调(F-10)与 list_holdings 的 SQL 层 qty > 0 过滤均已就位 (v0.x T-11 产物:_amount_view 提升公开 + SQL 去重 R-d + 归零行过滤);
  • 本轮实质增量 = 把既有「直接插 SQL 模拟流水」的口径用例升级为 T+1 真实链路场景 (回归面 R-回归 8「保留 + 补用例」/ R-回归 13「保留断言 + 补确认段场景」)—— T+1 模型下流水与持仓只在确认段产生(受理段零写入),直接插 SQL 的既有用例覆盖不了这一点。

落地(仅 tests/test_core_tools.py +2,生产代码零改动)

新增 t1_env fixture(seed_suitability_matrix + 最小受理/确认种子:C3 客户、转出 R2 债基 1 批 30000 份 + 持仓 30000、转入 R4 股基、两端 T 日净值 1.3604/1.9194、5 档费率、60 天日历)

  • _quiet_th()(全阈值推高让引擎静默,不依赖 settings 与 conftest autouse 隔离):
  1. test_confirm_then_sum_amount_counts_once(验收 11 · 确认段场景):受理全转 30000 → confirm_one(T+1=2026-09-07)落两条同 gid 流水 → 同窗口插一笔无组普通赎回 100000 → query_recent_trades:sum_amount = 40812.00 + 100000 = 140812.00(转入端不计)、 total_count = 3(明细全量)、sum_trades_on_date(T+1) 跨口径一致。
  2. test_confirm_full_transfer_holding_zero_row_excluded(F-12 / R-回归 13 · 确认段场景): 先直查 SQL 证明转出端 core_holding.qty=0 行仍在库(台账留痕,D 决策), 再断言 list_holdings / query_holdings 只剩转入端、qty 与 res["in_qty"] 逐字节一致。

既有 3 条口径级用例与 test_concentration_profile_filters_zero_qty(集中度读取方)保留不动。

执行期裁定 2 条(留痕)

  1. 转出端流水 amount = 费前转出金额(40812.00 = 30000×1.3604,与 PRD §5.3.2 示例 68020.00 同语义;赎回费 204.06 单列 redeem_fee 字段)—— 初版锚点误按「费后 40607.94」 断言,真跑第一轮即抓出(assert '40812.00' == '40607.94'),已在用例注释钉死口径。
  2. t1_env 必须调 _ddl.seed_suitability_matrix:C×R 矩阵是 check_suitability 的 L0 权威,裸 create_sqlite_engine() 缺矩阵 → 受理段全量 forbidden(SUIT_RISK_MISMATCH), 症状是 confirm_one 返回 missing(受理单根本没落)。与 conftest sqlite_engine fixture 同源(自检第 13 问:同一种子不得有两份实现)。

验证

  • pytest tests/test_core_tools.py 5/5 → 全量 846 passed / 10 skipped(基线 844 + 2,零回归);
  • 突变 2 组防假绿(均精准命中后还原,git status 仅测试文件改动、无 MUTATION 残留): ① 去掉 core_ro.list_holdings 的 AND h.qty > 0 → 精准 3 红 (新确认段用例 + 既有 test_query_holdings_excludes_zero_qty + test_concentration_profile_filters_zero_qty 的跨口径一致性断言); ② core_tools.query_recent_trades 绕开 amount_view 直加全量 → 精准 3 红 (新确认段用例 + 既有不翻倍用例 + 跨口径一致性断言);
  • 真库 verify_convert_tools.py 复跑 14/14、退出码 0、零残留(v0.x 口径真库证据零回归)。

三、文档回写

开发计划 v2.0 T-11 节(DoD 全勾 + 执行记录)· 交接文档.md(v4.0 → v4.1:头部版本块 / §0 导航表 / §B.11 状态块 + T-11 完成条目)· docs/memory/TODO.md(进度行 + T-11 勾选)· docs/memory/MEMORY.md(3 处进度)· AGENTS.md(进度行)· 本日志。

四、下一步

T-12 补偿脚本改写(R-14):cleanup_pending_convert.py 扫描对象迁 core_convert_request (accepted/nav_pending 超 2 交易日 → expired)+ rebuild_alerts.py 凭 Core 侧数据补写。 入口:开发计划 T-12 节。


T-12 执行记录(补偿 + SLA 清理切 T+1 链路 · 2026-09-12)

一、改动面(三处)

  1. scripts/agent/cleanup_pending_convert.py 整体重写(R-14):扫描对象迁 Core—— ConvertRequestRepository.list_inflight_before 扫 accepted/nav_pending 且 requested_at 严格早于 cutoff → 逐单 transition_status 条件 UPDATE 置 expired (S2 不硬删;并发双跑天然幂等,不加锁);SLA 边界 _slab_cutoff = 当前业务日 (今天开市取今天否则 previous_biz_day 回退)上推 convert_confirm_sla_days 个 交易日 00:00,与 confirm_batch._window_start 同口径,日历数据缺失降级自然日并 告警(宁可少清、不可误清);--days / --dry-run 参数、退出码 0/1、 不动 agent 镜像(T-4 契约:镜像不推 cancelled/expired)。
  2. compensate_convert(convert_service)从 __all__ 的 v1.0 Deprecated 组救回在役: 详情侧数据源 = Core 三件套前置校验(流水 ≥2 条 → 受理单 status=confirmed → 计费明细非空,任一不满足 state=missing 零写入不硬补);补写与确认段第⑧步 同口径(sync_mirror(completed) 聚合口径 nav=T 日净值 / nav_date=受理日 / fee=Σ逐批 / hold_days=min/max)+ decision='confirmed' 审计 phase='confirm-compensate'
    • engine_error 如实标记;预警侧幂等锚点不变(转出端 out_trade_id + has_engine_error_audit 中间态「人工核对」提示)。convert_fund / rebuild_convert_response 仍留 Deprecated(T-13 退役)。
  3. convert_request_repository +list_inflight_before(T-12 专用捞单,复用 R-2 条件 UPDATE 守卫,不复制 SQL——自检第 13 问)。

勘误:开发计划 T-12 节原写 scripts/agent/rebuild_alerts.py,实际在 scripts/demo/ (零改动——薄封装委托 compensate_convert,签名未变)。

二、联网核实(用户铁律 2026-09-12)

用户原话:「你不确定的都要联网查找,这个项目要贴近真实项目,这是铁律,比如你刚才说的过年假期问题。」

  1. 「基金转换申请失效」真实口径(多家基金公司《开放式基金业务规则》: 华安 / 富国 / 嘉实 / 苏新 / 太平 / 德邦 / 民生加银):转出不可赎回或转入不可申购 → 申请无效;账户/份额冻结 → 无效;TA T+1 确认、T+2 可查;无效申请资金退回原账户; 太平基金:转换转出参照巨额赎回规则。映射本项目:rejected(T+1 复核不通过)= 真实「无效申请」,expired 定位为运维兜底(释放占用 = 份额退回效果)——设计贴近真实 ✓。
  2. A 股连续休市历史最长 = 2020 年春节 10 个自然日(国务院办公厅延长春节假期; 上交所《关于2020年春节延长休市相关业务衔接安排的通知》:休市延至 2 月 2 日、 2 月 3 日恢复开市)——trading_calendar._MAX_SCAN_DAYS=256 的量级依据成立; 结论留痕进 verify 脚本头注释。WebSearch 三次超时后改换关键词命中; verify 造数的受理日 / SLA 边界全部改 previous_biz_day 链式走真库交易日历 (对任意假期安排鲁棒,不猜自然日)。

三、⚠️ 真库脚本暴露并修复:confirm_one 引擎异常不留痕(本轮最重要产出)

问题:confirm_one 第⑦步引擎异常只 logger.exception 不落审计—— engine_result=None → confirmed 审计 / return dict 的 engine_error=False。 全仓唯一不落痕路径(v1.0 阶段 1.5 落 decision='engine_error' 审计 convert_service.py:1013、trade_gateway 落 risk_engine_error),且补偿侧 has_engine_error_audit 的「人工核对」保护对 T+1 主链路永远失效 (单测 _prepare_failed_confirm_t1 是手插审计造数,看不见这个洞)。

修复(confirm_service.py 第⑦步 except):补写 decision='engine_error' 审计 (summary 带 phase/accept_date/confirm_date/两流水号)+ engine_error 标志贯穿 confirmed 审计与 return dict;D17「不阻断」语义保留(测试注释钉死 「不阻断但留痕 + 标记」);test_convert_confirm.py::test_engine_exception_... 断言翻转 is False → is True + 补审计计数断言(audit_log 无 convert_group_id 列, 按 input_summary LIKE %gid% 查)。

四、测试与真库验证

  • tests/test_convert_service.py:补偿用例 4 条改 T+1 断言 + 新增 2 (missing_when_request_not_confirmed / inserts_mirror_when_row_absent—— 造数注意:insert_audit_log 需补齐 risk_score/handler_* 四个 None 键、无 created_at);
  • tests/test_demo_scripts.py:cleanup 段重写(_seed_calendar_allopen 每天开市走 真实路径不测降级 + _t12_cleanup 新签名)+ 新增 test_cleanup_window_matches_confirm_batch_window(行为化等价:窗口下界日 confirm_batch 能捞到且 cleanup 不清;前一交易日两边都排除); 方向教训:days 越大 → cutoff 越早 → 候选越少(首版断言写反被自查抓出);
  • tests/test_convert_concurrency.py:并发补偿改 T+1 造数(core 库插 confirmed 受理单 + agent 库 UPDATE 镜像 failed——两库引擎别用混,pymysql 1146)+ 真库清场补 core_convert_request 删除(按 customer 与按 product 两路、先于 product 删——FK 1451);
  • 全量 849 passed / 10 skipped(846 + 3,零回归);
  • 真库 scripts/dev/verify_convert_compensate.py 整体重写为 T+1 链路: 受理(真实 resolve_accept_date 定 T、next_biz_day 定 T1,脚本对任意运行日稳) → confirm_one(patch sync_mirror + engine_hook=boom,第⑦⑧步双失败真实落痕) → Core 三件套对表 → 补偿 rebuilt / 复跑 skipped + 人工核对 / 真 JSON 列 LIKE / cleanup(ENUM 值域 / dry-run / 窗口同口径对表 _window_start / 幂等 / S2 / 镜像不推 / NEW 恒不进候选)→ 残留清零,56/56 退出码 0、残留 0(含幂等重跑); 真库断言口径修正 1 条:decision='confirmed' 审计真链路下恰 2 条 (确认段 phase='confirm' + 补偿 phase='confirm-compensate' 各 1、各司其职)—— v1.0「主审计 1 条」是单测造数(未跑 confirm_one)的盲区,真库端到端才验得出。

五、突变验证 4 组(全部被抓后还原)

# 突变 结果
1 cleanup _slab_cutoff 回推方向反转(previous→next_biz_day) 单测 5 红(含窗口等价用例)
2 compensate 前置 confirmed 守卫删除(只留 request_row is None) 单测 精准 1 红
3 cleanup 目标状态 expired→cancelled 真库 2 红(ENUM/状态断言)
4 补偿镜像 nav_stale=True 真库 1 红(nav_stale 断言)

六、文档回写

开发计划 v2.0 T-12 节(DoD 4/4 全勾 + 执行记录)· 交接文档.md(v4.1 → v4.2)· docs/memory/TODO.md(进度行 + T-12 勾选)· docs/memory/MEMORY.md(3 处进度)· AGENTS.md(进度行)· 本日志。

七、下一步

T-13(全量回归 + 集成 + 并发 + 性能补录 + convert_fund 退役,用户 2026-09-11 裁定并入):全量 pytest 基线回填本文件头 · 7+2 真库 verify 套件退出码 0 · CONVERT_STRESS 并发 · 性能实测(端到端 < 2s 硬门禁)· 紧池成功率重测 · convert_fund / rebuild_convert_response 删除 + v1.0 依赖链 (约 25 处测试 + verify_convert_service.py)改写。


T-13:全量门禁 + convert_fund 退役(2026-09-12 下午)

一、退役落地(convert_service.py 净删 468 行)

  • 删除 convert_fund(v1.0 两阶段实时八步编排)+ v1.0 专属依赖: _plan_and_quote / _pick_rate(死代码)/ _finalize_from_core / _write_main_audit
    • 17 个孤儿 import;模块 docstring 重写为 T+1 编排说明; __all__ 收敛为 6 个在役符号(accept_convert / cancel_convert / t1_t2_dates / compensate_convert / rebuild_convert_response / PROCESSING)。
  • 按实际修订:开发计划原「rebuild_convert_response T-13 退役」不成立 —— 它是 T-9 查询接口 confirmed 分支的在役读路径,保留在役 (PROCESSING 仅作 simulate 网关 202 判断常量,同样保留)。
  • verify_convert_service.py(v1.0 链路真库脚本)git rm,git 历史留档。

二、测试面改写(约 25 处映射落地)

  • test_convert_service.py 全量重写(805 → 约 440 行):v1.0 十组用例退役, 替代断言分布 = T-6/T-7/T-8 既有用例 + 补偿组 6 用例保留 + 新增 2 (confirm_mirror_write_failure_keeps_core_confirmed 镜像失败不回滚 Core / confirm_rejects_too_many_lots 批次上限 rejected 零流水 remain==200)。
  • 显式依赖注入踩坑(留痕):confirm_one 的 core_writer / request_repo 缺省构造会连真 MySQL —— 症状一:写侧 FK 1452(事务整体回滚,真库零污染); 症状二:rrepo.reject 条件 UPDATE 在真库 rowcount=0 → rejected 被误判 skipped。 测试 helper _services 必须把五个仓储全部钉在同一 sqlite 引擎。
  • test_convert_concurrency.py 全量重写为 T+1 并发面(v1.0 九用例 → 六用例):
    • 50 并发扣减争抢 → 并发 confirm_one 不超卖(受理段允许 R-3 TOCTOU 超授、 确认段 FIFO 哨兵兜底,硬不变量钉在确认后);
    • 紧池 80% → 紧池 100%(受理全 accepted + confirm_batch 串行);
    • 同键并发单次转换 → 同键并发受理单唯一(uk_idem + 客户级锁; 3b/3c 占位竞态确定性交错用例随 v1.0 占位语义退役,无 T+1 对应物);
    • 死锁重试 → 多客户并发 confirm_one(本轮实测内部重试吸收,外部退避 0 次);
    • 性能探针 → 受理/确认/端到端三段计量。
  • monkeypatch 边界(留痕):confirm_one 的 trade_id 走 format.new_id (与 convert_service._new_id 是两个符号),patch 罩不到 → _confirm / confirm_batch 显式传 id_factory=_new_id。
  • 并发语义裁定(S2):LotConflict(退避耗尽)= 并发 confirm_one(超纲场景, 生产由批处理锁保证串行 FR-C23)下的零扣减让路终态,受理单保持 accepted 占用保持、下轮批处理可再确认 —— 与 v1.0 用例 1 允许 LotConflict 同口径, 资金安全不变量独立成立。

三、门禁结果(六条 DoD 全勾)

DoD 结果
全量 pytest 830 passed / 7 skipped 退出码 0(849 → 830 = v1.0 用例组退役净减;skip 10 → 7 = 并发 stress 标记 7 → 4)
真库 verify 套件 9/9 退出码 0:accept 37 / confirm 83 / engine 39 / api 89 / compensate 56 / apply 24 / lots 20 / seed 全 PASS / tools 14
CONVERT_STRESS=1 6/6 退出码 0
性能(< 2s 硬门禁) 端到端 P50 79.2 / P95 110.3 / max 114.7 / mean 83.0 ms,余量约 17 倍(回填 PRD §9 第 18 条)
紧池(验收 31) 受理 50/50 + 串行确认 50/50 = 100%、扣减恰=池守恒(v1.0 同场景 80%;回填 PRD §9 第 31 条)
convert_fund 残留 grep 仅 4 处注释性历史说明、零调用;redeem 传 amount= 过期调用全仓零残留

四、计划外产出:全量复跑抓出过期脚本 1 处

verify_convert_lots.py(T-10 产物)B/C/D 组仍按 v1.0「金额赎回」传 amount= —— T-9 已按 D26/R-6 改份额申报(qty=),_maintain_lots redeem 分支 qty is None 时静默跳过(尽力而为语义)→ B/C FIFO 扣减失效、D 组兜底补建红 (A 组申购不受影响,故首轮仅此脚本红)。修正为 qty= 后 20/20。 「金额申购、份额赎回」行业铁律(2026-09-10 联网查证)的又一处落地。

五、性能实测口径(载体 test_performance_probe_accept_confirm_end_to_end)

n=59(n=60 剔除首个连接池冷启动样本)· 本机 MySQL 8.0.46 / 模拟库 / 单线程顺序 / 每笔 1 份 / 空引擎 hook:受理段 P50 37.9 / P95 53.8 / max 57.9; 确认事务(apply_convert)P50 10.0 / P95 25.3 / max 25.5; 确认全程 P50 41.4 / P95 61.3 / max 74.4;端到端 = 受理 + 确认两段之和。

五b、用户指出硬编码问题 → 全仓排查 + 补修(当日傍晚)

用户批评:「硬编码是个很低级的错误了,怎么你还会犯呢?再查一下还有没有。」—— 指 verify_convert_lots.py 的字面造数日期(NAV_DATE=date(2026,9,4) 等)。T-12 已立 「verify 造数的受理日/SLA 边界全部改 previous_biz_day 链式走真库交易日历、不猜自然日」 的模式,本轮修 lots 脚本时只修了 amount=→qty=,没有把同类脚本的日期硬编码一并清掉 —— 同一类错误在脚本层复发,教训:修 A 类问题时必须同 grep 一遍全仓的 A 类模式。

补修:verify_convert_lots.py + verify_convert_apply.py 共 9 处字面日期全部改 previous_biz_day 链式回推动态锚定(_init_dates(admin) 在 main 开头按真库日历算: TRADE_DATE=回推 3 个交易日、C1/C2=再回推 3/2 交易日保 T+2 可扣、A1/A2/持仓 as_of 按 自然日相对量保持 hold_days 档位断言不变);复跑 20/20 + 24/24 退出码 0, 动态生效自证 = confirmed_at 随回推日漂移(09-04 → 09-09)而断言同源跟随。

全仓硬编码排查结论(分类,非一刀切):

类别 结论
生产代码 app/ 零字面日期;cutoff/SLA/死锁重试/批上限全走 settings(B1/R-14 整改完整)
T-6~T-12 五个真库脚本(accept/confirm/engine/api/compensate) 零字面日期(各自任务期已按 T-12 模式合规)
verify_convert_lots.py / verify_convert_apply.py 违例 → 本轮已修(9 处)
verify_convert_seed.py 的 AS_OF 有意固定:与 08-seed-share-lot.sql 种子里的日期强绑定(验证对象就是那批固定数据),改动态反而错
calc_convert_demo.py 的 TRADE_DATE=2026-09-09 有意固定:PRD 验收 20「示例与种子强绑定」(demo/集成测试/种子三方逐字节一致),改动态违反验收
sqlite 单测固定日期(test_core_tools / test_trade_gateway 等) 确定性测试惯例:自插日历/净值配套数据、不连真库、自洽无漂移,保留

六、文档回写

开发计划 v2.0(文件头基线 830/7 + T-13 节 DoD 6/6 全勾 + 执行记录 ①⑧)· PRD v1.1(§9 第 18 条性能实测 + 第 31 条紧池 100% 回填)· 交接文档.md(v4.2 → v4.3, §B.11 状态段 + T-13 完成段 + 基线行)· docs/memory/TODO.md(进度行 + T-13 勾选, T-14T-17 待办拆出)· docs/memory/MEMORY.md(3 处进度)· AGENTS.md(进度行)· 本日志。

七、下一步

T-14(D27 产品舍入真正接入):calc.py product_round 接入 + core_share_rule 种子 + 4 位产品用例(覆盖率不得让降级路径当覆盖)。 其后 T-15~T-17。提交:代码 3bc84a0 + 文档(本次),均不 push(由用户定)。


T-14:D27 产品舍入真正接入 + 联网核实纠偏(2026-09-12)

一、接入面(生产代码 4 处,全部单点收敛)

文件 改动
app/repository/core_ro.py 新增 get_share_digits(product_id, business_type) —— 全仓唯一回退链单点(自检第 13 问):core_share_rule 规则行优先 → core_product.share_digits(T-1 列,DEFAULT 2)→ calc.PLACES(2)。import PLACES 有 convert_core_repository 先例(repository → convert.calc),无环
app/service/convert/calc.py in_qty 加可选参数 digits: int = PLACES(默认行为逐字节不变),内部改走 product_round(nav<=0 校验保留单点);product_round docstring 更新为「已接入主链路」
app/service/convert/confirm_service.py 确认段第⑤步:digits = core.get_share_digits(to_pid, _RULE_BT_CONVERT) → in_qty(in_amount, in_nav, digits);新增常量 _RULE_BT_CONVERT = "convert"(转入份额属「转换」业务,不取 subscribe —— 两型规则行可各配位数)
app/gateway/trade_gateway.py 申购折算:digits = core.get_share_digits(product_id, SUBSCRIBE) → sub_qty = product_round(amount / nav, digits)(redeem 为份额申报 D26,无折算无消费方)

边界裁定(开发计划 DoD 即边界):T-14 只接落库侧位数;响应展示维持既有 q(x, D2) 收敛规格(库内 DECIMAL(18,4) 本就高于展示位,展示规格是独立维度, 2 位产品路径零漂移)。PRD §2.5.2「份额展示 2 位(可被产品配置覆盖)」的后半句 (展示位数按产品覆盖)未落实,登记开口待用户裁定。

二、联网核实(用户铁律 2026-09-12 · 本轮最重要产出)

用户指示:「不确定的不要盲目相信 prd 和交接文档,而是联网查询确认,要和真实项目一致」—— T-14 的落地依据全部是外部事实断言,逐条联网核实:

# 断言(PRD/种子 v1.0 原口径) 核实结论 依据
1 主流份额 2 位四舍五入 ✅ 成立 民生加银转换规则第八条 / 富时基金业务规则 / 新华基金 / 泰康安泰(申购份额 2 位第 3 位四舍五入)
2 位数按产品配置(D27 机制) ✅ 成立且比 PRD 更硬 南方基金《关于调整旗下基金转换业务规则的公告》(2012):把统一的「四舍五入保留两位」改为「根据各基金基金合同和招募说明书的相关规定保留小数点后 2 位」——产品级配置的直接行业出处
3 舍入方式存在「舍去(TRUNCATE)」型 ✅ 成立(rounding_mode 字段有现实载体) 南方利众 C / 南方宝元转换公告(两位后舍去归基金资产)、邮储银行转换 FAQ、华宝未来混合、申万菱信业务规则、东方基金登记业务规则
4 南方截断 ✅ 成立 同上(南方自家公告即「舍去」型)
5 货基 1 元面值、份额 2 位 ✅ 成立 中银货币 / 融通易支付 / 鑫元货币基金合同
6 「指数基金申购/转入确认份额允许 4 位小数」(v1.0 种子给 3 产品配 4 位的唯一注释依据) ❌ 证伪:反证充分——易方达沪深300非银 ETF 基金合同、汇添富深证300 ETF 联接、华夏恒生 ETF 联接、科创板 ETF 联接全部 2 位;易方达创业板新能源 ETF 联接转换公告直接写「转入本基金的份额计算结果保留到小数点后两位」 v1.0 注释声称「见 PRD 已知差异」,PRD §2.5.3 实际无此依据——注释与 PRD 互相「引用」但源头不存在
7 「易方达 ETF 场外取整数位」 ⚠️ 未找到证据,且有反证(易方达自家 ETF/联接 2 位);真实「整数位」载体是场内渠道(招商瑞智优选 LOF 合同:场内申购先 2 位再截位到整数份),属渠道维度非产品维度 PRD §2.5.3 该句待用户裁定修订

种子纠偏(11-seed-share-rule.sql → v1.1):PROD-510300 / PROD-510500 / PROD-000002 共 9 行 4 → 2(42 行数不变,verify_convert_seed.py DoD ⑩ 只断言行数, 不炸);头注重写为上表 1~5 的核实结论 + 出处;刻意不配 truncate 行—— T-14 只接 digits,配了不消费即是半成品(TRUNCATE 接入为登记开口)。

三、测试(+7 → 全量 837 passed / 7 skipped,零回归)

文件 用例 覆盖
test_convert_calc.py test_in_qty_digits_four_places / test_in_qty_default_digits_matches_round2 纯函数层:4 位量化 / 默认 digits 与 round2 逐字节一致(DoD 3)
test_convert_confirm.py test_confirm_in_qty_four_places_via_share_rule(+subscribe 型 2 位干扰行证 business_type 区分度) 回退链第一层(DoD 1,非降级):in 流水 qty + 转入批次均 4 位
test_confirm_in_qty_falls_back_to_product_share_digits 回退链第二层:无规则行 + core_product.share_digits=4 → 4 位(用 4 位而非 2 位才能证明第二层真被消费,排除「恒 2」假实现)
test_confirm_in_qty_missing_rule_falls_back_default_two_places 回退链第三层:全缺(share_digits 置 NULL)→ 默认 2 位 == PRD 示例逐字节(DoD 2 显式降级 + DoD 3 链路锚点)
test_share_lot.py test_subscribe_qty_four_places_via_share_rule / test_subscribe_qty_falls_back_to_product_share_digits 申购侧对称两条(+convert 型干扰行)

⚠️ 4 位种子行的定位 = D27 机制验证载体(非真实业务常态),已在三处用例 docstring 与 product_round docstring 钉明,防止后人误当真实口径。回退链三层各有用例, 降级路径非唯一覆盖(DoD 2 的 PRD 示例用例同样走缺规则路径)。

四、突变验证 2 组(精准命中后还原,MUTATION 零残留)

# 突变 结果
1 confirm_service digits 接入改固定 digits = 2 2 红(4 位规则行 + product 回退两条;missing_rule 默认 2 位用例仍绿属预期)
2 trade_gateway digits 接入改固定 digits = 2 2 红(申购 4 位两条)

五、真库门禁(9/9 退出码 0)

verify_convert_seed.py 重灌后全 PASS(⑩ 42 行落库生效,全 2 位)+ accept 37 / confirm 83 / engine 39 / api 89 / compensate 56 / apply 24 / lots 20 / tools 14 —— 全部零回归(2 位产品路径与接入前逐字节一致)。

六、登记开口(2 项,待用户裁定,未顺手做)

  1. PRD §2.5.3 修订:「易方达 ETF 场外取整数位」未找到证据且有反证(易方达自家 ETF/联接 2 位);真实整数位载体是场内渠道(LOF/ETF 场内先 2 位再截位整数)。 PRD 已定稿,修订须用户拍板。
  2. rounding_mode='truncate' 消费接入:真实业务存在「舍去」型(南方/华宝/ 申万菱信等,核实 #3),D27 字段已建但 calc 未消费(product_round 固定 HALF_UP)。 属 T-14 计划范围外(开发计划改法只接 digits),接入方案 = product_round 加 mode 参数 + 回退链透传 + 种子配一对 truncate 产品,待用户拍板是否补做及归哪个任务。

七、下一步

T-15(D28 部分成交专项测试 + 边界补强):用例矩阵(占用被抢 / 部分确认后 释放 / 撤单与确认竞态 / actual_qty=0 → rejected R-10 补强)+ CONVERT_STRESS 两单争同一批。入口:开发计划 T-15 节。


T-15:D28 部分成交专项测试 + 边界补强(2026-09-12)

一、时序口径裁定(sqlite +1 → 838 passed / 8 skipped)

test_confirm_after_cancel_conflicts:撤单后确认 不抛 ConfirmConflict,返回 skipped + current_status='cancelled' —— confirm_one 对终态单(含 cancelled)是幂等 skip(批处理单笔容错语义),ConfirmConflict 仅在「读单 之后状态才变」的并发窗口抛(_confirm_request 条件 UPDATE 哨兵 rowcount≠1)。 断言:零流水零明细 + 受理占用释放。首版误断言抛异常(DID NOT RAISE)即该 口径的实证。

二、真库 +3 组 19 项(verify_convert_confirm 83 → 102/102 退出码 0)

组 场景 关键断言
E2 占用释放双向证明 确认前灌 15000 受理 → 再受理被 InsufficientShares 拦(占用生效)→ 确认吃掉物理份额 → 灌回 15000 再受理 accepted(占用记账不重复扣)→ E2b 置 expired 释放占用收尾
G2 actual_qty=0 边界 占用全被抢的单确认后 rejected(R-10 补强),零流水零明细零持仓变动
H2 撤单后确认 skipped + current_status='cancelled',零残留

R-10 语义实测钉死:部分确认把可用物理份额全部吃掉(余量归零), 「释放」的是受理占用而非物理份额——E2 组设计时曾误设余量即此坑。

三、CONVERT_STRESS 争批 + 突变验证(防御纵深实证)

  • test_two_confirms_race_same_lot_first_wins:池 2 份 + 两单各 1 份, Barrier(2) 经 monkeypatch 钉在 apply_convert 事务门口,构造「两单都完成 ④ 可用量复核、⑤ plan_lots 校验」后同时进事务的窄窗口;断言恰 1 confirmed + 1 rejected、扣减恰 1 份、恰 1 组 2 条流水、败者落 rejected。
  • 突变两次不红 = 防御纵深发现:单独拆事务内扣减哨兵不红(④⑤ 复核拦住)、 单独拆 ④ partial_qty 不红(⑤ plan_lots 拦住)→ 窄窗 + 同拆哨兵 ×3 全红、 还原 ×3 全绿。三道防线(④可用量复核 / ⑤plan_lots 校验 / 事务内 remain_qty >= :q 哨兵)拆任何一道,其余两道仍收敛 rejected——资金安全 不依赖单点。

T-16:D29 资金流中转建模(2026-09-12)

一、写入点(全仓唯一,反双重归属闭环)

convert_core_repository._apply_convert_once 在 _insert_trades 后加 _insert_cash_flows:2 条 —— out/redeem = out_amount(费前)+ in/subscribe = in_amount(净转入),remark = convert:{gid}、channel = online、occurred_at = traded_at;与流水/明细/持仓/受理置态同一事务, 幂等由 T-7 确认权哨兵天然保证。R-11「中转」语义 = 确认事务内出+入两条对冲, 无中间现金停留;费用不在资金流重复体现(与 core_trade 转出端同口径)。

二、DDL 与测试

  • tests/_ddl.py:sqlite 版 core_cash_flow + 并入 REQUIRED_CONVERT_TABLES 门禁(真库 01-ddl.sql 既有此表 F-14,无 ALTER)。
  • test_confirm_writes_no_cash_flow 反转为 test_confirm_writes_exactly_two_cash_flows(T-7 登记的「表建立后改 0 行 断言」闭环):恰 2 条 / 两端金额 / remark / 幂等重确认无第 3 条。
  • test_convert_core 两回滚用例补资金流零残留断言。

三、真库(verify_convert_apply 24 → 30 项全绿)+ FK 新坑

  • A2 组 5 项:条数恰 2 / (out,redeem)+(in,subscribe) 组合 / 两端金额 = req.out_amount·in_amount / 与 core_trade 逐条对账;B 组 +1:冲突路径 资金流 0 残留。confirm 脚本的「确认不写资金流」断言(T-7 时代登记)同步 反转 → 102 → 103 项全绿(全套终态复跑时暴露;对账项首版 r[0] 取 dict 行踩 KeyError,_rows 返回 mappings 须按列名取)。
  • 新坑(T-9 FK 坑同款):core_cash_flow.fk_cf_customer 挡住 core_customer 删除 —— 共 5 处清理段补删:verify_apply 与 test_convert_integration(T-16 收尾批,integration 13 个 ERROR 定位到此根因)+ verify_engine / verify_api / verify_compensate(T-17 收尾全套终态复跑批, 库内旧残留引发连环 FK)。删除条件用 remark LIKE 'convert:%' / 'convert:{_GROUP_LIKE}' 精准定位,不误删既有种子。
  • 突变验证:拆 _insert_cash_flows 调用 → 反转用例红(0 ≠ 2);还原 → core+confirm 37 全绿,MUTATION 零残留。

四、下一步

T-17(D30 share_class + A/C 互转):受理校验放行规则(R-12 口径裁定: 种子 share_class NULL 不参与 A/C 判定走一般转换;两端非空且不同才查 allow_ac_convert)+ 种子一对 A/C 产品 + 对照用例。入口:开发计划 T-17 节。


T-17:D30 share_class + A/C 互转(2026-09-12 · 转换线收官)

一、口径裁定(R-12 字面两处偏差,实现取业务正确口径,登记待用户追认)

  1. share_class NULL 不参与 A/C 判定:R-12 字面「不满足 → 400」若让 NULL 参与比较,会打穿全部既有受理用例(种子产品 share_class 全 NULL)。实现 = 两端非空且不同才触发 A/C 分支,NULL/同类走一般转换规则。
  2. 「share_class 相同 → 400」不采纳:非空同类别跨基金互转(同管理人同 TA) 本就走一般转换规则放行,无业务理由禁转;DoD 对照用例改为「同类放行」回归 保护(sqlite + 真库 H3 双证据)。

二、实现与核实

  • 实现(纯新增拦截分支):convert_service._validate_products ta_code 校验 后,两端 share_class 非空且不同时查 allow_ac_convert——任一端 =1 即两向 放行,全关 → CrossEntityNotSupported(错误码沿用,message 指明 A/C,任务书 改法①)。放行路径零改动,历史产品零回归。
  • 联网核实:A/C 互转 = 同一基金不同份额类别转换业务,需管理人「开通」 (摩根行业睿选 2025-11 公告)= allow_ac_convert 真实载体;转换后持有期 重新起算(公告明写,本仓转入新批次语义一致零改动);转换费 = 转出赎回费 + 申购补差费(深证监局,A/C 补差费不在本仓范围)。
  • 种子 ⑥ 段(09-seed-org.sql):双开对 110022'A'/005827'C',双关对照 110023'A'/003095'C',同类对照 510300'C';其余产品保持 NULL。

三、验证(全量 842 passed / 8 skipped · 真库 43/43)

  • sqlite +4:双开放行 / 单开放行(任一为 1)/ 双关 400(message + 不落单)/ 同类+NULL 放行回归保护。首跑发现:同用例第二次受理撞在途占用(150−100 可用 50),改申请 50——受理语义的顺带实证。
  • 真库 H 组 6 项(accept 37 → 43 退出码 0):H1 双开 accepted / H2 双关 400 语义 + 不落单 / H3 同类 'A'→'A' accepted。H3 首跑撞种子 ⑤ 段 min_redeem_qty=100(申请 50 被 BelowMinQty 拦)→ 改申请 100:种子产品做 载体必须通读既有段配置。
  • 突变验证:A/C 分支恒 False → closed 用例红;还原 → 全绿,MUTATION 零残留。
  • 模拟库近似留痕:真实「同基金 A/C」按基金合同绑定同组合,模拟库无 fund_code 字段,判定以「同管理人同 TA + share_class 非空且不同」近似。

五、下一步

T-1~T-17 全部完成,转换线收官。剩余:全套 9 个真库脚本复跑 + 三任务 (T-15/T-16/T-17)提交 + 交接文档/TODO/MEMORY 终态更新;之后按开发计划第 6 步 做集成测试。