アルゴトレーディングにおける注文タイプ:チェイシング付きリミットからバーチャルオーダーまで

アルゴリズム取引における注文タイプの完全ガイド:標準的な取引所注文、チェイシングリミット、時間ベース、バーチャル/シンセティック注文。コード例と実際のユースケース付き。

初心者が取引所のターミナルを開くと、2つのボタンが目に入る:「買い」と「売り」。アルゴトレーダーが自分のコードベースを開くと、27種類の注文タイプ、3層の抽象化、そしてノートパソコンを閉じて八百屋でキュウリを売りたくなるようなエッジケースの山が目に入る。しかし残念ながら、キュウリではUTC 3:59にファンディングレート・アービトラージはできない——だから一緒に掘り下げていこう。

この記事では、基本的な取引所注文から、システム内にのみ存在し板には一切表示されないシンセティックな仮想構造まで、全行程を辿る。TypeScript、Python、多少の苦痛、そして少しの悟りを期待してほしい。


1. 標準的な取引所注文:省略できない基礎

標準注文 標準注文タイプの分類:成行注文からアイスバーグ注文まで

複雑なものを構築する前に、基本的な構成要素を正しく理解していることを確認する必要がある。ストップリミットとストップマーケットを混同し、なぜストップが「発動しなかった」のか不思議に思う人が驚くほど多い(ネタバレ:発動はしたが、スリッページにより指値注文が約定しなかっただけだ)。

成行注文(Market order)

最もシンプルであると同時に最も危険なタイプ。取引所に「今すぐ、利用可能な任意の価格で買え/売れ」と伝える。取引所は板から最良価格から流動性を取得する。最良価格の数量が足りなければ——さらに深い価格帯にスリップする。

使用するタイミング: 緊急のポジション解消、価格よりスピードが重要なシグナルの執行。

落とし穴: 流動性の薄い市場では、100 BTCの成行注文が価格を数パーセント動かす可能性がある。インパクトを考慮せずに成行注文をモデル化するバックテストは空想にすぎない。

指値注文(Limit order)

具体的な価格を指定する。注文は板に入り、誰かがその価格に同意するまで待つ。指値買い注文の価格が現在の市場価格より高ければ——即座に約定する(成行注文のようだが、最大価格の保証付き)。

重要ポイント: 指値注文は約定を保証しない。価格があなたのレベルに到達して反転し、あなたはキューに残されたままになる可能性がある(これについて詳しくは板のキューポジションに関する記事を参照)。

ストップマーケットとストップリミット

ここから混乱が始まる。両タイプとも、トリガー価格(ストップ価格)に達した時に発動する「休眠」注文だ。しかし:

  • ストップマーケット:発動時に成行注文に変換。約定は保証されるが、価格は保証されない。
  • ストップリミット:発動時に指値注文に変換。価格は保証される(指定価格以上/以下)が、約定は保証されない。

ボラティリティの高い暗号資産市場では、ストップリミットが「空振り」することがある——価格がストップを突き抜け、指値注文が出されたが、市場はすでに先に行ってしまっている。未約定の指値注文と増大する損失が残される。これがストップロスにストップマーケットがより一般的に使われる理由だ。

トレーリングストップ

設定された距離で価格を「追随」するストップ。価格が上昇すればストップも上昇。価格が下落すればストップはその場に留まる。トレンドフォロー戦略で利益を保護するのに有用。

取引所のサポート: すべての取引所がネイティブのトレーリングストップに対応しているわけではない。アルゴトレーダーはプログラムで実装することが多い——パラメータ(コールバック率、発動価格、ステップサイズ)をより細かく制御できるためだ。

アイスバーグ注文

板に表示されるのは総量のごく一部だけの注文。1,000 BTCを買いたいが、板には10だけ表示する。最初の10が約定すると——次の10が現れる。

なぜ: 市場に真の意図を隠すため。板の大量注文は「大口が買い/売りたがっている」というシグナルを全員に発する。それに対してHFTアルゴリズムがフロントランニングを始め、価格があなたから離れていく。

注意点: 多くの暗号資産取引所では、アイスバーグ注文がサポートされていないか、同一数量のパターンで容易に検出される。高度なアルゴリズムは表示部分のサイズをランダム化する。

有効期間パラメータ:GTC、GTD、IOC、FOK

これらは独立した注文タイプではなく、有効期間(time-in-force)パラメータ——注文がどれだけの期間有効かを示す:

