レッスン5 — submit_market — 任意の価格を取る order
問い
Market order(価格を持たず、book が提示する価格を受け入れる)をどう実装するか? Limit との差分はコードのどこに宿るか?
原理(最小モデル)
- Market = Limit から price check と rest 処理を抜いたもの。 レッスン4 と同じ「クロスする限り辿る」ループだが
price <= limitガードがなく、rest-the-remainder もない。意味の差は 存在しないコード に宿る — boolean フラグでパラメタ化すると両方の本体が読めなくなる。 - 約定価格は常に maker の価格。 Market は価格を持たず book の提示を受ける。価格発見 = best bid と best ask のスプレッドが価格を決める規則。
- 同じ戻り型、異なる契約。
remaining_qtyは Limit では「rest した分」(常にQty(0))、Market では「破棄した分」(実際の残量)。型は同じ、FillResultの doc が両解釈を明示。 - 「何も起きなかった」はエラーでなく有効な結果。 空 book への Market buy は
FillResult { fills: vec![], remaining_qty: order.qty }を返す(Result::Errを投げない)。
具体例
ask = {100:[O_a(30)]}、Buy qty 50:
MARKET(leftover を破棄) LIMIT @100(leftover を rest)
walk asks(price guard なし) walk asks while ask_price ≤ 100
100: O_a 完全消費 → Fill(30) 100: O_a 完全消費 → Fill(30)
remaining = 20 remaining = 20
leftover: DISCARD leftover: REST as bid @100
AFTER bids: empty(20 が消えた) AFTER bids: {100:[new(20)]}
remaining_qty: Qty(20) remaining_qty: Qty(0)
同じ約定、leftover の運命だけが違う。差分は「rest-the-remainder ブロックの有無」。
失敗例(誤解)
「Market Buy に limit_price = u64::MAX、Sell に Price(0) を渡して submit_limit を呼べばいい」は誤り — price-check 除去には効くが rest-the-remainder は残る。約定しなかった qty を u64::MAX で rest しようとし、最高価格の phantom resting bid を作り、入ってくる sell を即マッチしてしまう。2 関数・2 意味論、別々に保つ。
ここまでで「Market は Limit のサブセット・leftover の運命」は着地した。ここから組み立てる(matching engine がこれで完成)。コードは完全形。
🛑 予測。 ask =
{100:[O_a(30)]}に 50 unit の Market buy。約定とremaining_qtyは? 同じ book で Limit buy @100 なら?(答え: Market → Fill(30 @100),remaining_qty=20(破棄、book に乗らない)。Limit → Fill(30 @100),remaining_qty=0(20 が bid @100 に rest)。)
ステップで組み立てる
Step 1: submit_market()(impl Book 内、submit_limit の後)
fn submit_market(&mut self, order: Order) -> FillResult {
let mut remaining = order.qty;
let mut fills = Vec::new();
match order.side {
Side::Buy => loop {
if remaining.0 == 0 {
break;
}
let Some(best_price) = self.asks.keys().next().copied() else {
break;
};
let queue = self
.asks
.get_mut(&best_price)
.expect("price level exists by construction");
fills.push(match_at_level(&order, best_price, queue, &mut remaining));
if queue.is_empty() {
self.asks.remove(&best_price);
}
},
Side::Sell => loop {
if remaining.0 == 0 {
break;
}
let Some(best_rev) = self.bids.keys().next().copied() else {
break;
};
let queue = self
.bids
.get_mut(&best_rev)
.expect("price level exists by construction");
fills.push(match_at_level(&order, best_rev.0, queue, &mut remaining));
if queue.is_empty() {
self.bids.remove(&best_rev);
}
},
}
FillResult {
fills,
remaining_qty: remaining,
}
}
submit_limit との差分は 3 つだけ: ループ内の price check なし(任意価格で取る)/ ループ後の rest-the-remainder なし(leftover 破棄)/ return の remaining_qty が Qty(0) でなく remaining(実際の残量)。ループの形は同じ、check を 2 個削り return 値を 1 個変えるだけ。
Step 2: submit() dispatcher 更新(todo! を置換)
pub fn submit(&mut self, order: Order) -> FillResult {
match order.order_type {
OrderType::Limit { price } => self.submit_limit(order, price),
OrderType::Market => self.submit_market(order),
}
}
1 行変更。dispatcher は「型駆動ルーティング、arm ごと 1 行」のまま。
答え合わせ
cd ~/code/openhl-reference && git checkout 55a9dff
diff -u ~/code/my-openhl/crates/clob/src/book.rs ./crates/clob/src/book.rs
git checkout main
本レッスン後は book.rs の最初の ~190 行。残り ~25 行は cancel()(レッスン6)。
合格基準
cargo check -p openhl-clob
clean(全関数に caller がいて unused 警告なし)。一時 smoke test(後で削除):
#[cfg(test)]
mod smoke {
use super::*;
#[test]
fn market_buy_takes_what_it_can_then_discards() {
let mut book = Book::new();
book.submit(Order {
id: OrderId(1),
account: AccountId(1),
side: Side::Sell,
qty: Qty(30),
order_type: OrderType::Limit { price: Price(100) },
});
let result = book.submit(Order {
id: OrderId(2),
account: AccountId(2),
side: Side::Buy,
qty: Qty(50),
order_type: OrderType::Market,
});
assert_eq!(result.fills.len(), 1);
assert_eq!(result.fills[0].qty, Qty(30));
assert_eq!(result.fills[0].price, Price(100));
assert_eq!(result.remaining_qty, Qty(20)); // DISCARDED, not rested
assert_eq!(book.best_bid(), None);
assert_eq!(book.best_ask(), None);
}
#[test]
fn market_buy_against_empty_book_returns_full_remainder() {
let mut book = Book::new();
let result = book.submit(Order {
id: OrderId(1),
account: AccountId(1),
side: Side::Buy,
qty: Qty(50),
order_type: OrderType::Market,
});
assert_eq!(result.fills.len(), 0);
assert_eq!(result.remaining_qty, Qty(50));
}
}
cargo test -p openhl-clob smoke で確認 → pass、その後 smoke を削除。よくあるミス: final FillResult を remaining_qty: Qty(0)(submit_limit から copy-paste)にする / Market 後に best_bid() が Some(rest-the-remainder に fall through)。
まとめ(3行)
- Market = Limit から price check + rest を抜いたもの。差は存在しないコードに宿るので、別関数に保つ(フラグ化しない)。
remaining_qtyは同じ型で契約が違う(Limit=常に0、Market=破棄量)。約定価格は常に maker の価格。- 空 book への Market は
Result::Errでなく fills 空 + full remainder を返す — total + side-effect-free が consensus state machine の規律。