Tipos de órdenes en el trading algorítmico: de la limit con persecución a las órdenes virtuales
Cuando un principiante abre un terminal de exchange, ve dos botones: "Comprar" y "Vender". Cuando un algotrader abre su código, ve veintisiete tipos de órdenes, tres niveles de abstracción y un montón de casos límite que le dan ganas de cerrar el portátil e irse a vender pepinos al mercado de agricultores. Pero los pepinos, por desgracia, no te dejan ejecutar un arbitraje de funding rate a las 3:59 UTC, así que manos a la obra.
En este artículo recorreremos todo el trayecto, desde las órdenes básicas de exchange hasta las construcciones virtuales sintéticas que existen solo dentro de tu sistema y nunca aparecen en el libro de órdenes. Espera TypeScript, Python, algo de dolor y un poco de iluminación.
1. Órdenes estándar de exchange: la base que no puedes saltarte
Clasificación de los tipos de órdenes estándar: de market a iceberg
Antes de construir algo complejo, debemos asegurarnos de entender correctamente los bloques básicos. Es sorprendente cuánta gente confunde stop-limit y stop-market, y luego se pregunta por qué su stop "no se activó" (spoiler: sí se activó, pero la orden limit no se ejecutó por el slippage).
Orden market
El tipo más simple y a la vez el más peligroso. Le dices al exchange: "Compra/vende ahora mismo, a cualquier precio disponible". El exchange toma liquidez del libro de órdenes, empezando por el mejor precio. Si el volumen en el mejor nivel no es suficiente, se desliza más allá.
Cuándo usarla: salida de emergencia de una posición, ejecución de una señal donde la velocidad importa más que el precio.
Peligros: en un mercado poco líquido, una orden market de 100 BTC puede mover el precio varios puntos porcentuales. Los backtests que modelan órdenes market sin tener en cuenta el impacto son pura fantasía.
Orden limit
Especificas un precio exacto. La orden entra en el libro de órdenes y espera hasta que alguien acepte tu precio. Si el precio de una orden limit de compra está por encima del mercado actual, se ejecuta de inmediato (como una orden market, pero con un precio máximo garantizado).
Punto clave: una orden limit no garantiza la ejecución. El precio puede llegar a tu nivel e invertirse, dejándote sentado en la cola (más sobre esto en nuestro artículo sobre la posición en la cola).
Stop-market y Stop-limit
Aquí es donde empieza la confusión. Ambos tipos son órdenes "dormidas" que se activan cuando se alcanza el precio de activación (stop price). Pero:
- Stop-market: al activarse, se convierte en una orden market. Garantiza la ejecución, pero no el precio.
- Stop-limit: al activarse, se convierte en una orden limit. Garantiza el precio (no peor que el especificado), pero no la ejecución.
En el volátil mercado cripto, una stop-limit puede "fallar": el precio atravesó el stop, la orden limit se colocó, pero el mercado ya voló más allá. Te quedas con una orden limit sin ejecutar y una pérdida creciente. Precisamente por eso el stop-market se usa más a menudo para los stop-loss.
Trailing stop
Un stop que "sigue" al precio a una distancia fija. El precio sube, el stop sube. El precio baja, el stop se queda en su sitio. Útil para proteger beneficios en estrategias de seguimiento de tendencia.
Soporte del exchange: no todos los exchanges soportan trailing stops nativos. Los algotraders suelen implementarlos programáticamente, lo que da más control sobre los parámetros (callback rate, precio de activación, tamaño del paso).
Orden iceberg
Una orden en la que solo una fracción del volumen total es visible en el libro de órdenes. Quieres comprar 1.000 BTC, pero muestras solo 10 en el libro. Cuando se ejecutan los primeros 10, aparecen los siguientes 10.
Por qué: para ocultar tus verdaderas intenciones al mercado. Una orden grande en el libro le indica a todos que "alguien grande quiere comprar/vender". En respuesta, los algoritmos de HFT empiezan a hacer front-running y el precio se aleja de ti.
Advertencia: en muchos exchanges cripto, las órdenes iceberg o no están soportadas o son fácilmente detectables por el patrón de volúmenes idénticos. Los algoritmos avanzados aleatorizan el tamaño de la porción visible.
Parámetros time-in-force: GTC, GTD, IOC, FOK
Estos no son tipos de órdenes separados, sino parámetros de time-in-force, es decir, cuánto tiempo vive una orden:
| Parámetro | Nombre completo | Comportamiento |
|---|---|---|
| GTC | Good Till Cancelled | Vive hasta que se cancela. El estándar por defecto |
| GTD | Good Till Date | Vive hasta una fecha/hora especificada |
| IOC | Immediate or Cancel | Se ejecuta de inmediato (total o parcialmente), el resto se cancela |
| FOK | Fill or Kill | Se ejecuta solo por completo y de inmediato. Si es imposible, se cancela por entero |
IOC vs FOK: la diferencia es crítica. IOC puede ejecutarse parcialmente: querías comprar 100 BTC, compraste 3, el resto se canceló. FOK es 100 o nada.
Post-only (Maker-only)
Una orden que tiene garantizado entrar en el libro de órdenes como maker y nunca se ejecuta como taker. Si en el momento de la colocación el precio provocara una ejecución inmediata, el exchange la rechaza (o ajusta el precio, según el exchange).
Por qué: las comisiones de maker suelen ser más bajas que las de taker (en Binance, 0,02 % frente a 0,04 % para los niveles VIP). Para un market maker que coloca miles de órdenes al día, la diferencia de comisiones es la diferencia entre ganancia y pérdida.
2. TWAP y VWAP: cómo las instituciones esconden un elefante en el libro de órdenes
Cuando un hedge fund quiere comprar una posición de 50 millones de dólares, no coloca una única orden market. Usa algoritmos de ejecución: algoritmos que dividen una orden grande en muchas más pequeñas y las ejecutan a lo largo del tiempo, minimizando el impacto en el mercado.
TWAP (Time-Weighted Average Price)
La idea es de lo más simple: dividir el volumen total en partes iguales y ejecutarlas en intervalos de tiempo iguales.
import asyncio
from datetime import datetime, timedelta
class TWAPExecutor:
"""
TWAP executor: splits a large order into equal parts
and executes them at equal time intervals.
"""
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 es más inteligente: tiene en cuenta el perfil típico de volumen de trading. Si normalmente se negocia el 30 % del volumen diario entre las 9:00 y las 10:00, VWAP ejecutará el 30 % de la orden en esa ventana. El objetivo es acercar el precio medio de ejecución lo máximo posible al VWAP del mercado.
class VWAPExecutor:
"""
VWAP executor: distributes volume proportionally
to the historical volume profile.
"""
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)
Diferencia TWAP vs VWAP: TWAP es más simple y predecible. VWAP ofrece un mejor precio medio, pero requiere un perfil de volumen fiable. En el mercado cripto, donde los volúmenes pueden estar inflados por wash-trading, el perfil VWAP debe construirse con cuidado.
3. Limit con persecución: cuando tu orden sabe perseguir el precio
Limit con persecución: la orden persigue un precio en movimiento con una agresividad configurable
Ahora la cosa se pone realmente interesante. Una orden limit estándar es una entidad pasiva: se sienta en el libro de órdenes y espera. Si el precio se movió, la orden queda sin ejecutar. Para un algotrader esto suele ser inaceptable: la señal de entrada se disparó, pero la posición no se construyó porque el mercado se movió un 0,1 %.
Una orden limit con persecución es un envoltorio programático alrededor de una orden limit que:
- Coloca una orden limit al mejor precio actual (o con un pequeño desplazamiento)
- Monitorea el precio vía WebSocket
- Si el precio se aleja de la orden, la cancela y la vuelve a colocar más cerca del precio actual
- Repite hasta que la orden se ejecuta o supera la desviación permitida
Parámetros clave
- chase_interval_ms: con qué frecuencia comprobar y recolocar la orden. 100 ms: agresivo, 1000 ms: relajado.
- max_chase_distance: desviación máxima respecto al precio inicial antes de que se cancele la orden. Protección contra la persecución de un mercado desbocado.
- aggression_level: cuán cerca del precio de mercado colocar la orden limit.
0: al mejor bid/ask (pasivo),1: cruzando el spread (agresivo, en la práctica un taker). - chase_on_partial: si continuar persiguiendo cuando la orden se ejecuta parcialmente.
Implementación en 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) {
// Timeout
if (Date.now() - this.startTime > this.params.timeoutMs) {
console.log("[CHASE] timeout reached, cancelling");
await this.cancelCurrent();
break;
}
// Get current order book
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;
// Calculate target price
let targetPrice: number;
if (this.params.side === "buy") {
targetPrice = bestBid + spread * this.params.aggression;
} else {
targetPrice = bestAsk - spread * this.params.aggression;
}
// Remember the initial price
if (this.initialPrice === null) {
this.initialPrice = targetPrice;
}
// Check 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;
}
// Check current order
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;
}
// Update filledQty for partial fills
if (order.filled > 0) {
const newFilled = order.filled - (
fills.reduce((s, f) => s + f.qty, 0) - this.filledQty
);
// Order is in place — do we need to reprice?
}
const currentPrice = parseFloat(order.price);
const priceDiff = Math.abs(currentPrice - targetPrice);
const tickSize = spread * 0.1 || 0.01;
if (priceDiff > tickSize) {
// Price moved — reprice
console.log(
`[CHASE] repricing: ${currentPrice} -> ` +
`${targetPrice.toFixed(4)}`
);
await this.cancelCurrent();
} else {
// Order is at the right price — wait
await this.sleep(this.params.chaseIntervalMs);
continue;
}
}
// Place new order
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 { /* order already filled or cancelled */ }
this.currentOrderId = null;
}
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
Cuándo la persecución es perjudicial
La persecución es una herramienta poderosa, pero es fácil convertirla en un generador de pérdidas:
- Spam de cancel/replace. Cada cancelación y recolocación es carga para la API. Los exchanges limitan las peticiones (rate-limit), y una persecución agresiva puede hacer que baneen tu clave de API.
- Selección adversa. Si el precio se te escapa, puede que el mercado sepa algo que tú no. Perseguir el precio en esa situación significa comprar en el máximo.
- Transición de maker a taker. Con alta agresividad estás pagando de hecho comisiones de taker, pero con retardo (cancelación + nueva orden). A veces es más simple colocar directamente una orden market.
4. Órdenes basadas en tiempo: precisión de milisegundos
Hay situaciones en las que necesitas ejecutar una orden no "al precio X" sino "en el momento T". ¿Suena raro? En realidad es toda una clase de estrategias.
Casos de uso
Arbitraje de funding rate. En los futuros perpetuos, el funding se paga cada 8 horas (00:00, 08:00, 16:00 UTC en Binance). Si el funding rate = +0,1 %, necesitas estar corto en el momento del settlement. Estrategia: abrir un corto unos segundos antes del settlement, cobrar el funding, cerrar la posición. El timing es crítico: un segundo de retraso significa funding perdido.
Aperturas/cierres de sesión. En los mercados tradicionales y algunos derivados cripto hay sesiones fijas. La subasta de apertura (NYSE, CME) es el momento en que la liquidez está en su pico. Colocar una orden 100 ms antes de la subasta es una ventaja.
Ejecución basada en noticias. Los datos de inflación se publican a una hora programada. El algoritmo extrae el número de un feed de noticias y coloca una orden en 50 ms. Aquí la ejecución basada en tiempo se combina con lógica orientada a eventos.
Implementación
class TimeBasedOrder {
constructor(
private exchange: any,
private symbol: string,
private side: "buy" | "sell",
private qty: number,
private orderType: "market" | "limit",
private limitPrice?: number
) {}
/**
* Schedule execution at a precise time.
* Uses a busy-wait loop for maximum precision.
*/
async executeAt(targetTime: Date): Promise<any> {
const targetMs = targetTime.getTime();
// Phase 1: coarse wait (sleep)
const coarseWait = targetMs - Date.now() - 500; // wake up 500ms early
if (coarseWait > 0) {
console.log(
`[TIME-ORDER] sleeping for ${(coarseWait / 1000).toFixed(1)}s`
);
await new Promise((r) => setTimeout(r, coarseWait));
}
// Phase 2: precise wait (busy-wait)
while (Date.now() < targetMs) {
// spin — burns CPU, but achieves ~1ms precision
}
// Phase 3: execution
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;
}
}
// Example: place an order exactly at 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);
Advertencia importante: la precisión de una orden basada en tiempo no está limitada por tu código sino por la latencia de red hacia el exchange. Si tu ping a la API es de 50 ms, incluso un busy-wait perfecto tendrá un delta de 50 ms. Para HFT serio se usa co-location: el servidor se ubica físicamente junto al motor de matching del exchange.
5. Órdenes virtuales/sintéticas: las invisibles dentro de tu sistema
Órdenes virtuales: las órdenes existen solo en la memoria del bot hasta que se dispara el trigger
Esta es quizá la herramienta más infravalorada del arsenal del algotrader. Una orden virtual (también conocida como orden sintética) es una orden que existe solo en tu sistema. No se envía al exchange hasta que se cumple una condición de activación (normalmente, que el precio alcance un nivel determinado).
Cómo funciona
- Tu algoritmo decide: "Quiero comprar BTC a 40.000 $"
- En lugar de enviar una orden limit al exchange, crea una orden virtual en memoria
- Se suscribe al stream de precios por WebSocket
- Cuando el bid/ask alcanza 40.000 $, envía una orden market o limit real al exchange
Por qué importan las órdenes virtuales
Sin fuga de información. Tu orden es invisible en el libro de órdenes. Nadie (ni otros traders, ni algoritmos de HFT, ni siquiera el propio exchange) conoce tus intenciones hasta el momento de la ejecución. Esto cambia radicalmente el equilibrio de poder.
Protección contra front-running. En los exchanges cripto, especialmente en los menos transparentes, existe una sospecha razonable de que la información sobre grandes órdenes limit puede usarse para front-running (incluso hay estudios sobre ello). Las órdenes virtuales eliminan ese riesgo.
Grid bots. Un grid bot clásico coloca una rejilla de 50-200 órdenes en distintos niveles de precio. Si las envías todas al exchange, son 200 órdenes en el libro que: (a) son visibles para todos, (b) consumen el límite de órdenes del exchange (normalmente 200-300 órdenes abiertas por cuenta), (c) si el precio se mueve bruscamente, todas se ejecutan y acabas con una posición enorme. Las órdenes virtuales resuelven los tres problemas.
Atrapar cuchillos que caen. Estrategia: colocar órdenes de compra virtuales en los niveles -5 %, -10 %, -15 % por debajo del precio actual. Si el mercado cae, las órdenes se disparan gradualmente. Si no cae, no arriesgas nada y no ocupas slots de órdenes del exchange.
Implementación en TypeScript
interface VirtualOrder {
id: string;
symbol: string;
side: "buy" | "sell";
triggerPrice: number;
qty: number;
/** Order type sent to the exchange upon triggering */
executionType: "market" | "limit";
/** For limit: offset from 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) {}
/**
* Create a virtual order. Nothing is sent to the exchange.
*/
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;
}
/**
* Called on every price tick (from 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);
}
}
}
/**
* Get all active virtual orders.
*/
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;
}
}
// --- Example: Grid bot with virtual orders ---
async function gridBot(exchange: any) {
const manager = new VirtualOrderManager(exchange);
const currentPrice = 42000;
const gridStep = 200; // grid step
const gridLevels = 20; // levels in each direction
const qtyPerLevel = 0.01; // BTC per level
// Create virtual grid
for (let i = 1; i <= gridLevels; i++) {
// Buy orders below current price
manager.addOrder({
symbol: "BTC/USDT",
side: "buy",
triggerPrice: currentPrice - gridStep * i,
qty: qtyPerLevel,
executionType: "limit",
limitOffset: 1, // limit price = trigger + 1 USDT
});
// Sell orders above current price
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 subscription (pseudocode for ccxt.pro)
while (true) {
const ticker = await exchange.watchTicker("BTC/USDT");
await manager.onPriceUpdate(
"BTC/USDT", ticker.bid, ticker.ask
);
}
}
Peligros de las órdenes virtuales
-
Brecha de latencia. Entre el momento en que ves el precio y el momento en que la orden real llega al exchange, pasa un tiempo. En un mercado volátil, el precio puede escaparse en esos 20-100 ms. Solución: enviar una orden limit ligeramente agresiva (con un margen).
-
Ejecuciones perdidas. Si el precio "atravesó" tu nivel en un solo tick (flash crash) y rebotó, puede que no reacciones a tiempo. Una orden limit normal en el libro se habría ejecutado; una virtual, no.
-
Gestión del estado. Las órdenes virtuales viven en memoria. Si el proceso se cae, las órdenes se pierden. Solución: almacenamiento persistente (Redis, SQLite, archivo) con recuperación al reiniciar.
6. Órdenes condicionales/inteligentes: combinatoria de órdenes
Cuando una sola orden no basta, los traders las combinan en construcciones condicionales. Algunas están soportadas de forma nativa en los exchanges, otras se implementan programáticamente.
OCO (One Cancels Other)
Dos órdenes están vinculadas: si una se ejecuta, la otra se cancela automáticamente. Ejemplo clásico: estás en una posición larga y quieres poner a la vez un take-profit y un stop-loss. La que se dispare primero, la otra debe cancelarse.
class OCOHandler:
"""
OCO: when one order fills, the other is cancelled.
"""
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):
"""Checks statuses and cancels the paired order."""
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)
Orden bracket
Una construcción de tres componentes: una orden principal de entrada + OCO para la salida (take-profit + stop-loss). En esencia, un ciclo de vida completo de la operación en una sola llamada:
- Entrada: orden limit de compra
- Take-profit: orden limit de venta (por encima)
- Stop-loss: orden stop-market de venta (por debajo)
Cuando la entrada se ejecuta, TP y SL se colocan automáticamente. Cuando cualquiera de las dos se ejecuta, la otra se cancela.
Lógica If-Then
La opción más flexible: cadenas de órdenes con condiciones arbitrarias:
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},
},
]
}
]
Estas construcciones no están soportadas de forma nativa por ningún exchange, solo mediante implementación programática. Esta es una de las razones por las que los sistemas de algotrading acaban inevitablemente desarrollando su propia capa de gestión de órdenes.
7. Cómo usan los market makers los tipos de órdenes especializados
El market making es su propio universo, y el conjunto de órdenes está a la altura. El trabajo de un market maker es cotizar continuamente bid y ask, ganar con el spread, minimizando al mismo tiempo la selección adversa (la situación en la que un trader informado opera contra ti).
Post-only como imprescindible
Para un market maker, post-only no es una opción, es un requisito. Si tu orden se ejecuta accidentalmente como taker, en lugar de recibir un rebate de maker, pagas una comisión de taker. A lo largo de miles de órdenes al día, eso es catastrófico.
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} # CRITICAL for market makers
)
ask = await exchange.create_order(
symbol, "limit", "sell", qty, ask_price,
{"postOnly": True}
)
return bid, ask
Órdenes ocultas (hidden)
En algunos exchanges (Kraken, Bitfinex) están disponibles las órdenes ocultas: no aparecen en el libro de órdenes pero están en el exchange y participan en el matching. El compromiso: pagas comisiones de taker incluso siendo maker, pero ganas anonimato.
Para un market maker, esta es una herramienta de gestión de inventario: si se ha acumulado una posición grande, puedes colocar una orden oculta para deshacerla sin revelar tu intención al mercado.
Órdenes ancladas (pegged)
Una orden anclada al mejor bid/ask. En Coinbase Advanced Trade, por ejemplo, puedes colocar una orden que sigue automáticamente el mejor bid y siempre se sitúa al frente de la cola. Es una orden con persecución nativa a nivel de exchange, pero está lejos de estar disponible universalmente.
Gestión masiva de órdenes
Los market makers profesionales usan APIs por lotes para cancelar y colocar simultáneamente decenas de órdenes en una sola petición HTTP. En Binance esto es batchOrders, en Bybit, place-batch-order. Esto reduce la latencia y la presión sobre el rate limit.
8. Tabla comparativa de tipos de órdenes
| Tipo de orden | Garantía de ejecución | Garantía de precio | Visible en el libro | Nativa en exchanges | Complejidad de implementación |
|---|---|---|---|---|---|
| Market | Sí | No | No (instantánea) | Sí | Ninguna |
| Limit | No | Sí | Sí | Sí | Ninguna |
| Stop-market | Sí (tras activarse) | No | No | Sí | Ninguna |
| Stop-limit | No | Sí | No (hasta activarse) | Sí | Ninguna |
| Trailing stop | Sí (tras activarse) | No | No | Parcial | Baja |
| Iceberg | No | Sí | Parcial | Parcial | Media |
| Post-only | No | Sí | Sí | Sí | Ninguna |
| TWAP | No (depende de los slices) | No | Parcial | No | Media |
| VWAP | No | No | Parcial | No | Alta |
| Limit con persecución | Mayor que limit | Parcial | Sí (orden actual) | No | Media |
| Basada en tiempo | Depende del tipo | Depende del tipo | No (hasta la hora T) | No | Baja |
| Virtual/Sintética | Menor que limit | Depende del tipo | No | No | Media |
| OCO | Sí (una de dos) | Parcial | Sí (ambas) | Parcial | Media |
| Bracket | Sí | Parcial | Sí | Rara | Alta |
| Hidden | No | Sí | No | Rara | Ninguna |
| Pegged | No | Dinámica | Sí | Muy rara | Alta (si es programática) |
Conclusión: la orden como bloque de construcción de la estrategia
Los tipos de órdenes no son solo "botones en una interfaz". Son primitivos fundamentales a partir de los cuales se construye la capa de ejecución de cualquier sistema de trading. La diferencia entre "la estrategia es rentable en backtesting" y "la estrategia es rentable en producción" suele residir justo aquí: en cómo exactamente envías las órdenes al exchange.
Algunas conclusiones prácticas:
- Empieza con órdenes estándar, asegúrate de entender los matices (stop-limit vs stop-market, IOC vs FOK). La mayoría de los errores ocurren aquí.
- Las órdenes virtuales son imprescindibles para los grid bots. Si vas a colocar más de 50 órdenes, no las envíes todas al exchange.
- La persecución se necesita cuando la tasa de ejecución importa más que el precio. Pero pon siempre max_chase_distance, o podrías desviarte muy lejos.
- La ejecución basada en tiempo es de nicho pero potente para el arbitraje de funding y las estrategias orientadas a eventos.
- Una capa propia de gestión de órdenes es inevitable para cualquier sistema serio de algotrading. Los tipos de órdenes nativos del exchange no bastan.
Si estás construyendo un sistema de trading y quieres profundizar, echa un vistazo a nuestros artículos sobre la posición en la cola del libro de órdenes, los métodos WebSocket en CCXT y el arbitraje de funding rate.
Referencias y fuentes
- CCXT Library - una biblioteca unificada para trabajar con exchanges cripto, con soporte para más de 100 exchanges
- Binance API Documentation - documentación de los tipos de órdenes de Binance
- Bybit API v5 - documentación de Bybit, incluidas las órdenes por lotes
- 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 - materiales sobre la estimación de la posición en la cola
- Trading Technologies (TT) - una plataforma profesional con tipos de órdenes avanzados
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.