Kaynağa Gözat

refactor#三线比例公式说明换成中文

hefuzheng 1 hafta önce
ebeveyn
işleme
8157385209

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


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


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

@@ -306,9 +306,9 @@ GATE_MISSING_TABLE = "业务表应存在"
 _HEDGE_ROOT = _design(_CLEARING_PLAN, "5.4 TRS 核对、订单和交收计算")
 _HEDGE_ROOT = _design(_CLEARING_PLAN, "5.4 TRS 核对、订单和交收计算")
 
 
 _BUY_FORMULAS: Mapping[str, str] = {
 _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 买入执行"),
+    "距预警比例": _design(_CHAIN_DESIGN, "4.2 买入执行"),
+    "距危险比例": _design(_CHAIN_DESIGN, "4.2 买入执行"),
+    "距强平比例": _design(_CHAIN_DESIGN, "4.2 买入执行"),
     "对冲记录成交股数": _HEDGE_ROOT,
     "对冲记录成交股数": _HEDGE_ROOT,
     "累计成交股数": _excel("买入订单", 14, "累计成交股数"),
     "累计成交股数": _excel("买入订单", 14, "累计成交股数"),
     "对冲含费成交均价": _excel("买入订单", 15, "对冲含费成交均价"),
     "对冲含费成交均价": _excel("买入订单", 15, "对冲含费成交均价"),
@@ -327,9 +327,9 @@ _BUY_FORMULAS: Mapping[str, str] = {
 }
 }
 
 
 _SELL_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 卖出执行"),
+    "距预警比例": _design(_CHAIN_DESIGN, "4.3 卖出执行"),
+    "距危险比例": _design(_CHAIN_DESIGN, "4.3 卖出执行"),
+    "距强平比例": _design(_CHAIN_DESIGN, "4.3 卖出执行"),
     "对冲记录成交股数": _HEDGE_ROOT,
     "对冲记录成交股数": _HEDGE_ROOT,
     "累计成交股数": _excel("卖出订单", 12, "累计成交股数"),
     "累计成交股数": _excel("卖出订单", 12, "累计成交股数"),
     "对冲含费成交均价": _excel("卖出订单", 13, "对冲含费成交均价"),
     "对冲含费成交均价": _excel("卖出订单", 13, "对冲含费成交均价"),

+ 19 - 9
code/post_clear_check/src/post_clear_check/order_rules.py

@@ -165,31 +165,35 @@ def _append_note_distance_lines(
     note: Mapping[str, Any] | None,
     note: Mapping[str, Any] | None,
     ambiguous: bool,
     ambiguous: bool,
 ) -> None:
 ) -> None:
-    """用已发布清算票据的最新价和价格线复算三档距离比例。"""
+    """用已发布清算票据的最新价和价格线复算三档距离比例。
+
+    卡片公式用中文;库列仍是英文。× 100 把比值写成百分数(单位 %),不是 × 100%。
+    """
 
 
     label = row_label(note) if note is not None else "票据"
     label = row_label(note) if note is not None else "票据"
     latest = as_decimal(None if note is None else note.get("latest_px"))
     latest = as_decimal(None if note is None else note.get("latest_px"))
+    # 库列名、价格线列名、卡片左侧字段名、价格线中文名。
     fields = (
     fields = (
-        ("distance_warning", "warn_price"),
-        ("distance_danger", "danger_price"),
-        ("distance_force", "force_price"),
+        ("distance_warning", "warn_price", "距预警比例", "预警价"),
+        ("distance_danger", "danger_price", "距危险比例", "危险价"),
+        ("distance_force", "force_price", "距强平比例", "强平价"),
     )
     )
-    for distance_column, price_column in fields:
+    for distance_column, price_column, distance_name, price_name in fields:
         price = as_decimal(None if note is None else note.get(price_column))
         price = as_decimal(None if note is None else note.get(price_column))
         calc = (
         calc = (
             None
             None
             if latest is None or price is None or latest == 0
             if latest is None or price is None or latest == 0
             else quantize((latest - price) / latest * Decimal(100), SCALE_AMT)
             else quantize((latest - price) / latest * Decimal(100), SCALE_AMT)
         )
         )
-        formula = f"{distance_column} = (latest_px − {price_column}) / latest_px × 100%"
+        formula = f"{distance_name} = (最新价 − {price_name}) / 最新价 × 100"
         if latest == 0:
         if latest == 0:
-            calc_text = "latest_px 为 0,无法计算"
+            calc_text = "最新价为 0,无法计算"
         elif latest is None or price is None:
         elif latest is None or price is None:
-            calc_text = f"latest_px 或 {price_column} 缺失,无法计算"
+            calc_text = f"最新价或{price_name}缺失,无法计算"
         else:
         else:
             calc_text = (
             calc_text = (
                 f"({format_decimal(latest, SCALE_PX)} - {format_decimal(price, SCALE_PX)}) / "
                 f"({format_decimal(latest, SCALE_PX)} - {format_decimal(price, SCALE_PX)}) / "
-                f"{format_decimal(latest, SCALE_PX)} × 100% = {format_decimal(calc, SCALE_AMT)}%"
+                f"{format_decimal(latest, SCALE_PX)} × 100 = {format_decimal(calc, SCALE_AMT)}%"
             )
             )
         cell = read_note_cell(note, distance_column, (), {})
         cell = read_note_cell(note, distance_column, (), {})
         if ambiguous:
         if ambiguous:
