فهرست منبع

refactor#明细含义清晰-步骤核对

hefuzheng 1 هفته پیش
والد
کامیت
6d6d304554

BIN
code/post_clear_check/src/post_clear_check/__pycache__/dimension_body.cpython-312.pyc


+ 132 - 14
code/post_clear_check/src/post_clear_check/dimension_body.py

@@ -6,6 +6,7 @@
 
 from __future__ import annotations
 
+import re
 from collections.abc import Sequence
 from datetime import date, datetime
 from decimal import Decimal
@@ -55,6 +56,52 @@ _ORDER_SECTIONS = (
     ("intraday", "盘中公式"),
     ("clear", "清算对照"),
 )
+_REPORT_CODE = re.compile(r"^(OTC|TRS)\d{3}$")
+# 各端默认可核的落库报表。OTC009、OTC012、TRS011、TRS015 不在此列。
+_CATALOG = {
+    DIM_ISSUE: ("OTC003", "OTC004", "OTC005", "OTC007", "OTC008"),
+    DIM_SALES: ("OTC001", "OTC002", "OTC003", "OTC004", "OTC005", "OTC007", "OTC008", "OTC010"),
+    DIM_TRS: (
+        "TRS001",
+        "TRS002",
+        "TRS003",
+        "TRS004",
+        "TRS005",
+        "TRS006",
+        "TRS007",
+        "TRS008",
+        "TRS009",
+        "TRS010",
+        "TRS012",
+        "TRS013",
+        "TRS014",
+        "TRS016",
+    ),
+}
+_REPORT_NAMES = {
+    "OTC001": "OTC001 资金账户资金报表",
+    "OTC002": "OTC002 资金账户现金变动记录报表",
+    "OTC003": "OTC003 共赢票据订单报表",
+    "OTC004": "OTC004 共赢票据每日切片详情报表",
+    "OTC005": "OTC005 共赢票据实时详情报表",
+    "OTC007": "OTC007 票据交易交收报表",
+    "OTC008": "OTC008 票据交易未交收报表",
+    "OTC010": "OTC010 资金账户现金余额已交收记录表",
+    "TRS001": "TRS001 用户概况",
+    "TRS002": "TRS002 主订单报表",
+    "TRS003": "TRS003 子订单报表",
+    "TRS004": "TRS004 成交明细报表",
+    "TRS005": "TRS005 用户持仓报表",
+    "TRS006": "TRS006 可用金额流水报表",
+    "TRS007": "TRS007 利息收取报表",
+    "TRS008": "TRS008 平仓已实现盈亏报表",
+    "TRS009": "TRS009 出入金报表",
+    "TRS010": "TRS010 用户现金结余报表",
+    "TRS012": "TRS012 融资利率变动报表",
+    "TRS013": "TRS013 应付利息明细报表",
+    "TRS014": "TRS014 每日净资产报表",
+    "TRS016": "TRS016 持仓成本金额利息计算表",
+}
 _VERDICT_RANK = {VERDICT_ERROR: 0, VERDICT_SUSPECT: 1, VERDICT_CORRECT: 2}
 
 
@@ -136,7 +183,7 @@ def _body(chain_name: str, views: tuple[_View, ...], business_date: date) -> lis
     else:
         if gates:
             pieces.append(_tight("批次门禁", [_gate_block(view) for view in gates]))
-        pieces.extend(_report_sections(rest, business_date))
+        pieces.extend(_report_sections(rest, business_date, chain_name))
     errors = [view for view in rest if view.verdict == VERDICT_ERROR]
     blocked = [view for view in rest if view.verdict == VERDICT_SUSPECT and view.calc_value is None]
     suspects = [view for view in rest if view.verdict == VERDICT_SUSPECT and view.calc_value is not None]
@@ -160,7 +207,9 @@ def _summary(chain_name: str, views: tuple[_View, ...]) -> str:
         if chain_name == DIM_ORDER:
             key = view.master_order_no or view.formula_text
         else:
-            key = view.report_code or view.formula_text
+            key = view.report_code if _is_report(view.report_code) else ""
+        if not key:
+            continue
         buckets[key] = _worse(buckets.get(key), view.verdict)
     counts = {VERDICT_CORRECT: 0, VERDICT_ERROR: 0, VERDICT_SUSPECT: 0}
     for verdict in buckets.values():
