Виды ордеров в алготрейдинге: от limit с chasing до virtual orders

Полный разбор типов ордеров в алгоритмической торговле: стандартные биржевые, chasing limit, time-based, virtual/synthetic orders. С примерами кода и реальными кейсами.

Когда новичок открывает терминал биржи, он видит две кнопки: «Купить» и «Продать». Когда алготрейдер открывает свой codebase, он видит двадцать семь типов ордеров, три уровня абстракции и пачку edge cases, от которых хочется закрыть ноутбук и пойти торговать огурцами на рынке. Но огурцами, к сожалению, не сделаешь funding rate арбитраж в 3:59 UTC — поэтому давайте разбираться.

В этой статье мы пройдём весь путь от базовых биржевых ордеров до синтетических виртуальных конструкций, которые живут только в вашей системе и никогда не появляются в стакане. Будет TypeScript, Python, боль и немного просветления.


1. Стандартные биржевые ордера: база, без которой никуда

Стандартные ордера Классификация стандартных типов ордеров: от market до iceberg

Прежде чем конструировать что-то сложное, нужно убедиться, что мы правильно понимаем базовые строительные блоки. Удивительно, сколько людей путают stop-limit и stop-market, а потом удивляются, почему их стоп «не сработал» (спойлер: сработал, но лимитный ордер не исполнился из-за проскальзывания).

Market order

Самый простой и одновременно самый опасный тип. Вы говорите бирже: «Купи/продай прямо сейчас, по любой доступной цене». Биржа берёт ликвидность из стакана, начиная с лучшей цены. Если объёма на лучшем уровне не хватает — проскальзывает дальше.

Когда использовать: экстренный выход из позиции, исполнение сигнала, где скорость важнее цены.

Подводные камни: на тонком рынке маркет-ордер на 100 BTC может сдвинуть цену на несколько процентов. Бэктесты, которые моделируют маркет-ордера без учёта impact — фантазия.

Limit order

Вы указываете конкретную цену. Ордер попадает в стакан и ждёт, пока кто-то согласится на вашу цену. Если цена лимитного bid-ордера выше текущей рыночной — он исполнится немедленно (как маркет, но с гарантией максимальной цены).

Ключевое: лимитный ордер не гарантирует исполнение. Цена может дойти до вашего уровня и развернуться, а вы останетесь в очереди (об этом подробно — в нашей статье про queue position).

Stop-market и Stop-limit

Тут начинается зона путаницы. Оба типа — это «спящие» ордера, которые активируются при достижении trigger price (стоп-цены). Но:

  • Stop-market: при срабатывании превращается в маркет-ордер. Гарантия исполнения, но не цены.
  • Stop-limit: при срабатывании превращается в лимитный ордер. Гарантия цены (не хуже указанной), но не исполнения.

На волатильном крипторынке stop-limit может «промахнуться» — цена пробила стоп, лимитный ордер выставился, но рынок уже улетел дальше. Вы остались с нерабочим лимитным ордером и растущим убытком. Именно поэтому для стоп-лоссов чаще используют stop-market.

Trailing stop

Стоп, который «следует» за ценой на заданном расстоянии. Цена растёт — стоп подтягивается вверх. Цена падает — стоп остаётся на месте. Полезен для защиты прибыли в трендовых стратегиях.

Реализация на биржах: не все биржи поддерживают нативный trailing stop. Часто алготрейдеры реализуют его программно — так больше контроля над параметрами (callback rate, activation price, step size).

Iceberg order

Или «айсберг» — ордер, у которого в стакане видна только часть объёма. Вы хотите купить 1000 BTC, но показываете в стакане только 10. Когда первые 10 исполняются — появляются следующие 10.

Зачем: чтобы не показывать рынку свои истинные намерения. Крупный ордер в стакане — это сигнал для всех, что «кто-то большой хочет купить/продать». В ответ HFT-алгоритмы начинают front-running, и цена уезжает от вас.

Нюанс: на многих криптобиржах iceberg-ордера либо не поддерживаются, либо легко детектируются по паттерну одинаковых объёмов. Продвинутые алгоритмы рандомизируют размер видимой части.

Параметры времени жизни: 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, или ничего.

Post-only (Maker-only)

Ордер, который гарантированно попадает в стакан как мейкер и никогда не исполняется как тейкер. Если при выставлении цена такова, что ордер бы исполнился немедленно — биржа его отклоняет (или сдвигает цену, в зависимости от биржи).

