注文マッチングエンジンは、あらゆる取引システムの中核を担う存在です。マッチングアルゴリズム、データ管理、通信インターフェースなど、いくつかの主要なコンポーネントから構成されています。その主な役割は、市場参加者の売買注文をマッチングし、取引を実行し、取引記録を生成することです。毎日膨大な流動性が金融市場を通じて移動していますが、このプロセスを効率的に実行するのが注文マッチングエンジンです。

注文マッチングエンジンがなければ、取引プラットフォーム(FX、商品、株式、CFDなど資産の種類を問わず)は実際の取引を成立させることができません。実際、取引システム内の他のすべてのモジュールは、注文マッチングエンジンを支えるために存在すると言っても過言ではありません。

注文マッチングの簡単な歴史

注文マッチングという概念は、19世紀に遡ります。当時の取引はオープンアウトクライ方式によって手動で行われていました。ニューヨーク証券取引所(NYSE)のような取引所では、トレーダーたちが取引フロアで「買い」「売り」と大声で叫びながら注文を出し、取引を成立させていました。これは当時としては効果的な方法でした。

その後、電子技術の進歩により、市場は徐々に自動化されたシステムへと移行していきました。1971年には、NASDAQが世界初の電子注文マッチングエンジンを導入し、人ではなくコンピューターが取引の実行を担うようになりました。この革新により、スピード・効率性・拡張性が飛躍的に向上し、現代の電子取引の基盤が築かれました。

注文マッチングの仕組み

ステップ説明
1. 注文の受信トレーダーは取引プラットフォームを通じて、価格・数量・注文の種類(成行または指値)などの詳細を含む売買注文を提出します。
2. 注文のキュー化マッチングエンジンは、事前に定められたルール(通常は価格優先・次に時間優先)に従って注文をキューに並べます。
3. 注文のマッチングエンジンは板情報をスキャンし、マッチする相手を探します。買い注文の価格が売り注文の価格以上であれば、取引が成立します。
4. 取引の確認マッチングが成立すると、エンジンは取引を確認し、双方と清算システムに通知を送信します。

注文板の管理

注文板(オーダーブック)は、注文マッチングエンジンにとって重要な構成要素であり、現在のすべての指値注文を追跡します。

  • 買い注文板:高い価格順に買い注文をリスト。価格が高い注文ほど優先的に約定されます。
  • 売り注文板:低い価格順に売り注文をリスト。価格が低い注文から順に執行されます。

注文板を適切に管理することは、市場の流動性を維持し、価格発見プロセスを支える上で不可欠です。市場状況の変化に応じて、注文板はリアルタイムで更新されなければなりません。

成行注文と指値注文の違い

成行注文(マーケットオーダー)

  • 現在の最良価格で即時に執行されます。
  • スピードと確実な執行を優先しますが、価格のコントロールはできません。
  • 流動性が低い市場では、スリッページが発生する可能性があります。

指値注文(リミットオーダー)

  • トレーダーが希望する最低売却価格または最高購入価格を指定します。
  • 注文板に登録され、指定価格に達したときにのみ執行されます。
  • 価格のコントロールは可能ですが、必ずしも注文が約定されるとは限りません。

一般的な注文マッチングアルゴリズム(例付き)

以下は、現在の市場で広く使用されている3つの代表的な注文マッチングアルゴリズムと、それぞれの簡単な実装例です。

1. 価格・時間優先方式(FIFO)

これは現代の市場で最も一般的に使用されているアルゴリズムです。注文はまず価格によって優先順位が決まり(より有利な価格が先)、同一価格の場合は時間順(早く出された注文が先)で処理されます。

例:

  • 買い注文A:100株 @ 500円(10:00)
  • 買い注文B:100株 @ 500円(10:01)

→ 売り注文が500円で来た場合、注文Aが優先的にマッチングされます。

import heapq

class Order:
    def __init__(self, order_id, order_type, price, quantity):
        self.order_id = order_id
        self.order_type = order_type  # 'buy' or 'sell'
        self.price = price
        self.quantity = quantity
        self.timestamp = None

    def __lt__(self, other):
        if self.order_type == 'buy':
            # For buy orders, higher prices have priority; if prices are equal, earlier orders have priority
            return (self.price, -self.timestamp) > (other.price, -other.timestamp)
        else:
            # For sell orders, lower prices have priority; if prices are equal, earlier orders have priority
            return (self.price, self.timestamp) < (other.price, other.timestamp)

