Ordertypes in Algorithmic Trading: van Limit met Chasing tot Virtuele Orders
Wanneer een beginner een exchange-terminal opent, ziet hij twee knoppen: "Kopen" en "Verkopen". Wanneer een algo-trader zijn codebase opent, ziet hij zevenentwintig ordertypes, drie abstractieniveaus en een stapel edge cases die hem doen verlangen naar het dichtklappen van de laptop en het verkopen van komkommers op de boerenmarkt. Maar komkommers laten je helaas geen funding rate arbitrage om 3:59 UTC uitvoeren — dus laten we erin duiken.
In dit artikel doorlopen we de hele reis van basis exchange-orders tot synthetische virtuele constructies die alleen binnen jouw systeem bestaan en nooit in het orderboek verschijnen. Verwacht TypeScript, Python, wat pijn en een beetje verlichting.
1. Standaard Exchange-Orders: de Basis die Je Niet Kunt Overslaan
Classificatie van standaard ordertypes: van market tot iceberg
Voordat we iets complex bouwen, moeten we ervoor zorgen dat we de basisbouwstenen goed begrijpen. Het is verbazingwekkend hoeveel mensen stop-limit en stop-market door elkaar halen, en zich vervolgens afvragen waarom hun stop "niet triggerde" (spoiler: hij triggerde wel, maar de limit-order werd niet gevuld door slippage).
Market order
Het eenvoudigste en tegelijkertijd gevaarlijkste type. Je zegt tegen de exchange: "koop/verkoop nu, tegen elke beschikbare prijs." De exchange neemt liquiditeit uit het orderboek, beginnend bij de beste prijs. Als het volume op het beste niveau niet genoeg is — glijdt hij verder.
Wanneer gebruiken: noodmatige positie-uitgang, het uitvoeren van een signaal waarbij snelheid belangrijker is dan prijs.
Valkuilen: op een dunne markt kan een market order voor 100 BTC de prijs meerdere procenten bewegen. Backtests die market orders modelleren zonder rekening te houden met impact zijn pure fantasie.
Limit order
Je geeft een exacte prijs op. De order komt in het orderboek en wacht totdat iemand akkoord gaat met jouw prijs. Als de prijs van een limit-biedorder boven de huidige markt ligt — wordt deze onmiddellijk gevuld (zoals een market order, maar met een gegarandeerde maximumprijs).
Belangrijk punt: een limit order garandeert geen uitvoering. De prijs kan je niveau bereiken en omkeren, waardoor je in de wachtrij blijft zitten (meer hierover in ons artikel over wachtrijpositie).
Stop-market en Stop-limit
Hier begint de verwarring. Beide types zijn "slapende" orders die geactiveerd worden wanneer de trigger-prijs (stop price) wordt bereikt. Maar:
- Stop-market: bij het triggeren wordt deze omgezet naar een market order. Garandeert uitvoering, maar niet de prijs.
- Stop-limit: bij het triggeren wordt deze omgezet naar een limit order. Garandeert de prijs (niet slechter dan opgegeven), maar niet de uitvoering.
Op de volatiele cryptomarkt kan een stop-limit "missen" — de prijs blies door de stop heen, de limit order werd geplaatst, maar de markt is al voorbijgevlogen. Je blijft achter met een niet-gevulde limit order en een groeiend verlies. Precies daarom wordt stop-market vaker gebruikt voor stop-losses.
Trailing stop
Een stop die de prijs "volgt" op een ingestelde afstand. Prijs stijgt — de stop beweegt mee omhoog. Prijs daalt — de stop blijft op zijn plaats. Nuttig om winsten te beschermen in trendvolgende strategieën.
Exchange-ondersteuning: niet alle exchanges ondersteunen native trailing stops. Algo-traders implementeren deze vaak programmatisch — dit geeft meer controle over parameters (callback rate, activatieprijs, stapgrootte).
Iceberg order
Een order waarbij slechts een fractie van het totale volume zichtbaar is in het orderboek. Je wilt 1.000 BTC kopen, maar toont slechts 10 in het boek. Wanneer de eerste 10 gevuld zijn — verschijnen de volgende 10.
Waarom: om je werkelijke intenties voor de markt te verbergen. Een grote order in het boek signaleert aan iedereen dat "iemand groot wil kopen/verkopen." Als reactie beginnen HFT-algoritmes met front-running, en de prijs beweegt van je weg.
Kanttekening: op veel crypto-exchanges worden iceberg-orders óf niet ondersteund óf gemakkelijk gedetecteerd door het patroon van identieke volumes. Geavanceerde algoritmes randomiseren de grootte van het zichtbare deel.
Time-in-force parameters: GTC, GTD, IOC, FOK
Dit zijn geen aparte ordertypes maar time-in-force parameters — hoe lang een order leeft:
| Parameter | Volledige Naam | Gedrag |
|---|---|---|
| GTC | Good Till Cancelled | Leeft totdat geannuleerd. De standaard default |
| GTD | Good Till Date | Leeft tot een opgegeven datum/tijd |
| IOC | Immediate or Cancel | Voert onmiddellijk uit (volledig of gedeeltelijk), rest wordt geannuleerd |
| FOK | Fill or Kill | Voert alleen volledig en onmiddellijk uit. Indien onmogelijk — volledig geannuleerd |
IOC vs FOK: het verschil is cruciaal. IOC kan gedeeltelijk gevuld worden — je wilde 100 BTC kopen, kocht 3, de rest werd geannuleerd. FOK is alles of niets.
Post-only (Alleen Maker)
Een order die gegarandeerd als maker in het orderboek terechtkomt en nooit als taker wordt uitgevoerd. Als op het moment van plaatsen de prijs onmiddellijke uitvoering zou veroorzaken — wijst de exchange deze af (of past de prijs aan, afhankelijk van de exchange).
Waarom: maker-kosten zijn meestal lager dan taker-kosten (op Binance — 0,02% vs 0,04% voor VIP-niveaus). Voor een market maker die duizenden orders per dag plaatst, is het kostenverschil het verschil tussen winst en verlies.
2. TWAP en VWAP: Hoe Instituties een Olifant in het Orderboek Verbergen
Wanneer een hedgefonds een positie van $50 miljoen wil kopen, plaatst het geen enkele market order. Het gebruikt uitvoeringsalgoritmes — algoritmes die een grote order in veel kleinere orders opsplitsen en deze in de loop van de tijd uitvoeren, om de marktimpact te minimaliseren.
TWAP (Time-Weighted Average Price)
Het idee is doodeenvoudig: splits het totale volume in gelijke delen en voer uit op gelijke tijdsintervallen.
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 is slimmer: het houdt rekening met het typische handelsvolumeprofiel. Als 30% van het dagvolume doorgaans tussen 9:00 en 10:00 wordt verhandeld, zal VWAP 30% van de order uitvoeren tijdens dat venster. Het doel is om de gemiddelde uitvoeringsprijs zo dicht mogelijk bij de markt-VWAP te krijgen.
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)
Verschil TWAP vs VWAP: TWAP is eenvoudiger en voorspelbaarder. VWAP levert een betere gemiddelde prijs maar vereist een betrouwbaar volumeprofiel. Op de cryptomarkt, waar volumes wash-traded kunnen worden, moet het VWAP-profiel zorgvuldig worden opgebouwd.
3. Limit met Chasing: Wanneer Jouw Order de Prijs Kan Achtervolgen
Chasing limit: de order achtervolgt een bewegende prijs met configureerbare agressie
Nu wordt het echt interessant. Een standaard limit order is een passieve entiteit: hij zit in het orderboek en wacht. Als de prijs beweegt — blijft de order ongevuld. Voor een algo-trader is dit vaak onacceptabel: het instapsignaal ging af, maar de positie werd niet opgebouwd omdat de markt 0,1% bewoog.
Chasing limit order is een programmatische wrapper rond een limit order die:
- Een limit order plaatst tegen de huidige beste prijs (of met een kleine offset)
- De prijs monitort via WebSocket
- Als de prijs zich van de order verwijdert — annuleert en herplaatst dichter bij de huidige prijs
- Herhaalt totdat de order gevuld is of de toegestane afwijking overschrijdt
Belangrijkste Parameters
- chase_interval_ms — hoe vaak de order te controleren en te herplaatsen. 100ms — agressief, 1000ms — ontspannen.
- max_chase_distance — maximale afwijking van de initiële prijs voordat de order wordt geannuleerd. Bescherming tegen het achtervolgen van een weglopende markt.
- aggression_level — hoe dicht bij de marktprijs de limit order te plaatsen.
0— op de beste bid/ask (passief),1— de spread overschrijden (agressief, effectief een taker). - chase_on_partial — of het achtervolgen moet doorgaan als de order gedeeltelijk gevuld is.
TypeScript-implementatie
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));
}
}
Wanneer Chasing Schadelijk Is
Chasing is een krachtig hulpmiddel, maar het is gemakkelijk om het te veranderen in een verliesgenerator:
- Cancel/replace spam. Elke annulering en herplaatsing is een belasting voor de API. Exchanges beperken verzoeken (rate limit), en agressieve chasing kan ervoor zorgen dat je API-sleutel wordt verbannen.
- Adverse selectie. Als de prijs van je wegloopt — weet de markt misschien iets dat jij niet weet. De prijs achtervolgen in deze situatie betekent kopen op de top.
- Overgang van Maker naar Taker. Bij hoge agressie betaal je effectief taker-kosten, maar met vertraging (cancel + nieuwe order). Soms is het eenvoudiger om gewoon een market order te plaatsen.
4. Tijdgebaseerde Orders: Milliseconde-precisie
Er zijn situaties waarin je een order niet "tegen prijs X" maar "op tijdstip T" moet uitvoeren. Klinkt vreemd? Het is eigenlijk een hele klasse van strategieën.
Use cases
Funding rate arbitrage. Bij perpetual futures wordt funding elke 8 uur betaald (00:00, 08:00, 16:00 UTC op Binance). Als de funding rate = +0,1%, moet je short zijn op het moment van afwikkeling. Strategie: open een short een paar seconden voor afwikkeling, incasseer de funding, sluit de positie. Timing is cruciaal — een seconde vertraging betekent gemiste funding.
Sessieopeningen/-sluitingen. Op traditionele markten en sommige crypto-derivaten bestaan vaste sessies. De openingsveiling (NYSE, CME) is het moment waarop de liquiditeit op zijn hoogst is. Een order 100ms voor de veiling plaatsen is een voordeel.
Nieuwsgebaseerde uitvoering. Inflatiecijfers worden op een gepland tijdstip vrijgegeven. Het algoritme parseert het getal uit een nieuwsfeed en plaatst een order binnen 50ms. Hier wordt tijdgebaseerde uitvoering gecombineerd met event-driven logica.
Implementatie
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);
Belangrijke kanttekening: de precisie van een tijdgebaseerde order wordt niet beperkt door jouw code, maar door de netwerklatentie naar de exchange. Als jouw ping naar de API 50ms is, zal zelfs een perfecte busy-wait een delta van 50ms hebben. Voor serieuze HFT wordt co-locatie gebruikt — de server staat fysiek naast de matching engine van de exchange.
5. Virtuele/Synthetische Orders: de Onzichtbaren in Jouw Systeem
Virtuele orders: orders bestaan alleen in het geheugen van de bot totdat de trigger afgaat
Dit is misschien wel het meest onderschatte hulpmiddel in het arsenaal van de algo-trader. Een virtuele order (ook bekend als synthetische order) is een order die alleen in jouw systeem bestaat. Hij wordt niet naar de exchange gestuurd totdat aan een triggervoorwaarde is voldaan (doorgaans — het bereiken van een bepaald prijsniveau).
Hoe Het Werkt
- Jouw algoritme beslist: "Ik wil BTC kopen tegen $40.000"
- In plaats van een limit order naar de exchange te sturen, maakt het een virtuele order aan in het geheugen
- Het abonneert zich op de WebSocket-prijsstream
- Wanneer bid/ask $40.000 bereikt — stuurt het een echte market- of limit-order naar de exchange
Waarom Virtuele Orders Belangrijk Zijn
Geen informatielekkage. Jouw order is onzichtbaar in het orderboek. Niemand — geen andere traders, geen HFT-algoritmes, zelfs de exchange zelf niet — weet van jouw intenties tot het moment van uitvoering. Dit verschuift fundamenteel de machtsbalans.
Bescherming tegen front-running. Op crypto-exchanges, vooral minder transparante, bestaat een redelijk vermoeden dat informatie over grote limit-orders gebruikt kan worden voor front-running (er zijn zelfs studies hierover). Virtuele orders elimineren dit risico.
Grid bots. Een klassieke grid bot plaatst een raster van 50-200 orders op verschillende prijsniveaus. Als je ze allemaal naar de exchange stuurt — zijn dat 200 orders in het boek die: (a) voor iedereen zichtbaar zijn, (b) de orderlimiet op de exchange opgebruiken (doorgaans 200-300 open orders per account), (c) als de prijs sterk beweegt, worden ze allemaal gevuld en eindig je met een enorme positie. Virtuele orders lossen alle drie de problemen op.
Vallende messen opvangen. Strategie: plaats virtuele koop-orders op niveaus -5%, -10%, -15% onder de huidige prijs. Als de markt daalt — triggeren orders geleidelijk. Als hij niet daalt — riskeer je niets en gebruik je geen exchange-orderslots.
TypeScript-implementatie
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
);
}
}
Valkuilen van Virtuele Orders
-
Latentiegat. Tussen het moment dat je de prijs ziet en het moment dat de echte order de exchange bereikt, verstrijkt tijd. Op een volatiele markt kan de prijs in die 20-100ms wegvliegen. Oplossing: stuur een licht agressieve limit order (met een buffer).
-
Gemiste fills. Als de prijs je niveau "doorboorde" in één tick (flash crash) en terugveerde — reageer je mogelijk niet op tijd. Een gewone limit order die in het boek zit, zou gevuld zijn; een virtuele — niet.
-
Statusbeheer. Virtuele orders leven in het geheugen. Als het proces crasht — gaan orders verloren. Oplossing: persistente opslag (Redis, SQLite, bestand) met herstel bij herstart.
6. Conditionele/Slimme Orders: Ordercombinatoriek
Wanneer een enkele order niet voldoende is, combineren traders ze in conditionele constructies. Sommige worden native ondersteund op exchanges, andere worden programmatisch geïmplementeerd.
OCO (One Cancels Other)
Twee orders zijn gekoppeld: als de ene wordt uitgevoerd — wordt de andere automatisch geannuleerd. Klassiek voorbeeld: je hebt een longpositie en wilt zowel een take-profit als een stop-loss instellen. Welke er ook eerst triggert — de andere moet geannuleerd worden.
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)
Bracket order
Een constructie met drie componenten: een primaire instaporder + OCO voor de uitstap (take-profit + stop-loss). In wezen een compleet trade-levenscyclus in één enkele aanroep:
- Instap: limit koop-order
- Take-profit: limit verkoop-order (boven)
- Stop-loss: stop-market verkoop-order (onder)
Wanneer de instap wordt gevuld, worden TP en SL automatisch geplaatst. Wanneer een van beide wordt gevuld — wordt de andere geannuleerd.
If-Then Logica
De flexibelste optie — orderketens met willekeurige voorwaarden:
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},
},
]
}
]
Dergelijke constructies worden door geen enkele exchange native ondersteund — alleen programmatische implementatie. Dit is een van de redenen waarom algotrading-systemen onvermijdelijk hun eigen order-managementlaag ontwikkelen.
7. Hoe Market Makers Gespecialiseerde Ordertypes Gebruiken
Market making is een eigen universum, en het gereedschap voor orders past daarbij. Het werk van een market maker is om continu bid en ask te quoten, geld verdienen aan de spread, terwijl adverse selectie wordt geminimaliseerd (de situatie waarin een geïnformeerde trader tegen je handelt).
Post-only als Must-Have
Voor een market maker is post-only geen optie — het is een vereiste. Als jouw order per ongeluk wordt uitgevoerd als taker — in plaats van een maker-rebate te ontvangen, betaal je een taker-fee. Over duizenden orders per dag is dat catastrofaal.
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
Hidden orders
Op sommige exchanges (Kraken, Bitfinex) zijn verborgen orders beschikbaar — ze verschijnen niet in het orderboek maar staan wel op de exchange en nemen deel aan matching. De afweging: je betaalt taker-kosten zelfs als maker, maar je krijgt anonimiteit.
Voor een market maker is dit een hulpmiddel voor voorraadbeheer: als een grote positie is opgebouwd, kun je een verborgen order plaatsen om deze af te bouwen zonder jouw intentie aan de markt te onthullen.
Pegged orders
Een order die vastgezet is aan de beste bid/ask. Op Coinbase Advanced Trade, bijvoorbeeld, kun je een order plaatsen die automatisch de beste bid volgt en altijd vooraan in de wachtrij staat. Dit is een native chasing-order op exchange-niveau — maar verre van universeel beschikbaar.
Bulk order management
Professionele market makers gebruiken batch-API's om tientallen orders tegelijkertijd in één HTTP-verzoek te annuleren en te plaatsen. Op Binance is dit batchOrders, op Bybit — place-batch-order. Dit vermindert latentie en rate-limit-druk.
8. Vergelijkingstabel Ordertypes
| Ordertype | Uitvoeringsgarantie | Prijsgarantie | Zichtbaar in Boek | Native op Exchanges | Implementatiecomplexiteit |
|---|---|---|---|---|---|
| Market | Ja | Nee | Nee (direct) | Ja | Geen |
| Limit | Nee | Ja | Ja | Ja | Geen |
| Stop-market | Ja (na trigger) | Nee | Nee | Ja | Geen |
| Stop-limit | Nee | Ja | Nee (tot trigger) | Ja | Geen |
| Trailing stop | Ja (na trigger) | Nee | Nee | Gedeeltelijk | Laag |
| Iceberg | Nee | Ja | Gedeeltelijk | Gedeeltelijk | Middel |
| Post-only | Nee | Ja | Ja | Ja | Geen |
| TWAP | Nee (afhankelijk van slices) | Nee | Gedeeltelijk | Nee | Middel |
| VWAP | Nee | Nee | Gedeeltelijk | Nee | Hoog |
| Chasing limit | Hoger dan limit | Gedeeltelijk | Ja (huidige order) | Nee | Middel |
| Tijdgebaseerd | Afhankelijk van type | Afhankelijk van type | Nee (tot tijd T) | Nee | Laag |
| Virtueel/Synthetisch | Lager dan limit | Afhankelijk van type | Nee | Nee | Middel |
| OCO | Ja (een van twee) | Gedeeltelijk | Ja (beide) | Gedeeltelijk | Middel |
| Bracket | Ja | Gedeeltelijk | Ja | Zelden | Hoog |
| Hidden | Nee | Ja | Nee | Zelden | Geen |
| Pegged | Nee | Dynamisch | Ja | Zeer zelden | Hoog (indien programmatisch) |
Conclusie: De Order als Bouwsteen van Strategie
Ordertypes zijn niet zomaar "knoppen in een interface." Het zijn fundamentele primitieven waaruit de uitvoeringslaag van elk handelssysteem wordt opgebouwd. Het verschil tussen "strategie is winstgevend in backtesting" en "strategie is winstgevend in productie" ligt vaak precies hier — in hoe je orders exact naar de exchange stuurt.
Een paar praktische inzichten:
- Begin met standaardorders, zorg dat je de nuances begrijpt (stop-limit vs stop-market, IOC vs FOK). De meeste fouten gebeuren hier.
- Virtuele orders zijn een must-have voor grid bots. Als je meer dan 50 orders plaatst — stuur ze niet allemaal naar de exchange.
- Chasing is nodig wanneer fill rate belangrijker is dan prijs. Maar stel altijd max_chase_distance in — anders kun je ver afdrijven.
- Tijdgebaseerde uitvoering is niche maar krachtig voor funding arb en event-driven strategieën.
- Een custom order-managementlaag is onvermijdelijk voor elk serieus algotrading-systeem. Native exchange-ordertypes zijn niet genoeg.
Als je een handelssysteem bouwt en dieper wilt gaan — bekijk onze artikelen over wachtrijpositie in het orderboek, WebSocket-methodes in CCXT, en funding rate arbitrage.
Referenties en Bronnen
- CCXT Library — een uniforme bibliotheek voor het werken met crypto-exchanges, met ondersteuning voor 100+ exchanges
- Binance API Documentatie — documentatie van Binance-ordertypes
- Bybit API v5 — Bybit-documentatie, inclusief 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 — materiaal over het schatten van wachtrijposities
- Trading Technologies (TT) — een professioneel platform met geavanceerde ordertypes
Auteurs
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.