Зачем: maker fee обычно ниже taker fee (на Binance — 0.02% vs 0.04% для VIP-уровней). Для маркетмейкера, который выставляет тысячи ордеров в день, разница в комиссиях — это разница между прибылью и убытком.


2. TWAP и VWAP: как институционалы прячут слона в стакане

Когда хедж-фонд хочет купить позицию на $50 млн, он не ставит один маркет-ордер. Он использует execution algorithms — алгоритмы, которые разбивают крупный ордер на множество мелких и исполняют их во времени, минимизируя market impact.

TWAP (Time-Weighted Average Price)

Идея предельно проста: разбиваем общий объём на равные части и исполняем через равные промежутки времени.

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 (Volume-Weighted Average Price)

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 vs VWAP: TWAP проще и предсказуемее. VWAP даёт лучшую среднюю цену, но требует reliable volume profile. На крипторынке, где объёмы могут быть wash-traded, VWAP-профиль нужно строить аккуратно.


3. Limit с chasing: когда ваш ордер умеет бегать за ценой

Chasing limit ордера Chasing limit: ордер преследует уходящую цену с настраиваемой агрессивностью

А вот тут начинается самое интересное. Стандартный лимитный ордер — это пассивная сущность: он стоит в стакане и ждёт. Если цена ушла — ордер остался неисполненным. Для алготрейдера это часто неприемлемо: сигнал на вход получен, а позиция не набрана, потому что рынок сдвинулся на 0.1%.

Chasing limit order — это программная обёртка вокруг лимитного ордера, которая:

  1. Выставляет лимитный ордер по текущей лучшей цене (или с небольшим смещением)
  2. Следит за ценой через WebSocket
  3. Если цена уходит от ордера — отменяет его и перевыставляет ближе к текущей цене
  4. Повторяет, пока ордер не исполнится или не превысит допустимое отклонение

Ключевые параметры

  • chase_interval_ms — как часто проверять и перевыставлять ордер. 100ms — агрессивно, 1000ms — спокойно.
  • max_chase_distance — максимальное отклонение от начальной цены, после которого ордер отменяется. Защита от погони за улетевшим рынком.
  • aggression_level — насколько близко к рыночной цене ставить лимитный ордер. 0 — на лучший bid/ask (пассивно), 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;
      }

      // Проверяем max chase distance
      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));
  }
}

Когда chasing — зло

Chasing — мощный инструмент, но его легко превратить в генератор убытков:

  1. Cancel/replace spam. Каждая отмена и перевыставление — это нагрузка на API. Биржи лимитируют частоту запросов, и при агрессивном chasing вы можете получить бан API ключа.
  2. Adverse selection. Если цена убегает от вас — возможно, рынок знает что-то, чего не знаете вы. Погоня за ценой в такой ситуации — покупка на верхушке.
  3. Maker → Taker transition. При высокой aggression вы де-факто платите taker fee, но с задержкой (cancel + new order). Иногда проще сразу поставить маркет-ордер.

4. Time-based ордера: точность до миллисекунды

Есть ситуации, когда нужно исполнить ордер не «по цене X», а «в момент времени T». Звучит странно? На самом деле это целый класс стратегий.

Use cases

Funding rate арбитраж. На бессрочных фьючерсах funding выплачивается каждые 8 часов (00:00, 08:00, 16:00 UTC на Binance). Если funding rate = +0.1%, нужно быть в шорте в момент расчёта. Стратегия: открыть шорт за несколько секунд до settlement, получить funding, закрыть позицию. Timing здесь критичен — секунда опоздания означает пропущенный funding.

Открытие/закрытие сессий. На традиционных рынках и некоторых крипто-деривативах есть фиксированные сессии. Аукцион открытия (NYSE, CME) — момент, когда ликвидность максимальна. Выставить ордер за 100ms до аукциона — преимущество.

News-based execution. Данные по инфляции выходят в 15:30 MSK. Алгоритм парсит число из новостного фида и за 50ms выставляет ордер. Тут time-based execution сочетается с event-driven логикой.

Реализация

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

  /**
   * Запланировать исполнение на точное время.
   * Использует busy-wait loop для максимальной точности.
   */
  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: точное ожидание (busy-wait)
    while (Date.now() < targetMs) {
      // spin — тратим 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;
  }
}

// Пример: выставить ордер ровно в 00:00:00 UTC (funding settlement)
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);

