FABRKNT
Step 5. Liquidation:レバレッジ環境における非単調性の発見と清算エンジンの構築
レッスン 4 / 14·CONTENT25 分50 XP
コース
Step 5. Liquidation:レバレッジ環境における非単調性の発見と清算エンジンの構築
レッスンの役割
CONTENT
順序
4 / 14

レッスン3 — AccountSnapshot + CloseOrderSpec — エンジンの入出力型

問い

liquidation はアカウントごとに unrealized PnL((mark - entry) * size)と equity(collateral + pnl)を計算する。だが funding::Position(account, size) しか持たない。エンジンの入力型はどう設計し、出力(close order)には何を持たせるべきか?

原理(最小モデル)

  • liquidation は funding::Position を再利用せず独自 AccountSnapshot を定義する。 Position(account, size)、liquidation は (account, size, avg_entry, collateral) が要る。2 crate・2 snapshot 型・cross-coupling なし。bridge がそれぞれを自分の台帳から組み立てる。
  • funding と共有する「snapshot」の規律。 エンジンは呼び出し側が組み立てた snapshot を consume、可変 state を所有しない。proptest が determinism バグを捕まえられるのはこの I/O-free な純粋さゆえ。
  • CloseOrderSpec に price フィールドを持たせない。 liquidation は常に market で close(エンジンは価格を選ばない)。bridge が clob::Action::SubmitMarket に変換し、板の次の価格で約定。
  • Side/Qty を liquidation-local の新型でなく openhl_clob から借りる。 matching engine が話すのと同じ概念。並行する Side enum を 2 つ置くと drift する翻訳サーフェスが生まれる。

具体例

types モジュールが確定する入出力 = エンジンと外界の唯一の接触面:

   [上流: bridge/clearing (台帳の所有者)] ─tick ごとに snapshot 構築─►
   入力: AccountSnapshot { account, position_size, avg_entry, collateral }   (不変・read-only・Copy)
              │
   ★ liquidation エンジン (L4 notional/pnl → L5 equity/ratio → L6 health → L7 close_order_spec)
              │
   出力: CloseOrderSpec { account, side, qty }   (price なし=market、Liquidatable/Underwater のみ emit)
              │                                   + セクション3-4 で InsuranceFundDelta も並行 emit
   [下流: bridge → matching engine (CLOB)]

(a) 2 つの型がエンジンと外界の唯一の接触面、(b) 入力 snapshot も出力 spec も不変 — エンジンは台帳を更新しない(所有権は bridge に残る)。レッスン0の「リスク計算専用の不変 snapshot 型を分離して依存をクリーンに保つ」の具体形。

失敗例(誤解)

AccountSnapshotopenhl-funding 側に置いて両 crate で共有」は誤り — funding は avg_entrycollateral も必要としない。funding::Position に足すと funding snapshot が無駄に膨らみ、bridge は funding が無視するフィールドにまで値を入れる羽目に。2 crate・2 snapshot 型が正しい(bridge が正典台帳を持ち、tick ごとに 2 つの view を生成するコストは安い)。


ここまでで「2 crate・2 snapshot 型」は着地した。ここから 2 つの I/O 型を足して types モジュールを閉じる(受動的データ型、テストはレッスン4)。コードは完全形。

🛑 予測。 unrealized PnL の式 (mark - entry) * size で、funding::Position から得られない入力は何か、なぜ funding では不要だったか?(答え: avg_entry(PnL 項)と collateral(equity)。funding の式 size * mark * rate に entry 係数はなく、collateral も読まない(funding の settlement delta は bridge で balance に適用、台帳管理は bridge に閉じる)。liquidation の仕事は collateral + pnl がしきい値を下回ったかを 測る ことなので両方が要る。仕事が違えば snapshot も違う。)

ステップで組み立てる

Step 1: AccountSnapshot を追記

MarginHealth の後に:

