41 KiB
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 隔离):
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)跨口径一致。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 条(留痕)
- 转出端流水
amount= 费前转出金额(40812.00 = 30000×1.3604,与 PRD §5.3.2 示例 68020.00 同语义;赎回费 204.06 单列redeem_fee字段)—— 初版锚点误按「费后 40607.94」 断言,真跑第一轮即抓出(assert '40812.00' == '40607.94'),已在用例注释钉死口径。 t1_env必须调_ddl.seed_suitability_matrix:C×R 矩阵是check_suitability的 L0 权威,裸create_sqlite_engine()缺矩阵 → 受理段全量forbidden(SUIT_RISK_MISMATCH), 症状是confirm_one返回missing(受理单根本没落)。与 conftestsqlite_enginefixture 同源(自检第 13 问:同一种子不得有两份实现)。
验证
pytest tests/test_core_tools.py5/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)
一、改动面(三处)
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)。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 退役)。
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)
用户原话:「你不确定的都要联网查找,这个项目要贴近真实项目,这是铁律,比如你刚才说的过年假期问题。」
- 「基金转换申请失效」真实口径(多家基金公司《开放式基金业务规则》:
华安 / 富国 / 嘉实 / 苏新 / 太平 / 德邦 / 民生加银):转出不可赎回或转入不可申购 →
申请无效;账户/份额冻结 → 无效;TA T+1 确认、T+2 可查;无效申请资金退回原账户;
太平基金:转换转出参照巨额赎回规则。映射本项目:
rejected(T+1 复核不通过)= 真实「无效申请」,expired定位为运维兜底(释放占用 = 份额退回效果)——设计贴近真实 ✓。 - 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)。
- 17 个孤儿 import;模块 docstring 重写为 T+1 编排说明;
- 按实际修订:开发计划原「
rebuild_convert_responseT-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% 回填)· T-17 待办拆出)· 交接文档.md(v4.2 → v4.3,
§B.11 状态段 + T-13 完成段 + 基线行)· docs/memory/TODO.md(进度行 + T-13 勾选,
T-14docs/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 项,待用户裁定,未顺手做)
- PRD §2.5.3 修订:「易方达 ETF 场外取整数位」未找到证据且有反证(易方达自家 ETF/联接 2 位);真实整数位载体是场内渠道(LOF/ETF 场内先 2 位再截位整数)。 PRD 已定稿,修订须用户拍板。
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 字面两处偏差,实现取业务正确口径,登记待用户追认)
share_classNULL 不参与 A/C 判定:R-12 字面「不满足 → 400」若让 NULL 参与比较,会打穿全部既有受理用例(种子产品 share_class 全 NULL)。实现 = 两端非空且不同才触发 A/C 分支,NULL/同类走一般转换规则。- 「share_class 相同 → 400」不采纳:非空同类别跨基金互转(同管理人同 TA) 本就走一般转换规则放行,无业务理由禁转;DoD 对照用例改为「同类放行」回归 保护(sqlite + 真库 H3 双证据)。
二、实现与核实
- 实现(纯新增拦截分支):
convert_service._validate_productsta_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 条,均非缺陷)
- 业务脚本跑完后
seed --no-reset断言 FAIL = 预期防护行为:9 个脚本 全跑完后批次 61→67 行、Σremain 漂移(CUST-3001/PROD-510300转入批次 +108279 份;持仓快照滞后为 T+1 已知设计)→ seed 的静态快照断言 (②批次守恒 / ④档位覆盖)显式 FAIL。seed 断言的权威时点 = 重灌后、 业务脚本运行前;各脚本自身的隔离断言(清理后残留 0)为运行态权威。 收尾重灌即恢复(v0.x 集成测试同款现象,核查单⑥防护语义)。 - 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 等收尾事项。