レッスン2 — Money 型 — price / premium / notional の newtype
問い
compute_premium(mark, index) の引数を取り違えて (index, mark) と呼んでも、両方 u64 ならコンパイルが通り、符号が反転した premium が production まで届く(全 long が受け取るべきときに支払う)。この invisible bug をコンパイル時に潰すには?
原理(最小モデル)
- newtype が引数順バグをコンパイルエラーにする。
u64をMarkPrice/IndexPriceでラップすれば、取り違えは production に届く invisible bug でなく型エラーになる。 - 型エイリアスは「型」でない。
type MarkPrice = u64はドキュメントであって安全性でない(引数を入れ替えても通る)。別アイデンティティが要るならstruct MarkPrice(pub u64)。 - 内部フィールドは
pub。 目的はクロスフィード防止であって値検証でない。pubならcompute.rsはmark.0で演算できる(.value()経由にならない)。検証はこの crate の仕事でない。 - 符号の有無はドメインの意味で決める。
MarkPrice/IndexPriceがu64=負の価格は上流の不変条件違反。Premium/Notionalがi64=方向そのものがデータ。 - 符号規約は型定義の 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/IndexPrice は u64(負の価格は上流の不変条件違反、ここで再検証しない — funding crate は入力が well-formed と信頼する)。同形だが別の 意味 で、compute_premium(mark, index) が (IndexPrice, MarkPrice) をコンパイル時拒否する。Premium/Notional は i64(符号付き、方向がデータの一部)。符号規約を 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, Hash — Default はレッスン4 fixture に要る)。
まとめ(3行)
- newtype(
MarkPrice/IndexPrice/Premium/Notional)が引数順バグをコンパイルエラーにする — 型あたり ~5 行で production まで見えないバグクラスを潰す。型エイリアスは新型を作らないので不可。 - 内部フィールドは
pub(クロスフィード防止だけが目的、値検証はしない)。MarkPrice/IndexPriceはu64、Premium/Notionalはi64(方向がデータ)。 - 符号規約は型定義の 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)へ。