@@ -1394,6 +1398,9 @@ def _shared_formulas() -> list[str]:
 
 
 def _buy_formulas() -> list[str]:
 def _buy_formulas() -> list[str]:
     return [
     return [
+        "距预警比例 = (最新价 − 预警价) / 最新价 × 100",
+        "距危险比例 = (最新价 − 危险价) / 最新价 × 100",
+        "距强平比例 = (最新价 − 强平价) / 最新价 × 100",
         "执行订单名义本金(结算币种) = 执行订单名义本金(交易币种)按结算汇率换算",
         "执行订单名义本金(结算币种) = 执行订单名义本金(交易币种)按结算汇率换算",
         "订单买入股数 = 执行订单名义本金(交易币种) / 订单成交均价",
         "订单买入股数 = 执行订单名义本金(交易币种) / 订单成交均价",
         "订单成交均价 = 对冲含费成交均价",
         "订单成交均价 = 对冲含费成交均价",
@@ -1404,6 +1411,9 @@ def _buy_formulas() -> list[str]:
 
 
 def _sell_formulas() -> list[str]:
 def _sell_formulas() -> list[str]:
     return [
     return [
+        "距预警比例 = (最新价 − 预警价) / 最新价 × 100",
+        "距危险比例 = (最新价 − 危险价) / 最新价 × 100",
+        "距强平比例 = (最新价 − 强平价) / 最新价 × 100",
         "执行名义本金(交易币种) = 对冲完成且累计成交股数为 0 时为 0,否则为订单名义本金(交易币种)",
         "执行名义本金(交易币种) = 对冲完成且累计成交股数为 0 时为 0,否则为订单名义本金(交易币种)",
         "订单卖出股数 = 比例 × 当前票据股票数量",
         "订单卖出股数 = 比例 × 当前票据股票数量",
         "卖出票据面值 = 比例 × 当前票据面值",
         "卖出票据面值 = 比例 × 当前票据面值",

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


+ 18 - 10
code/post_clear_check/tests/test_order_rules.py

@@ -129,7 +129,7 @@ def test_sell_final_pnl_uses_the_same_row() -> None:
 
 
 
 
 def test_note_distance_ratios_use_cleared_prices_and_round_half_up() -> None:
 def test_note_distance_ratios_use_cleared_prices_and_round_half_up() -> None:
-    """风控距离比例用票据清算结果重算,按百分数保留两位。"""
+    """风控距离比例用票据清算结果重算,按百分数保留两位;卡片公式用中文且 × 100。"""
 
 
     note = {
     note = {
         "id": 31,
         "id": 31,
@@ -143,16 +143,22 @@ def test_note_distance_ratios_use_cleared_prices_and_round_half_up() -> None:
         "distance_force": Decimal("30.02"),
         "distance_force": Decimal("30.02"),
     }
     }
     lines = check_order(_order(_sub(), notes=(note,)))
     lines = check_order(_order(_sub(), notes=(note,)))
-    warning = _one(lines, "distance_warning")
+    warning = _one(lines, "距预警比例")
     assert warning.verdict == "correct"
     assert warning.verdict == "correct"
     assert warning.calc_value == Decimal("10.01")
     assert warning.calc_value == Decimal("10.01")
     assert warning.check_value == Decimal("10.01")
     assert warning.check_value == Decimal("10.01")
-    danger = _one(lines, "distance_danger")
+    assert warning.formula_text == "距预警比例 = (最新价 − 预警价) / 最新价 × 100"
+    assert warning.calc_text == "(200.000000 - 179.990000) / 200.000000 × 100 = 10.01%"
+    assert "100%" not in warning.formula_text
+    assert "× 100%" not in warning.calc_text
+    danger = _one(lines, "距危险比例")
     assert danger.verdict == "correct"
     assert danger.verdict == "correct"
     assert danger.calc_value == Decimal("20.01")
     assert danger.calc_value == Decimal("20.01")
-    force = _one(lines, "distance_force")
+    assert danger.formula_text == "距危险比例 = (最新价 − 危险价) / 最新价 × 100"
+    force = _one(lines, "距强平比例")
     assert force.verdict == "correct"
     assert force.verdict == "correct"
     assert force.calc_value == Decimal("30.02")
     assert force.calc_value == Decimal("30.02")
+    assert force.formula_text == "距强平比例 = (最新价 − 强平价) / 最新价 × 100"
 
 
 
 
 def test_note_distance_ratio_mismatch_is_error_and_zero_latest_is_suspect() -> None:
 def test_note_distance_ratio_mismatch_is_error_and_zero_latest_is_suspect() -> None:
@@ -167,14 +173,15 @@ def test_note_distance_ratio_mismatch_is_error_and_zero_latest_is_suspect() -> N
         "distance_danger": Decimal("20.00"),
         "distance_danger": Decimal("20.00"),
         "distance_force": Decimal("30.00"),
         "distance_force": Decimal("30.00"),
     }
     }
-    warning = _one(check_order(_order(_sub(), notes=(note,))), "distance_warning")
+    warning = _one(check_order(_order(_sub(), notes=(note,))), "距预警比例")
     assert warning.verdict == "error"
     assert warning.verdict == "error"
     assert warning.calc_value == Decimal("10.00")
     assert warning.calc_value == Decimal("10.00")
 
 
     note["latest_px"] = Decimal("0")
     note["latest_px"] = Decimal("0")
-    warning = _one(check_order(_order(_sub(), notes=(note,))), "distance_warning")
+    warning = _one(check_order(_order(_sub(), notes=(note,))), "距预警比例")
     assert warning.verdict == "suspect"
     assert warning.verdict == "suspect"
     assert warning.calc_value is None
     assert warning.calc_value is None
+    assert warning.calc_text == "最新价为 0,无法计算"
 
 
 
 
 def test_sell_order_checks_note_distance_ratios() -> None:
 def test_sell_order_checks_note_distance_ratios() -> None:
@@ -193,9 +200,9 @@ def test_sell_order_checks_note_distance_ratios() -> None:
         }
         }
     )
     )
     lines = check_order(order)
     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"
