hefuzheng 1 долоо хоног өмнө
parent
commit
9b80984dab

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


+ 38 - 15
code/post_clear_check/src/post_clear_check/trs_trade.py

@@ -256,24 +256,23 @@ def _detail_values(
 
 def _match_master(ctx: TrsContext, user: UserView, row: Mapping[str, Any]) -> tuple[str | None, Mapping[str, Any] | None]:
     token = _text(row.get("master_order_no"))
+    # 报表 master_order_no 是算法号时,只对算法母单。子单/清算委托上盖的 algo_no
+    # 只是累加成交金额用,不能当成主单命中,否则多条子单会把无对冲的 CRM 算法单判成「没有对上」。
+    algo_hits = [src for src in user.rows("trade_algo_master") if token and _text(src.get("algo_no")) == token]
+    if len(algo_hits) == 1:
+        return "trade_algo_master", algo_hits[0]
+    if len(algo_hits) > 1:
+        return "trade_algo_master", None
+
     hedges = [hedge for hedge in ctx.hedges if hedge.entrust_no == token or hedge.algo_no == token]
     order_name = _order_name(user)
-    prefer = "trade_algo_master" if any(hedge.algo_no == token for hedge in hedges) else None
-    if prefer is None and any(hedge.entrust_no == token for hedge in hedges):
-        prefer = order_name
     refs = {hedge.ext_ref for hedge in hedges}
-    if prefer:
-        matched = _hits(user, prefer, token, refs)
-        if len(matched) == 1:
-            return prefer, matched[0]
-        if len(matched) > 1:
-            return prefer, None
-    found: list[tuple[str, Mapping[str, Any]]] = []
-    for key in (order_name, "trade_algo_master"):
-        for src in _hits(user, key, token, refs):
-            found.append((key, src))
-    if len(found) == 1:
-        return found[0]
+    # 普通委托主单只按委托号 / 对冲 ext_ref;禁止再用 algo_no 去扫子单。
+    matched = _order_hits(user, order_name, token, refs)
+    if len(matched) == 1:
+        return order_name, matched[0]
+    if len(matched) > 1:
+        return order_name, None
     return None, None
 
 
@@ -316,6 +315,30 @@ def _hits(user: UserView, key: str, token: str | None, refs: set[str]) -> list[M
     return found
 
 
+def _order_hits(
+    user: UserView, key: str, token: str | None, refs: set[str]
+) -> list[Mapping[str, Any]]:
+    """主单级普通委托匹配:委托号或对冲 ext_ref,不用子单上的 algo_no。"""
+
+    found: list[Mapping[str, Any]] = []
+    seen: set[str] = set()
+    for src in user.rows(key):
+        linked = False
+        if token and _text(src.get("entrust_no")) == token:
+            linked = True
+        elif _text(src.get("ext_ref")) in refs:
+            linked = True
+        if not linked:
+            continue
+        ident = src.get("id")
+        mark = f"obj:{id(src)}" if ident is None else biz_text(ident)
+        if mark in seen:
+            continue
+        seen.add(mark)
+        found.append(src)
+    return found
+
+
 def _fill_source(ctx: TrsContext, user: UserView) -> tuple[str | None, list[Mapping[str, Any]]]:
     if user.has("clr_trade_fill") and not source_is_live(ctx, "clr_trade_fill"):
         return "clr_trade_fill", user.rows("clr_trade_fill")

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


+ 59 - 0
code/post_clear_check/tests/test_trs_reports.py

@@ -402,6 +402,65 @@ def test_algo_master_filled_amount_sums_child_fills_not_master_column() -> None:
     assert line.calc_value == Decimal("100.00")
 
 
+def test_crm_algo_master_ignores_child_algo_no_stamp() -> None:
+    """无对冲的 CRM 算法主单:子单上盖的 algo_no 不得把主单判成没有对上。"""
+
+    data = _data()
+    data["trs_users"]["U1"]["trade_algo_master"] = [
+        {
+            "id": 18,
+            "algo_no": "ALGO6",
+            "ext_ref": "algo-order:2:crm",
+            "user_id": 1,
+            "filled_qty": Decimal("1000"),
+            "filled_amount": Decimal("11479.00"),
+            "amount_trade": Decimal("11416.50"),
+            "amount_settle": Decimal("13439.10"),
+            "master_qty": Decimal("1000"),
+            "price": Decimal("0"),
+            "filled_avg_px": Decimal("11.479"),
+        }
+    ]
+    data["trs_users"]["U1"]["clr_trade_order"] = [
+        {
+            "id": 100 + i,
+            "src_id": 100 + i,
+            "entrust_no": f"18-{i}-1",
+            "algo_no": "ALGO6",
+            "algo_child_id": i,
+            "user_id": 1,
+            "filled_qty": Decimal("100"),
+        }
+        for i in range(1, 11)
+    ]
+    data["trs_users"]["U1"]["clr_trade_fill"] = [
+        {"id": i, "entrust_no": f"18-{i}-1", "filled_amount": Decimal("1147.90")} for i in range(1, 11)
+    ]
+    data["trs_users"]["U1"]["reports"][T_TRS002] = [
+        {
+            "id": 89,
+            "business_date": DAY,
+            "account": "U1",
+            "master_order_no": "ALGO6",
+            "filled_qty": Decimal("1000"),
+            "filled_amount": Decimal("11479.00"),
+            "entrust_qty": Decimal("1000"),
+            "entrust_price": Decimal("0"),
+            "filled_price": Decimal("11.479"),
+            "occur_amount_trade": Decimal("11416.50"),
+            "occur_amount_settle": Decimal("13439.10"),
+        }
+    ]
+    lines = check_trs_reports(data)
+    qty = _exact(lines, "成交数量 = trade_order 或算法母单上同号的列")
+    assert qty.verdict == "correct"
+    assert qty.calc_value == Decimal("1000")
+    amount = _exact(lines, "成交金额 = trade_order 或算法母单上同号的列")
+    assert amount.verdict == "correct"
+    assert amount.calc_value == Decimal("11479.00")
+    assert all("没有对上的业务行" not in line.calc_text for line in lines if "TRS002" in line.check_text)
+
+
 def test_realized_pnl_matches_leaf_sell_fill_sum() -> None:
     """TRS008 一行对一笔叶子卖出子单,实现盈亏等于该子单成交回报之和。"""
 

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

@@ -273,7 +273,7 @@ Excel 没有增额的计算公式(「票据」第 14 行「增额金额」只
 | 封存成交 | 按 `trade_date` 等于 T、证券市场等于本轮市场重查到的源表委托、成交,必须与本批 cleared 的 `src_id` 集合相同 | 多出或缺少都是错误。不把实时持仓数量当 T 日起点 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.3 TRS(10 步)」 |
 | 执行根 | 自动单按 `ext_ref`、补录单按委托号或母单号必须唯一存在;从未建单的合法结局不要求存在;算法子单不当成没有订单 | — | 研发设计 `docs/清算/三端清算最终实施方案.md`「5.4 TRS 核对、订单和交收计算」 |
 | 成交数量 | 必须出现 | 算法母单或普通委托的成交数量等于对冲记录。被核对的委托是 cleared | 产品原型 `TRS/4 业务引起数据变化及计算公式/4 业务引起数据变化及计算公式.md`「持仓与交易引起的字段变化及计算公式」 |
-| 报表行 | 成交数量大于 0 的普通主订单或算法母单必须有 TRS002 行,成交数量大于 0 的执行订单必须有 TRS003 行,有成交事实时 TRS004 必须有对应行。已撤、废单、未成交(成交数量为 0)不进这三张报表。对冲带回委托号或母单号,本身不要求有行 | 报表列等于 cleared 的委托或成交,不读实时委托。成交金额等于该委托成交回报 `fill_amount` 之和(算法母单、拆单主单累加其执行子单),四舍五入到分;不是成交数量乘成交均价。TRS004 没有成交金额列,不核这一列 | 产品原型 TRS002、TRS003、TRS004(见 4.13);研发设计清算报表按成交回报汇总 |
+| 报表行 | 成交数量大于 0 的普通主订单或算法母单必须有 TRS002 行,成交数量大于 0 的执行订单必须有 TRS003 行,有成交事实时 TRS004 必须有对应行。已撤、废单、未成交(成交数量为 0)不进这三张报表。对冲带回委托号或母单号,本身不要求有行 | 报表列等于 cleared 的委托或成交,不读实时委托。TRS002 的 `master_order_no` 是算法号时只对算法母单(含无发行对冲的 CRM 母单);子单上盖的 `algo_no` 只供累加成交金额,不能当主单命中。成交金额等于该委托成交回报 `fill_amount` 之和(算法母单、拆单主单累加其执行子单),四舍五入到分;不是成交数量乘成交均价。TRS004 没有成交金额列,不核这一列 | 产品原型 TRS002、TRS003、TRS004(见 4.13);研发设计清算报表按成交回报汇总 |
 | T日委托 | cleared 里该用户的成交必须能挂上 cleared 委托 | 交易日不晚于 T。后续成交不在这批里,不按 T 日用户汇率重算它们的发生额 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.3 TRS(10 步)」 |
 | 持仓 | TRS005 必须有对应行 | 持仓数量等于本批 cleared 的 `clr_user_position.position_qty`。持仓成本金额等于同一行的 `book_cost_amount`,不对 `cost_amount` | 产品原型 `TRS/4 …/4 业务引起数据变化及计算公式.md`「持仓与交易引起的字段变化及计算公式」;产品原型 TRS005 |
 | 持仓行 | 有 T 日持仓时 cleared 必须有该行;没有持仓时可以没有 | 同一行上 `position_qty`、`cost_amount`、`cost_px`、`book_cost_amount` 是第 5 步的值。`available_qty` 等于 `position_qty`,`frozen_qty` 为 0。`market_value`、`float_pnl`、`book_float_pnl` 是第 6 步写在同一行上的结果。不另找一份第 5 步快照,也不在本节点重算估值 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.3 TRS(10 步)」 |