Важный нюанс: точность time-based ордера ограничена не вашим кодом, а сетевой задержкой до биржи. Если пинг до API — 50ms, то даже идеальный busy-wait даст дельту в 50ms. Для серьёзного HFT используют co-location — сервер стоит физически рядом с matching engine биржи.


5. Virtual/synthetic orders: невидимки в вашей системе

Virtual orders для grid-ботов Virtual orders: ордера существуют только в памяти бота до момента триггера

Это, пожалуй, самый недооценённый инструмент в арсенале алготрейдера. Virtual order (он же synthetic order) — это ордер, который существует только в вашей системе. На биржу он не отправляется до тех пор, пока не выполнится триггерное условие (обычно — цена достигла определённого уровня).

Как это работает

  1. Ваш алгоритм определяет: «Хочу купить BTC по $40,000»
  2. Вместо отправки лимитного ордера на биржу, он создаёт виртуальный ордер в памяти
  3. Подписывается на WebSocket-поток цен
  4. Когда bid/ask достигает $40,000 — отправляет реальный market или limit ордер на биржу

Зачем нужны virtual orders

Отсутствие information leakage. Ваш ордер не виден в стакане. Никто — ни другие трейдеры, ни HFT-алгоритмы, ни сама биржа — не знает о ваших намерениях до момента исполнения. Это принципиально меняет баланс сил.

Защита от front-running. На криптобиржах, особенно менее прозрачных, существует обоснованное подозрение, что информация о крупных лимитных ордерах может использоваться для front-running (есть даже исследования об этом). Virtual orders элиминируют этот риск.

Grid-боты. Классический grid-бот выставляет сетку из 50-200 ордеров на разных ценовых уровнях. Если выставить все на биржу — это 200 ордеров в стакане, которые: (а) видны всем, (б) занимают лимит ордеров на бирже (обычно 200-300 открытых ордеров на аккаунт), (в) при резком движении все исполнятся, и вы получите огромную позицию. Virtual orders решают все три проблемы.

Catching falling knives. Стратегия: выставить виртуальные ордера на покупку на уровнях -5%, -10%, -15% от текущей цены. Если рынок падает — ордера постепенно триггерятся. Если не падает — вы ничем не рискуете и не занимаете лимиты на бирже.

Реализация на TypeScript

interface VirtualOrder {
  id: string;
  symbol: string;
  side: "buy" | "sell";
  triggerPrice: number;
  qty: number;
  /** Тип ордера, который отправится на биржу при срабатывании */
  executionType: "market" | "limit";
  /** Для limit: смещение от trigger price */
  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;
  }
}

// --- Пример: Grid-бот с virtual orders ---

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
    );
  }
}

Подводные камни virtual orders

  1. Latency gap. Между моментом, когда вы видите цену, и моментом, когда реальный ордер доходит до биржи, проходит время. На волатильном рынке цена может улететь за эти 20-100ms. Решение: отправлять слегка агрессивный лимитный ордер (с запасом).

  2. Missed fills. Если цена «прошила» ваш уровень за один тик (flash crash) и вернулась обратно — вы можете не успеть. Обычный лимитный ордер в стакане бы исполнился, virtual — нет.

  3. State management. Virtual orders живут в памяти. Если процесс упал — ордера потеряны. Решение: персистентное хранение (Redis, SQLite, файл) с recovery при рестарте.


6. Conditional/smart orders: комбинаторика ордеров

Когда одного ордера недостаточно, трейдеры комбинируют их в условные конструкции. Некоторые поддерживаются нативно на биржах, другие реализуются программно.

OCO (One Cancels Other)

Два ордера связаны: если исполняется один — второй автоматически отменяется. Классический пример: вы в позиции long и хотите поставить и тейк-профит, и стоп-лосс. Какой бы ни сработал первым — второй должен быть отменён.

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,
    ):
        # Take-profit: лимитный ордер на продажу
        tp = await self.exchange.create_limit_sell_order(
            self.symbol, qty, take_profit_price
        )
        self.order_a_id = tp["id"]

        # Stop-loss: stop-market ордер
        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)

Bracket order

Трёхкомпонентная конструкция: основной ордер на вход + OCO для выхода (take-profit + stop-loss). По сути, полный цикл сделки в одном вызове:

  1. Entry: лимитный ордер на покупку
  2. Take-profit: лимитный ордер на продажу (выше)
  3. Stop-loss: stop-market ордер на продажу (ниже)

При исполнении entry автоматически выставляются 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},
            },
        ]
    }
]