+    assert _one(lines, "距预警比例").verdict == "correct"
+    assert _one(lines, "距危险比例").verdict == "correct"
+    assert _one(lines, "距强平比例").verdict == "correct"
 
 
 
 
 def test_sell_ratio_without_exec_notional_is_not_checked() -> None:
 def test_sell_ratio_without_exec_notional_is_not_checked() -> None:
@@ -315,7 +322,8 @@ def test_force_and_maturity_use_sell_formulas() -> None:
     force = _formulas(_sell_order(direction="buy", op_type="force"))
     force = _formulas(_sell_order(direction="buy", op_type="force"))
     maturity = _formulas(_sell_order(direction="sell", op_type="maturity"))
     maturity = _formulas(_sell_order(direction="sell", op_type="maturity"))
     assert sell == force == maturity
     assert sell == force == maturity
-    assert all("强平" not in text and "到期" not in text for text in force)
+    # 不因 op_type 另造「强平…」「到期…」公式;距强平比例是三线字段名,不算另造。
+    assert all(not text.startswith(("强平", "到期")) for text in force)
     assert not any(text.startswith("票据发行价") for text in force)
     assert not any(text.startswith("票据发行价") for text in force)
     assert any(text.startswith("最终盈亏") for text in force)
     assert any(text.startswith("最终盈亏") for text in force)
     buy = _formulas(_order(_issue_sub()))
     buy = _formulas(_order(_issue_sub()))

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

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

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

@@ -149,7 +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` 只在核对门禁上与这份候选比,不一致才记错误。
    - 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 个值时分几条语句读。
    - 同一 IN 列表超过 500 个值时分几条语句读。
    - OTC007、OTC008 行按票据号挂到订单上,只核业务类型(`buy_note`、`sell_note`)与主订单方向一致、交易日相同的行。同一票据另一张订单的行不算进来;哪张已读主单都对不上的行按「有行但无法对应主订单编号」记可疑。
    - 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 时无法计算。
+   - 主订单买入、卖出执行链路用已发布 `cleared/0` 的 `note_sub_a` 核对三个风控距离比例:卡片上写 `距预警比例 = (最新价 − 预警价) / 最新价 × 100`、`距危险比例 = (最新价 − 危险价) / 最新价 × 100`、`距强平比例 = (最新价 − 强平价) / 最新价 × 100`;结果按百分数记(单位 %),四舍五入到 2 位后比较。最新价为 0 时无法计算。
 6. 结论行在调用核对函数的地方挂到业务链路(见下表),再按产生来源收成最多四张方向卡写入自有库,发给飞书。顺序是订单链路、发行报表、销售报表、TRS 报表。没有结论行的方向不发。一张卡上有多条时,总结论取最严重的一档:`error` 高于 `suspect`,`suspect` 高于 `correct`。方向卡主题是错误时,正确行仍写在正文里。`check_run` 上的三个计数是方向卡张数,不是结论行数。门禁没开跑时仍只落清算批次卡,不发四张空卡。
 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` 为准。
 方向卡的 `card_kind` 是 `dimension`,`chain_name` 是 `order_chain`、`issue_report`、`sales_report`、`trs_report`,`biz_key` 是营业日。结论行上的内部链路代码不改。没挂链或归属键为空的行仍是未归错误,放进产生它的那个方向。发行门禁写进发行报表的批次门禁,销售、TRS 同理。订单卡的摘要只引用三端门禁有没有差异。链路的口径和节点以 `doc/业务链路核对设计.md` 为准。