Переглянути джерело

refactor#完善每单链路描述

hefuzheng 1 тиждень тому
батько
коміт
5ef8523e61

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


+ 82 - 22
code/post_clear_check/src/post_clear_check/dimension_body.py

@@ -197,30 +197,85 @@ def _order_sections(rows: Sequence[_View]) -> list[str]:
 
 
 
 
 def _link_summary(rows: Sequence[_View]) -> str:
 def _link_summary(rows: Sequence[_View]) -> str:
-    """每单链路不列表。按主单汇总执行事实和对冲执行根。"""
+    """每单链路写两段说明,不列表。只根据已经算出的执行事实和对冲执行根,不补未核对的票据和已撤单。"""
 
 
     if not rows:
     if not rows:
         return "本轮没有执行事实或对冲执行根结论。"
         return "本轮没有执行事实或对冲执行根结论。"
-    orders: dict[str, str] = {}
-    for view in rows:
-        key = view.master_order_no or view.formula_text
-        orders[key] = _worse(orders.get(key), view.verdict)
+    facts = [view for view in rows if view.formula_text.startswith("执行事实")]
+    hedges = [view for view in rows if "执行根" in view.formula_text]
+    parts: list[str] = []
+    if facts:
+        parts.append(_fact_paragraph(facts))
+    if hedges:
+        parts.append(_hedge_paragraph(hedges))
+    covered = {id(view) for view in facts}
+    covered.update(id(view) for view in hedges)
+    rest = [view for view in rows if id(view) not in covered]
+    if rest:
+        parts.append(_plain_count("其余每单链路节点", rest))
+    if not parts:
+        parts.append(_plain_count("每单链路", rows))
+    return "\n\n".join(parts)
+
+
+def _fact_paragraph(rows: Sequence[_View]) -> str:
+    """执行事实:input 的成交名义本金、订单成交均价与实时源表。"""
+
+    orders = _worst_orders(rows)
+    ok = sum(1 for verdict in orders.values() if verdict == VERDICT_CORRECT)
+    text = (
+        f"已完成或已结算的 ORD 主单 {len(orders)} 笔。"
+        "执行事实核对 input 封存的成交名义本金、订单成交均价与实时源表是否一致:"
+        f"{ok} 笔一致。"
+    )
+    return _with_failures(text, rows)
+
+
+def _hedge_paragraph(rows: Sequence[_View]) -> str:
+    """对冲执行根:成交股数对照。自动单走 ext_ref,补录走委托号或母单号。"""
+
+    ok = sum(1 for view in rows if view.verdict == VERDICT_CORRECT)
+    text = (
+        f"需要 TRS 执行根的对冲 {len(rows)} 条,成交股数与执行根对照:{ok} 条一致。"
+        "自动单按 ext_ref(HR 加对冲主键)定位,补录按委托号或算法母单号。"
+        "自定义补录和从未建单不要求执行根。"
+    )
+    return _with_failures(text, rows)
+
+
+def _plain_count(title: str, rows: Sequence[_View]) -> str:
+    orders = _worst_orders(rows)
     counts = {VERDICT_CORRECT: 0, VERDICT_ERROR: 0, VERDICT_SUSPECT: 0}
     counts = {VERDICT_CORRECT: 0, VERDICT_ERROR: 0, VERDICT_SUSPECT: 0}
     for verdict in orders.values():
     for verdict in orders.values():
         counts[verdict] += 1
         counts[verdict] += 1
-    text = (
-        f"本轮主单 {len(orders)} 笔。执行事实与对冲执行根:"
+    return (
+        f"{title} {len(orders)} 笔:"
         f"正确 {counts[VERDICT_CORRECT]} 笔,"
         f"正确 {counts[VERDICT_CORRECT]} 笔,"
         f"错误 {counts[VERDICT_ERROR]} 笔,"
         f"错误 {counts[VERDICT_ERROR]} 笔,"
         f"可疑 {counts[VERDICT_SUSPECT]} 笔。"
         f"可疑 {counts[VERDICT_SUSPECT]} 笔。"
     )
     )