Такие конструкции не поддерживаются ни одной биржей нативно — только программная реализация. Это одна из причин, почему алготрейдинг-системы неизбежно обрастают собственным order management layer.


7. Как маркетмейкеры используют специальные типы ордеров

Маркетмейкинг — это отдельная вселенная, и набор ордеров здесь соответствующий. Задача маркетмейкера — постоянно котировать bid и ask, зарабатывая на спреде, при этом минимизируя adverse selection (ситуацию, когда информированный трейдер торгует против вас).

Post-only как must-have

Для маркетмейкера post-only — не опция, а обязательное требование. Если ваш ордер случайно исполнится как тейкер — вместо получения maker rebate вы заплатите taker fee. На тысячах ордеров в день это катастрофа.

# Маркетмейкер: выставляем двустороннюю котировку
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

Hidden orders

На некоторых биржах (Kraken, Bitfinex) доступны hidden (скрытые) ордера — они не отображаются в стакане, но при этом находятся на бирже и участвуют в матчинге. Компромисс: вы платите taker fee даже как мейкер, но получаете анонимность.

Для маркетмейкера это инструмент для управления инвентарём: если накопилась большая позиция, можно поставить hidden ордер на разгрузку, не показывая рынку своё намерение.

Pegged orders

Ордер, привязанный к лучшему bid/ask. На Coinbase Advanced Trade, например, можно выставить ордер, который автоматически следит за best bid и всегда стоит на первом месте в очереди. Это нативный chasing order на уровне биржи — но доступен далеко не везде.

Bulk order management

Профессиональные маркетмейкеры используют batch-API для одновременной отмены и выставления десятков ордеров в одном HTTP-запросе. На Binance это batchOrders, на Bybit — place-batch-order. Это снижает задержку и нагрузку на rate limits.


8. Сравнительная таблица типов ордеров

Тип ордераГарантия исполненияГарантия ценыВиден в стаканеНативный на биржахСложность реализации
MarketДаНетНет (мгновенный)ДаНулевая
LimitНетДаДаДаНулевая
Stop-marketДа (после триггера)НетНетДаНулевая
Stop-limitНетДаНет (до триггера)ДаНулевая
Trailing stopДа (после триггера)НетНетЧастичноНизкая
IcebergНетДаЧастичноЧастичноСредняя
Post-onlyНетДаДаДаНулевая
TWAPНет (зависит от слайсов)НетЧастичноНетСредняя
VWAPНетНетЧастичноНетВысокая
Chasing limitВыше лимитногоЧастичноДа (текущий ордер)НетСредняя
Time-basedЗависит от типаЗависит от типаНет (до момента T)НетНизкая
Virtual/SyntheticНиже лимитногоЗависит от типаНетНетСредняя
OCOДа (один из двух)ЧастичноДа (оба)ЧастичноСредняя
BracketДаЧастичноДаРедкоВысокая
HiddenНетДаНетРедкоНулевая
PeggedНетДинамическаяДаОчень редкоВысокая (если программно)

Заключение: ордер как строительный блок стратегии

Типы ордеров — это не просто «кнопки в интерфейсе». Это фундаментальные примитивы, из которых строится execution layer любой торговой системы. Разница между «стратегия прибыльная в бэктесте» и «стратегия прибыльная в продакшене» часто лежит именно здесь — в том, как именно вы отправляете ордера на биржу.

Несколько практических выводов:

  1. Начните со стандартных ордеров, убедитесь, что понимаете нюансы (stop-limit vs stop-market, IOC vs FOK). Большинство ошибок — здесь.
  2. Virtual orders — must-have для grid-ботов. Если вы выставляете больше 50 ордеров — не отправляйте их все на биржу.
  3. Chasing нужен, когда fill rate важнее цены. Но всегда ставьте max_chase_distance — иначе можно уехать очень далеко.
  4. Time-based execution — нишевый, но мощный инструмент для funding arb и event-driven стратегий.
  5. Собственный order management layer — неизбежность для любой серьёзной алготрейдинг-системы. Биржевых типов ордеров недостаточно.

Если вы строите торговую систему и хотите разобраться глубже — смотрите наши статьи про queue position в ордербуке, WebSocket-методы CCXT и funding rate арбитраж.


Ссылки и источники

  • CCXT Library — унифицированная библиотека для работы с криптобиржами, поддержка 100+ бирж
  • Binance API Documentation — документация по типам ордеров Binance
  • Bybit API v5 — документация Bybit, включая batch orders
  • 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) — профессиональная платформа с продвинутыми типами ордеров