パラメータ正式名称動作
GTCGood Till Cancelledキャンセルまで有効。デフォルトの標準
GTDGood Till Date指定された日時まで有効
IOCImmediate or Cancel即座に執行(全部または一部)、残りはキャンセル
FOKFill or Kill全量を即座に執行のみ。不可能な場合——全量キャンセル

IOC vs FOK: 違いは決定的だ。IOCは部分約定が可能——100 BTCを買いたかったが3だけ買えて、残りはキャンセルされた。FOKは100か、さもなくばゼロだ。

ポストオンリー(メイカーオンリー)

メイカーとして板に入ることが保証され、テイカーとしては決して約定しない注文。注文時の価格で即座に約定してしまう場合——取引所がそれを拒否する(または価格を調整する、取引所による)。

なぜ: メイカー手数料は通常テイカー手数料より低い(BinanceではVIPレベルで0.02% vs 0.04%)。毎日数千の注文を出すマーケットメイカーにとって、手数料の差は利益と損失の差だ。


2. TWAPとVWAP:機関投資家が板に象を隠す方法

ヘッジファンドが5,000万ドルのポジションを構築したい場合、1つの成行注文は出さない。エグゼキューション・アルゴリズム——大口注文を多数の小口注文に分割し、時間をかけて執行することでマーケットインパクトを最小化するアルゴリズムを使用する。

TWAP(時間加重平均価格)

アイデアは極めてシンプル:総量を等分し、等間隔の時間で執行する。

import asyncio
from datetime import datetime, timedelta

class TWAPExecutor:
    """
    TWAPエグゼキューター:大口注文を等分に分割し、
    等間隔の時間で執行する。
    """
    def __init__(self, exchange, symbol: str, side: str,
                 total_qty: float, duration_minutes: int, num_slices: int):
        self.exchange = exchange
        self.symbol = symbol
        self.side = side
        self.total_qty = total_qty
        self.slice_qty = total_qty / num_slices
        self.interval = (duration_minutes * 60) / num_slices
        self.num_slices = num_slices
        self.executed_qty = 0.0
        self.fills: list[dict] = []

    async def execute(self):
        for i in range(self.num_slices):
            remaining = self.total_qty - self.executed_qty
            qty = min(self.slice_qty, remaining)
            if qty <= 0:
                break

            try:
                order = await self.exchange.create_order(
                    symbol=self.symbol,
                    type="market",
                    side=self.side,
                    amount=qty,
                )
                self.executed_qty += float(order["filled"])
                self.fills.append(order)
                print(f"[TWAP] slice {i+1}/{self.num_slices}: "
                      f"filled {order['filled']} @ {order['average']}")
            except Exception as e:
                print(f"[TWAP] slice {i+1} failed: {e}")

            if i < self.num_slices - 1:
                await asyncio.sleep(self.interval)

        avg_price = (
            sum(f["cost"] for f in self.fills) /
            sum(f["filled"] for f in self.fills)
        ) if self.fills else 0
        print(f"[TWAP] done: {self.executed_qty}/{self.total_qty} "
              f"avg price: {avg_price:.2f}")

VWAP(出来高加重平均価格)

VWAPはよりスマートだ:典型的な出来高プロファイルを考慮する。9:00から10:00の間に通常日次出来高の30%が取引されるなら、VWAPはその時間帯に注文の30%を執行する。目標は、平均約定価格を市場VWAPにできるだけ近づけることだ。

class VWAPExecutor:
    """
    VWAPエグゼキューター:過去の出来高プロファイルに
    比例してボリュームを配分する。
    """
    def __init__(self, exchange, symbol: str, side: str,
                 total_qty: float, volume_profile: list[float]):
        self.exchange = exchange
        self.symbol = symbol
        self.side = side
        self.total_qty = total_qty
        # volume_profile: 各スライスの総出来高に対する割合
        # 例:[0.15, 0.25, 0.30, 0.20, 0.10] — 5期間
        total_weight = sum(volume_profile)
        self.weights = [w / total_weight for w in volume_profile]

    async def execute(self, interval_seconds: float = 60.0):
        executed = 0.0
        for i, weight in enumerate(self.weights):
            qty = self.total_qty * weight
            remaining = self.total_qty - executed
            qty = min(qty, remaining)

            if qty <= 0:
                break

            order = await self.exchange.create_order(
                symbol=self.symbol,
                type="market",
                side=self.side,
                amount=qty,
            )
            executed += float(order["filled"])
            print(f"[VWAP] period {i+1}: weight={weight:.2%}, "
                  f"filled={order['filled']} @ {order['average']}")

            await asyncio.sleep(interval_seconds)