-    bad = [key for key, verdict in orders.items() if verdict != VERDICT_CORRECT]
-    if bad:
-        text += "未通过:" + "、".join(bad[:8])
-        if len(bad) > 8:
-            text += f"…等共 {len(bad)} 笔"
-        text += "。"
-    return text
+
+
+def _worst_orders(rows: Sequence[_View]) -> dict[str, str]:
+    orders: dict[str, str] = {}
+    for view in rows:
+        key = view.master_order_no or view.formula_text
+        orders[key] = _worse(orders.get(key), view.verdict)
+    return orders
+
+
+def _with_failures(text: str, rows: Sequence[_View]) -> str:
+    """未通过的单另起一行,写主单号和代入,不把原因再用分号粘在句末。"""
+
+    failed = [view for view in rows if view.verdict != VERDICT_CORRECT]
+    if not failed:
+        return text
+    lines = [text, "未通过:"]
+    for view in failed:
+        who = view.master_order_no or "主单"
+        detail = view.calc_text or view.check_text or view.verdict
+        lines.append(f"{who}:{detail}")
+    return "\n".join(lines)
 
 
 
 
 def _field_blocks(rows: Sequence[_View]) -> list[str]:
 def _field_blocks(rows: Sequence[_View]) -> list[str]:
@@ -278,28 +333,33 @@ def _report_block(rows: Sequence[_View], business_date: date) -> str:
     for view in rows:
     for view in rows:
         verdict = _worse(verdict, view.verdict)
         verdict = _worse(verdict, view.verdict)
     names = _formula_names(rows)
     names = _formula_names(rows)
-    expect = ";".join(names[:6])
-    if len(names) > 6:
-        expect += f";其余 {len(names) - 6} 列见依据"
     correct = sum(1 for view in rows if view.verdict == VERDICT_CORRECT)
     correct = sum(1 for view in rows if view.verdict == VERDICT_CORRECT)
     bases = _unique(view.basis for view in rows if view.basis)
     bases = _unique(view.basis for view in rows if view.basis)
-    basis = ";".join(bases[:4])
-    if len(bases) > 4:
-        basis += f";另有 {len(bases) - 4} 条依据"
     label_text = {VERDICT_CORRECT: "正确", VERDICT_ERROR: "错误", VERDICT_SUSPECT: "可疑"}[verdict]
     label_text = {VERDICT_CORRECT: "正确", VERDICT_ERROR: "错误", VERDICT_SUSPECT: "可疑"}[verdict]
     actual = f"{len(rows)} 列无差异" if verdict == VERDICT_CORRECT else f"{correct}/{len(rows)} 列无差异,其余见下方清单"
     actual = f"{len(rows)} 列无差异" if verdict == VERDICT_CORRECT else f"{correct}/{len(rows)} 列无差异,其余见下方清单"
     return _lines(
     return _lines(
         (
         (
             f"报表:{label}",
             f"报表:{label}",
             f"筛选:{screen}",
             f"筛选:{screen}",
-            f"期望:{expect}",
+            _stacked("期望", names),
             f"实据:{actual}",
             f"实据:{actual}",
-            f"依据:{basis or '见各列登记'}",
+            _stacked("依据", bases or ["见各列登记"]),
             f"结论:{label_text}",
             f"结论:{label_text}",
         )
         )
     )
     )
 
 
 
 
+def _stacked(label: str, items: Sequence[str]) -> str:
+    """多条依据或期望各占一行。用分号拼在一起时,原型路径会挤成一行。"""
+
+    kept = [item for item in items if item]
+    if not kept:
+        return f"{label}:"
+    if len(kept) == 1:
+        return f"{label}:{kept[0]}"
+    return f"{label}:{kept[0]}\n" + "\n".join(kept[1:])
+
+
 def _formula_names(rows: Sequence[_View]) -> list[str]:
 def _formula_names(rows: Sequence[_View]) -> list[str]:
     found: list[str] = []
     found: list[str] = []
     for view in rows:
     for view in rows:

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


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

@@ -1352,6 +1352,44 @@ def test_dimension_order_body_collapses_correct_fields() -> None:
     assert "正确另有" not in body
     assert "正确另有" not in body
 
 
 
 
