Files
group_xinghuo_jinrong/docs/memory/2026-09-12.md
T

46 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 步 做集成测试。


T-14/T-17 开口闭环(2026-09-12 用户三项拍板)+ truncate 舍去法接入

一、三项拍板与执行

# 事项 拍板 执行
1 PRD §2.5.3「易方达 ETF 场外取整数位」 改 已按核实结论修订(该句无证据且有反证,真实整数位载体为场内渠道),版本块加修订注记
2 rounding_mode='truncate' 消费接入 补接 当轮完成(见下)
3 T-17 R-12 口径裁定(NULL 不参与判定 / 同类走一般规则) 认可 开发计划 R-12 条目回写定稿 + T-17 执行记录同步

审核教训留痕:PRD v1.1 经独立 AI 三轮审查(0 阻断级)但「易方达 ETF 场外 取整数位」无出处一句未被任何一轮抓出——逻辑矛盾审查抓不住外部事实断言, 只能靠执行期联网核实兜底。「外部事实必须联网核实」铁律再添一例证。

二、truncate 接入(生产代码 3 处 + 单点扩展)

  • core_ro.get_share_rounding:位数+模式回退链单点(规则行 → core_product → 默认 (2, 'half_up')),逐层独立回退、mode 合法性读取即 校验(half_up/truncate 白名单);get_share_digits 转薄包装(T-14 既有调用与测试零破坏)。
  • calc.product_round / in_qty:加 mode 参数(_ROUND_MODES: half_up → ROUND_HALF_UP / truncate → ROUND_DOWN),非法 mode ValueError。
  • 确认段(confirm_service)与网关申购折算(trade_gateway):两处透传。
  • 载体:09-seed-org.sql ⑦ 段 PROD-005828.rounding_mode='truncate' (11-seed 规则行未覆盖 → 走 product 层回退;无任何转换场景引用 → 全套 零回归)。11-seed 头注「登记开口」同步改「已接入」。

三、验证(全量 849 passed / 8 skipped · 真库 9/9)

  • 测试 +7(842 → 849):calc 4(truncate/half_up 分化 34945.53 vs .54、 整数边界向零、非法 mode、in_qty 透传 16.66 vs 16.67)+ 确认段 2(链路 truncate 舍去生效;回退链逐层独立 + 规则行整体优先遮蔽 product 层错配)+ 申购段 1(对称,166.66 vs 166.67)。首跑踩坑:非法值断言放在规则行存在 之后 → DID NOT RAISE(规则行遮蔽 product 层)——即遮蔽语义本身,改用例 先删规则行。
  • 突变:两调用点同拆 mode 透传 → 2 链路用例红;还原绿,MUTATION 零残留。
  • 真库 9/9 退出码 0:09 ⑦ 段幂等灌库后 verify_convert_seed 新增 ⑫ 断言(载体在位)通过;其余 8 脚本零回归。

四、下一步

开口清零。转换线(T-1~T-17 + 开口闭环)全部完成;下一步 = 开发计划 第 6 步集成测试。


第 6 步 · 集成测试 ✅(2026-09-12 · 转换线全流程收官)

范围对齐 v0.x 第 6 步惯例 + 开发计划 T-13 门禁总则 + PRD §9 验收 18/31 「待第 6 步重测」注记。全程零代码改动——纯验证性收口,无一处需要修复。

一、执行记录(五步全过)

# 步骤 结果
1 按 SOP 重灌双库:verify_convert_seed.py(DROP + 重建 00~11 全套 SQL + 自动补跑 prepare_risk_demo.sql,演示态「剩余 275 天」) DoD 断言全 PASS(含 ⑨a 日历 503 交易日 / ⑩ 份额规则 42 行 / ⑫ truncate 载体)
2 全量 pytest 回归 849 passed / 8 skipped,零失败(重灌后真库集成模块全真跑,与基线一致)
3 CONVERT_STRESS=1 并发套件(真库) 7/7 通过(T-15 争批用例加入后由 6 演进为 7 条)
4 全套 9 个真库 verify 脚本复跑 9/9 退出码 0:accept 43 / apply 30 / lots 20 / tools 14 / confirm 103 / engine 39 / api 89 / compensate 56 / seed 全 PASS
5 残留检查 + 收尾重灌 grep MUTATION 零命中;grep 1 = 1 唯一命中 core_ro.py:662 动态 WHERE 初始占位(正常模式、基线提交内,非突变残留);收尾重灌后 seed 复验全 PASS,库恢复干净演示态

