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

レッスン2 — Money 型 — price / premium / notional の newtype

問い

compute_premium(mark, index) の引数を取り違えて (index, mark) と呼んでも、両方 u64 ならコンパイルが通り、符号が反転した premium が production まで届く(全 long が受け取るべきときに支払う)。この invisible bug をコンパイル時に潰すには?

原理(最小モデル)

  • newtype が引数順バグをコンパイルエラーにする。 u64MarkPrice/IndexPrice でラップすれば、取り違えは production に届く invisible bug でなく型エラーになる。
  • 型エイリアスは「型」でない。 type MarkPrice = u64 はドキュメントであって安全性でない(引数を入れ替えても通る)。別アイデンティティが要るなら struct MarkPrice(pub u64)
  • 内部フィールドは pub 目的はクロスフィード防止であって値検証でない。pub なら compute.rsmark.0 で演算できる(.value() 経由にならない)。検証はこの crate の仕事でない。
  • 符号の有無はドメインの意味で決める。 MarkPrice/IndexPriceu64=負の価格は上流の不変条件違反。Premium/Notionali64=方向そのものがデータ。
  • 符号規約は型定義の doc に pin。 Premium の定義に「正=mark>index、longs が払う」と書けば、全 consumer の single source of truth になる。

具体例

// 🔴 raw u64 — signature 上は両方 u64
fn compute_premium(mark: u64, index: u64) -> i64 { /* ... */ }
let mark = 100_u64; let index = 105_u64;
compute_premium(mark, index);   // ✨ 意図通り
compute_premium(index, mark);   // 🔴 取り違えても COMPILE OK → 符号反転が production まで届く

// 🟢 newtype — 型システムが意図を覚えている
fn compute_premium(mark: MarkPrice, index: IndexPrice) -> Premium { /* ... */ }
let mark = MarkPrice(100); let index = IndexPrice(105);
compute_premium(mark, index);   // ✨ OK
compute_premium(index, mark);   // ❌ COMPILE ERROR: expected MarkPrice, found IndexPrice

差は実行時挙動でなく「ビルドが通るか」。u64 版は production まで気付けないバグを、newtype はキーボードを叩く数秒で見つける。型あたり ~5 行で production に出るまで見えないバグクラス を防ぐ — これが newtype パターンの存在意義だ。

失敗例(誤解)

type MarkPrice = u64; type IndexPrice = u64; のエイリアスで十分」は誤り — 型エイリアスは新型を作らず、既存型をリネームするだけ。両者とも u64 のままで compute_premium(some_index, some_mark) は静かに通る。エイリアスは長いジェネリック型の可読性(type FillSink = Arc<Mutex<Vec<Fill>>>)のためで、意味的に異なる値の区別のためでない。


ここまでで「newtype が取り違えを潰す」は着地した。ここから 4 newtype を types.rs に足す(compute も clock もテストもなし — 純粋な型定義。最初のテストはレッスン4 の compute_premium)。コードは完全形。

🛑 予測。 MarkPrice(pub u64) の内部を pub にする理由は? private にして .value() getter を置いたら?(答え: compute.rs が生値で演算する(i128::from(mark.0) - ...)。private + getter だとどこも mark.value() を書く羽目に。pub 内部は「クロスフィード防止だけが目的、検証なし」の openhl 慣習 — clob::Price(pub u64) と同形・同理由。newtype の仕事は取り違えを型エラーにすることで、値検証でない。)

ステップで組み立てる

Step 1: 4 newtype を types.rs に append

RATE_SCALE の後ろに:

/// Mark price in minor units. Same scale convention as `clob::Price`, but a
/// distinct type so callers can't accidentally feed an orderbook price into
/// the funding math where an index/oracle price is expected.
///
/// `MarkPrice` is a single u64 not a signed-fixed-point, because prices are
/// always positive (zero or negative price would be a system invariant
/// violation handled upstream, not here).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MarkPrice(pub u64);

/// Index price (off-chain oracle reference). Same scale as `MarkPrice`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IndexPrice(pub u64);

/// Premium = `(mark - index) / index`, scaled by [`RATE_SCALE`].
///
/// Sign convention: positive when mark > index (longs are overpaying,
/// funding will be positive → longs pay shorts).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Premium(pub i64);

/// Signed quote-currency delta. Positive = account receives, negative =
/// account pays. Funding settlement produces one [`Notional`] per non-flat
/// position per tick.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Notional(pub i64);

MarkPrice/IndexPriceu64(負の価格は上流の不変条件違反、ここで再検証しない — funding crate は入力が well-formed と信頼する)。同形だが別の 意味 で、compute_premium(mark, index)(IndexPrice, MarkPrice) をコンパイル時拒否する。Premium/Notionali64(符号付き、方向がデータの一部)。符号規約を doc に pin(「正 = mark > index、longs が払う」)— これが下流の single source of truth。

Notional の符号と position 方向の対応:

┌─────────────────────────────┬───────────────────┬───────────────────┐
│ 市場の状態                  │ Long ポジション   │ Short ポジション  │
├─────────────────────────────┼───────────────────┼───────────────────┤
│ Mark > Index (Premium 正)   │ Notional(負)→支払 │ Notional(正)→受取 │
│ Mark < Index (Premium 負)   │ Notional(正)→受取 │ Notional(負)→支払 │
└─────────────────────────────┴───────────────────┴───────────────────┘

Notional の符号 = 「そのアカウントの quote balance に足す差分」(market 方向でなく、bridge が balance += notional.0 でそのまま適用できるアカウント視点)。レッスン7 の apply_funding がこの 4 セルを 4 行で実装する。

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

pub use types::{IndexPrice, MarkPrice, Notional, Premium, RATE_SCALE};

アルファベット順。pub use types::* でなく explicit にする — 内部 helper の偶発公開を防ぐ(explicit re-export は public API のチェックリスト、名前 1 つ 1 つが意図的決定)。

Step 3: コンパイル

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

warning は 2 つに減る([Premium] リンクが解決)。[FundingRate]/[FundingClock] はレッスン3/8 で解決。

答え合わせ

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 は Notional まで(最初の 4 newtype)一致、lib.rs は 5 名前の re-export。

合格基準

cargo build -p openhl-funding が通り、warning 2。よくあるエラー: tuple-struct でなく MarkPrice { value: u64 }E0381)/ Premium(pub u64) と符号間違い(E0277)/ derive 欠け(完全集合 Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, HashDefault はレッスン4 fixture に要る)。

まとめ(3行)

  • newtype(MarkPrice/IndexPrice/Premium/Notional)が引数順バグをコンパイルエラーにする — 型あたり ~5 行で production まで見えないバグクラスを潰す。型エイリアスは新型を作らないので不可。
  • 内部フィールドは pub(クロスフィード防止だけが目的、値検証はしない)。MarkPrice/IndexPriceu64Premium/Notionali64(方向がデータ)。
  • 符号規約は型定義の doc に pin する(Premium 正 = longs が払う)— 数値型で最も記憶違いが起きやすい部分の single source of truth。

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

型 roster を完成させる(FundingRate/PositionSize/Position/Settlement/FundingParams + HL デフォルト)。焦点は newtype から parameter-object パターンと HL デフォルトの根拠(なぜ 1 時間、なぜ 4% cap、なぜ divisor 8)へ。