فهرست منبع

fix#误报修复.

hefuzheng 1 هفته پیش
والد
کامیت
4b0f9102db

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


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


+ 23 - 9
code/post_clear_check/src/post_clear_check/trs_account.py

@@ -3,14 +3,14 @@
 同行等式只做列注释里写明的那几条。授信和信用四列不单造公式。
 TRS001 的可用、冻结对清算资金账户,持仓市值对清算持仓市值合计。
 TRS010 现金结余用本行四列相加。TRS014 持仓市值用同一合计,不乘清算价。
-TRS016 持仓成本金额对清算持仓 cost_amount 合计。
+TRS016 持仓成本金额对清算持仓 book_cost_amount 合计(与 TRS005 账面成本同源);同用户同结算币种多通道行先汇总报表再比。
 有非零持仓成本时,finance_rate 不得为 NULL(与区间导出要求一致);利率缺失记错误。
 持仓成本为 0 且利率为 NULL 时不产生计息行。
 """
 
 from __future__ import annotations
 
-from collections.abc import Mapping
+from collections.abc import Mapping, Sequence
 from decimal import Decimal
 from typing import Any
 
@@ -388,7 +388,13 @@ def check_holding_interest(ctx: TrsContext, lines: list[CheckLine]) -> None:
     for account, row in rows:
         where = where_text(T_TRS016, account, None)
         _interest_line(ctx, lines, account, row, where)
-        _holding_cost_line(ctx, lines, account, row, where)
+    # 持仓成本按用户+结算币种汇总后再对清算账面成本,避免通道行各自去对用户合计。
+    cost_groups: dict[tuple[str, str | None], list[Mapping[str, Any]]] = {}
+    for account, row in rows:
+        cost_groups.setdefault((account, _text_ccy(row)), []).append(row)
+    for (account, ccy), group in sorted(cost_groups.items(), key=lambda item: (item[0][0], item[0][1] or "")):
+        where = where_text(T_TRS016, account, None)
+        _holding_cost_line(ctx, lines, account, group, ccy, where)
 
 
 def _interest_line(
@@ -460,23 +466,31 @@ def _holding_cost_line(
     ctx: TrsContext,
     lines: list[CheckLine],
     account: str,
-    row: Mapping[str, Any],
+    rows: Sequence[Mapping[str, Any]],
+    ccy: str | None,
     where: str,
 ) -> None:
-    # 按用户和结算币种把清算持仓 cost_amount 加总。不按证券或通道拆开。
+    # 原型:TRS016 成本来自 TRS005,TRS005 用账面成本。按用户+结算币种加总 book_cost_amount。
     calc_text, calc_value = _named_summary(
-        ctx.users[account], _text_ccy(row), "clr_user_position", "cost_amount", "sum"
+        ctx.users[account], ccy, "clr_user_position", "book_cost_amount", "sum"
     )
+    if all("holding_cost" in row for row in rows):
+        report_cost = sum_column(rows, "holding_cost")
+        merged: Mapping[str, Any] = {"holding_cost": report_cost}
+    elif any("holding_cost" in row for row in rows):
+        merged = {"holding_cost": None}
+    else:
+        merged = {}
     emit(
         lines,
         formula="持仓成本金额 = 清算持仓成本合计",
-        basis="持仓成本金额等于该用户该结算币种清算持仓 cost_amount 合计",
+        basis="持仓成本金额等于该用户该结算币种清算持仓 book_cost_amount 合计",
         calc_text=calc_text,
         calc_value=calc_value,
         report_name=TRS016_NAME,
         column="持仓成本金额",
-        row=row,
-        field="holding_cost",
+        row=merged if "holding_cost" in merged else None,
+        field="holding_cost" if "holding_cost" in merged else None,
         where=where,
         scale=SCALE_AMT,
     )

+ 78 - 8
code/post_clear_check/src/post_clear_check/trs_flow.py

@@ -71,16 +71,32 @@ def check_cash_flow(ctx: TrsContext, lines: list[CheckLine]) -> None:
 
 
 def _holdings(ctx: TrsContext, lines: list[CheckLine]) -> None:
+    """TRS005 按通道拆行;清算持仓按证券+渠道聚合。先按对齐键汇总报表再对清算。"""
+
+    grouped: dict[tuple[str, str | None, str | None, str | None], list[Mapping[str, Any]]] = {}
     for account, row in report_rows(ctx, T_TRS005):
+        key = (
+            account,
+            _text(row.get("sec_code")),
+            _text(row.get("trade_channel")),
+            _text(row.get("settle_ccy")),
+        )
+        grouped.setdefault(key, []).append(row)
+    for (account, sec_code, _channel, _ccy), rows in sorted(grouped.items(), key=lambda item: item[0]):
         user = ctx.users[account]
-        key, positions, live = _position_source(ctx, user)
+        source_key, positions, live = _position_source(ctx, user)
         where = where_text(T_TRS005, account, None)
-        aligned = _align(row, positions, ("sec_code", "trade_channel", "settle_ccy"))
+        if sec_code:
+            where = f"{where} / {sec_code}" if where else sec_code
+        # 用组内首行对齐清算持仓;同键多通道行的数量与账面成本先加总再比。
+        sample = rows[0]
+        aligned = _align(sample, positions, ("sec_code", "trade_channel", "settle_ccy"))
+        merged = _merged_holding_report(rows, sample)
         _position_measure(
             lines,
-            row,
+            merged,
             where,
-            key,
+            source_key,
             aligned,
             "position_qty",
             "持仓数量",
@@ -90,9 +106,9 @@ def _holdings(ctx: TrsContext, lines: list[CheckLine]) -> None:
         )
         _position_measure(
             lines,
-            row,
+            merged,
             where,
-            key,
+            source_key,
             aligned,
             "book_cost_amount",
             "持仓成本金额",
@@ -103,6 +119,27 @@ def _holdings(ctx: TrsContext, lines: list[CheckLine]) -> None:
         )
 
 
+def _merged_holding_report(
+    rows: Sequence[Mapping[str, Any]],
+    sample: Mapping[str, Any],
+) -> dict[str, Any]:
+    """同对齐键的多通道报表行合成一行,数量与账面成本取合计。"""
+
+    merged = dict(sample)
+    if all("position_qty" in row for row in rows):
+        merged["position_qty"] = _sum(rows, "position_qty")
+    else:
+        merged.pop("position_qty", None)
+    if all("position_cost_settle" in row for row in rows):
+        merged["position_cost_settle"] = _sum(rows, "position_cost_settle")
+    elif any("position_cost_settle" in row for row in rows):
+        # 部分行缺列:保留键且值为空,emit 记缺失,避免静默少加。
+        merged["position_cost_settle"] = None
+    else:
+        merged.pop("position_cost_settle", None)
+    return merged
+
+
 def _position_source(
     ctx: TrsContext,
     user: UserView,
@@ -597,7 +634,7 @@ def _in_out(ctx: TrsContext, lines: list[CheckLine]) -> None:
             if id(record) in used:
                 continue
             where = where_text(T_TRS009, account, None)
-            calc_text, calc_value, _force = _named("fund_in_out_record", "amount", record, SCALE_AMT)
+            calc_text, calc_value, _force = _signed_in_out_amount(record)
             emit(
                 lines,
                 formula="金额 = 出入金流水的同号列",
@@ -628,7 +665,8 @@ def _in_out_amount(
     elif "amount" not in record:
         calc_text, calc_value, force = "fund_in_out_record 没有同号列 amount", None, None
     else:
-        calc_text, calc_value, force = _named("fund_in_out_record", "amount", record, SCALE_AMT)
+        # 流水存绝对值+direction;报表 Amount 入正出负。按方向赋符号后再比。
+        calc_text, calc_value, force = _signed_in_out_amount(record)
     emit(
         lines,
         formula="金额 = 出入金流水的同号列",
@@ -645,6 +683,38 @@ def _in_out_amount(
     )
 
 
+def _signed_in_out_amount(record: Mapping[str, Any]) -> tuple[str, Decimal | None, str | None]:
+    """流水 amount 按 direction/dw 转成与报表一致的带符号金额。"""
+
+    present, number = present_number(record, "amount")
+    if not present or number is None:
+        return "fund_in_out_record.amount 缺失", None, None
+    direction = _text(record.get("direction")) if "direction" in record else _text(record.get("dw"))
+    signed = _apply_in_out_sign(number, direction)
+    if direction in {"out", "W", "w"}:
+        note = "出金取负"
+    elif direction in {"in", "D", "d"}:
+        note = "入金取正"
+    else:
+        note = "方向未知,沿用原值"
+    return (
+        f"fund_in_out_record.amount = {format_decimal(signed, SCALE_AMT)}({note})",
+        signed,
+        None,
+    )
+
+
+def _apply_in_out_sign(amount: Decimal, direction: str | None) -> Decimal:
+    """入金为正、出金为负;与 TRS009 原型 Amount 符号规则一致。"""
+
+    magnitude = abs(amount)
+    if direction in {"out", "W", "w"}:
+        return -magnitude
+    if direction in {"in", "D", "d"}:
+        return magnitude
+    return amount
+
+
 def _in_out_direction(
     lines: list[CheckLine],
     row: Mapping[str, Any],

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


+ 109 - 8
code/post_clear_check/tests/test_trs_reports.py

@@ -106,6 +106,92 @@ def test_fund_in_out_matches_source_id() -> None:
     assert direction.verdict == "correct"
 
 
+def test_fund_in_out_withdrawal_uses_negative_sign() -> None:
+    """流水出金金额为正、direction=out;报表 Amount 为负。按方向赋符号后再比。"""
+
+    data = _data()
+    data["trs_users"]["U1"]["reports"][T_TRS009] = [
+        {
+            "id": 5,
+            "business_date": DAY,
+            "client_code": "U1",
+            "ref": "14",
+            "amount": Decimal("-80000.00"),
+            "dw": "W",
+        }
+    ]
+    data["trs_users"]["U1"]["fund_in_out_record"] = [
+        {
+            "id": 36,
+            "user_id": 1,
+            "source_id": 14,
+            "source_no": "WTH1",
+            "direction": "out",
+            "amount": Decimal("80000.00"),
+            "business_date": DAY,
+        }
+    ]
+    lines = check_trs_reports(data)
+    amount = _exact(lines, "金额 = 出入金流水的同号列")
+    direction = _exact(lines, "方向 = 出入金流水的方向")
+    assert amount.verdict == "correct"
+    assert amount.calc_value == Decimal("-80000.00")
+    assert amount.check_value == Decimal("-80000.00")
+    assert direction.verdict == "correct"
+
+
+def test_cleared_position_sums_channel_report_rows() -> None:
+    """同一证券多通道报表行先汇总数量与账面成本,再对清算聚合持仓。"""
+
+    data = _data()
+    data["trs_users"]["U1"]["clr_user_position"] = [
+        {
+            "user_id": 1,
+            "sec_code": "688286",
+            "trade_channel": "qfii",
+            "settle_ccy": "HKD",
+            "position_qty": Decimal("69000"),
+            "book_cost_amount": Decimal("4775248.42"),
+            "cost_amount": Decimal("5288671.41"),
+        }
+    ]
+    data["trs_users"]["U1"]["reports"][T_TRS005] = [
+        {
+            "id": 1,
+            "business_date": DAY,
+            "account": "U1",
+            "channel_name": "华盛001",
+            "sec_code": "688286",
+            "trade_channel": "qfii",
+            "settle_ccy": "HKD",
+            "position_qty": Decimal("68900"),
+            "position_cost_settle": Decimal("4775248.42"),
+        },
+        {
+            "id": 2,
+            "business_date": DAY,
+            "account": "U1",
+            "channel_name": "模拟网关001",
+            "sec_code": "688286",
+            "trade_channel": "qfii",
+            "settle_ccy": "HKD",
+            "position_qty": Decimal("100"),
+            "position_cost_settle": Decimal("0.00"),
+        },
+    ]
+    lines = check_trs_reports(data)
+    qty_lines = [line for line in lines if line.formula_text == "持仓数量 = 清算后持仓的同号列"]
+    cost_lines = [line for line in lines if line.formula_text == "持仓成本金额 = 清算持仓账面成本"]
+    assert len(qty_lines) == 1
+    assert qty_lines[0].verdict == "correct"
+    assert qty_lines[0].calc_value == Decimal("69000")
+    assert qty_lines[0].check_value == Decimal("69000")
+    assert len(cost_lines) == 1
+    assert cost_lines[0].verdict == "correct"
+    assert cost_lines[0].calc_value == Decimal("4775248.42")
+    assert cost_lines[0].check_value == Decimal("4775248.42")
+
+
 def test_interest_collect_matches_period_keys() -> None:
     """TRS007 利息收取金额对 fund_interest_collect 同区间同币种。"""
 
@@ -199,11 +285,13 @@ def test_daily_position_mv_sums_cleared_market_value() -> None:
     assert line.calc_value == Decimal("7")
 
 
-def test_holding_cost_sums_cost_amount() -> None:
+def test_holding_cost_sums_book_cost_amount() -> None:
+    """TRS016 持仓成本对清算账面成本合计,并对同币种多通道报表行先汇总。"""
+
     data = _data()
     data["trs_users"]["U1"]["clr_user_position"] = [
-        {"user_id": 1, "settle_ccy": "CNY", "cost_amount": Decimal("3")},
-        {"user_id": 1, "settle_ccy": "CNY", "cost_amount": Decimal("2")},
+        {"user_id": 1, "settle_ccy": "CNY", "book_cost_amount": Decimal("3"), "cost_amount": Decimal("1")},
+        {"user_id": 1, "settle_ccy": "CNY", "book_cost_amount": Decimal("2"), "cost_amount": Decimal("9")},
     ]
     data["trs_users"]["U1"]["reports"][T_TRS016] = [
         {
@@ -211,13 +299,26 @@ def test_holding_cost_sums_cost_amount() -> None:
             "nature_date": DAY,
             "account": "U1",
             "settle_ccy": "CNY",
-            "holding_cost": Decimal("5"),
+            "channel_account": "A",
+            "holding_cost": Decimal("4"),
             "finance_rate": None,
-        }
+        },
+        {
+            "id": 2,
+            "nature_date": DAY,
+            "account": "U1",
+            "settle_ccy": "CNY",
+            "channel_account": "B",
+            "holding_cost": Decimal("1"),
+            "finance_rate": None,
+        },
     ]
-    line = _exact(check_trs_reports(data), "持仓成本金额 = 清算持仓成本合计")
-    assert line.verdict == "correct"
-    assert line.calc_value == Decimal("5")
+    lines = [line for line in check_trs_reports(data) if line.formula_text == "持仓成本金额 = 清算持仓成本合计"]
+    assert len(lines) == 1
+    assert lines[0].verdict == "correct"
+    assert lines[0].calc_value == Decimal("5")
+    assert lines[0].check_value == Decimal("5")
+    assert "book_cost_amount" in lines[0].calc_text
 
 
 def test_payable_interest_uses_report_formula_and_uncollected_must_appear() -> None:

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

@@ -275,7 +275,7 @@ Excel 没有增额的计算公式(「票据」第 14 行「增额金额」只
 | 成交数量 | 必须出现 | 算法母单或普通委托的成交数量等于对冲记录。被核对的委托是 cleared | 产品原型 `TRS/4 业务引起数据变化及计算公式/4 业务引起数据变化及计算公式.md`「持仓与交易引起的字段变化及计算公式」 |
 | 报表行 | 成交数量大于 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 |
+| 持仓 | TRS005 必须有对应行 | 持仓数量等于本批 cleared 的 `clr_user_position.position_qty`。持仓成本金额等于同一行的 `book_cost_amount`,不对 `cost_amount`。TRS005 按通道拆行时,先按证券+交易渠道+结算币种汇总报表再对清算聚合持仓 | 产品原型 `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 步)」 |
 | 候选成交 | cleared 的委托和成交必须出现 | `trade_date` 不晚于 T。源表里 `trade_date` 晚于 T 的委托和成交,主键不得出现在 cleared 的 `src_id` | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.3 TRS(10 步)」 |
 | 平仓已实现盈亏 | 有卖出时 TRS008 必须有行 | 报表列等于业务行 | 产品原型 TRS008 |
@@ -296,11 +296,11 @@ Excel 没有增额的计算公式(「票据」第 14 行「增额金额」只
 | 基础总资产 | 必须出现 | 本金金额 + 融资金额 | 产品原型 `TRS/4 …/4 业务引起数据变化及计算公式.md`「重点字段与计算公式」基础总资产 |
 | 杠杆 | 必须出现 | 融资金额 = 本金金额 × 杠杆比例 | 同文件「用户交易或后台操作引起的字段变化及计算公式」 |
 | 总资产 | 必须出现 | 可用金额 + 冻结金额 + 持仓市值(基金持仓市值本期按 0) | 同文件「重点字段与计算公式」总资产 |
-| 可用金额流水、出入金 | 有变动时 TRS006、TRS009 必须有行 | 报表列等于业务行。TRS006 的金额来自本批原始流水快照和 `clearing_fund_ledger`,不读实时流水 | 产品原型 TRS006、TRS009 |
+| 可用金额流水、出入金 | 有变动时 TRS006、TRS009 必须有行 | 报表列等于业务行。TRS006 的金额来自本批原始流水快照和 `clearing_fund_ledger`,不读实时流水。TRS009 流水 `amount` 按 `direction` 赋符号后与报表 Amount 比较(入正出负) | 产品原型 TRS006、TRS009 |
 | 计息 | 有融资利率时必须出现 | 应付利息 = 计息金额 × 融资利率 ÷ 100 ÷ 365。报表年化是百分数,3.2542 表示 3.2542%。利率变动、利息收取与明细一致 | 同文件「重点字段与计算公式」应付利息;产品原型 TRS007、TRS012、TRS013、TRS016;研发设计 `docs/清算/三端清算最终实施方案.md`「4.3 用户汇率和应付利息」 |
 | T日债务 | 本批 `interest_input` 的 `debt` 行必须保留 T 日债务 | 等于计息日不晚于 T、且未收取或收取营业日晚于 T 的利息合计。T+1 已收取的明细仍算在这笔债务里,不能因为当前状态已是 collected 就冲掉 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.3 TRS(10 步)」 |
 | 汇率调整 | 差额不为 0 时本批快照必须有一条 `clearing_fund_ledger`;差额为 0 时必须没有 | 金额等于 T 日 cleared 成交现金减 T 日原始买卖凭证。成交现金按卖出加 `amount_settle`、买入减 `amount_settle`。原始凭证只取封存快照里的 `stock_buy`、`stock_sell`,不读实时 `fund_ledger`。后续成交两边都不进 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.3 TRS(10 步)」 |
-| 资金账户 | 报表有该币种时,cleared 必须有该用户该币种 | 本金、融资、可用、冻结是第 5 步写上的列。第 6 步不替换这张表。TRS001 可用金额等于 `clr_fund_account.available`,冻结金额等于 `frozen`。TRS001 与 TRS014 的持仓市值等于该用户该结算币种 `clr_user_position.market_value` 合计,不乘清算价。TRS016 持仓成本金额等于该用户该结算币种 `cost_amount` 合计;有非零持仓成本时 `finance_rate` 不得为空(与区间导出「未维护月度利率则拒绝导出」一致,缺利率记错误)。TRS010 现金结余等于本行承前现金、出入金额、实际盈亏、已付利息之和。TRS013 当日应付利息等于(融资金额 + 授信借贷 + 信用借贷)× 年化 / 100 / 365,年化按百分数;`status=uncollected` 且计息日为本营业日的应付利息必须有报表行 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.3 TRS(10 步)」;产品原型 TRS001、TRS010、TRS013、TRS014、TRS016 |
+| 资金账户 | 报表有该币种时,cleared 必须有该用户该币种 | 本金、融资、可用、冻结是第 5 步写上的列。第 6 步不替换这张表。TRS001 可用金额等于 `clr_fund_account.available`,冻结金额等于 `frozen`。TRS001 与 TRS014 的持仓市值等于该用户该结算币种 `clr_user_position.market_value` 合计,不乘清算价。TRS016 持仓成本金额等于该用户该结算币种 `book_cost_amount` 合计(与 TRS005 账面成本同源;同币种多通道报表行先汇总再比);有非零持仓成本时 `finance_rate` 不得为空(与区间导出「未维护月度利率则拒绝导出」一致,缺利率记错误)。TRS010 现金结余等于本行承前现金、出入金额、实际盈亏、已付利息之和。TRS013 当日应付利息等于(融资金额 + 授信借贷 + 信用借贷)× 年化 / 100 / 365,年化按百分数;`status=uncollected` 且计息日为本营业日的应付利息必须有报表行 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.3 TRS(10 步)」;产品原型 TRS001、TRS010、TRS013、TRS014、TRS016 |
 | 候选资金 | 封存的非交易事实必须出现 | 营业日不晚于 T。源表里营业日晚于 T 的非交易流水、持仓流水和基础调整不得出现在本批快照 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.3 TRS(10 步)」 |
 | 净资产 | 必须出现 | 总资产 − 融资金额 − 应付利息(授信、信用四项本期不做,不单造公式) | 同文件「重点字段与计算公式」净资产 |
 

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
doc/使用说明.md


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است