@@ -180,9 +229,10 @@ def _summary(chain_name: str, views: tuple[_View, ...]) -> str:
                 rows.append(view.calc_text or view.check_text)
     else:
         rows.append(
-            f"本方向正确 {counts[VERDICT_CORRECT]} {unit},"
+            f"本方向报表正确 {counts[VERDICT_CORRECT]} 张,"
             f"错误 {counts[VERDICT_ERROR]},"
             f"可疑 {counts[VERDICT_SUSPECT]}。"
+            + _missing_note(chain_name, set(buckets))
         )
     return "\n".join(rows)
 
@@ -319,16 +369,55 @@ def _field_blocks(rows: Sequence[_View]) -> list[str]:
     return blocks
 
 
-def _report_sections(rows: Sequence[_View], business_date: date) -> list[str]:
-    """每张报表一块,六行对齐巡检稿。没有报表编号的步骤节点单独成块。"""
+def _is_report(code: str) -> bool:
+    """只有 OTC、TRS 加三位数字才是日常报表。票据号、交收步骤、钱包节点都不是。"""
+
+    return bool(_REPORT_CODE.match(code))
+
+
+def _step_name(formula: str) -> str:
+    """步骤名取公式等号左边。不拿第一条的票据号或子订单号当标题。"""
+
+    head, sep, _rest = formula.partition(" = ")
+    name = head.strip() if sep else formula.strip()
+    return name or "步骤"
+
+
+def _missing_note(chain_name: str, present: set[str]) -> str:
+    """摘要末尾写目录是否到齐。缺的列出编号,齐了只写张数。"""
+
+    catalog = _CATALOG.get(chain_name, ())
+    if not catalog:
+        return ""
+    missing = [code for code in catalog if code not in present]
+    if missing:
+        return "目录内未出现结论:" + "、".join(missing) + "。"
+    return f"目录内 {len(catalog)} 张报表均有结论。"
+
 
-    grouped: dict[str, list[_View]] = {}
+def _report_sections(rows: Sequence[_View], business_date: date, chain_name: str) -> list[str]:
+    """OTC、TRS 编号各成一块。没有编号的步骤另起「步骤核对」,不叫报表。"""
+
+    reports: dict[str, list[_View]] = {}
+    steps: dict[str, list[_View]] = {}
     for view in rows:
-        grouped.setdefault(view.report_code or view.formula_text, []).append(view)
-    blocks = [_report_block(group, business_date) for _key, group in grouped.items()]
-    if not blocks:
-        return ["分报表\n本轮没有报表结论。"]
-    return blocks
+        if _is_report(view.report_code):
+            reports.setdefault(view.report_code, []).append(view)
+        else:
+            steps.setdefault(view.formula_text, []).append(view)
+    catalog = _CATALOG.get(chain_name, ())
+    order = [code for code in catalog if code in reports]
+    order.extend(code for code in reports if code not in order)
+    blocks = [_report_block(reports[code], business_date) for code in order]
+    step_blocks = [_step_block(formula, group, business_date) for formula, group in steps.items()]
+    pieces: list[str] = []
+    if blocks:
+        pieces.extend(blocks)
+    elif not step_blocks:
+        pieces.append("本轮没有报表结论。")
+    if step_blocks:
+        pieces.append(_tight("步骤核对", step_blocks))
+    return pieces
 
 
 def _report_block(rows: Sequence[_View], business_date: date) -> str:
@@ -359,6 +448,32 @@ def _report_block(rows: Sequence[_View], business_date: date) -> str:
     )
 
 