+def test_link_summary_describes_facts_and_hedge_roots() -> None:
+    """每单链路写成执行事实、对冲执行根两段,不把结论收成正确几笔。"""
+
+    body = render_body(
+        business_date=DAY,
+        trade_date=DAY,
+        batch_no="IB",
+        chain_name=DIM_ORDER,
+        record_no="13",
+        lines=(
+            _dim("执行事实列与源表一致", VERDICT_CORRECT, master_order_no="ORD1", calc="一致", calc_value=None, check_value=None),
+            _dim("执行事实列与源表一致", VERDICT_CORRECT, master_order_no="ORD2", calc="一致", calc_value=None, check_value=None),
+            _dim(
+                "对冲记录成交股数 = TRS 执行根的成交股数",
+                VERDICT_CORRECT,
+                master_order_no="ORD1",
+                calc="db_trs.trade_order.ext_ref=HR66 600",
+                calc_value=Decimal("600"),
+                check_value=Decimal("600"),
+            ),
+            _dim(
+                "对冲记录成交股数 = TRS 执行根的成交股数",
+                VERDICT_ERROR,
+                master_order_no="ORD2",
+                calc="db_trs.trade_order.ext_ref=HR70 没有行",
+                calc_value=None,
+                check_value=Decimal("100"),
+            ),
+        ),
+    )
+    assert "已完成或已结算的 ORD 主单 2 笔。执行事实核对 input 封存的成交名义本金、订单成交均价与实时源表是否一致:2 笔一致。" in body
+    assert "需要 TRS 执行根的对冲 2 条,成交股数与执行根对照:1 条一致。" in body
+    assert "自动单按 ext_ref(HR 加对冲主键)定位,补录按委托号或算法母单号。" in body
+    assert "自定义补录和从未建单不要求执行根。" in body
+    assert "ORD2:db_trs.trade_order.ext_ref=HR70 没有行" in body
+    assert "正确 8 笔,错误 0 笔,可疑 0 笔" not in body
+
+
 def test_dimension_report_body_keeps_correct_rows_on_error_theme() -> None:
 def test_dimension_report_body_keeps_correct_rows_on_error_theme() -> None:
     """报表卡即使总结论是错误,正确列仍留在分报表,错误行逐条写期望和实据。"""
     """报表卡即使总结论是错误,正确列仍留在分报表,错误行逐条写期望和实据。"""
 
 
@@ -1397,6 +1435,45 @@ def test_dimension_report_body_keeps_correct_rows_on_error_theme() -> None:
     assert "筛选:主订单 ORD001 · 资金账户 FA00000001" in body
     assert "筛选:主订单 ORD001 · 资金账户 FA00000001" in body
 
 
 
 
+def test_report_bases_are_one_per_line() -> None:
+    """多条依据各占一行。分号拼在一起时,原型路径会挤成一行。"""
+
+    body = render_body(
+        business_date=DAY,
+        trade_date=DAY,
+        batch_no="IB",
+        chain_name=DIM_SALES,
+        record_no="12",
+        lines=(
+            _dim(
+                "股票数量",
+                VERDICT_CORRECT,
+                check="OTC003 共赢票据订单报表.股票数量 = 1",
+                report_code="OTC003",
+                basis="产品原型 订单报表.md 「股票数量」",
+            ),
+            _dim(
+                "价格",
+                VERDICT_CORRECT,
+                check="OTC003 共赢票据订单报表.价格 = 1",
+                report_code="OTC003",
+                basis="产品原型 订单报表.md 「价格」",
+            ),
+            _dim(
+                "总金额",
+                VERDICT_CORRECT,
+                check="OTC003 共赢票据订单报表.总金额 = 1",
+                report_code="OTC003",
+                basis="产品原型 订单报表.md 「总金额」",
+            ),
+        ),
+    )
+    assert "依据:产品原型 订单报表.md 「股票数量」\n产品原型 订单报表.md 「价格」\n产品原型 订单报表.md 「总金额」" in body
+    assert "另有" not in body
+    assert "依据:产品原型 订单报表.md 「股票数量」;" not in body
+    assert "期望:股票数量\n价格\n总金额" in body
+
+
 def test_dimension_long_body_keeps_fields_and_errors() -> None:
 def test_dimension_long_body_keeps_fields_and_errors() -> None:
     """方向卡不按 3500 字删掉盘中公式。错误行和字段都留在正文里。"""
     """方向卡不按 3500 字删掉盘中公式。错误行和字段都留在正文里。"""
 
 

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