二、两条「待第 6 步重测」验收的重测结论

  • 验收 31 紧池成功率 100%:test_tight_pool_accept_all_then_confirm_100pct 通过——50 并发受理各 1000、池恰 50000 → 受理 50/50 + confirm_batch 串行确认 50/50 = 100%,扣减恰=池守恒无超卖。T+1 模型核心收益 (v0.x 同场景 80%)再次实测成立。
  • 验收 18 性能:test_performance_probe_accept_confirm_end_to_end 通过 (n=59)——端到端 P50 78.8 / P95 94.8 / max 229.4 ms(< 2s 硬门禁, 余量约 20 倍);受理段 P50 36.5 / 确认事务 P50 10.4 ms(均 < 100ms 预估)。 与 T-13 记录(P50 79.2 / P95 110.3)同量级,PRD §9 第 18 条已回填数字 仍准确,无需改写。

三、执行期归因留痕(2 条,均非缺陷)

  1. 业务脚本跑完后 seed --no-reset 断言 FAIL = 预期防护行为:9 个脚本 全跑完后批次 61→67 行、Σremain 漂移(CUST-3001/PROD-510300 转入批次 +108279 份;持仓快照滞后为 T+1 已知设计)→ seed 的静态快照断言 (②批次守恒 / ④档位覆盖)显式 FAIL。seed 断言的权威时点 = 重灌后、 业务脚本运行前;各脚本自身的隔离断言(清理后残留 0)为运行态权威。 收尾重灌即恢复(v0.x 集成测试同款现象,核查单⑥防护语义)。
  2. CONVERT_STRESS 用例数 6→7 是 T-15 演进而非回归: test_two_confirms_race_same_lot_first_wins(Barrier 窄窗争批,断言恰 1 confirmed + 1 rejected)为 T-15 新增;旧记录「6/6」以本次「7/7」为准。

四、结论与下一步

第 6 步集成测试全部通过,基金转换线(T+1 受理/确认分离模型)AIcoding 六步全流程收官:第 1 步讨论 ✅ → 第 2 步 PRD v1.1 ✅ → 第 3 步架构 v2.1 ✅ → 第 4 步开发计划 v2.0 ✅ → 第 5 步 T-1~T-17 ✅ → 第 6 步集成测试 ✅。 后续可选项(由用户拍板):分支推送 / 里程碑 tag 等收尾事项。


种子补拉 + 全套重测 ✅(2026-09-12 · 用户指示「大更新后重新抓数据生成种子再重测」)

一、净值种子更新(06-seed-nav.sql · 提交 d2d00dd)

  • 重抓:python scripts/core/fetch_nav.py --days 60(东方财富接口,14 产品 × 60 净值日), 抓取时间 2026-09-12 14:56,区间滚动为 2026-06-19 ~ 2026-09-11——补上 09-11(周五)净值。
  • diff 核对(零修订实证):新旧文件行级对比 = 仅新增 09-11 的 14 行 + 窗口起点 06-18 的 14 行滚动挤出 + 行尾符号调整(09-10 从末日 ; 变中间日 ,); 同日期同产品净值值零变化(09-10 两次抓取值逐项一致)。demo 锚定的 T=2026-09-09 净值(PROD-110022=1.3604 / PROD-003095=1.9194)不受影响。
  • 实际价值:下周一(09-14)起真库脚本 previous_biz_day 回推链锚定 09-11 时不再缺 T 日净值(旧种子截至 09-10,缺 09-11 会让确认段 nav_pending)。

二、交易日历种子(10-seed-trade-calendar.sql · 无需重生成,留痕依据)

  • 联网核实:2026 年上交所休市安排仍为 2025-12-22 年度公告(上证公告〔2025〕45 号), 无临时调整;脚本 SSE_CLOSED_2026 的中秋(09-2527)/国庆(10-0107) 区间与公告一致、种子休市行在位。
  • 交叉校验(gen_trade_calendar.py --verify-nav):用最新净值校验 「净值日 ⊆ 交易日」不变式——510300 共 169 个净值发布日(原 168 + 新增 09-11) 全部落在交易日,0 偏差;503 交易日不变。