TWAPとVWAPの違い: TWAPはよりシンプルで予測可能。VWAPはより良い平均価格を実現するが、信頼性の高い出来高プロファイルが必要。出来高がウォッシュトレードされている可能性のある暗号資産市場では、VWAPプロファイルを慎重に構築する必要がある。


3. チェイシング付きリミット:注文が価格を追いかける時

チェイシング指値注文 チェイシングリミット:設定可能なアグレッシブさで動く価格を追いかける注文

ここからが本当に面白くなる。標準的な指値注文は受動的な存在だ:板に座って待つ。価格が動いたら——注文は未約定のまま残る。アルゴトレーダーにとって、これはしばしば受け入れられない:エントリーシグナルが出たのに、市場が0.1%動いたためにポジションが構築できなかった。

チェイシング指値注文(Chasing limit order) は指値注文のプログラムラッパーで、以下を行う:

  1. 現在の最良価格(または小さなオフセット付き)で指値注文を出す
  2. WebSocket経由で価格を監視する
  3. 価格が注文から離れたら——キャンセルして現在の価格に近い位置で再発注する
  4. 注文が約定するか許容偏差を超えるまで繰り返す

主要パラメータ

  • chase_interval_ms — チェックと再発注の頻度。100ms——アグレッシブ、1000ms——穏やか。
  • max_chase_distance — 初期価格からの最大偏差。これを超えると注文がキャンセルされる。暴走する市場を追いかけることへの防御。
  • aggression_level — 市場価格にどれだけ近く指値注文を置くか。0——最良ビッド/アスク(パッシブ)、1——スプレッドを渡る(アグレッシブ、事実上テイカー)。
  • chase_on_partial — 注文が部分約定した場合に追いかけ続けるか。

TypeScript実装

interface ChasingOrderParams {
  symbol: string;
  side: "buy" | "sell";
  totalQty: number;
  /** 0 = passive (at best bid/ask), 1 = cross spread */
  aggression: number;
  /** max price deviation from initial price */
  maxChaseDistance: number;
  /** how often to re-evaluate, ms */
  chaseIntervalMs: number;
  /** stop chasing after this many ms */
  timeoutMs: number;
}

class ChasingLimitOrder {
  private currentOrderId: string | null = null;
  private filledQty = 0;
  private initialPrice: number | null = null;
  private startTime = Date.now();

  constructor(
    private exchange: any, // ccxt exchange instance
    private params: ChasingOrderParams
  ) {}

  async execute(): Promise<{ filledQty: number; avgPrice: number }> {
    const fills: Array<{ qty: number; price: number }> = [];

    while (this.filledQty < this.params.totalQty) {
      // タイムアウト
      if (Date.now() - this.startTime > this.params.timeoutMs) {
        console.log("[CHASE] timeout reached, cancelling");
        await this.cancelCurrent();
        break;
      }

      // 現在の板を取得
      const book = await this.exchange.fetchOrderBook(
        this.params.symbol, 5
      );
      const bestBid = book.bids[0][0];
      const bestAsk = book.asks[0][0];
      const spread = bestAsk - bestBid;

      // 目標価格を計算
      let targetPrice: number;
      if (this.params.side === "buy") {
        targetPrice = bestBid + spread * this.params.aggression;
      } else {
        targetPrice = bestAsk - spread * this.params.aggression;
      }

      // 初期価格を記録
      if (this.initialPrice === null) {
        this.initialPrice = targetPrice;
      }

      // 最大チェイス距離をチェック
      const deviation = Math.abs(targetPrice - this.initialPrice);
      if (deviation > this.params.maxChaseDistance) {
        console.log(
          `[CHASE] max deviation exceeded: ${deviation.toFixed(4)} > ` +
          `${this.params.maxChaseDistance}`
        );
        await this.cancelCurrent();
        break;
      }

      // 現在の注文をチェック
      if (this.currentOrderId) {
        const order = await this.exchange.fetchOrder(
          this.currentOrderId, this.params.symbol
        );

        if (order.status === "closed") {
          fills.push({ qty: order.filled, price: order.average });
          this.filledQty += order.filled;
          this.currentOrderId = null;
          continue;
        }

        // 部分約定のfilledQtyを更新
        if (order.filled > 0) {
          const newFilled = order.filled - (
            fills.reduce((s, f) => s + f.qty, 0) - this.filledQty
          );
          // 注文はそのまま——価格変更が必要か?
        }

        const currentPrice = parseFloat(order.price);
        const priceDiff = Math.abs(currentPrice - targetPrice);
        const tickSize = spread * 0.1 || 0.01;

        if (priceDiff > tickSize) {
          // 価格が動いた——価格を変更
          console.log(
            `[CHASE] repricing: ${currentPrice} -> ` +
            `${targetPrice.toFixed(4)}`
          );
          await this.cancelCurrent();
        } else {
          // 注文は正しい価格にある——待機
          await this.sleep(this.params.chaseIntervalMs);
          continue;
        }
      }

      // 新しい注文を出す
      const remainingQty = this.params.totalQty - this.filledQty;
      const order = await this.exchange.createLimitOrder(
        this.params.symbol,
        this.params.side,
        remainingQty,
        targetPrice
      );
      this.currentOrderId = order.id;
      console.log(
        `[CHASE] placed ${this.params.side} ${remainingQty} ` +
        `@ ${targetPrice.toFixed(4)}`
      );

      await this.sleep(this.params.chaseIntervalMs);
    }

    const totalCost = fills.reduce((s, f) => s + f.qty * f.price, 0);
    const avgPrice = this.filledQty > 0 ? totalCost / this.filledQty : 0;
    return { filledQty: this.filledQty, avgPrice };
  }