+def _step_block(formula: str, rows: Sequence[_View], business_date: date) -> str:
+    """一条清算步骤。标题用步骤名,对象落在实据里,不写成报表名。"""
+
+    scope = _scope(rows)
+    screen = f"营业日 {_day(business_date)}"
+    if scope:
+        screen = f"{screen},{scope}"
+    verdict = VERDICT_CORRECT
+    for view in rows:
+        verdict = _worse(verdict, view.verdict)
+    correct = sum(1 for view in rows if view.verdict == VERDICT_CORRECT)
+    bases = _unique(view.basis for view in rows if view.basis)
+    label_text = {VERDICT_CORRECT: "正确", VERDICT_ERROR: "错误", VERDICT_SUSPECT: "可疑"}[verdict]
+    actual = f"{len(rows)} 条无差异" if verdict == VERDICT_CORRECT else f"{correct}/{len(rows)} 条无差异,其余见下方清单"
+    return _lines(
+        (
+            f"步骤:{_step_name(formula)}",
+            f"筛选:{screen}",
+            _expect_line([formula]),
+            f"实据:{actual}",
+            _basis_line(bases or ["见各列登记"]),
+            f"结论:{label_text}",
+        )
+    )
+
+
 def _expect_line(items: Sequence[str]) -> str:
     """多个字段写在同一句里,用逗号分开,句号收尾。"""
 
@@ -437,9 +552,12 @@ def _detail(view: _View) -> str:
     """错误、可疑、无法展开逐条写计算过程。"""
 
     rows: list[str] = []
-    label = _report_label(view)
-    if view.report_code or label != view.formula_text:
-        rows.append(f"报表:{label}")
+    if _is_report(view.report_code):
+        rows.append(f"报表:{_report_label(view)}")
+    else:
+        rows.append(f"步骤:{_step_name(view.formula_text)}")
+        if view.check_text and view.check_text != view.formula_text:
+            rows.append(f"对象:{view.check_text}")
     scope = _scope((view,))
     if scope:
         rows.append(f"筛选:{scope}")

BIN
code/post_clear_check/tests/__pycache__/test_feishu.cpython-312-pytest-9.1.1.pyc


+ 38 - 0
code/post_clear_check/tests/test_feishu.py

@@ -24,6 +24,7 @@ from post_clear_check.chain import (
     CHAIN_TRADE,
     CHAIN_UNASSIGNED,
     CHAINS,
+    DIM_ISSUE,
     DIM_ORDER,
     DIM_SALES,
 )
@@ -1516,6 +1517,43 @@ def test_report_bases_group_same_file_and_expect_uses_commas() -> None:
     assert "期望:股票数量,发行费用(结算币种),报表用途。" in body
 
 
+def test_step_nodes_are_not_called_reports() -> None:
+    """交收、增额、钱包、流水是步骤。只有 OTC、TRS 编号才叫报表,缺的编号写在摘要。"""
+
+    derive = "交收推演 = work/4 按子订单唯一,历史未交收仍在,金额与状态符合交收规则"
+    body = render_body(
+        business_date=DAY,
+        trade_date=DAY,
+        batch_no="IB",
+        chain_name=DIM_ISSUE,
+        record_no="16",
+        lines=(
+            _dim(derive, VERDICT_CORRECT, check="子订单 1 交收推演:一致", calc_value=None, check_value=None),
+            _dim(derive, VERDICT_CORRECT, check="子订单 2 交收推演:一致", calc_value=None, check_value=None),
+            _dim(
+                "增额主键 = input 中归属本营业日的增额主键集合等于实时同日增额主键",
+                VERDICT_CORRECT,
+                check="票据 ABCDELTA-20261029012 增额主键",
+                calc_value=None,
+                check_value=None,
+            ),
+            _dim(
+                "名义本金",
+                VERDICT_CORRECT,
+                check="OTC003 共赢票据订单报表.名义本金 = 1",
+                report_code="OTC003",
+            ),
+        ),
+    )
+    assert "报表:子订单 1 交收推演" not in body
+    assert "报表:票据 ABCDELTA-20261029012 增额主键" not in body
+    assert "步骤:交收推演" in body
+    assert "步骤:增额主键" in body
+    assert "实据:2 条无差异" in body
+    assert "报表:OTC003 共赢票据订单报表" in body
+    assert "目录内未出现结论:OTC004、OTC005、OTC007、OTC008。" in body
+
+
 def test_stored_topic_collapses_repeated_prototype_files() -> None:
     """已经落库的字段块在发送前再排。同一份原型只留一次,期望改成一句。"""
 

+ 6 - 4
doc/业务链路核对设计.md

