Виды ордеров в алготрейдинге: от limit с chasing до virtual 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 параметры — как долго ордер живёт:
| Параметр | Расшифровка | Поведение |
|---|---|---|
| GTC | Good Till Cancelled | Живёт до отмены. Стандарт по умолчанию |
| GTD | Good Till Date | Живёт до указанной даты/времени |
| IOC | Immediate or Cancel | Исполняется немедленно (целиком или частично), остаток отменяется |
| FOK | Fill 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
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: ордер преследует уходящую цену с настраиваемой агрессивностью
А вот тут начинается самое интересное. Стандартный лимитный ордер — это пассивная сущность: он стоит в стакане и ждёт. Если цена ушла — ордер остался неисполненным. Для алготрейдера это часто неприемлемо: сигнал на вход получен, а позиция не набрана, потому что рынок сдвинулся на 0.1%.
Chasing limit order — это программная обёртка вокруг лимитного ордера, которая:
- Выставляет лимитный ордер по текущей лучшей цене (или с небольшим смещением)
- Следит за ценой через WebSocket
- Если цена уходит от ордера — отменяет его и перевыставляет ближе к текущей цене
- Повторяет, пока ордер не исполнится или не превысит допустимое отклонение
Ключевые параметры
- 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 — мощный инструмент, но его легко превратить в генератор убытков:
- Cancel/replace spam. Каждая отмена и перевыставление — это нагрузка на API. Биржи лимитируют частоту запросов, и при агрессивном chasing вы можете получить бан API ключа.
- Adverse selection. Если цена убегает от вас — возможно, рынок знает что-то, чего не знаете вы. Погоня за ценой в такой ситуации — покупка на верхушке.
- 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: ордера существуют только в памяти бота до момента триггера
Это, пожалуй, самый недооценённый инструмент в арсенале алготрейдера. Virtual order (он же synthetic order) — это ордер, который существует только в вашей системе. На биржу он не отправляется до тех пор, пока не выполнится триггерное условие (обычно — цена достигла определённого уровня).
Как это работает
- Ваш алгоритм определяет: «Хочу купить BTC по $40,000»
- Вместо отправки лимитного ордера на биржу, он создаёт виртуальный ордер в памяти
- Подписывается на WebSocket-поток цен
- Когда 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
-
Latency gap. Между моментом, когда вы видите цену, и моментом, когда реальный ордер доходит до биржи, проходит время. На волатильном рынке цена может улететь за эти 20-100ms. Решение: отправлять слегка агрессивный лимитный ордер (с запасом).
-
Missed fills. Если цена «прошила» ваш уровень за один тик (flash crash) и вернулась обратно — вы можете не успеть. Обычный лимитный ордер в стакане бы исполнился, virtual — нет.
-
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,
):
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)
Bracket order
Трёхкомпонентная конструкция: основной ордер на вход + OCO для выхода (take-profit + stop-loss). По сути, полный цикл сделки в одном вызове:
- Entry: лимитный ордер на покупку
- Take-profit: лимитный ордер на продажу (выше)
- Stop-loss: stop-market ордер на продажу (ниже)
При исполнении entry автоматически выставляются TP и SL. При исполнении любого из них — второй отменяется.
If-Then логика
Самый гибкий вариант — цепочки ордеров с произвольными условиями:
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 любой торговой системы. Разница между «стратегия прибыльная в бэктесте» и «стратегия прибыльная в продакшене» часто лежит именно здесь — в том, как именно вы отправляете ордера на биржу.
Несколько практических выводов:
- Начните со стандартных ордеров, убедитесь, что понимаете нюансы (stop-limit vs stop-market, IOC vs FOK). Большинство ошибок — здесь.
- Virtual orders — must-have для grid-ботов. Если вы выставляете больше 50 ордеров — не отправляйте их все на биржу.
- Chasing нужен, когда fill rate важнее цены. Но всегда ставьте max_chase_distance — иначе можно уехать очень далеко.
- Time-based execution — нишевый, но мощный инструмент для funding arb и event-driven стратегий.
- Собственный 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) — профессиональная платформа с продвинутыми типами ордеров
MarketMaker.cc Team
Количественные исследования и стратегии