Types d'ordres dans le trading algorithmique : de la limite avec poursuite aux ordres virtuels
Lorsqu'un débutant ouvre un terminal d'exchange, il voit deux boutons : "Acheter" et "Vendre". Lorsqu'un trader algo ouvre sa base de code, il voit vingt-sept types d'ordres, trois niveaux d'abstraction et une pile de cas limites qui lui donnent envie de refermer son ordinateur portable et d'aller vendre des concombres au marché fermier. Mais les concombres, hélas, ne vous permettront pas de faire de l'arbitrage de funding rate à 3h59 UTC, alors mettons-nous au travail.
Dans cet article, nous parcourrons tout le chemin, des ordres d'exchange de base jusqu'aux constructions virtuelles synthétiques qui n'existent qu'au sein de votre système et n'apparaissent jamais dans le carnet d'ordres. Attendez-vous à du TypeScript, du Python, un peu de douleur et un brin d'illumination.
1. Les ordres standard des exchanges : la base que vous ne pouvez pas sauter
Classification des types d'ordres standard : de market à iceberg
Avant de construire quoi que ce soit de complexe, nous devons nous assurer de bien comprendre les blocs de base. Il est surprenant de voir combien de gens confondent stop-limit et stop-market, puis se demandent pourquoi leur stop "ne s'est pas déclenché" (spoiler : il s'est bien déclenché, mais l'ordre limite n'a pas été exécuté à cause du slippage).
Ordre market
Le type le plus simple et en même temps le plus dangereux. Vous dites à l'exchange : "Achète/vends immédiatement, à n'importe quel prix disponible." L'exchange prend de la liquidité dans le carnet d'ordres, en commençant par le meilleur prix. Si le volume au meilleur niveau ne suffit pas, il glisse plus loin.
Quand l'utiliser : sortie d'urgence d'une position, exécution d'un signal où la vitesse compte plus que le prix.
Pièges : sur un marché peu liquide, un ordre market de 100 BTC peut faire bouger le prix de plusieurs pour cent. Les backtests qui modélisent des ordres market sans tenir compte de l'impact relèvent de la pure fantaisie.
Ordre limite
Vous spécifiez un prix exact. L'ordre entre dans le carnet d'ordres et attend que quelqu'un accepte votre prix. Si le prix d'un ordre limite d'achat est au-dessus du marché actuel, il s'exécute immédiatement (comme un ordre market, mais avec un prix maximal garanti).
Point clé : un ordre limite ne garantit pas l'exécution. Le prix peut atteindre votre niveau puis repartir, vous laissant assis dans la file d'attente (plus de détails dans notre article sur la position dans la file).
Stop-market et Stop-limit
C'est ici que la confusion commence. Les deux types sont des ordres "endormis" qui s'activent lorsque le prix de déclenchement (stop price) est atteint. Mais :
- Stop-market : au déclenchement, se convertit en ordre market. Garantit l'exécution, mais pas le prix.
- Stop-limit : au déclenchement, se convertit en ordre limite. Garantit le prix (pas pire que celui spécifié), mais pas l'exécution.
Sur le marché crypto volatil, un stop-limit peut "manquer" : le prix a percé le stop, l'ordre limite a été placé, mais le marché s'est déjà envolé plus loin. Il vous reste un ordre limite non exécuté et une perte qui grandit. C'est exactement pourquoi le stop-market est plus couramment utilisé pour les stop-loss.
Trailing stop
Un stop qui "suit" le prix à une distance fixée. Le prix monte, le stop remonte. Le prix baisse, le stop reste en place. Utile pour protéger les profits dans les stratégies de suivi de tendance.
Prise en charge par les exchanges : tous les exchanges ne prennent pas en charge les trailing stops natifs. Les traders algo les implémentent souvent de manière programmatique, ce qui donne plus de contrôle sur les paramètres (callback rate, prix d'activation, taille du pas).
Ordre iceberg
Un ordre dont seule une fraction du volume total est visible dans le carnet d'ordres. Vous voulez acheter 1 000 BTC, mais vous n'en affichez que 10 dans le carnet. Quand les 10 premiers sont exécutés, les 10 suivants apparaissent.
Pourquoi : pour cacher vos véritables intentions au marché. Un gros ordre dans le carnet signale à tout le monde que "quelqu'un de gros veut acheter/vendre". En réaction, les algorithmes HFT commencent le front-running et le prix s'éloigne de vous.
Réserve : sur de nombreux exchanges crypto, les ordres iceberg sont soit non pris en charge, soit facilement détectés par le motif de volumes identiques. Les algorithmes avancés randomisent la taille de la portion visible.
Paramètres time-in-force : GTC, GTD, IOC, FOK
Ce ne sont pas des types d'ordres distincts mais des paramètres de time-in-force, c'est-à-dire combien de temps un ordre vit :
| Paramètre | Nom complet | Comportement |
|---|---|---|
| GTC | Good Till Cancelled | Vit jusqu'à annulation. Le standard par défaut |
| GTD | Good Till Date | Vit jusqu'à une date/heure spécifiée |
| IOC | Immediate or Cancel | S'exécute immédiatement (en totalité ou en partie), le reste est annulé |
| FOK | Fill or Kill | S'exécute uniquement en totalité et immédiatement. Si c'est impossible, il est entièrement annulé |
IOC vs FOK : la différence est cruciale. IOC peut s'exécuter partiellement : vous vouliez acheter 100 BTC, vous en avez acheté 3, le reste a été annulé. FOK, c'est 100 ou rien.
Post-only (Maker-only)
Un ordre qui a la garantie d'entrer dans le carnet en tant que maker et qui ne s'exécute jamais comme taker. Si, au moment du placement, le prix provoquerait une exécution immédiate, l'exchange le rejette (ou ajuste le prix, selon l'exchange).
Pourquoi : les frais maker sont généralement plus bas que les frais taker (sur Binance, 0,02 % contre 0,04 % pour les paliers VIP). Pour un market maker qui place des milliers d'ordres par jour, la différence de frais fait la différence entre profit et perte.
2. TWAP et VWAP : comment les institutions cachent un éléphant dans le carnet d'ordres
Quand un hedge fund veut acheter une position de 50 M$, il ne place pas un seul ordre market. Il utilise des algorithmes d'exécution : des algorithmes qui découpent un gros ordre en de nombreux plus petits et les exécutent dans le temps, en minimisant l'impact sur le marché.
TWAP (Time-Weighted Average Price)
L'idée est on ne peut plus simple : découper le volume total en parts égales et les exécuter à intervalles de temps égaux.
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 est plus malin : il tient compte du profil de volume de trading typique. Si 30 % du volume quotidien se traite habituellement entre 9h00 et 10h00, VWAP exécutera 30 % de l'ordre pendant cette fenêtre. L'objectif est de rapprocher le prix moyen d'exécution le plus possible du VWAP du marché.
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)
Différence TWAP vs VWAP : TWAP est plus simple et plus prévisible. VWAP offre un meilleur prix moyen mais nécessite un profil de volume fiable. Sur le marché crypto, où les volumes peuvent être gonflés par du wash-trading, le profil VWAP doit être construit avec soin.
3. La limite avec poursuite : quand votre ordre sait poursuivre le prix
Limite avec poursuite : l'ordre poursuit un prix en mouvement avec une agressivité configurable
Là, ça devient vraiment intéressant. Un ordre limite standard est une entité passive : il reste dans le carnet d'ordres et attend. Si le prix a bougé, l'ordre reste non exécuté. Pour un trader algo, c'est souvent inacceptable : le signal d'entrée s'est déclenché, mais la position n'a pas été construite parce que le marché a bougé de 0,1 %.
Un ordre limite avec poursuite est un wrapper programmatique autour d'un ordre limite qui :
- Place un ordre limite au meilleur prix actuel (ou avec un léger décalage)
- Surveille le prix via WebSocket
- Si le prix s'éloigne de l'ordre, l'annule et le replace plus près du prix actuel
- Répète jusqu'à ce que l'ordre soit exécuté ou dépasse l'écart autorisé
Paramètres clés
- chase_interval_ms : à quelle fréquence vérifier et replacer l'ordre. 100 ms : agressif, 1000 ms : détendu.
- max_chase_distance : écart maximal par rapport au prix initial avant l'annulation de l'ordre. Protection contre la poursuite d'un marché qui s'emballe.
- aggression_level : à quelle distance du prix de marché placer l'ordre limite.
0: au meilleur bid/ask (passif),1: en traversant le spread (agressif, de fait un taker). - chase_on_partial : s'il faut continuer la poursuite lorsque l'ordre est partiellement exécuté.
Implémentation 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));
}
}
Quand la poursuite est nuisible
La poursuite est un outil puissant, mais il est facile de la transformer en générateur de pertes :
- Spam de cancel/replace. Chaque annulation et replacement est une charge pour l'API. Les exchanges limitent les requêtes (rate-limit), et une poursuite agressive peut faire bannir votre clé d'API.
- Sélection adverse. Si le prix vous échappe, le marché sait peut-être quelque chose que vous ignorez. Poursuivre le prix dans cette situation, c'est acheter au sommet.
- Transition de maker à taker. Avec une forte agressivité, vous payez de fait des frais taker, mais avec un délai (annulation + nouvel ordre). Parfois, il est plus simple de placer directement un ordre market.
4. Ordres temporisés : précision à la milliseconde
Il existe des situations où vous devez exécuter un ordre non pas "au prix X" mais "à l'instant T". Ça paraît étrange ? C'est en réalité toute une classe de stratégies.
Cas d'usage
Arbitrage de funding rate. Sur les futures perpétuels, le funding est payé toutes les 8 heures (00:00, 08:00, 16:00 UTC sur Binance). Si le funding rate = +0,1 %, vous devez être short au moment du settlement. Stratégie : ouvrir un short quelques secondes avant le settlement, encaisser le funding, fermer la position. Le timing est crucial : une seconde de retard signifie un funding manqué.
Ouvertures/clôtures de session. Sur les marchés traditionnels et certains dérivés crypto, il y a des sessions fixes. L'enchère d'ouverture (NYSE, CME) est le moment où la liquidité est à son pic. Placer un ordre 100 ms avant l'enchère est un edge.
Exécution sur actualité. Les données d'inflation sont publiées à une heure programmée. L'algorithme extrait le chiffre d'un fil d'actualités et place un ordre en 50 ms. Ici, l'exécution temporisée se combine à une logique événementielle.
Implémentation
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);
Réserve importante : la précision d'un ordre temporisé n'est pas limitée par votre code mais par la latence réseau vers l'exchange. Si votre ping vers l'API est de 50 ms, même un busy-wait parfait aura un delta de 50 ms. Pour du HFT sérieux, on utilise la co-location : le serveur est physiquement placé à côté du moteur de matching de l'exchange.
5. Ordres virtuels/synthétiques : les invisibles dans votre système
Ordres virtuels : les ordres n'existent que dans la mémoire du bot jusqu'au déclenchement du trigger
C'est peut-être l'outil le plus sous-estimé de l'arsenal du trader algo. Un ordre virtuel (également appelé ordre synthétique) est un ordre qui n'existe que dans votre système. Il n'est pas envoyé à l'exchange tant qu'une condition de déclenchement n'est pas remplie (généralement, le prix atteignant un certain niveau).
Comment ça marche
- Votre algorithme décide : "Je veux acheter du BTC à 40 000 $"
- Au lieu d'envoyer un ordre limite à l'exchange, il crée un ordre virtuel en mémoire
- S'abonne au flux de prix WebSocket
- Quand le bid/ask atteint 40 000 $, il envoie un vrai ordre market ou limite à l'exchange
Pourquoi les ordres virtuels comptent
Aucune fuite d'information. Votre ordre est invisible dans le carnet d'ordres. Personne — ni les autres traders, ni les algorithmes HFT, ni même l'exchange lui-même — ne connaît vos intentions jusqu'au moment de l'exécution. Cela déplace fondamentalement le rapport de force.
Protection contre le front-running. Sur les exchanges crypto, surtout les moins transparents, il existe un soupçon raisonnable que l'information sur les gros ordres limites puisse être utilisée pour du front-running (il existe même des études à ce sujet). Les ordres virtuels éliminent ce risque.
Grid bots. Un grid bot classique place une grille de 50 à 200 ordres à différents niveaux de prix. Si vous les envoyez tous à l'exchange, ce sont 200 ordres dans le carnet qui : (a) sont visibles par tous, (b) consomment la limite d'ordres de l'exchange (généralement 200 à 300 ordres ouverts par compte), (c) si le prix bouge brusquement, s'exécutent tous et vous vous retrouvez avec une position gigantesque. Les ordres virtuels résolvent ces trois problèmes.
Attraper des couteaux qui tombent. Stratégie : placer des ordres d'achat virtuels aux niveaux -5 %, -10 %, -15 % sous le prix actuel. Si le marché chute, les ordres se déclenchent progressivement. S'il ne chute pas, vous ne risquez rien et n'occupez aucun slot d'ordre de l'exchange.
Implémentation 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
);
}
}
Pièges des ordres virtuels
-
Écart de latence. Entre le moment où vous voyez le prix et le moment où le vrai ordre atteint l'exchange, du temps s'écoule. Sur un marché volatil, le prix peut s'envoler pendant ces 20 à 100 ms. Solution : envoyer un ordre limite légèrement agressif (avec une marge).
-
Exécutions manquées. Si le prix a "percé" votre niveau en un seul tick (flash crash) puis a rebondi, vous pourriez ne pas réagir à temps. Un ordre limite ordinaire présent dans le carnet aurait été exécuté ; un ordre virtuel, non.
-
Gestion de l'état. Les ordres virtuels vivent en mémoire. Si le processus plante, les ordres sont perdus. Solution : stockage persistant (Redis, SQLite, fichier) avec récupération au redémarrage.
6. Ordres conditionnels/intelligents : combinatoire des ordres
Quand un seul ordre ne suffit pas, les traders les combinent en constructions conditionnelles. Certaines sont prises en charge nativement par les exchanges, d'autres sont implémentées de manière programmatique.
OCO (One Cancels Other)
Deux ordres sont liés : si l'un s'exécute, l'autre est automatiquement annulé. Exemple classique : vous êtes en position longue et voulez placer à la fois un take-profit et un stop-loss. Celui qui se déclenche en premier, l'autre doit être annulé.
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)
Ordre bracket
Une construction à trois composantes : un ordre d'entrée principal + OCO pour la sortie (take-profit + stop-loss). En substance, un cycle de vie complet de la position en un seul appel :
- Entrée : ordre limite d'achat
- Take-profit : ordre limite de vente (au-dessus)
- Stop-loss : ordre stop-market de vente (en dessous)
Quand l'entrée est exécutée, TP et SL sont placés automatiquement. Quand l'un des deux est exécuté, l'autre est annulé.
Logique If-Then
L'option la plus flexible : des chaînes d'ordres avec des conditions arbitraires :
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},
},
]
}
]
De telles constructions ne sont prises en charge nativement par aucun exchange, seulement par implémentation programmatique. C'est l'une des raisons pour lesquelles les systèmes d'algotrading finissent inévitablement par développer leur propre couche de gestion des ordres.
7. Comment les market makers utilisent les types d'ordres spécialisés
Le market making est un univers à part entière, et la boîte à outils d'ordres est à l'avenant. Le travail d'un market maker est de coter en continu bid et ask, de gagner sur le spread, tout en minimisant la sélection adverse (la situation où un trader informé opère contre vous).
Post-only comme incontournable
Pour un market maker, post-only n'est pas une option, c'est une exigence. Si votre ordre s'exécute accidentellement comme taker, au lieu de recevoir un rebate maker, vous payez des frais taker. Sur des milliers d'ordres par jour, c'est catastrophique.
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
Ordres cachés (hidden)
Sur certains exchanges (Kraken, Bitfinex), les ordres cachés sont disponibles : ils n'apparaissent pas dans le carnet d'ordres mais sont sur l'exchange et participent au matching. Le compromis : vous payez des frais taker même en tant que maker, mais vous gagnez en anonymat.
Pour un market maker, c'est un outil de gestion d'inventaire : si une grosse position s'est accumulée, vous pouvez placer un ordre caché pour la déboucler sans révéler votre intention au marché.
Ordres ancrés (pegged)
Un ordre ancré au meilleur bid/ask. Sur Coinbase Advanced Trade, par exemple, vous pouvez placer un ordre qui suit automatiquement le meilleur bid et se tient toujours en tête de la file. C'est un ordre à poursuite natif au niveau de l'exchange, mais il est loin d'être universellement disponible.
Gestion groupée des ordres
Les market makers professionnels utilisent des API par lots pour annuler et placer simultanément des dizaines d'ordres en une seule requête HTTP. Sur Binance, c'est batchOrders, sur Bybit, place-batch-order. Cela réduit la latence et la pression sur le rate limit.
8. Tableau comparatif des types d'ordres
| Type d'ordre | Garantie d'exécution | Garantie de prix | Visible dans le carnet | Natif sur les exchanges | Complexité d'implémentation |
|---|---|---|---|---|---|
| Market | Oui | Non | Non (instantané) | Oui | Aucune |
| Limite | Non | Oui | Oui | Oui | Aucune |
| Stop-market | Oui (après déclenchement) | Non | Non | Oui | Aucune |
| Stop-limit | Non | Oui | Non (jusqu'au déclenchement) | Oui | Aucune |
| Trailing stop | Oui (après déclenchement) | Non | Non | Partiel | Faible |
| Iceberg | Non | Oui | Partiel | Partiel | Moyenne |
| Post-only | Non | Oui | Oui | Oui | Aucune |
| TWAP | Non (selon les slices) | Non | Partiel | Non | Moyenne |
| VWAP | Non | Non | Partiel | Non | Élevée |
| Limite avec poursuite | Plus élevée que limite | Partielle | Oui (ordre actuel) | Non | Moyenne |
| Temporisé | Selon le type | Selon le type | Non (jusqu'à l'instant T) | Non | Faible |
| Virtuel/Synthétique | Plus faible que limite | Selon le type | Non | Non | Moyenne |
| OCO | Oui (un des deux) | Partielle | Oui (les deux) | Partiel | Moyenne |
| Bracket | Oui | Partielle | Oui | Rare | Élevée |
| Hidden | Non | Oui | Non | Rare | Aucune |
| Pegged | Non | Dynamique | Oui | Très rare | Élevée (si programmatique) |
Conclusion : l'ordre comme brique de construction de la stratégie
Les types d'ordres ne sont pas juste des "boutons dans une interface". Ce sont des primitives fondamentales à partir desquelles se construit la couche d'exécution de tout système de trading. La différence entre "la stratégie est rentable en backtesting" et "la stratégie est rentable en production" réside souvent précisément ici : dans la manière exacte dont vous envoyez les ordres à l'exchange.
Quelques enseignements pratiques :
- Commencez par les ordres standard, assurez-vous de comprendre les nuances (stop-limit vs stop-market, IOC vs FOK). La plupart des erreurs se produisent ici.
- Les ordres virtuels sont incontournables pour les grid bots. Si vous placez plus de 50 ordres, ne les envoyez pas tous à l'exchange.
- La poursuite est nécessaire quand le taux d'exécution compte plus que le prix. Mais fixez toujours max_chase_distance, sinon vous pourriez dériver très loin.
- L'exécution temporisée est de niche mais puissante pour l'arbitrage de funding et les stratégies événementielles.
- Une couche de gestion des ordres sur mesure est inévitable pour tout système d'algotrading sérieux. Les types d'ordres natifs des exchanges ne suffisent pas.
Si vous construisez un système de trading et voulez aller plus loin, consultez nos articles sur la position dans la file du carnet d'ordres, les méthodes WebSocket dans CCXT et l'arbitrage de funding rate.
Références et sources
- CCXT Library - une bibliothèque unifiée pour travailler avec les exchanges crypto, prenant en charge plus de 100 exchanges
- Binance API Documentation - documentation des types d'ordres de Binance
- Bybit API v5 - documentation de Bybit, y compris les ordres par lots
- 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 - documents sur l'estimation de la position dans la file
- Trading Technologies (TT) - une plateforme professionnelle avec des types d'ordres avancés
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.