FABRKNT
Step 4. Funding:決定論的数学パイプラインと Funding ステートマシンの構築
Determinism と型
レッスン 4 / 12·CONTENT35 分70 XP
コース
Step 4. Funding:決定論的数学パイプラインと Funding ステートマシンの構築
レッスンの役割
CONTENT
順序
4 / 12

レッスン3 — Position 型 — roster 完成 + HL デフォルト

問い

compute_rateinterval_secs/rate_cap/divisor の 3 値が要る。positional 引数で渡すと、後で 4 つ目を足したとき全呼び出し箇所が壊れる。config の進化をまたいで呼び出し箇所を安定させるには? そして HL の「divisor 8 / 4% cap / 1h」は何を encode しているか?

原理(最小モデル)

  • 同じ形・別の役割 = 別の型。 FundingRatePremiumRATE_SCALE スケールの i64 だが、premium=生の dislocation、rate=divisor+clamp 後の出力。別型なら pipeline を型で強制(compute_rate を通さない premium を apply_funding に渡せない)。
  • 方向+大きさを符号付き整数 1 つで。 PositionSize(i64):正=long / 負=short / 0=flat。enum+magnitude より小さく・速く・数学が単純。符号規約は doc に。
  • スナップショット型 vs stateful エンティティ。 Position(account, size) だけ、entry price も PnL も履歴も持たない。広い state は owning layer(vault/clearing)の仕事、funding crate は狭い snapshot を処理する。
  • parameter-object パターン。 3 値を FundingParams にまとめれば config 拡張で呼び出し箇所が壊れない(成長するのは struct だけ)。グループ自体がドメイン概念のときに使う。
  • HL デフォルトの非対称 2 段構え。 divisor=8 で typical daily を 3×premium に持ち上げ、cap=4%/interval で worst daily を 96% に切る。

具体例

              セマンティクス上の意図           実際の挙動
              ─────────────────────           ───────────────
divisor = 8 = 「1 日を 8 分割」                でも settle/適用は毎時 (24 回/日)
                ↓                                ↓
  premium / 8 × 8 = premium                    premium / 8 × 24 = 3 × premium
  → 1 日分の premium がそのまま                  → 「狙った daily 量」より 3 倍

そこで cap (4%/interval):
  普通の市場では post-divisor rate ≪ 4% で cap に当たらず、実効 daily ≒ 3 × premium。
  異常時 (oracle outage 等) でも 毎時 4% で clamp → 最悪 daily = 4% × 24 = 96%/day で必ず止まる。

HL は「divisor で typical daily を 3×premium に持ち上げ、cap で worst daily を 96% に切る」非対称 2 段構え。divisor 単体(1 日 8 settlement)から素直に出る値より、cap のほうが厳しい絶対上限を提供する。

失敗例(誤解)

Position も先物の損益計算のため entry price を持つべき」は誤り — それは owning layer の仕事。vault/clearing が entry price を追跡し PnL を計算する。funding crate はその下流で、現在 の position snapshot に 現在 の funding を適用するだけ。snapshot 型は narrow に保ち、owning layer が wide な型を持てばいい。


ここまでで「pipeline を型で強制」「parameter object」「HL デフォルト」は着地した。ここから 5 型を足して roster を閉じ、AccountId import も入れる(これで セクション1 完了、rustdoc warning もゼロへ)。コードは完全形。

🛑 予測。 FundingParams { interval_secs, rate_cap, divisor } を struct にまとめる理由は(compute_rate(premium, interval, cap, divisor) でなく)?(答え: parameter-object が config 進化をまたいで呼び出し箇所を安定させる。後で min_settlement_threshold を足してもシグネチャは compute_rate(premium, params) のまま — 成長するのは struct だけ。positional 版は新パラメータごとに全呼び出し箇所が壊れる。安定して一緒に動く値で、グループ自体がドメイン概念のときにまとめる。)

ステップで組み立てる

Step 1: AccountId import を追加

types.rs の module doc の後、RATE_SCALE の前に:

use openhl_clob::AccountId;

レッスン1 の Cargo.toml dep で準備済み。AccountId は自前の型でないので re-export しない(依存先の型は呼び出し側に直接 import させる — re-export すると同じ型に 2 つの import path ができて依存が不透明になる)。

Step 2: Premium の後ろに FundingRate を append

/// Per-interval funding rate. Same scale as [`Premium`]; positive means
/// longs pay shorts. A rate of `RATE_SCALE / 100` = 1% per interval.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FundingRate(pub i64);

Premium と同形(同 i64・同 derive)だが別型 — premium=生の dislocation、rate=divisor+clamp 後に position へ適用される値。compute_rate を通さない premium を apply_funding に渡せない(型が pipeline 順序を強制)。

Step 3: PositionSize を append

/// Signed position size in base units. Positive = long, negative = short,
/// zero = flat. Accounts with zero size aren't included in settlement
/// snapshots — see [`Position`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PositionSize(pub i64);

符号付き整数 1 つで long(>0)/short(<0)/flat(==0)。{ direction: Direction, magnitude: u64 } の 2 フィールド表現より小さく(8 vs ~16 byte)・速く(hot path で enum dispatch 不要)・数学が単純(size.0 の乗算で符号が自然に伝播)。「zero size は settlement snapshot に含めない」が load-bearing(apply_funding が filter、レッスン7)。

Step 4: Position を append

/// A single account's net position on the market. The funding state machine
/// treats positions as a per-tick *snapshot* — it never owns or mutates
/// them. The owning layer (vault / clearing) is responsible for tracking
/// `Position` over time and producing snapshots at each tick.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Position {
    pub account: AccountId,
    pub size: PositionSize,
}