@@ -510,18 +510,20 @@ TRS 的字段公式不在 Excel 里,在产品原型 `TRS/4 业务引起数据
 
 ### 7.3 三张报表
 
-发行、销售、TRS 用同一版式:批次门禁、摘要、分报表、错误清单、可疑清单、无法展开。某一张报表没有结论行时,不编一块「正确」。
+发行、销售、TRS 用同一版式:批次门禁、摘要、分报表、步骤核对、错误清单、可疑清单、无法展开。某一张报表没有结论行时,不编一块「正确」,摘要写明目录里缺了哪个编号。
 
-批次门禁只收这一端的门禁行。没有门禁差异时不写这一章。摘要按报表编号计正确、错误、可疑的张数,门禁行不计入张数,但门禁错误仍让这张卡的总结论成为错误。
+批次门禁只收这一端的门禁行。没有门禁差异时不写这一章。摘要只按 OTC、TRS 编号计正确、错误、可疑的张数,门禁行和步骤节点不计入张数,但它们的错误仍让这张卡的总结论成为错误。发行目录是 OTC003、OTC004、OTC005、OTC007、OTC008。销售目录是 OTC001、OTC002、OTC003、OTC004、OTC005、OTC007、OTC008、OTC010。TRS 目录是 TRS001 到 TRS010、TRS012、TRS013、TRS014、TRS016。OTC009、OTC012、TRS011、TRS015 不进目录。
 
-同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论。主题是错误时,正确列仍留在分报表,不写「正确与可疑留在核对明细」。
+分报表只收带这些编号的结论行。交收推演、T 日交收、增额主键、增额累计、期初钱包、T 日钱包、候选范围、原始流水、票据与子票据比对是步骤节点,写在「步骤核对」。标题用公式等号左边的步骤名。不拿第一条的票据号、子订单号或币种当报表名。
+
+同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论。没有报表编号的,清单里写步骤和对象。主题是错误时,正确列仍留在分报表,不写「正确与可疑留在核对明细」。
 
 ```text
 销售报表 错误
 
 摘要
 
-本方向正确 1 张报表,错误 1,可疑 0。
+本方向报表正确 1 张,错误 1,可疑 0。目录内未出现结论:OTC003、OTC004、OTC005、OTC007、OTC008、OTC010。
 
 分报表
 

+ 1 - 1
doc/使用说明.md

@@ -178,7 +178,7 @@ OTC009、OTC012 是实时导出,没有落库结果,`report_topup_payment`、
 
 订单链路正文是摘要、每单链路、盘中公式、清算对照、关联报表、错误清单、可疑清单、无法展开。摘要里的笔数按主单计。每单链路写两段:执行事实是否与实时源表一致;需要 TRS 执行根的对冲,成交股数是否对上,并说明自动单按 ext_ref、补录按委托号或母单号。不列表,也不把未核对的票据和已撤单写进去。同一字段的正确行收成一块,结论写「正确(n/n)」。清算对照除订单自己的已发布执行列、工作快照外,再写交收台账、销售资金流水、TRS 发生额与汇率、清算留痕;没有对应结论行就写「本轮未核」,不编造金额。关联报表按发行/销售的 OTC003、OTC004、OTC007/OTC008 展开期望、实据和依据。三端门禁在摘要里各一行。
 
-三张报表正文是批次门禁、摘要、分报表、错误清单、可疑清单、无法展开。没有门禁行时不写批次门禁。同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论。方向卡是错误主题时,正确列仍留在分报表。
+三张报表正文是批次门禁、摘要、分报表、步骤核对、错误清单、可疑清单、无法展开。没有门禁行时不写批次门禁。摘要只按 OTC、TRS 编号计张数,并写目录是否到齐。票据号、子订单交收推演、增额、钱包、原始流水是步骤,写在「步骤核对」,标题用步骤名,不叫报表。同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论;没有报表编号的写步骤和对象。方向卡是错误主题时,正确列仍留在分报表。OTC009、OTC012 不进目录。TRS011、TRS015 不进目录。
 
 章节之间空一行。一张报表或一个字段内部用单个换行。期望的多个字段用逗号连成一句。同一份原型只写一次,字段接在文件名后面。空清单写成「错误清单:无」。方向卡不按 3500 字删报表或公式段。请求体超过约 28KB 时按结论行拆成多张,每张带同一个记录编号和「第几张,共几张」,整卡仍是一个 `notify_status`。公式里的制表符会去掉,`<` `>` 会改成全角。发送失败时日志带上飞书返回的 `msg`。