  private async cancelCurrent(): Promise<void> {
    if (this.currentOrderId) {
      try {
        await this.exchange.cancelOrder(
          this.currentOrderId, this.params.symbol
        );
      } catch { /* 注文は既に約定またはキャンセル済み */ }
      this.currentOrderId = null;
    }
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

チェイシングが害になる場合

チェイシングは強力なツールだが、損失製造機に変わりやすい:

  1. キャンセル/再発注のスパム。 キャンセルと再発注のたびにAPIに負荷がかかる。取引所はリクエスト頻度を制限しており、アグレッシブなチェイシングはAPIキーの停止につながる可能性がある。
  2. 逆選択(Adverse selection)。 価格があなたから逃げている場合——市場はあなたが知らないことを知っているのかもしれない。この状況で価格を追いかけることは、天井で買うことを意味する。
  3. メイカーからテイカーへの移行。 高いアグレッションではテイカー手数料を支払うことになるが、遅延がある(キャンセル+新規注文)。時には最初から成行注文を出す方がシンプルだ。

4. 時間ベース注文:ミリ秒単位の精度

「価格Xで」ではなく「時刻Tに」注文を執行する必要がある状況がある。奇妙に聞こえるか?実はこれは戦略の一大分類だ。

ユースケース

ファンディングレート・アービトラージ。 無期限先物では、ファンディングは8時間ごとに支払われる(BinanceではUTC 00:00、08:00、16:00)。ファンディングレート=+0.1%の場合、決済の瞬間にショートである必要がある。戦略:決済の数秒前にショートを開き、ファンディングを受け取り、ポジションをクローズ。タイミングが決定的——1秒の遅れはファンディングの取り逃しを意味する。

セッションのオープン/クローズ。 伝統的な市場や一部の暗号デリバティブには固定セッションがある。オープニングオークション(NYSE、CME)は流動性がピークに達する瞬間だ。オークションの100ms前に注文を出すことは優位性となる。

ニュースベースの執行。 インフレデータが予定時刻に発表される。アルゴリズムがニュースフィードから数値を解析し、50ms以内に注文を出す。ここでは時間ベースの執行がイベントドリブンロジックと組み合わされる。

実装

class TimeBasedOrder {
  constructor(
    private exchange: any,
    private symbol: string,
    private side: "buy" | "sell",
    private qty: number,
    private orderType: "market" | "limit",
    private limitPrice?: number
  ) {}

  /**
   * 正確な時刻に実行をスケジュール。
   * 最大精度のためにビジーウェイトループを使用。
   */
  async executeAt(targetTime: Date): Promise<any> {
    const targetMs = targetTime.getTime();

    // フェーズ1:粗い待機(sleep)
    const coarseWait = targetMs - Date.now() - 500; // 500ms前に起床
    if (coarseWait > 0) {
      console.log(
        `[TIME-ORDER] sleeping for ${(coarseWait / 1000).toFixed(1)}s`
      );
      await new Promise((r) => setTimeout(r, coarseWait));
    }

    // フェーズ2:精密待機(ビジーウェイト)
    while (Date.now() < targetMs) {
      // スピン——CPUを消費するが、約1msの精度を実現
    }

    // フェーズ3:執行
    const sendTime = Date.now();
    const order = await this.exchange.createOrder(
      this.symbol,
      this.orderType,
      this.side,
      this.qty,
      this.limitPrice
    );

    console.log(
      `[TIME-ORDER] executed at ${new Date(sendTime).toISOString()}, ` +
      `target was ${targetTime.toISOString()}, ` +
      `delta: ${sendTime - targetMs}ms`
    );

    return order;
  }
}

// 例:UTC 00:00:00ちょうどに注文を出す(ファンディング決済)
const executor = new TimeBasedOrder(exchange, "BTC/USDT", "sell", 0.1, "market");
const target = new Date("2026-03-24T00:00:00.000Z");
await executor.executeAt(target);

重要な注意点: 時間ベース注文の精度はコードではなく、取引所へのネットワーク遅延によって制限される。APIへのpingが50msなら、完璧なビジーウェイトでも50msのデルタが生じる。本格的なHFTでは、コロケーション——サーバーを取引所のマッチングエンジンの物理的隣に置くことが利用される。


5. バーチャル/シンセティック注文:システム内の不可視の存在

グリッドボット用バーチャル注文 バーチャル注文:トリガーが発動するまでボットのメモリ内にのみ存在する注文

これはおそらく、アルゴトレーダーの武器庫で最も過小評価されているツールだ。バーチャルオーダー(シンセティックオーダーとも呼ばれる)は、あなたのシステム内にのみ存在する注文だ。トリガー条件(通常は価格が特定のレベルに達すること)が満たされるまで、取引所には送信されない。

仕組み

  1. アルゴリズムが決定する:「BTCを$40,000で買いたい」
  2. 取引所に指値注文を送信する代わりに、メモリ内にバーチャル注文を作成する
  3. WebSocket価格ストリームを購読する
  4. ビッド/アスクが$40,000に達したら——取引所に実際の成行注文または指値注文を送信する

バーチャル注文が重要な理由

情報漏洩がない。 あなたの注文は板に表示されない。他のトレーダーも、HFTアルゴリズムも、取引所自体も——執行の瞬間まであなたの意図を知らない。これは力の均衡を根本的に変える。

フロントランニングからの保護。 暗号資産取引所、特に透明性の低い取引所では、大口指値注文の情報がフロントランニングに利用されるという合理的な疑いがある(これに関する研究さえある)。バーチャル注文はこのリスクを排除する。

グリッドボット。 古典的なグリッドボットは、異なる価格レベルに50〜200の注文を配置する。すべてを取引所に送信すると——板に200の注文が並ぶことになり、それは:(a) 全員に可視、(b) 取引所の注文上限を消費(通常アカウントあたり200〜300の未約定注文)、(c) 急激な値動きですべてが約定し、巨大なポジションを抱えることになる。バーチャル注文は3つの問題すべてを解決する。

フォーリングナイフを掴む。 戦略:現在の価格から-5%、-10%、-15%のレベルにバーチャル買い注文を配置。市場が下落すれば——注文は段階的にトリガーされる。下落しなければ——リスクはゼロで、取引所の注文枠も使用しない。

TypeScript実装

interface VirtualOrder {
  id: string;
  symbol: string;
  side: "buy" | "sell";
  triggerPrice: number;
  qty: number;
  /** トリガー時に取引所に送信される注文タイプ */
  executionType: "market" | "limit";
  /** 指値の場合:トリガー価格からのオフセット */
  limitOffset?: number;
  status: "pending" | "triggered" | "filled" | "failed";
}

class VirtualOrderManager {
  private orders: Map<string, VirtualOrder> = new Map();
  private orderCounter = 0;

  constructor(private exchange: any) {}

  /**
   * バーチャル注文を作成。取引所には何も送信されない。
   */
  addOrder(params: Omit<VirtualOrder, "id" | "status">): string {
    const id = `virt_${++this.orderCounter}`;
    this.orders.set(id, { ...params, id, status: "pending" });
    console.log(
      `[VIRTUAL] created ${params.side} ${params.qty} ` +
      `${params.symbol} @ trigger ${params.triggerPrice}`
    );
    return id;
  }

  /**
   * 各価格ティックで呼び出される(WebSocketから)。
   */
  async onPriceUpdate(
    symbol: string, bestBid: number, bestAsk: number
  ): Promise<void> {
    for (const [id, order] of this.orders) {
      if (order.symbol !== symbol || order.status !== "pending") continue;

      const triggered =
        (order.side === "buy" && bestAsk <= order.triggerPrice) ||
        (order.side === "sell" && bestBid >= order.triggerPrice);

      if (!triggered) continue;

      order.status = "triggered";
      console.log(
        `[VIRTUAL] ${id} triggered! bid=${bestBid} ask=${bestAsk}`
      );

      try {
        let realOrder: any;

        if (order.executionType === "market") {
          realOrder = await this.exchange.createMarketOrder(
            order.symbol, order.side, order.qty
          );
        } else {
          const limitPrice = order.side === "buy"
            ? order.triggerPrice + (order.limitOffset ?? 0)
            : order.triggerPrice - (order.limitOffset ?? 0);
          realOrder = await this.exchange.createLimitOrder(
            order.symbol, order.side, order.qty, limitPrice
          );
        }

        order.status = "filled";
        console.log(
          `[VIRTUAL] ${id} filled: ${realOrder.filled} ` +
          `@ ${realOrder.average ?? realOrder.price}`
        );
      } catch (err) {
        order.status = "failed";
        console.error(`[VIRTUAL] ${id} execution failed:`, err);
      }
    }
  }

  /**
   * すべてのアクティブなバーチャル注文を取得。
   */
  getPendingOrders(): VirtualOrder[] {
    return [...this.orders.values()].filter(
      (o) => o.status === "pending"
    );
  }

  cancelOrder(id: string): boolean {
    const order = this.orders.get(id);
    if (order && order.status === "pending") {
      this.orders.delete(id);
      return true;
    }
    return false;
  }
}

// --- 例:バーチャル注文を使ったグリッドボット ---

async function gridBot(exchange: any) {
  const manager = new VirtualOrderManager(exchange);
  const currentPrice = 42000;
  const gridStep = 200;      // グリッドのステップ幅
  const gridLevels = 20;     // 各方向のレベル数
  const qtyPerLevel = 0.01;  // レベルあたりのBTC数量

  // バーチャルグリッドを作成
  for (let i = 1; i <= gridLevels; i++) {
    // 現在価格より下の買い注文
    manager.addOrder({
      symbol: "BTC/USDT",
      side: "buy",
      triggerPrice: currentPrice - gridStep * i,
      qty: qtyPerLevel,
      executionType: "limit",
      limitOffset: 1,  // limit price = trigger + 1 USDT
    });

    // 現在価格より上の売り注文
    manager.addOrder({
      symbol: "BTC/USDT",
      side: "sell",
      triggerPrice: currentPrice + gridStep * i,
      qty: qtyPerLevel,
      executionType: "limit",
      limitOffset: 1,
    });
  }

  console.log(
    `[GRID] created ${gridLevels * 2} virtual orders, ` +
    `0 on exchange`
  );

  // WebSocket購読(ccxt.proの疑似コード)
  while (true) {
    const ticker = await exchange.watchTicker("BTC/USDT");
    await manager.onPriceUpdate(
      "BTC/USDT", ticker.bid, ticker.ask
    );
  }
}

バーチャル注文の落とし穴

  1. レイテンシーギャップ。 価格を見た瞬間から実際の注文が取引所に届くまでに時間がかかる。ボラティリティの高い市場では、その20〜100msの間に価格が飛んでしまう可能性がある。解決策:少しアグレッシブな指値注文(余裕を持って)を送信する。

  2. 約定の取りこぼし。 価格が1ティックであなたのレベルを「突き抜けて」(フラッシュクラッシュ)戻った場合——間に合わない可能性がある。板に置かれた通常の指値注文なら約定していたが、バーチャル注文では約定しない。

  3. 状態管理。 バーチャル注文はメモリ内に存在する。プロセスがクラッシュすると——注文は失われる。解決策:永続的ストレージ(Redis、SQLite、ファイル)と再起動時のリカバリ。


6. 条件付き/スマート注文:注文の組み合わせ論

単一の注文では不十分な場合、トレーダーは条件付き構造に組み合わせる。一部は取引所でネイティブにサポートされ、他はプログラムで実装される。

OCO(One Cancels Other)

2つの注文がリンクされる:一方が約定すると——もう一方が自動的にキャンセルされる。典型的な例:ロングポジションにいて、テイクプロフィットとストップロスの両方を設定したい。どちらが先に発動しても——もう一方はキャンセルされなければならない。

class OCOHandler:
    """
    OCO:一方の注文が約定すると、もう一方がキャンセルされる。
    """
    def __init__(self, exchange, symbol: str):
        self.exchange = exchange
        self.symbol = symbol
        self.order_a_id: str | None = None
        self.order_b_id: str | None = None

    async def place(
        self,
        take_profit_price: float,
        stop_loss_price: float,
        qty: float,
    ):
        # テイクプロフィット:指値売り注文
        tp = await self.exchange.create_limit_sell_order(
            self.symbol, qty, take_profit_price
        )
        self.order_a_id = tp["id"]

        # ストップロス:ストップマーケット注文
        sl = await self.exchange.create_order(
            self.symbol, "stop", "sell", qty,
            None, {"stopPrice": stop_loss_price}
        )
        self.order_b_id = sl["id"]

        print(f"[OCO] TP @ {take_profit_price}, SL @ {stop_loss_price}")

    async def monitor(self):
        """ステータスを確認し、ペアの注文をキャンセルする。"""
        while True:
            if self.order_a_id:
                a = await self.exchange.fetch_order(
                    self.order_a_id, self.symbol
                )
                if a["status"] == "closed":
                    print("[OCO] take-profit filled, cancelling stop-loss")
                    await self.exchange.cancel_order(
                        self.order_b_id, self.symbol
                    )
                    break

            if self.order_b_id:
                b = await self.exchange.fetch_order(
                    self.order_b_id, self.symbol
                )
                if b["status"] == "closed":
                    print("[OCO] stop-loss filled, cancelling take-profit")
                    await self.exchange.cancel_order(
                        self.order_a_id, self.symbol
                    )
                    break

            await asyncio.sleep(0.5)

ブラケット注文

3コンポーネント構造:メインのエントリー注文 + 出口用OCO(テイクプロフィット+ストップロス)。本質的に、1回の呼び出しで完全な取引サイクル:

  1. エントリー: 指値買い注文
  2. テイクプロフィット: 指値売り注文(上方)
  3. ストップロス: ストップマーケット売り注文(下方)

エントリーが約定すると、TPとSLが自動的に発注される。いずれかが約定すると——もう一方がキャンセルされる。

If-Thenロジック

最も柔軟なオプション——任意の条件を持つ注文チェーン:

# BTCが50000を上抜けたら——ETHを購入
# その後ETHが4000に達したら——利益確定
# ETHが3500を下回ったら——ストップロス

rules = [
    {
        "condition": {"symbol": "BTC/USDT", "price_above": 50000},
        "action": {"type": "market_buy", "symbol": "ETH/USDT", "qty": 10},
        "then": [
            {
                "condition": {"symbol": "ETH/USDT", "price_above": 4000},
                "action": {"type": "market_sell", "symbol": "ETH/USDT", "qty": 10},
            },
            {
                "condition": {"symbol": "ETH/USDT", "price_below": 3500},
                "action": {"type": "market_sell", "symbol": "ETH/USDT", "qty": 10},
            },
        ]
    }
]

このような構造はどの取引所でもネイティブにサポートされていない——プログラムによる実装のみだ。これが、アルゴトレーディングシステムが不可避的に独自の注文管理レイヤーを発展させる理由の1つだ。


7. マーケットメイカーはどのように特殊な注文タイプを使うか

マーケットメイキングは独立した世界であり、注文ツールキットもそれに対応している。マーケットメイカーの仕事は、ビッドとアスクを継続的に提示し、スプレッドで収益を得ながら、逆選択(情報を持ったトレーダーがあなたに対して取引する状況)を最小化することだ。

ポストオンリーは必須

マーケットメイカーにとって、ポストオンリーはオプションではなく必須だ。注文が誤ってテイカーとして約定すると——メイカーリベートを受け取る代わりにテイカー手数料を支払うことになる。1日に数千の注文で、これは壊滅的だ。

# マーケットメイカー:両サイドの気配を提示
async def quote(exchange, symbol, mid_price, half_spread, qty):
    bid_price = mid_price - half_spread
    ask_price = mid_price + half_spread

    bid = await exchange.create_order(
        symbol, "limit", "buy", qty, bid_price,
        {"postOnly": True}  # マーケットメイカーにとって必須
    )
    ask = await exchange.create_order(
        symbol, "limit", "sell", qty, ask_price,
        {"postOnly": True}
    )
    return bid, ask

ヒドゥン注文

一部の取引所(Kraken、Bitfinex)ではヒドゥン(非表示)注文が利用可能——板には表示されないが、取引所上に存在しマッチングに参加する。トレードオフ:メイカーでもテイカー手数料を支払うが、匿名性を得られる。

マーケットメイカーにとって、これはインベントリ管理のツールだ:大きなポジションが蓄積された場合、市場に意図を示さずにヒドゥン注文でポジションを解消できる。

ペグド注文

最良ビッド/アスクにペグ(連動)された注文。Coinbase Advanced Tradeでは、例えば、自動的に最良ビッドを追跡し常にキューの先頭に位置する注文を出せる。これは取引所レベルのネイティブチェイシング注文だが——どこでも利用可能というわけではない。

バルク注文管理

プロのマーケットメイカーはバッチAPIを使用して、単一のHTTPリクエストで数十の注文を同時にキャンセル・発注する。BinanceではbatchOrders、Bybitではplace-batch-orderだ。これによりレイテンシーとレートリミットの圧力が軽減される。


8. 注文タイプ比較表

注文タイプ約定保証価格保証板に表示取引所ネイティブ実装の複雑さ
成行ありなしなし(即時)ありなし
指値なしありありありなし
ストップマーケットあり(トリガー後)なしなしありなし
ストップリミットなしありなし(トリガーまで)ありなし
トレーリングストップあり(トリガー後)なしなし一部低
アイスバーグなしあり一部一部中
ポストオンリーなしありありありなし
TWAPなし(スライスに依存)なし一部なし中
VWAPなしなし一部なし高
チェイシングリミット指値より高い一部あり(現在の注文)なし中
時間ベースタイプに依存タイプに依存なし(時刻Tまで)なし低
バーチャル/シンセティック指値より低いタイプに依存なしなし中
OCOあり(2つのうち1つ)一部あり(両方)一部中
ブラケットあり一部ありまれ高
ヒドゥンなしありなしまれなし
ペグドなし動的あり非常にまれ高(プログラム実装の場合)

結論:戦略の構成要素としての注文

注文タイプは「インターフェースのボタン」ではない。あらゆる取引システムの執行レイヤーを構築する基本的なプリミティブだ。「バックテストで利益が出る戦略」と「本番環境で利益が出る戦略」の違いは、しばしばまさにここにある——取引所に注文をどのように送信するかだ。

いくつかの実践的な示唆:

  1. 標準注文から始めよう、ニュアンスを理解していることを確認しよう(ストップリミット vs ストップマーケット、IOC vs FOK)。ほとんどのミスはここで起こる。
  2. バーチャル注文はグリッドボットの必須ツール。 50以上の注文を出すなら——すべてを取引所に送信してはいけない。
  3. フィルレートが価格より重要な時にチェイシングが必要。 ただし必ずmax_chase_distanceを設定すること——さもないと非常に遠くまで行ってしまう可能性がある。
  4. 時間ベースの執行はニッチだが、ファンディングレート・アービトラージやイベントドリブン戦略には強力なツール。
  5. 独自の注文管理レイヤーは、本格的なアルゴトレーディングシステムにとって不可避。取引所ネイティブの注文タイプだけでは不十分だ。

取引システムを構築していてさらに深く学びたい方は、板のキューポジション、CCXTのWebSocketメソッド、ファンディングレート・アービトラージに関する記事をご覧ください。


参考文献とソース

  • CCXT Library — 暗号資産取引所向け統合ライブラリ、100以上の取引所に対応
  • Binance API Documentation — Binanceの注文タイプに関するドキュメント
  • Bybit API v5 — バッチ注文を含むBybitドキュメント
  • Moallemi, C. & Yuan, K. (2017). The Value of Queue Position in a Limit Order Book. Columbia Business School Research Paper
  • Cartea, A., Jaimungal, S., & Penalva, J. (2015). Algorithmic and High-Frequency Trading. Cambridge University Press
  • Avellaneda, M. & Stoikov, S. (2008) — High-frequency trading in a limit order book. Quantitative Finance
  • Erik Rigtorp — Order Queue Position Estimation — キューポジション推定に関する資料
  • Trading Technologies (TT) — 高度な注文タイプを備えたプロフェッショナル取引プラットフォーム