|
|
@@ -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}")
|