(account, size) だけ — entry_price/realized_pnl なし(owning layer の仕事)。doc が ownership 契約を明示(「never owns or mutates them」)。Default を付けない — AccountId::default() = AccountId(0) は多くのアカウントシステムで sentinel。identity を担う struct に偶発的なデフォルト構築を許さない。

Step 5: Settlement を append

/// Output of applying a funding rate to one position. The bridge layer
/// translates these into balance updates against each account's quote
/// balance.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Settlement {
    pub account: AccountId,
    pub delta: Notional,
}

apply_funding の出力、非 flat position 1 つにつき 1 つ。位置インデックスでなく account を再度持つのは、filter で入力 position と出力 settlement の長さが違うから(インデックス対応の管理を呼び出し側から切り離す)。struct-array vs parallel-array のトレードオフで struct-array を選択(コスト = 冗長な AccountId 1 つ、メリット = インデックス管理不要)。

Step 6: FundingParams + hyperliquid_default を append

/// Network parameters that govern funding cadence and magnitude.
///
/// `divisor` represents "settlements per day": HL settles 8 times per day,
/// so `premium / 8` is the per-interval rate. Higher divisor → smaller rate
/// per tick (and inverse: lower divisor concentrates the same daily target
/// rate into fewer payments).
///
/// `rate_cap` is the absolute maximum |rate| per interval. Production
/// networks set this to bound the worst-case payment an extreme oracle
/// dislocation can produce. Zero `rate_cap` disables funding entirely.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FundingParams {
    pub interval_secs: u64,
    pub rate_cap: FundingRate,
    pub divisor: u32,
}

impl FundingParams {
    /// Hyperliquid-style defaults: 1-hour interval, ±4%/hour cap, 8× divisor.
    /// 8× divisor with a 1-hour interval means the *target* daily premium
    /// would be applied across 24 hours' worth of ticks at 1/8 of the premium
    /// each — i.e., 24/8 = 3× the premium per day. That asymmetry is
    /// intentional: HL caps more aggressively than the divisor alone implies.
    #[must_use]
    pub const fn hyperliquid_default() -> Self {
        Self {
            interval_secs: 3600,
            // 4% per interval = 40_000_000 ppb (since 0.04 × 1e9 = 4e7).
            rate_cap: FundingRate(40_000_000),
            divisor: 8,
        }
    }
}

HL デフォルトの理由: interval_secs: 3600(毎時 — basis dislocation を素早く感じ取れ、block time noise に支配されない)/ rate_cap: FundingRate(40_000_000)(4%/interval、oracle 異常への保険 — index を一時的に 50% 動かせる攻撃者でも 1 tick で 50% を抜けない)/ divisor: 8(1 日 8 settlement だが 24 interval にまたがり適用)。const fn(コンパイル時定数に使える)+ #[must_use](値生成が目的の関数で結果破棄は常にバグ)。

Step 7: lib.rs re-export を更新

pub use types::{
    FundingParams, FundingRate, IndexPrice, MarkPrice, Notional, Position, PositionSize,
    Premium, Settlement, RATE_SCALE,
};

アルファベット順、合計 10 名前(9 型 + RATE_SCALE)。

Step 8: コンパイル

cargo build -p openhl-funding
warning: unresolved link to `FundingClock`
    Finished `dev` profile [unoptimized + debuginfo] in 0.4s

warning は 1 つに(残るは FundingClock のみ、レッスン8 で解決)。

セクション1 のデータパイプライン

定義した 9 型は、セクション2(レッスン4〜7)で組み立てる純粋計算パイプラインの語彙だ:

  MarkPrice  ──┐
               ├─► (レッスン4: compute_premium) ─► Premium ──┐
  IndexPrice ──┘                                       │
                                                       ▼
  FundingParams ───────────────────────────► (レッスン6: compute_rate)
   { rate_cap, divisor, … }                            │
                                                       ▼
                                                  FundingRate ──┐
                                                                ├─► (レッスン7: apply_funding) ──► Vec<Settlement>
  Position (snapshot)             ──────────────────────────────┘                            { account, delta: Notional }
   { account, size: PositionSize }

型レベルで強制する 3 点: ① PremiumFundingRate は同 i64 だが別型(compute_rate を通さず apply_funding に渡すとコンパイルエラー、pipeline 順序を型で守る)② 入力 Position と出力 Settlement を別型に(Settlement の再適用を型で塞ぐ)③ FundingParams は枝として並走(後から閾値が増えても矢印は増えない)。

答え合わせ

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

types.rs は cd94137完全一致(9 型 + RATE_SCALE + hyperliquid_default、~110 行)。lib.rs は compute/clock の re-export だけ欠ける。セクション1 完了。

合格基準

cargo build -p openhl-funding が warning ≤1(FundingClock のみ、レッスン8 で解決)。よくあるエラー: E0432openhl-clob dep 欠け)/ Notional 綴り間違い / const fn 内の関数呼び出し(FundingRate(40_000_000) の tuple-struct リテラルを使う)。

まとめ(3行)

  • 9 型で funding の語彙が完成: FundingRatePremium と同形・別型で pipeline 順序を型強制)/ PositionSize(符号付き 1 整数で long/short/flat)/ Position(narrow な snapshot)/ Settlement(出力)/ FundingParams
  • FundingParams は parameter-object で config 進化に強い。HL デフォルトは非対称 2 段構え(divisor で typical を 3×premium に、cap で worst を 96%/day に切る)。
  • Position は entry price/PnL を持たない snapshot(state 追跡は owning layer)。AccountId(0) sentinel 回避で Default を付けない。types.rs は cd94137 と完全一致。

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

compute.rs を始める — crate 最初の数学 compute_premium(8 行)。設計判断 3 つ(index==0Premium(0) で扱う / i128 中間値で overflow 回避 / i64 へ saturate)と、crate 最初の unit test 4 つを追加する。