レッスン2 — MarginRatio + MarginHealth — エンジンが返す分類型
問い
margin_health を表す型は何にすべきか? bool(liquidatable か否か)で足りるか? エンジンはアカウントごとに「新規リスクを取れるか / force-close すべきか / close だけで不足カバーできるか」の判断をする。これを 1 つの型でどう表すか?
原理(最小モデル)
MarginRatioはtypealias でなく newtype。 newtype なら「bps スケールの ratio を期待する所に生の i64 を渡した」をコンパイル時に捕まえる(funding のMarkPrice(pub u64)vsu64と同じ規律)。MarginHealthはちょうど 4 variants。Safe/AtRisk/Liquidatable/Underwater、それぞれ異なるエンジン動作を許可する。どれを潰しても他の部分が必要とする情報が失われる。- enum のカーディナリティ = それが許可する action のカーディナリティ。 エンジンが下流で 3 判断(新規可否 / force-close 可否 / close だけで足りるか)するから 4 variant。
MarginHealthにPartialOrd/Ordを derive しない。 variants は worsening order を成すが、health > Safeの順序比較はコード臭。matches!(health, Liquidatable | Underwater)のほうが意図が明示的で exhaustiveness check も効く。
具体例
4 variant が許可する action のマトリクス:
│ (a) 新規ポジ開ける? │ (b) Force-close? │ (c) close だけで不足カバー? │
Safe │ ✅ yes │ ❌ no │ N/A (close 不要) │
AtRisk │ ❌ no │ ❌ no │ N/A (close 不要) │
Liquidatable │ ❌ no │ ✅ yes │ ✅ yes (equity 残あり) │
Underwater │ ❌ no │ ✅ yes │ ❌ no → insurance fund 吸収 │
下流挙動: Safe→運用継続 / AtRisk→警告・新規拒否 / Liquidatable→close+fee+残返却 / Underwater→close+不足を fund 補填
各 variant がそのまま「許可される action のセット」を表す。state machine の variants は、自分がトリガーする下流 action の数だけ存在する。
失敗例(誤解)
「MarginHealth は bool(liquidatable か否か)でいい」は誤り — エンジンは 1 でなく 3 つの下流判断を要求する。bool だと (a)「新規ポジを開けるか」と (c)「insurance fund が関与するか」を 1 ビットに潰す。Liquidatable と Underwater をマージすると「insurance fund を呼ぶべきか」の信号が型から消え、エンジンが equity を再計算する羽目に。
ここまでで「4 variant = 3 判断 + 1」は着地した。ここから 2 つの分類型を足す(受動的データ型なのでテストなし、最初の挙動テストはレッスン4)。コードは完全形。
🛑 予測。
MarginHealthは何 variants 必要か?(ヒント: エンジンは各アカウントで 3 判断 — (a) 新規リスクを取れる? (b) force-close すべき? (c) close だけで不足カバーできる?)(答え: 3 問 → 4 variants。Safe=(a)yes、AtRisk=(a)no/(b)no、Liquidatable=(a)no/(b)yes/(c)yes、Underwater=(a)no/(b)yes/(c)no。3-variant に潰すとLiquidatable/Underwaterがマージされ「insurance fund が関与するか」の信号が消える。)
ステップで組み立てる
Step 1: src/types.rs に追記
LiquidationParams の impl の後に:
/// Account margin ratio = `equity / notional`, scaled by [`MARGIN_SCALE`].
///
/// Sign: usually non-negative; can be negative when the account is
/// "underwater" — accumulated losses have driven equity below zero, and
/// liquidating the position alone cannot cover the deficit. The insurance
/// fund absorbs that shortfall.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MarginRatio(pub i64);
/// Margin health classification given the account's current margin ratio
/// and the network's params. Four states, in decreasing health order.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MarginHealth {
/// Margin ratio ≥ initial margin requirement. Healthy: the account
/// can open new positions or increase existing ones.
Safe,
/// Margin ratio ∈ [maintenance, initial). Allowed to hold existing
/// positions but not to add risk. Production UIs typically warn the
/// user.
AtRisk,
/// Margin ratio < maintenance, equity still ≥ 0. The engine should
/// liquidate the position at market; the account's remaining equity
/// (after the liquidation fee) returns to the account.
Liquidatable,
/// Margin ratio < 0 (equity is negative). Closing the position at
/// any price won't fully cover losses. The insurance fund absorbs
/// the shortfall.
Underwater,
}
MarginRatio(pub i64) は newtype(守るべき不変量がないので透明な pub フィールド、ゲッターで隠さない)。MarginHealth は PartialOrd/Ord を derive しない(worsening order を成すが順序比較はコード臭 — matches! で明示的に書く)。variant doc は条件でなく authorization(エンジンが何をすべきか)を語る。
Step 2: src/lib.rs を更新
pub use types::{LiquidationParams, MarginHealth, MarginRatio, MARGIN_SCALE};
レッスン1 の MarginHealth rustdoc warning がここで解消する。
Step 3: コンパイル
cargo build -p openhl-liquidation が warning ゼロ。
答え合わせ
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 は MarginHealth::Underwater まで一致(次の AccountSnapshot/CloseOrderSpec はレッスン3)。lib.rs は compute モジュールと追加 re-export を除いて一致。
合格基準
cargo build -p openhl-liquidation が warning ゼロ。よくあるエラー: E0432(re-export 行の typo)/ ambiguous re-export(既存を拡張せず別行を追加 — すべて 1 つの pub use types::{...} に収める)。
まとめ(3行)
MarginRatio(pub i64)は newtype(alias でない)— ゼロコストで本物の型区別を生む。値が「整数」を超えた意味を運ぶときは newtype。MarginHealthは 4 variants = エンジンの 3 下流判断(新規/force-close/insurance 関与)に対応。enum のカーディナリティは許可する action のカーディナリティに揃える。PartialOrd/Ordを derive しない(health > AtRiskはどの worse か言わない)—matches!で明示的に書き、exhaustiveness check を効かせる。比較可能 enum はたいていコード臭。
次のレッスン(レッスン3)
AccountSnapshot(全 margin 関数の入力)と CloseOrderSpec(エンジンが bridge へ渡す出力)を追加し types モジュールを閉じる。funding::Position を再利用せず独自 snapshot を起こす理由を見る。