レッスン1 — MARGIN_SCALE + LiquidationParams — リスクエンジンのダイヤル
問い
margin ratio を float なしで全 validator が byte-identical に分類したい。何を基準単位に選ぶ? funding は ppb(RATE_SCALE = 1e9)だったが liquidation も同じ精度が要るか? そしてネットワークのリスクパラメータ(initial / maintenance / fee)をどう持つ?
原理(最小モデル)
- margin の固定小数点単位は basis points(bps)。 bps = 4 桁精度 = 実際の取引所(HL / Binance / Drift)が margin を表現する解像度。
RATE_SCALEと同じ i64-saturating 規律で、違うのはスケールだけ。 - margin と rate で scale が違う。 funding rate は 0.0001〜0.04(ppb 必要)、margin 要件は 0.02〜0.10(bps で十分)。マグニチュード差 2 桁 → スケールも 2 桁ずらす。実際のレンジをカバーする最小スケールを選ぶ(使わない精度を買わない)。
LiquidationParamsはネットワーク状態であってユーザー状態でない。 10% / 2% / 1.5% は consensus パラメータ(genesis で 1 回設定、governance を経て変更)。構造体にまとめて magic constant をcompute.rsに散らさない。hyperliquid_default()はconst fn。 static / fixture / コンパイル時 assertion で使える。#[must_use]で構築して捨てる事故を禁じる(コンパイラを静的解析として駆動する)。- 3 つの独立した
u32フィールド、タプルでない。 名前付きフィールドがinitial/maintenance取り違えを防ぐ(位置タプルは安全性を失うだけで実行時利益ゼロ)。
具体例
RATE_SCALE と MARGIN_SCALE の解像度差:
Step 4(Funding) Step 5(Liquidation)
スケール定数 RATE_SCALE = 1_000_000_000 MARGIN_SCALE = 10_000
(parts-per-billion, 10⁹) (basis points, 10⁴)
精度 9 decimal digits 4 decimal digits
扱う典型レンジ 0.0001% — 4% / interval 2% — 10% (maintenance) / 10% — 50% (initial)
本番で意味ある最小ステップ 0.0001% (= 10 ppb) 1 bp = 0.01%
1.0 を表す raw 値 1_000_000_000 10_000
解像度はドメインの慣例単位に合わせる。funding は per-billion でしか表せない差を扱うので ppb、margin は本番設定が bps 整数(200 / 500 bps)で来るので bps。揃えると使わない精度のため i64 ヘッドルームを浪費する。
失敗例(誤解)
「レッスン5/6 で使うなら openhl-clob/openhl-funding も dev-dep でいい」は誤り — production の compute.rs 関数シグネチャが MarkPrice/AccountId を使う(test 専用でない)。pub fn シグネチャに現れる型は dev-only でなく通常 dep。
ここまでで「なぜ bps」は着地した。ここから空 crate を「公開された scale 定数 + エンジンを支配するパラメータ」を持つ実 crate に育てる(テストなし — 値であって挙動でない。最初のテストはレッスン4)。コードは完全形。
🛑 予測。 funding は
RATE_SCALE = 1e9(ppb、9 桁)なのに liquidation はMARGIN_SCALE = 10_000(bps、4 桁)— なぜ?(答え: 必要な解像度は意味のある最小ステップに従う。funding rate の 0.0001% は高ボリュームトレーダーに意味がある差なので ppb が正しい。maintenance margin が 0.02% か 0.05% かは engine 層で意味のある差にならない(本番は bps 整数で設定)。実際のレンジをカバーする最小スケールを選ぶ。)
ステップで組み立てる
Step 1: Cargo.toml を更新
[dependencies]
openhl-clob = { path = "../clob" }
openhl-funding = { path = "../funding" }
[dev-dependencies]
proptest = { workspace = true }
[lints]
workspace = true
openhl-clob(AccountId/Side/Qty)+ openhl-funding(MarkPrice/PositionSize/Notional)— 両方 production の型シグネチャに乗る(dev-dep でない)。proptest はレッスン5/6 で使うので今宣言。
Step 2: src/types.rs を作成
//! Core types for the liquidation engine.
//!
//! Pure data — no I/O, no allocation. Every type is `Copy`-friendly so the
//! engine can be invoked on snapshots taken at the bridge layer without
//! lifetime gymnastics. The convention follows `openhl-funding`: the
//! liquidation crate never owns mutable state in compute; it computes
//! over snapshots that the caller assembled.
//!
//! ### Why fixed-point integers, not floats
//!
//! Same answer as `openhl-funding`: consensus determinism. Every validator
//! must reach the same `MarginHealth` from the same inputs, and float
//! arithmetic varies bit-for-bit across compilers and CPUs. We use signed
//! integers scaled by [`MARGIN_SCALE`] (basis points, 10⁴) for margin
//! ratios.
/// Scale factor for `MarginRatio` — basis points (1 bp = 0.01%).
///
/// A raw value of `MARGIN_SCALE` represents `100%`; `MARGIN_SCALE / 10`
/// (= 1_000) represents `10%`. Bps is the conventional unit for margin
/// in TradFi and in crypto perp venues (Hyperliquid, Binance, Drift all
/// express margin requirements in bps).
pub const MARGIN_SCALE: i64 = 10_000;
/// Network parameters governing the margin model.
///
/// Bps convention: `initial_margin_bps = 1000` means a 10% initial margin
/// requirement. Maintenance must be ≤ initial; if a misconfigured network
/// sets them equal, every position at exactly that threshold classifies as
/// `Liquidatable` (the conservative default).
///
/// `liquidation_fee_bps` is charged on the notional being closed, paid
/// out of the account's collateral, and credited to the insurance fund.
/// A typical HL-style value is 1–2% (100–200 bps).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LiquidationParams {
/// Initial margin requirement in bps (e.g., 1000 = 10%).
pub initial_margin_bps: u32,
/// Maintenance margin requirement in bps (e.g., 200 = 2%).
pub maintenance_margin_bps: u32,
/// Liquidation fee in bps, charged on closed notional.
pub liquidation_fee_bps: u32,
}
impl LiquidationParams {
/// Hyperliquid-style defaults: 10% initial, 2% maintenance, 1.5% fee.
/// Real production deployments use tiered maintenance (higher margin
/// for larger position sizes) — out of scope for compute.
#[must_use]
pub const fn hyperliquid_default() -> Self {
Self {
initial_margin_bps: 1_000,
maintenance_margin_bps: 200,
liquidation_fee_bps: 150,
}
}
#[must_use]
pub const fn initial_margin_bps(&self) -> u32 {
self.initial_margin_bps
}
#[must_use]
pub const fn maintenance_margin_bps(&self) -> u32 {
self.maintenance_margin_bps
}
#[must_use]
pub const fn liquidation_fee_bps(&self) -> u32 {
self.liquidation_fee_bps
}
}
MARGIN_SCALE: i64(margin ratio の乗算は i128 中間→i64 saturate なので、最初から i64 にして各サイトの as i64 キャストを散らさない)。pub フィールド + const fn ゲッター(透明な params、カプセル化境界なし)。hyperliquid_default() は const fn(static に乗る)+ #[must_use]。
Step 3: src/lib.rs を更新
//! `openhl-liquidation` — perpetual-position liquidation engine.
//!
//! Pure compute: no I/O, no async, no networking. Liquidation
//! decisions are deterministic functions over `(account_snapshot, mark,
//! params)`. Every validator on the chain must reach the same
//! [`MarginHealth`] from the same inputs; if two validators classify the
//! same account differently, the chain forks.
//!
//! ### Hyperliquid-shape liquidation, in one paragraph
//!
//! Perpetual contracts are levered positions backed by deposited
//! collateral. As the mark price moves against an open position,
//! unrealized PnL eats into the account's equity. When `equity / notional`
//! drops below the network's maintenance-margin requirement, the engine
//! force-closes the position at market — opposite side, full size, no
//! limit price. The liquidation fee is debited from collateral and
//! credited to the insurance fund. Any residual collateral, after fee
//! and PnL settlement, stays with the account. If equity went negative
//! before the close (the account is "underwater"), the insurance fund
//! absorbs the deficit instead of the position closing solvently.
pub mod types;
pub use types::{LiquidationParams, MARGIN_SCALE};
pub use types::* でなく explicit(public API のチェックリスト)。[MarginHealth] クロス参照はレッスン2 まで warning(funding と同じ扱い、抑制しない)。
Step 4: コンパイル
cargo build -p openhl-liquidation が通り、rustdoc warning 1 つ(MarginHealth 未解決、レッスン2 で解消)。
答え合わせ
cd ~/code/openhl-reference && git checkout 22eedf9
diff -u ~/code/my-openhl/crates/liquidation/Cargo.toml ./crates/liquidation/Cargo.toml
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
Cargo.toml は完全一致、types.rs は最初の ~50 行(module doc + MARGIN_SCALE + LiquidationParams)、lib.rs は最初の ~25 行(crate doc + pub mod types; + 2 re-export)。
合格基準
cargo build -p openhl-liquidation が warning 1(MarginHealth)。よくあるエラー: E0463(openhl-clob/openhl-funding dep 忘れ)/ E0583(pub mod compute を先取り)/ manifest parse([dev-dependencies] typo)。
まとめ(3行)
- margin の固定小数点単位は
MARGIN_SCALE = 10_000(bps)— 取引所が margin を表現する慣例解像度。funding の ppb と違うのはマグニチュード差 2 桁ぶんスケールをずらすから(使わない精度を買わない)。 LiquidationParams(initial/maintenance/fee bps)はネットワーク状態。hyperliquid_default()(10%/2%/1.5%)はconst fn+#[must_use]、3 つの名前付きu32フィールド(タプルでない)。- 依存は
openhl-clob(AccountId/Side/Qty)+openhl-funding(MarkPrice/PositionSize/Notional)の通常 dep。テストなし(値であって挙動でない)。
次のレッスン(レッスン2)
MarginRatio newtype と MarginHealth enum(Safe/AtRisk/Liquidatable/Underwater)を追加。これ以降の 5 レッスンはどれもこの型を return/consume する。bool でも u8 でもなく 4-variant enum を選ぶ理由を見る。