class OrderBook:
    def __init__(self):
        self.buy_orders = []  # Max-heap for buy orders (negative prices for max-heap)
        self.sell_orders = []  # Min-heap for sell orders
        self.timestamp_counter = 0

    def add_order(self, order):
        order.timestamp = self.timestamp_counter
        self.timestamp_counter += 1

        if order.order_type == 'buy':
            heapq.heappush(self.buy_orders, order)
        else:
            heapq.heappush(self.sell_orders, order)

    def match_orders(self):
        matches = []

        while self.buy_orders and self.sell_orders:
            highest_buy_order = self.buy_orders[0]
            lowest_sell_order = self.sell_orders[0]

            if highest_buy_order.price >= lowest_sell_order.price:
                quantity_to_trade = min(highest_buy_order.quantity, lowest_sell_order.quantity)
                matches.append((highest_buy_order.order_id, lowest_sell_order.order_id, quantity_to_trade))

                highest_buy_order.quantity -= quantity_to_trade
                lowest_sell_order.quantity -= quantity_to_trade

                if highest_buy_order.quantity == 0:
                    heapq.heappop(self.buy_orders)
                if lowest_sell_order.quantity == 0:
                    heapq.heappop(self.sell_orders)
            else:
                break

        return matches

# Example usage
order_book = OrderBook()

# Add some buy and sell orders
order_book.add_order(Order(1, 'buy', 101, 10))
order_book.add_order(Order(2, 'buy', 102, 5))
order_book.add_order(Order(3, 'sell', 100, 8))
order_book.add_order(Order(4, 'sell', 99, 10))

# Match orders
matches = order_book.match_orders()

for match in matches:
    print(f"Matched Buy Order {match[0]} with Sell Order {match[1]} for {match[2]} units")

2. プロラタ(比例配分)方式

同一価格帯に複数の注文がある場合、注文サイズに応じて比例的にマッチングが行われます。より大きな注文には、より多くの数量が割り当てられます。この方式は先物取引所などでよく採用されています。

例:

  • 買い注文A:300株 @ 1000円
  • 買い注文B:700株 @ 1000円

→ 売り注文が500株 @ 1000円で出された場合、注文Aに150株、注文Bに350株が割り当てられます。

import heapq
from collections import defaultdict

class Order:
    def __init__(self, order_id, order_type, price, quantity):
        self.order_id = order_id
        self.order_type = order_type  # 'buy' or 'sell'
        self.price = price
        self.quantity = quantity
        self.timestamp = None

class OrderBook:
    def __init__(self):
        self.buy_orders = defaultdict(list)  # Buy orders grouped by price
        self.sell_orders = defaultdict(list)  # Sell orders grouped by price
        self.timestamp_counter = 0

    def add_order(self, order):
        order.timestamp = self.timestamp_counter
        self.timestamp_counter += 1

        if order.order_type == 'buy':
            self.buy_orders[order.price].append(order)
        else:
            self.sell_orders[order.price].append(order)

    def match_orders(self):
        matches = []

        # Sort buy and sell prices
        buy_prices = sorted(self.buy_orders.keys(), reverse=True)
        sell_prices = sorted(self.sell_orders.keys())

        while buy_prices and sell_prices:
            highest_buy_price = buy_prices[0]
            lowest_sell_price = sell_prices[0]

            if highest_buy_price >= lowest_sell_price:
                buy_orders_at_price = self.buy_orders[highest_buy_price]
                sell_orders_at_price = self.sell_orders[lowest_sell_price]

                # Calculate total buy and sell quantities at this price
                total_buy_quantity = sum(order.quantity for order in buy_orders_at_price)
                total_sell_quantity = sum(order.quantity for order in sell_orders_at_price)

                # Determine the amount to trade based on the smaller side
                quantity_to_trade = min(total_buy_quantity, total_sell_quantity)

                # Pro-rata allocation for buy and sell orders
                for order in buy_orders_at_price:
                    proportion = order.quantity / total_buy_quantity
                    traded_quantity = quantity_to_trade * proportion
                    matches.append((order.order_id, lowest_sell_price, traded_quantity))
                    order.quantity -= traded_quantity

                for order in sell_orders_at_price:
                    proportion = order.quantity / total_sell_quantity
                    traded_quantity = quantity_to_trade * proportion
                    matches.append((order.order_id, highest_buy_price, traded_quantity))
                    order.quantity -= traded_quantity

                # Remove fully matched orders
                self.buy_orders[highest_buy_price] = [o for o in buy_orders_at_price if o.quantity > 0]
                self.sell_orders[lowest_sell_price] = [o for o in sell_orders_at_price if o.quantity > 0]

                if not self.buy_orders[highest_buy_price]:
                    del self.buy_orders[highest_buy_price]
                    buy_prices.pop(0)

                if not self.sell_orders[lowest_sell_price]:
                    del self.sell_orders[lowest_sell_price]
                    sell_prices.pop(0)
            else:
                break

        return matches

