Bladeren bron

fix#可疑项误报修复.

hefuzheng 1 week geleden
bovenliggende
commit
906aac3225

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


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_common.cpython-312.pyc


+ 4 - 1
code/post_clear_check/src/post_clear_check/chain.py

@@ -422,7 +422,10 @@ _USER_ASSET_FORMULAS: Mapping[str, str] = {
     "基础总资产": _prototype(_TRS_FORMULA_MD, _TRS_KEY_FIELDS, "基础总资产"),
     "总资产": _prototype(_TRS_FORMULA_MD, _TRS_KEY_FIELDS, "总资产"),
     "净资产": _prototype(_TRS_FORMULA_MD, _TRS_KEY_FIELDS, "净资产"),
-    "当日计息金额": _prototype(_TRS_FORMULA_MD, _TRS_KEY_FIELDS, "应付利息"),
+    "当日计息金额": _prototype(
+        f"{_TRS_DAILY}/16 Holdings Amount Interest Report/16 Holdings Amount Interest Report.md",
+        "融资利息计算公式",
+    ),
     # 与区间导出同一门槛:非零持仓成本必须有当月有效融资利率。
     "有效月度融资利率": _prototype(
         f"{_TRS_DAILY}/16 Holdings Amount Interest Report/16 Holdings Amount Interest Report.md",

+ 15 - 7
code/post_clear_check/src/post_clear_check/trs_account.py

@@ -5,6 +5,7 @@ TRS001 的可用、冻结对清算资金账户,持仓市值对清算持仓市
 TRS010 现金结余用本行四列相加。TRS014 持仓市值用同一合计,不乘清算价。
 TRS016 持仓成本金额对清算持仓 book_cost_amount 合计(与 TRS005 账面成本同源);同用户同结算币种多通道行先汇总报表再比。
 有非零持仓成本时,finance_rate 不得为 NULL(与区间导出要求一致);利率缺失记错误。
+当日计息按百分数利率 /100,CNY 年天数 360、HKD 365。无清算持仓时市值与成本合计为 0。
 持仓成本为 0 且利率为 NULL 时不产生计息行。
 """
 
@@ -44,7 +45,7 @@ from post_clear_check.trs_common import (
     add_text,
     emit,
     is_zero,
-    lookup_year_days,
+    year_days_for_trs016,
     one_column,
     plain,
     report_rows,
@@ -433,28 +434,32 @@ def _interest_line(
         return
     if rate_missing:
         return
-    seen, year_days = lookup_year_days(ctx, ctx.users[account], row)
+    # 原型与清算一致:利率按百分数先除 100;CNY 年天数 360,HKD 365。
+    ccy = _text_ccy(row)
+    seen, year_days = year_days_for_trs016(ccy)
     if cost is None or rate is None or not seen or year_days is None or year_days == 0:
         missing = []
         if cost is None:
             missing.append("持仓成本金额缺失")
         if rate is None:
             missing.append("当月融资利率缺失")
-        if not seen or year_days is None:
-            missing.append("年天数缺失")
+        if not ccy:
+            missing.append("结算币种缺失")
+        elif not seen or year_days is None:
+            missing.append(f"结算币种 {ccy} 不支持年天数")
         elif year_days == 0:
             missing.append("年天数为 0")
         calc_text, calc_value = ",".join(missing), None
     else:
-        calc_value = cost * rate / year_days
+        calc_value = cost * rate / Decimal("100") / year_days
         calc_text = (
             f"{format_decimal(cost, SCALE_AMT)} × {format_decimal(rate, SCALE_RATE6)}"
-            f" / {plain(year_days)} = {format_decimal(calc_value, SCALE_AMT)}"
+            f" / 100 / {plain(year_days)} = {format_decimal(calc_value, SCALE_AMT)}"
         )
     emit(
         lines,
         formula=F_INTEREST,
-        basis="当日计息金额=持仓成本金额×当月融资利率/年天数",
+        basis="当日计息金额=持仓成本金额×当月融资利率/100/年天数(CNY=360,HKD=365)",
         calc_text=calc_text,
         calc_value=calc_value,
         report_name=TRS016_NAME,
@@ -520,7 +525,10 @@ def _named_summary(
 ) -> tuple[str, Decimal | None]:
     """只读指定清算表的指定列。空持仓合计为 0。"""
 
+    # 无持仓用户取数时不会写入空列表;合计口径仍按 0,避免把「无持仓」误报成缺表。
     if not user.has(source):
+        if aggregate == "sum":
+            return f"{source}.{column} 合计 0.00", Decimal(0)
         return f"取数结果没有 {source}", None
     rows = [row for row in user.rows(source) if column in row]
     if not rows and user.rows(source):

+ 12 - 2
code/post_clear_check/src/post_clear_check/trs_common.py

@@ -55,7 +55,7 @@ F_LEVERAGE = "杠杆倍数 = 融资金额 / 本金金额"
 F_LEVERAGE_CONFIG = "杠杆倍数 = 资金账户配置杠杆"
 F_CASH_ASSET = "总资产 = 现金 + 持仓市值"
 F_NET = "净资产 = 总资产 − 融资及借贷 − 信用本金 − 应付利息"
-F_INTEREST = "当日计息金额 = 持仓成本金额 × 当月融资利率 / 年天数"
+F_INTEREST = "当日计息金额 = 持仓成本金额 × 当月融资利率 / 100 / 年天数"
 # 与区间导出一致:有非零持仓成本时,当月结算币种必须有年化大于 0 的月度利率。
 F_MONTHLY_RATE = "有效月度融资利率 = 当月结算币种在 cfg_holdings_interest_rate 维护且年化大于 0"
 
@@ -562,8 +562,18 @@ def _belongs(row: Mapping[str, Any], account: str, hedges: Sequence[Hedge], buck
     return _account_by_user_id(row.get("user_id"), {account: bucket}) == account
 
 
+def year_days_for_trs016(ccy: str | None) -> tuple[bool, Decimal | None]:
+    """TRS016 原型:CNY 用 360,HKD 用 365。其他币种不算。"""
+
+    if ccy == "CNY":
+        return True, Decimal("360")
+    if ccy == "HKD":
+        return True, Decimal("365")
+    return False, None
+
+
 def lookup_year_days(ctx: TrsContext, user: UserView, row: Mapping[str, Any]) -> tuple[bool, Decimal | None]:
-    """年天数只认英文列 year_days。不默认 365 或 360。"""
+    """兼容旧注入:显式 year_days 仍可读;生产路径用 year_days_for_trs016。"""
 
     for source in (row, user.raw):
         if "year_days" in source:

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


+ 81 - 13
code/post_clear_check/tests/test_trs_reports.py

@@ -432,23 +432,85 @@ def test_null_finance_rate_with_zero_cost_skips_interest_line() -> None:
     assert F_MONTHLY_RATE not in formulas
 
 
-def test_interest_without_year_days_is_suspect() -> None:
+def test_empty_cleared_position_market_value_is_zero() -> None:
+    """无 clr_user_position 键时持仓市值合计为 0,与报表 0 对齐为正确。"""
+
+    data = _data(
+        profile=_profile(
+            available_amt=Decimal("10"),
+            frozen_amt=Decimal("0"),
+            position_mv=Decimal("0"),
+            total_asset=Decimal("10"),
+            total_float_pnl=Decimal("-20"),
+        )
+    )
+    data["trs_users"]["U1"]["clr_fund_account"] = [
+        {"user_id": 1, "ccy": "CNY", "available": Decimal("10"), "frozen": Decimal("0")}
+    ]
+    data["trs_users"]["U1"]["reports"][T_TRS014] = [
+        {
+            "id": 1,
+            "business_date": DAY,
+            "account": "U1",
+            "settle_ccy": "CNY",
+            "position_mv": Decimal("0"),
+            "total_asset": Decimal("10"),
+            "credit_principal": Decimal("0"),
+            "payable_interest": Decimal("0"),
+            "net_asset": Decimal("10"),
+        }
+    ]
+    lines = check_trs_reports(data)
+    profile_mv = [line for line in lines if line.formula_text == "持仓市值 = 用户资金与持仓汇总"]
+    daily_mv = _exact(lines, "持仓市值 = 清算持仓市值合计")
+    assert len(profile_mv) == 1
+    assert profile_mv[0].verdict == "correct"
+    assert profile_mv[0].calc_value == Decimal("0.00")
+    assert daily_mv.verdict == "correct"
+    assert daily_mv.calc_value == Decimal("0.00")
+
+
+def test_interest_cny_uses_360_and_percent_rate() -> None:
+    """CNY:成本 × 利率 / 100 / 360。"""
+
     data = _data()
-    data["trs_users"]["U1"]["reports"][T_TRS016] = [_interest_row()]
+    data["trs_users"]["U1"]["reports"][T_TRS016] = [
+        _interest_row(holding_cost=Decimal("36000"), finance_rate=Decimal("1"), interest=Decimal("1"))
+    ]
     line = _exact(check_trs_reports(data), F_INTEREST)
-    assert line.verdict == "suspect"
-    assert line.calc_value is None
-    assert "年天数缺失" in line.calc_text
+    assert line.verdict == "correct"
+    assert line.calc_value == Decimal("1.00")
+    assert line.check_value == Decimal("1.00")
+    assert "/ 100 / 360" in line.calc_text
+
 
+def test_interest_hkd_uses_365_and_percent_rate() -> None:
+    """HKD:成本 × 利率 / 100 / 365。"""
 
-def test_interest_uses_given_year_days() -> None:
     data = _data()
-    data["year_days"] = Decimal("365")
-    data["trs_users"]["U1"]["reports"][T_TRS016] = [_interest_row(interest=Decimal("1"))]
+    data["trs_users"]["U1"]["reports"][T_TRS016] = [
+        _interest_row(
+            settle_ccy="HKD",
+            holding_cost=Decimal("36500"),
+            finance_rate=Decimal("1"),
+            interest=Decimal("1"),
+        )
+    ]
     line = _exact(check_trs_reports(data), F_INTEREST)
     assert line.verdict == "correct"
     assert line.calc_value == Decimal("1.00")
-    assert line.check_value == Decimal("1.00")
+    assert "/ 100 / 365" in line.calc_text
+
+
+def test_interest_unsupported_ccy_is_suspect() -> None:
+    data = _data()
+    data["trs_users"]["U1"]["reports"][T_TRS016] = [
+        _interest_row(settle_ccy="USD", interest=Decimal("1"))
+    ]
+    line = _exact(check_trs_reports(data), F_INTEREST)
+    assert line.verdict == "suspect"
+    assert line.calc_value is None
+    assert "不支持年天数" in line.calc_text
 
 
 def test_does_not_emit_trs015() -> None:
@@ -995,13 +1057,19 @@ def _profile(**overrides: object) -> dict:
     return row
 
 
-def _interest_row(interest: Decimal | None = Decimal("9")) -> dict:
+def _interest_row(
+    interest: Decimal | None = Decimal("1"),
+    *,
+    settle_ccy: str = "CNY",
+    holding_cost: Decimal = Decimal("36000"),
+    finance_rate: Decimal = Decimal("1"),
+) -> dict:
     return {
         "id": 9,
         "nature_date": DAY,
         "account": "U1",
-        "settle_ccy": "CNY",
-        "holding_cost": Decimal("365"),
-        "finance_rate": Decimal("1"),
+        "settle_ccy": settle_ccy,
+        "holding_cost": holding_cost,
+        "finance_rate": finance_rate,
         "interest": interest,
     }

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

@@ -299,10 +299,10 @@ Excel 没有增额的计算公式(「票据」第 14 行「增额金额」只
 | 杠杆 | 必须出现 | 融资金额 = 本金金额 × 杠杆比例 | 同文件「用户交易或后台操作引起的字段变化及计算公式」 |
 | 总资产 | 必须出现 | 可用金额 + 冻结金额 + 持仓市值(基金持仓市值本期按 0) | 同文件「重点字段与计算公式」总资产 |
 | 可用金额流水、出入金 | 有变动时 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 用户汇率和应付利息」 |
+| 计息 | 有融资利率时必须出现 | TRS013 应付利息 = 计息金额 × 融资利率 ÷ 100 ÷ 365。TRS016 当日计息金额 = 持仓成本金额 × 当月融资利率 ÷ 100 ÷ 年天数(CNY=360,HKD=365)。报表年化是百分数,3.2542 表示 3.2542%。利率变动、利息收取与明细一致 | TRS013:同文件「重点字段与计算公式」应付利息;TRS016:产品原型 `16 Holdings Amount Interest Report.md`「融资利息计算公式」;TRS007、TRS012 同计息口径 |
 | 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 持仓成本金额等于该用户该结算币种 `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 |
+| 资金账户 | 报表有该币种时,cleared 必须有该用户该币种 | 本金、融资、可用、冻结是第 5 步写上的列。第 6 步不替换这张表。TRS001 可用金额等于 `clr_fund_account.available`,冻结金额等于 `frozen`。TRS001 与 TRS014 的持仓市值等于该用户该结算币种 `clr_user_position.market_value` 合计,不乘清算价;该用户本批没有清算持仓行时合计为 0,与报表 0 对齐为正确。TRS016 持仓成本金额等于该用户该结算币种 `book_cost_amount` 合计(与 TRS005 账面成本同源;同币种多通道报表行先汇总再比);有非零持仓成本时 `finance_rate` 不得为空(与区间导出「未维护月度利率则拒绝导出」一致,缺利率记错误)。TRS016 当日计息金额 = 持仓成本金额 × 当月融资利率 / 100 / 年天数(CNY=360,HKD=365),利率按百分数。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 步)」 |
 | 净资产 | 必须出现 | 总资产 − 融资金额 − 应付利息(授信、信用四项本期不做,不单造公式) | 同文件「重点字段与计算公式」净资产 |
 

File diff suppressed because it is too large
+ 0 - 0
doc/使用说明.md


Some files were not shown because too many files changed in this diff