基金转换 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 步集成测试)
This commit is contained in:
2026-09-12 13:42:49 +08:00
parent 1a615bbbef
commit cde0c226fe
20 changed files with 710 additions and 37 deletions
+69 -1
View File
@@ -49,7 +49,7 @@ from app.repository.convert_request_repository import ( # noqa: E402
from app.repository.core_ro import CoreReadOnlyRepository # noqa: E402
from app.repository.risk_repository import RiskRepository # noqa: E402
from app.service.convert.convert_service import accept_convert # noqa: E402
from app.service.convert.errors import InsufficientShares # noqa: E402
from app.service.convert.errors import CrossEntityNotSupported, InsufficientShares # noqa: E402
from app.service.convert.trading_calendar import ( # noqa: E402
next_biz_day,
parse_cutoff,
@@ -508,6 +508,74 @@ def main() -> int:
"WHERE table_schema = :db AND table_name = 'core_convert_request' "
"AND column_name = 'requested_at'",
db=settings.mysql_core_database), 3)
# ── H. D30 同基金 A/C 份额互转(T-17 / R-12 · 真库种子产品)──
print("\n【H】A/C 互转:双开放行 / 双关 400 / 同类别走一般规则")
# 种子就位(09-seed-org.sql ⑥ 段的幂等重申;rowcount 同时证明产品存在)
with core_admin.begin() as conn:
for pid, sc, ac in (
("PROD-110022", "A", 1), ("PROD-005827", "C", 1),
("PROD-110023", "A", 0), ("PROD-003095", "C", 0),
("PROD-510300", "C", 0),
):
n = conn.execute(
text("UPDATE core_product SET share_class = :sc, allow_ac_convert = :ac "
"WHERE product_id = :p"),
{"sc": sc, "ac": ac, "p": pid},
).rowcount
if n != 1:
raise RuntimeError(f"种子产品缺失:{pid}(先灌 scripts/core/reset.ps1)")
# A 端持仓 1200 份(避开 PROD-110022 min_hold=1000 的强制全转干扰)
conn.execute(
text("DELETE FROM core_product_nav WHERE product_id = 'PROD-110022' AND nav_date = :d"),
{"d": day},
)
conn.execute(
text("INSERT INTO core_product_nav (product_id, nav, daily_chg_pct, nav_date) "
"VALUES ('PROD-110022', 1.0300, 0, :d)"),
{"d": day},
)
conn.execute(
text("INSERT INTO core_share_lot (lot_id, customer_id, product_id, qty, "
"remain_qty, nav, confirmed_at) "
"VALUES ('LOT-ACV-SEED', :c, 'PROD-110022', 1200, 1200, 1.0300, :cat)"),
{"c": CUSTOMER, "cat": datetime(2026, 1, 5, 10, 0, 0)},
)
conn.execute(
text("INSERT INTO core_holding (customer_id, product_id, qty, cost_amount, "
"market_value, pnl_pct, as_of) "
"VALUES (:c, 'PROD-110022', 1200, 1200.00, 1236.00, 0, :d)"),
{"c": CUSTOMER, "d": day},
)
# H1:'A'→'C' 双端开通 → 放行
ac_open = _accept(_base_req(
from_product_id="PROD-110022", to_product_id="PROD-005827",
qty=Decimal("100"), client_request_id="ACV-REQ-AC-OPEN"), submit_at)
check("H1 'A'→'C' 双开通受理 accepted", ac_open["status"], "accepted")
# H2:'A'→'C' 双端未开通 → 400 语义沿用 CROSS_ENTITY_NOT_SUPPORTED(换 A/C 文案)
ac_err = None
try:
_accept(_base_req(
from_product_id="PROD-110023", to_product_id="PROD-003095",
qty=Decimal("100"), client_request_id="ACV-REQ-AC-CLOSED"), submit_at)
except CrossEntityNotSupported as exc:
ac_err = exc
check("H2 双关被拒", bool(ac_err), True)
check("H2 错误码沿用 CROSS_ENTITY_NOT_SUPPORTED",
ac_err.error_code if ac_err else None, "CROSS_ENTITY_NOT_SUPPORTED")
check("H2 提示指明 A/C 互转", "A/C" in (ac_err.message if ac_err else ""), True)
check("H2 被拒不落单",
q1(core_admin, "SELECT COUNT(*) FROM core_convert_request "
"WHERE customer_id = :c AND to_product_id = 'PROD-003095'", c=CUSTOMER), 0)
# H3:'A'→'A' 同类别 → 不触发 A/C 判定,走一般转换规则放行
# (申请 100 = 种子 min_redeem_qty;转后余 1000 恰不触发 min_hold 强制全转)
ac_same = _accept(_base_req(
from_product_id="PROD-110022", to_product_id="PROD-110023",
qty=Decimal("100"), client_request_id="ACV-REQ-AC-SAME"), submit_at)
check("H3 同类别 'A'→'A' 走一般规则 accepted", ac_same["status"], "accepted")
finally:
cleanup_core(core_admin)
cleanup_agent(agent_admin)