/// Snapshot of one account's perpetual-market state, assembled by the
/// bridge layer before invoking the liquidation engine. Same "snapshot"
/// model as `openhl_funding::Position`: the engine treats this as a
/// per-tick read-only view, never mutates it.
///
/// `avg_entry` is the volume-weighted average price at which the account
/// opened its current net position. The owning layer (vault / clearing)
/// is responsible for maintaining this across fills.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AccountSnapshot {
    pub account: AccountId,
    pub position_size: PositionSize,
    pub avg_entry: MarkPrice,
    pub collateral: Notional,
}

avg_entryMarkPrice 型で持つ(entry 価格と mark は同じ unit-of-account、別 EntryPrice 型は全 PnL サイトで変換を要して利益ゼロ)。collateral: Notional は signed(account_equity = collateral + unrealized_pnl を signed sum のまま流す — unsigned だと全 equity サイトで as i64 キャスト。レッスン4 の符号トリックはこの「計算経路を全部 signed で統一」前提の上に成り立つ)。doc コメントが呼び出し側の契約(owning layer が avg_entry を維持)を明示。

Step 2: CloseOrderSpec を追記

/// Specification for a single liquidation close order, generated by the
/// engine and consumed by the bridge layer. The bridge encodes this as
/// `openhl_clob::Action::SubmitMarket` and routes it through the matching
/// engine.
///
/// Always a market order — liquidation accepts any available price.
/// Always the opposite side of the position: a long position closes via
/// `Side::Sell`, a short via `Side::Buy`. Quantity is the absolute value
/// of the position size.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CloseOrderSpec {
    pub account: AccountId,
    pub side: Side,
    pub qty: Qty,
}

price フィールドなし(liquidation は価格を選ばず market、matching engine が板の深さで約定)。side: Side / qty: Qtyopenhl_clob を再利用(並行 Side enum を 2 つ置くと変換レイヤーが drift の温床に — 1 enum・1 真実)。PositionSize(i64)→Qty(u64) の変換(unsigned_abs())はレッスン7 で行う。

Step 3: src/lib.rs を更新

pub use types::{
    AccountSnapshot, CloseOrderSpec, LiquidationParams, MarginHealth, MarginRatio, MARGIN_SCALE,
};

Step 4: コンパイル

cargo build -p openhl-liquidation が warning も error もゼロ。types モジュール完成。

答え合わせ

cd ~/code/openhl-reference && git checkout 22eedf9
diff -u ~/code/my-openhl/crates/liquidation/src/types.rs ./crates/liquidation/src/types.rs
diff -u ~/code/my-openhl/crates/liquidation/src/lib.rs ./crates/liquidation/src/lib.rs
git checkout main

types.rs は 22eedf9byte-for-byte 完全一致(セクション1 完了)。lib.rs はまだ pub mod compute; と compute 系 re-export が揃わない(レッスン4〜7)。

合格基準

cargo build -p openhl-liquidation が warning ゼロ。よくあるエラー: E0432Qty import 欠け — types.rs 冒頭が use openhl_clob::{AccountId, Qty, Side}; + use openhl_funding::{MarkPrice, Notional, PositionSize}; のままか確認)/ Notional 未解決(同 import 行)。

まとめ(3行)

  • AccountSnapshot { account, position_size, avg_entry, collateral } は liquidation-local(funding と共有しない — 仕事が違えば snapshot も違う)。不変・read-only・Copy で、エンジンは台帳を所有しない。
  • CloseOrderSpec { account, side, qty } は price なし(liquidation は market、価格選択は bridge 下のレイヤー)。Side/Qtyopenhl_clob を借りる(境界の語彙は共有、内部型だけ特殊化)。
  • collateral を signed Notional にして計算経路を全部 signed で統一(境界で変換、キャスト漏れの静かなバグを型不一致として根絶)。types モジュールは 22eedf9 と完全一致。

次のレッスン(レッスン4)

compute モジュール開始。notional_value + unrealized_pnl が crate 最初の挙動テストを呼び込む。long/short どちらでも符号が正しく揃う signed-multiplication のトリックと、i64 overflow を i128 中間値で防ぐ規律を見る。