Bläddra i källkod

feat#支持三线比例核对。

hefuzheng 1 vecka sedan
förälder
incheckning
dd675c628d

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


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


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


+ 6 - 0
code/post_clear_check/src/post_clear_check/chain.py

@@ -306,6 +306,9 @@ GATE_MISSING_TABLE = "业务表应存在"
 _HEDGE_ROOT = _design(_CLEARING_PLAN, "5.4 TRS 核对、订单和交收计算")
 
 _BUY_FORMULAS: Mapping[str, str] = {
+    "distance_warning": _design(_CHAIN_DESIGN, "4.2 买入执行"),
+    "distance_danger": _design(_CHAIN_DESIGN, "4.2 买入执行"),
+    "distance_force": _design(_CHAIN_DESIGN, "4.2 买入执行"),
     "对冲记录成交股数": _HEDGE_ROOT,
     "累计成交股数": _excel("买入订单", 14, "累计成交股数"),
     "对冲含费成交均价": _excel("买入订单", 15, "对冲含费成交均价"),
@@ -324,6 +327,9 @@ _BUY_FORMULAS: Mapping[str, str] = {
 }
 
 _SELL_FORMULAS: Mapping[str, str] = {
+    "distance_warning": _design(_CHAIN_DESIGN, "4.3 卖出执行"),
+    "distance_danger": _design(_CHAIN_DESIGN, "4.3 卖出执行"),
+    "distance_force": _design(_CHAIN_DESIGN, "4.3 卖出执行"),
     "对冲记录成交股数": _HEDGE_ROOT,
     "累计成交股数": _excel("卖出订单", 12, "累计成交股数"),
     "对冲含费成交均价": _excel("卖出订单", 13, "对冲含费成交均价"),

+ 8 - 0
code/post_clear_check/src/post_clear_check/columns.py

@@ -215,6 +215,14 @@ NOTE_SUB_COLUMNS = (
     "add_amt",
     "dividend_amt",
     "cur_px",
+    # 风控距离在清算时随价格线和最新价重算,核对已发布清算结果。
+    "latest_px",
+    "warn_price",
+    "danger_price",
+    "force_price",
+    "distance_warning",
+    "distance_danger",
+    "distance_force",
     "init_fx",
     "a_yield_ratio",
     # 第 2 步新票据的期初用初始数量和初始价格;买入完成后状态已是存续。

+ 56 - 0
code/post_clear_check/src/post_clear_check/order_rules.py

@@ -34,6 +34,7 @@ from post_clear_check.hedge_link import (
 from post_clear_check.order_cell import (
     Cell,
     CheckLine,
+    NOTE_SUB,
     finish,
     finish_cell,
     finish_number,
@@ -109,7 +110,9 @@ def check_order(
     trades = _rows(order.get("trade_order"))
     algos = _rows(order.get("trade_algo_master"))
     notes = _rows(order.get("note_sub_a"))
+    note, note_ambiguous = _one_note(notes)
     lines: list[CheckLine] = []
+    _append_note_distance_lines(lines, note, note_ambiguous)
     _append_hedge_lines(lines, hedges, trades, algos)
     # 两条都是执行事实读 input、结算口径读 work/3,公式不同,分开走。
     if side == "buy":
@@ -157,6 +160,59 @@ def _one_note(notes: Sequence[Mapping[str, Any]]) -> tuple[Mapping[str, Any] | N
     return None, False
 
 
+def _append_note_distance_lines(
+    lines: list[CheckLine],
+    note: Mapping[str, Any] | None,
+    ambiguous: bool,
+) -> None:
+    """用已发布清算票据的最新价和价格线复算三档距离比例。"""
+
+    label = row_label(note) if note is not None else "票据"
+    latest = as_decimal(None if note is None else note.get("latest_px"))
+    fields = (
+        ("distance_warning", "warn_price"),
+        ("distance_danger", "danger_price"),
+        ("distance_force", "force_price"),
+    )
+    for distance_column, price_column in fields:
+        price = as_decimal(None if note is None else note.get(price_column))
+        calc = (
+            None
+            if latest is None or price is None or latest == 0
+            else quantize((latest - price) / latest * Decimal(100), SCALE_AMT)
+        )
+        formula = f"{distance_column} = (latest_px − {price_column}) / latest_px × 100%"
+        if latest == 0:
+            calc_text = "latest_px 为 0,无法计算"
+        elif latest is None or price is None:
+            calc_text = f"latest_px 或 {price_column} 缺失,无法计算"
+        else:
+            calc_text = (
+                f"({format_decimal(latest, SCALE_PX)} - {format_decimal(price, SCALE_PX)}) / "
+                f"{format_decimal(latest, SCALE_PX)} × 100% = {format_decimal(calc, SCALE_AMT)}%"
+            )
+        cell = read_note_cell(note, distance_column, (), {})
+        if ambiguous:
+            calc_text = "主订单对应多张票据,无法确定用于计算的票据"
+            calc = None
+        finish(
+            lines,
+            formula=formula,
+            basis="",
+            calc_text=calc_text,
+            calc_value=calc,
+            check_text=cell_text(NOTE_SUB, label, distance_column, cell, SCALE_AMT),
+            check_value=cell.number,
+            verdict=(
+                VERDICT_ERROR if cell.absent or cell.number is None
+                else VERDICT_SUSPECT if calc is None
+                else equal_verdict(calc, cell.number, SCALE_AMT)
+            ),
+            live=False,
+            scale=SCALE_AMT,
+        )
+
+
 def _summarize(hedges: Sequence[Mapping[str, Any]]) -> _Sums:
     """没有对冲记录时,金额和股数的和是 0。缺金额列时不算 0。"""
 

+ 70 - 0
code/post_clear_check/tests/test_order_rules.py

@@ -128,6 +128,76 @@ def test_sell_final_pnl_uses_the_same_row() -> None:
     assert sold.check_value == Decimal("70.00")
 
 
+def test_note_distance_ratios_use_cleared_prices_and_round_half_up() -> None:
+    """风控距离比例用票据清算结果重算,按百分数保留两位。"""
+
+    note = {
+        "id": 31,
+        "note_no": "N1",
+        "latest_px": Decimal("200"),
+        "warn_price": Decimal("179.99"),
+        "danger_price": Decimal("159.98"),
+        "force_price": Decimal("139.97"),
+        "distance_warning": Decimal("10.01"),
+        "distance_danger": Decimal("20.01"),
+        "distance_force": Decimal("30.02"),
+    }
+    lines = check_order(_order(_sub(), notes=(note,)))
+    warning = _one(lines, "distance_warning")
+    assert warning.verdict == "correct"
+    assert warning.calc_value == Decimal("10.01")
+    assert warning.check_value == Decimal("10.01")
+    danger = _one(lines, "distance_danger")
+    assert danger.verdict == "correct"
+    assert danger.calc_value == Decimal("20.01")
+    force = _one(lines, "distance_force")
+    assert force.verdict == "correct"
+    assert force.calc_value == Decimal("30.02")
+
+
+def test_note_distance_ratio_mismatch_is_error_and_zero_latest_is_suspect() -> None:
+    note = {
+        "id": 31,
+        "note_no": "N1",
+        "latest_px": Decimal("100"),
+        "warn_price": Decimal("90"),
+        "danger_price": Decimal("80"),
+        "force_price": Decimal("70"),
+        "distance_warning": Decimal("9.99"),
+        "distance_danger": Decimal("20.00"),
+        "distance_force": Decimal("30.00"),
+    }
+    warning = _one(check_order(_order(_sub(), notes=(note,))), "distance_warning")
+    assert warning.verdict == "error"
+    assert warning.calc_value == Decimal("10.00")
+
+    note["latest_px"] = Decimal("0")
+    warning = _one(check_order(_order(_sub(), notes=(note,))), "distance_warning")
+    assert warning.verdict == "suspect"
+    assert warning.calc_value is None
+
+
+def test_sell_order_checks_note_distance_ratios() -> None:
+    order = _sell_order()
+    note = order["note_sub_a"][0]
+    assert isinstance(note, dict)
+    note.update(
+        {
+            "latest_px": Decimal("100"),
+            "warn_price": Decimal("90"),
+            "danger_price": Decimal("80"),
+            "force_price": Decimal("70"),
+            "distance_warning": Decimal("10"),
+            "distance_danger": Decimal("20"),
+            "distance_force": Decimal("30"),
+        }
+    )
+    lines = check_order(order)
+    assert _one(lines, "distance_warning").verdict == "correct"
+    assert _one(lines, "distance_danger").verdict == "correct"
+    assert _one(lines, "distance_force").verdict == "correct"
+
+
 def test_sell_ratio_without_exec_notional_is_not_checked() -> None:
     """执行名义本金和当前名义本金都没有时,比例三列不核。"""
 

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

@@ -128,6 +128,7 @@
 | T日订单结算 | `work/3`、`work/4`、`cleared/0` 都必须有该子单 | 结算口径列一致 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.1 发行端(6 步)」 |
 | 当前投影 | 实时子单必须存在 | 实时结算口径列等于 `cleared`。执行事实列由执行事实节点核对,清算不回写这两列 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.1 发行端(6 步)」 |
 | 报表行 | 两端 OTC003 必须有该主订单的行 | 报表列等于 `cleared` 子单。不用回写 `after_val`。发行端总金额不含承销费用,不再另出「含承销费用」的可疑行;销售端承销费用等于已发布销售订单的 `exec_underwrite_fee_settle`,发行子单没有这一列 | 产品原型 OTC003(见 4.13) |
+| 风控距离比例 | `cleared/0` 的 `note_sub_a` 必须有唯一关联票据 | `distance_warning = (latest_px − warn_price) / latest_px × 100%`、`distance_danger = (latest_px − danger_price) / latest_px × 100%`、`distance_force = (latest_px − force_price) / latest_px × 100%`,分别四舍五入到 2 位后与清算结果中的比例比较。最新价为 0 时无法计算,比例或输入缺失按订单规则记结论 | 本设计确认的风控距离比例公式;读取发行端步骤 5 发布的 `cleared/0` |
 | 对冲仓位 | 有清算副本时必须出现 | 股数、已结算实现盈亏等于本批 `hedge_position` 清算副本按票据汇总。没有副本时不读实时持仓,仍记可疑 | 研发设计发行端 OTC003 用清算副本 `hedge_position` |
 
 ### 4.3 卖出执行
@@ -150,6 +151,7 @@
 | T日订单结算 | `work/3`、`work/4`、`cleared/0` 都必须有该子单 | 结算口径列一致 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.1 发行端(6 步)」 |
 | 当前投影 | 实时子单必须存在 | 实时结算口径列等于 `cleared`。执行事实列由执行事实节点核对 | 研发设计 `docs/tmp/data_check_tool/doc/清算步骤核对改造方案.md`「5.1 发行端(6 步)」 |
 | 报表行 | 两端 OTC003 必须有该主订单的行 | 报表列等于 `cleared` 子单。不用回写 `after_val` | 产品原型 OTC003(见 4.13) |
+| 风控距离比例 | `cleared/0` 的 `note_sub_a` 必须有唯一关联票据 | 买入执行相同:按上述公式重算并比对三个比例 | 本设计确认的风控距离比例公式;读取发行端步骤 5 发布的 `cleared/0` |
 
 ### 4.4 资金变动
 

+ 1 - 0
doc/使用说明.md

@@ -149,6 +149,7 @@ python -m post_clear_check
    - OTC001 的钱包比对和现金可用读 T 日已发布候选派生出的钱包(`kind=cleared`,当日买卖先置 0,再算现金余额、冻结和可用)。科目都是正数;报表买入待交收展示为负值,比较和代入公式时取相反数。没有已发布候选或缺本币种行时这几条记可疑。账户资产、资金变动、资金交收的步骤节点另读销售 `kind=input` 与 `kind=cleared`:第 3、4 步用 input 重算后与 cleared 比,不读 `pre_clearing`。input 里已经有该订单 posted 的解冻凭证时,不再把交易冻结扣第二次。重放 input 流水按主键 id 升序,不按查询返回的幂等哈希顺序。发行和销售日报只读 `status=active`,同一营业日的 superseded、deprecated 旧行不核对。等值港币资产净值按现金可用、冻结、票据估值各自换算后量化到分再相加(与销售报表一致);整笔资产净值推不出唯一方向时改用三项总和推断。乘、除量化到分后结果相同时,方向视为已确定。汇率为 1 的同币种订单结算汇率与名义本金换算记正确,不再因乘除同值记可疑。钱包只重算本批 `input` 与 `cleared` 的 T 日结果,不读实时钱包。发行交收台账读 `work/4`,`cleared/0` 应与它一致,不读实时台账。日终 `daycut_fund_eod` 只在核对门禁上与这份候选比,不一致才记错误。
    - 同一 IN 列表超过 500 个值时分几条语句读。
    - OTC007、OTC008 行按票据号挂到订单上,只核业务类型(`buy_note`、`sell_note`)与主订单方向一致、交易日相同的行。同一票据另一张订单的行不算进来;哪张已读主单都对不上的行按「有行但无法对应主订单编号」记可疑。
+   - 主订单买入、卖出执行链路用已发布 `cleared/0` 的 `note_sub_a` 核对三个风控距离比例:分别按 `(latest_px − warn_price) / latest_px × 100%`、`(latest_px − danger_price) / latest_px × 100%`、`(latest_px − force_price) / latest_px × 100%` 计算,四舍五入到 2 位后比较。`latest_px` 为 0 时无法计算。
 6. 结论行在调用核对函数的地方挂到业务链路(见下表),再按产生来源收成最多四张方向卡写入自有库,发给飞书。顺序是订单链路、发行报表、销售报表、TRS 报表。没有结论行的方向不发。一张卡上有多条时,总结论取最严重的一档:`error` 高于 `suspect`,`suspect` 高于 `correct`。方向卡主题是错误时,正确行仍写在正文里。`check_run` 上的三个计数是方向卡张数,不是结论行数。门禁没开跑时仍只落清算批次卡,不发四张空卡。
 
 方向卡的 `card_kind` 是 `dimension`,`chain_name` 是 `order_chain`、`issue_report`、`sales_report`、`trs_report`,`biz_key` 是营业日。结论行上的内部链路代码不改。没挂链或归属键为空的行仍是未归错误,放进产生它的那个方向。发行门禁写进发行报表的批次门禁,销售、TRS 同理。订单卡的摘要只引用三端门禁有没有差异。链路的口径和节点以 `doc/业务链路核对设计.md` 为准。