基金转换 T+1 模型:T-1~T-6 落地(DDL/数据层/语义收窄/锁/受理事务)

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,零回归。
This commit is contained in:
2026-09-11 19:21:15 +08:00
parent 3b781f8823
commit f056fcd007
40 changed files with 6545 additions and 3105 deletions
+21 -19
View File
@@ -30,7 +30,7 @@ from app.service.convert import calc, fee # noqa: E402
from app.service.convert.types import FeeRule, Lot # noqa: E402
#: 与 PRD §5.3 同一交易日(也是 06-seed-nav.sql 的净值日期)。
TRADE_DATE = date(2026, 9, 4)
TRADE_DATE = date(2026, 9, 9)
#: 07-seed-fee-rule.sql 的赎回费五档(22 号文 §10 下限)。
RULES = [
@@ -42,30 +42,32 @@ RULES = [
]
# 两端同属**华夏模拟基金 / TA-CN-001**(满足转换前置硬约束:同管理人 + 同 TA)。
NAV_OUT = Decimal("1.0300") # PROD-110022 稳健债基 A(bond)
NAV_IN = Decimal("0.9500") # PROD-003095 医药健康精选(stock / 主动偏股型)
# T-2b:净值取 06-seed-nav.sql 在 T=2026-09-09 的**真实净值**(PRD §5.3.2 声明来源,
# 禁止用虚构值——验收 20「示例与种子强绑定」)。
NAV_OUT = Decimal("1.3604") # PROD-110022 稳健债基 A(bond)@2026-09-09
NAV_IN = Decimal("1.9194") # PROD-003095 医药健康精选(stock / 主动偏股型)@2026-09-09
SUB_RATE_OUT = Decimal("0.0030") # 转出端申购费率(债基,22 号文 §8 上限 0.3%)
SUB_RATE_IN = Decimal("0.0080") # 转入端申购费率(主动偏股型,上限 0.8%)
REQUESTED_QTY = Decimal("50000.0000")
#: PRD §5.3 期望值 —— 实算结果必须逐项命中。
#: PRD §5.3 期望值 —— 实算结果必须逐项命中(T-2b 由生产 calc 实算回填,禁手算)。
EXPECTED = {
"批次 1 amount": "30900.00",
"批次 1 fee": "154.50",
"批次 2 amount": "20600.00",
"批次 2 fee": "309.00",
"out_amount": "51500.00",
"redeem_fee": "463.50",
"convert_amount": "51036.50",
"diff_fee(B)": "252.40",
"in_amount": "50784.10",
"in_qty(B)": "53456.95",
"rounding_diff": "-0.0026",
"diff_fee(A)": "253.91",
"in_qty(A)": "53455.36",
"批次 1 amount": "40812.00",
"批次 1 fee": "204.06",
"批次 2 amount": "27208.00",
"批次 2 fee": "408.12",
"out_amount": "68020.00",
"redeem_fee": "612.18",
"convert_amount": "67407.82",
"diff_fee(B)": "333.36",
"in_amount": "67074.46",
"in_qty(B)": "34945.54",
"rounding_diff": "-0.0049",
"diff_fee(A)": "335.36",
"in_qty(A)": "34944.49",
"同费率 diff_fee": "0.00",
"同费率 in_qty": "53722.63",
"同费率 in_qty": "35119.21",
}
@@ -160,7 +162,7 @@ def main() -> int:
return 1
print()
print(f"✅ 全部 {len(EXPECTED)} 项与 PRD §5.3 一致(口径 B in_qty=53456.95)")
print(f"✅ 全部 {len(EXPECTED)} 项与 PRD §5.3 一致(口径 B in_qty=34945.54)")
return 0