# Example usage
order_book = OrderBook()

# Add some buy and sell orders
order_book.add_order(Order(1, 'buy', 100, 10))
order_book.add_order(Order(2, 'buy', 100, 20))
order_book.add_order(Order(3, 'sell', 100, 15))
order_book.add_order(Order(4, 'sell', 100, 5))

# Match orders
matches = order_book.match_orders()

for match in matches:
    print(f"Order {match[0]} matched at price {match[1]} for {match[2]:.2f} units")

3. ハイブリッド方式

価格・時間優先方式とプロラタ方式を組み合わせたアルゴリズムです。市場構造や参加者の行動に応じて、エンジンがマッチングロジックを動的に調整します。柔軟かつ公平性の高いソリューションを提供する目的で設計されています。

例:

  • 市場が流動的な場合は価格・時間優先を適用し、流動性が低下した場合には比例配分の要素を強化するなどの対応が可能です。
import heapq
from collections import defaultdict

class Order:
    def __init__(self, order_id, order_type, price, quantity):
        self.order_id = order_id
        self.order_type = order_type  # 'buy' or 'sell'
        self.price = price
        self.quantity = quantity
        self.timestamp = None

    def __lt__(self, other):
        return self.timestamp < other.timestamp

class OrderBook:
    def __init__(self):
        self.buy_orders = defaultdict(list)  # Buy orders grouped by price
        self.sell_orders = defaultdict(list)  # Sell orders grouped by price
        self.timestamp_counter = 0

    def add_order(self, order):
        order.timestamp = self.timestamp_counter
        self.timestamp_counter += 1

        if order.order_type == 'buy':
            heapq.heappush(self.buy_orders[order.price], order)
        else:
            heapq.heappush(self.sell_orders[order.price], order)

    def match_orders(self):
        matches = []

        # Sort buy and sell prices
        buy_prices = sorted(self.buy_orders.keys(), reverse=True)
        sell_prices = sorted(self.sell_orders.keys())

        while buy_prices and sell_prices:
            highest_buy_price = buy_prices[0]
            lowest_sell_price = sell_prices[0]

            if highest_buy_price >= lowest_sell_price:
                buy_orders_at_price = self.buy_orders[highest_buy_price]
                sell_orders_at_price = self.sell_orders[lowest_sell_price]

                # Calculate total buy and sell quantities at this price
                total_buy_quantity = sum(order.quantity for order in buy_orders_at_price)
                total_sell_quantity = sum(order.quantity for order in sell_orders_at_price)

                # Determine the amount to trade based on the smaller side
                quantity_to_trade = min(total_buy_quantity, total_sell_quantity)

                # Priority based matching and then pro-rata allocation
                while quantity_to_trade > 0 and buy_orders_at_price and sell_orders_at_price:
                    buy_order = buy_orders_at_price[0]
                    sell_order = sell_orders_at_price[0]

                    if buy_order.quantity <= sell_order.quantity:
                        matches.append((buy_order.order_id, sell_order.order_id, buy_order.quantity))
                        sell_order.quantity -= buy_order.quantity
                        quantity_to_trade -= buy_order.quantity
                        heapq.heappop(buy_orders_at_price)
                        if sell_order.quantity == 0:
                            heapq.heappop(sell_orders_at_price)
                    else:
                        matches.append((buy_order.order_id, sell_order.order_id, sell_order.quantity))
                        buy_order.quantity -= sell_order.quantity
                        quantity_to_trade -= sell_order.quantity
                        heapq.heappop(sell_orders_at_price)
                        if buy_order.quantity == 0:
                            heapq.heappop(buy_orders_at_price)

                # Remove empty price levels
                if not buy_orders_at_price:
                    del self.buy_orders[highest_buy_price]
                    buy_prices.pop(0)

                if not sell_orders_at_price:
                    del self.sell_orders[lowest_sell_price]
                    sell_prices.pop(0)

            else:
                break

        return matches

# Example usage
order_book = OrderBook()

# Add some buy and sell orders
order_book.add_order(Order(1, 'buy', 100, 15))
order_book.add_order(Order(2, 'buy', 100, 25))
order_book.add_order(Order(3, 'sell', 100, 30))
order_book.add_order(Order(4, 'sell', 100, 10))

# Match orders
matches = order_book.match_orders()

for match in matches:
    print(f"Buy Order {match[0]} matched with Sell Order {match[1]} for {match[2]:.2f} units")