三、新种子下全套重测(全绿)

项 结果
重灌双库(verify_convert_seed.py) DoD 断言全 PASS
全量 pytest 849 passed / 8 skipped 零失败
CONVERT_STRESS=1 7/7
真库 verify 套件(按真实退出码复验) 9/9 退出码 0(accept 43 / apply 30 / lots 20 / tools 14 / confirm 103 / engine 39 / api 89 / compensate 56 / seed 全 PASS)
收尾重灌 全 PASS,库恢复干净演示态

方法论留痕:种子数据类文件(净值/日历)的正确维护节奏 = 演示/跑测前 重跑 fetch_nav.py 补最新净值(脚本头注本就内置此指引),日历在交易所公布 新安排(年度公告/临时调整)时重跑 gen_trade_calendar.py——两者都是 「贴真实项目」铁律在数据层的落点。


README 更新 + 分支推送 ✅(2026-09-12 · 用户指示)

一、README 更新(重点 = 本次更新 + 前端已留接口)

  • 新增「最新更新(2026-09-12)」章节:基金转换 T+1 全流程收官(AIcoding 六步、 集成测试 849 绿 / CONVERT_STRESS 7/7 / 真库 9/9 / 紧池 100% / 性能 P95 94.8ms)、 种子补拉(净值至 09-11)、关键能力清单、v0.x convert_fund 退役。
  • 「前端接入契约」新增 3)基金转换(契约逐项对照实现核对后写入): ① 受理 POST /api/simulate/trade(convert 分支,份额申报 qty,202 + 受理回执; 回执字段按 convert_service 实际返回核对:blocked/accepted/idempotent/status/ convert_group_id/client_request_id/requested_qty/qty/forced_full_transfer/ min_hold_action/accept_date/confirm_date/available_date/cancel_deadline/estimated) ② 撤单 POST /api/simulate/trade/convert/{gid}/cancel(409 CANCEL_NOT_ALLOWED) ③ 查询 GET /api/simulate/trade/convert/{gid}(未确认 = processing 可轮询) ④ 确认批处理 POST /api/admin/convert/confirm?accept_date=(risk_officer); 错误码按 errors.py 常量逐一核对(TOO_MANY_LOTS/INSUFFICIENT_SHARES/ CROSS_ENTITY_NOT_SUPPORTED/CANCEL_NOT_ALLOWED/CONCURRENT_CONFLICT)。
  • 其余:头部定位改「风控 + 基金转换交付分支」、交付状态基线 510→849、 「核心接口一览」补 convert 四端点、快速启动补 verify_convert_seed.py 非交互重灌与 fetch_nav.py 补拉指引、文档地图补基金转换三文档、 「前端」章节从三件事扩为四件事(+基金转换受理/轮询/撤单)。

二、推送(用户授权)

  • 本轮提交(README + 文档回写)随分支全部推送 origin/risk-control-agent; 推送后以 git ls-remote 校验远程 = 本地 HEAD(本仓 refs/remotes/** 写入 不落盘的已知异常,判断远程存亡只能用 git ls-remote)。
  • 合并 main 仍由合并执行人负责(手册《合并注意事项-风控模块并入main.md》), AI 不发起 PR / 不执行合并。

三、里程碑 tag convert-m1(用户授权「打,你自己决定」)

  • 命名:convert-m1 —— 对齐既有 risk-m1~m4 的「模块-里程碑」命名惯例 (tag 本体英文、中文名走 MEMORY §0 对照表):基金转换 · T+1 受理/确认分离 模型全流程收官。
  • 指向:annotated tag 指向收官提交 292770f(含全部代码 + 文档 + README 契约的收官点);tag 对象 70449f9。
  • 校验:git ls-remote --tags origin convert-m1 实测在远程, convert-m1^{commit} = 292770f ✓。
  • 与 risk-m4 的差别:risk-m4 当时仅本地未 push;本 tag 随收官授权一并 推送远程(分支已在远程,tag 共享才有里程碑意义)。