@@ -506,7 +506,7 @@ TRS 的字段公式不在 Excel 里,在产品原型 `TRS/4 业务引起数据
 无
 无
 ```
 ```
 
 
-章节之间空一行。一张报表或一个字段内部用单个换行,五行连在一起。空的错误、可疑、无法展开各写一行「:无」。
+章节之间空一行。一张报表或一个字段内部用单个换行,五行连在一起。同一格里有多条期望或多条依据时,每条单独一行,不用分号拼成一行,也不再收成「另有若干条」。空的错误、可疑、无法展开各写一行「:无」。
 
 
 ### 7.3 三张报表
 ### 7.3 三张报表
 
 

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

@@ -176,11 +176,11 @@ OTC009、OTC012 是实时导出,没有落库结果,`report_topup_payment`、
 
 
 飞书按订单链路、发行报表、销售报表、TRS 报表的顺序发。标题形如 `订单链路 可疑`、`发行报表 正确`。失败重发在末尾加「重发」。标题不写系统和清算步骤。颜色按这张卡的总结论:有错误为红,否则有可疑为橙,否则为蓝。
 飞书按订单链路、发行报表、销售报表、TRS 报表的顺序发。标题形如 `订单链路 可疑`、`发行报表 正确`。失败重发在末尾加「重发」。标题不写系统和清算步骤。颜色按这张卡的总结论:有错误为红,否则有可疑为橙,否则为蓝。
 
 
-订单链路正文是摘要、每单链路、盘中公式、清算对照、关联报表、错误清单、可疑清单、无法展开。摘要里的笔数按主单计。同一字段的正确行收成一块,结论写「正确(n/n)」。关联报表只写本轮主单在三张报表卡上的对错笔数,不把报表列再算一遍。三端门禁在摘要里各一行。
+订单链路正文是摘要、每单链路、盘中公式、清算对照、关联报表、错误清单、可疑清单、无法展开。摘要里的笔数按主单计。每单链路写两段:执行事实是否与实时源表一致;需要 TRS 执行根的对冲,成交股数是否对上,并说明自动单按 ext_ref、补录按委托号或母单号。不列表,也不把未核对的票据和已撤单写进去。同一字段的正确行收成一块,结论写「正确(n/n)」。关联报表只写本轮主单在三张报表卡上的对错笔数,不把报表列再算一遍。三端门禁在摘要里各一行。
 
 
 三张报表正文是批次门禁、摘要、分报表、错误清单、可疑清单、无法展开。没有门禁行时不写批次门禁。同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论。方向卡是错误主题时,正确列仍留在分报表。
 三张报表正文是批次门禁、摘要、分报表、错误清单、可疑清单、无法展开。没有门禁行时不写批次门禁。同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论。方向卡是错误主题时,正确列仍留在分报表。
 
 
-章节之间空一行。一张报表或一个字段内部用单个换行。空清单写成「错误清单:无」。方向卡不按 3500 字删报表或公式段。请求体超过约 28KB 时按结论行拆成多张,每张带同一个记录编号和「第几张,共几张」,整卡仍是一个 `notify_status`。公式里的制表符会去掉,`<` `>` 会改成全角。发送失败时日志带上飞书返回的 `msg`。
+章节之间空一行。一张报表或一个字段内部用单个换行。多条依据、多条期望各占一行。空清单写成「错误清单:无」。方向卡不按 3500 字删报表或公式段。请求体超过约 28KB 时按结论行拆成多张,每张带同一个记录编号和「第几张,共几张」,整卡仍是一个 `notify_status`。公式里的制表符会去掉,`<` `>` 会改成全角。发送失败时日志带上飞书返回的 `msg`。
 
 
 已有库要先放宽两道 CHECK,再启动。不要重跑 `schema.sql`,那会清空已有轮次。没执行时新的方向卡插不进去,这一轮会整段回滚。
 已有库要先放宽两道 CHECK,再启动。不要重跑 `schema.sql`,那会清空已有轮次。没执行时新的方向卡插不进去,这一轮会整段回滚。