Ordertypen im algorithmischen Handel: Von der nachziehenden Limit-Order bis zu virtuellen Orders
Wenn ein Anfänger ein Börsenterminal öffnet, sieht er zwei Schaltflächen: "Kaufen" und "Verkaufen". Wenn ein Algo-Trader seine Codebasis öffnet, sieht er siebenundzwanzig Ordertypen, drei Abstraktionsebenen und einen Haufen Sonderfälle, die ihn dazu bringen, den Laptop zuzuklappen und lieber Gurken auf dem Bauernmarkt zu verkaufen. Aber Gurken lassen dich leider keine Funding-Rate-Arbitrage um 3:59 UTC fahren - also legen wir los.
In diesem Artikel gehen wir den gesamten Weg von einfachen Börsenorders bis zu synthetischen virtuellen Konstruktionen durch, die nur innerhalb deines Systems existieren und niemals im Orderbuch auftauchen. Erwarte TypeScript, Python, ein wenig Schmerz und ein bisschen Erleuchtung.
1. Standard-Börsenorders: Das Fundament, das du nicht überspringen kannst
Klassifizierung der Standard-Ordertypen: von Market bis Iceberg
Bevor wir etwas Komplexes bauen, müssen wir sicherstellen, dass wir die grundlegenden Bausteine richtig verstehen. Es ist erstaunlich, wie viele Menschen Stop-Limit und Stop-Market verwechseln und sich dann wundern, warum ihr Stop "nicht ausgelöst hat" (Spoiler: er hat ausgelöst, aber die Limit-Order wurde wegen Slippage nicht ausgeführt).
Market-Order
Der einfachste und zugleich gefährlichste Typ. Du sagst der Börse: "Kaufe/verkaufe genau jetzt, zu jedem verfügbaren Preis." Die Börse entnimmt dem Orderbuch Liquidität, beginnend beim besten Preis. Reicht das Volumen auf der besten Ebene nicht aus - rutscht sie weiter.
Wann einsetzen: Not-Ausstieg aus einer Position, Ausführung eines Signals, bei dem Geschwindigkeit wichtiger ist als der Preis.
Fallstricke: In einem dünnen Markt kann eine Market-Order über 100 BTC den Preis um mehrere Prozent bewegen. Backtests, die Market-Orders ohne Berücksichtigung des Market Impact modellieren, sind reine Fantasie.
Limit-Order
Du gibst einen exakten Preis an. Die Order gelangt ins Orderbuch und wartet, bis jemand deinem Preis zustimmt. Liegt der Preis einer Limit-Kauforder über dem aktuellen Markt - wird sie sofort ausgeführt (wie eine Market-Order, aber mit garantiertem Höchstpreis).
Kernpunkt: Eine Limit-Order garantiert keine Ausführung. Der Preis kann dein Niveau erreichen und umkehren, sodass du in der Warteschlange sitzen bleibst (mehr dazu in unserem Artikel über die Warteschlangenposition).
Stop-Market und Stop-Limit
Hier beginnt die Verwirrung. Beide Typen sind "schlafende" Orders, die aktiviert werden, wenn der Auslösepreis (Stop-Preis) erreicht wird. Aber:
- Stop-Market: wandelt sich beim Auslösen in eine Market-Order um. Garantiert Ausführung, aber nicht den Preis.
- Stop-Limit: wandelt sich beim Auslösen in eine Limit-Order um. Garantiert den Preis (nicht schlechter als angegeben), aber nicht die Ausführung.
Im volatilen Krypto-Markt kann eine Stop-Limit "verfehlen" - der Preis durchbricht den Stop, die Limit-Order wird platziert, aber der Markt ist bereits vorbeigeflogen. Du bleibst mit einer nicht ausgeführten Limit-Order und einem wachsenden Verlust zurück. Genau deshalb wird für Stop-Losses häufiger Stop-Market verwendet.
Trailing-Stop
Ein Stop, der dem Preis in festgelegtem Abstand "folgt". Preis steigt - der Stop wandert nach oben. Preis fällt - der Stop bleibt stehen. Nützlich, um Gewinne in trendfolgenden Strategien zu schützen.
Börsenunterstützung: Nicht alle Börsen unterstützen native Trailing-Stops. Algo-Trader implementieren sie oft programmatisch - das gibt mehr Kontrolle über die Parameter (Callback-Rate, Aktivierungspreis, Schrittweite).
Iceberg-Order
Eine Order, bei der nur ein Bruchteil des Gesamtvolumens im Orderbuch sichtbar ist. Du willst 1.000 BTC kaufen, zeigst aber nur 10 im Buch. Wenn die ersten 10 ausgeführt sind - erscheinen die nächsten 10.
Warum: um deine wahren Absichten vor dem Markt zu verbergen. Eine große Order im Buch signalisiert allen, dass "jemand Großes kaufen/verkaufen will". Als Reaktion beginnen HFT-Algorithmen mit Front-Running, und der Preis entfernt sich von dir.
Vorbehalt: Auf vielen Krypto-Börsen werden Iceberg-Orders entweder nicht unterstützt oder anhand des Musters identischer Volumina leicht erkannt. Fortgeschrittene Algorithmen randomisieren die Größe des sichtbaren Teils.
Time-in-Force-Parameter: GTC, GTD, IOC, FOK
Das sind keine eigenen Ordertypen, sondern Time-in-Force-Parameter - wie lange eine Order lebt:
| Parameter | Vollständiger Name | Verhalten |
|---|---|---|
| GTC | Good Till Cancelled | Lebt bis zur Stornierung. Der Standard-Default |
| GTD | Good Till Date | Lebt bis zu einem festgelegten Datum/Zeitpunkt |
| IOC | Immediate or Cancel | Wird sofort ausgeführt (ganz oder teilweise), der Rest wird storniert |
| FOK | Fill or Kill | Wird nur vollständig und sofort ausgeführt. Ist das unmöglich - wird sie komplett storniert |
IOC vs FOK: Der Unterschied ist entscheidend. IOC kann teilweise ausgeführt werden - du wolltest 100 BTC kaufen, hast 3 gekauft, der Rest wurde storniert. FOK ist entweder 100 oder nichts.
Post-only (Maker-only)
Eine Order, die garantiert als Maker ins Orderbuch gelangt und niemals als Taker ausgeführt wird. Würde der Preis zum Zeitpunkt der Platzierung eine sofortige Ausführung verursachen - lehnt die Börse sie ab (oder passt den Preis an, je nach Börse).
Warum: Maker-Gebühren sind in der Regel niedriger als Taker-Gebühren (bei Binance - 0,02 % vs 0,04 % für VIP-Stufen). Für einen Market Maker, der täglich Tausende Orders platziert, ist der Gebührenunterschied der Unterschied zwischen Gewinn und Verlust.
2. TWAP und VWAP: Wie Institutionen einen Elefanten im Orderbuch verstecken
Wenn ein Hedgefonds eine Position von 50 Mio. $ kaufen will, platziert er keine einzelne Market-Order. Er nutzt Ausführungsalgorithmen - Algorithmen, die eine große Order in viele kleinere aufteilen und über die Zeit ausführen, um den Market Impact zu minimieren.
TWAP (Time-Weighted Average Price)
Die Idee ist denkbar einfach: das Gesamtvolumen in gleiche Teile aufteilen und in gleichen Zeitintervallen ausführen.
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 ist intelligenter: Es berücksichtigt das typische Handelsvolumenprofil. Wenn typischerweise 30 % des Tagesvolumens zwischen 9:00 und 10:00 gehandelt werden, führt VWAP 30 % der Order in diesem Fenster aus. Ziel ist es, den durchschnittlichen Ausführungspreis so nah wie möglich an den Markt-VWAP zu bringen.
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)
Unterschied TWAP vs VWAP: TWAP ist einfacher und vorhersehbarer. VWAP liefert einen besseren Durchschnittspreis, erfordert aber ein zuverlässiges Volumenprofil. Auf dem Krypto-Markt, wo Volumina durch Wash-Trading verfälscht sein können, muss das VWAP-Profil sorgfältig aufgebaut werden.
3. Limit mit Nachziehen: Wenn deine Order dem Preis hinterherjagt
Nachziehende Limit-Order: Die Order jagt einem sich bewegenden Preis mit konfigurierbarer Aggressivität hinterher
Jetzt wird es richtig interessant. Eine Standard-Limit-Order ist ein passives Objekt: Sie liegt im Orderbuch und wartet. Bewegte sich der Preis - bleibt die Order unausgeführt. Für einen Algo-Trader ist das oft inakzeptabel: Das Einstiegssignal feuerte, aber die Position wurde nicht aufgebaut, weil sich der Markt um 0,1 % bewegte.
Eine nachziehende Limit-Order ist ein programmatischer Wrapper um eine Limit-Order, der:
- Eine Limit-Order zum aktuellen besten Preis platziert (oder mit einem kleinen Versatz)
- Den Preis über WebSocket überwacht
- Wenn sich der Preis von der Order entfernt - sie storniert und näher am aktuellen Preis neu platziert
- Dies wiederholt, bis die Order ausgeführt ist oder die zulässige Abweichung überschritten wird
Wichtige Parameter
- chase_interval_ms - wie oft die Order geprüft und neu platziert wird. 100 ms - aggressiv, 1000 ms - entspannt.
- max_chase_distance - maximale Abweichung vom Ausgangspreis, bevor die Order storniert wird. Schutz vor dem Nachjagen eines davonlaufenden Marktes.
- aggression_level - wie nah am Marktpreis die Limit-Order platziert wird.
0- beim besten Bid/Ask (passiv),1- Überkreuzen des Spreads (aggressiv, faktisch ein Taker). - chase_on_partial - ob das Nachziehen fortgesetzt wird, wenn die Order teilweise ausgeführt ist.
TypeScript-Implementierung
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));
}
}
Wann Nachziehen schädlich ist
Nachziehen ist ein mächtiges Werkzeug, das sich aber leicht in einen Verlustgenerator verwandeln lässt:
- Cancel/Replace-Spam. Jedes Stornieren und Neuplatzieren belastet die API. Börsen begrenzen Anfragen per Rate-Limit, und aggressives Nachziehen kann deinen API-Schlüssel sperren lassen.
- Adverse Selection. Wenn der Preis vor dir davonläuft - weiß der Markt vielleicht etwas, das du nicht weißt. Dem Preis in dieser Situation hinterherzujagen bedeutet, am Höchststand zu kaufen.
- Übergang von Maker zu Taker. Bei hoher Aggressivität zahlst du faktisch Taker-Gebühren, aber mit Verzögerung (Cancel + neue Order). Manchmal ist es einfacher, gleich eine Market-Order zu platzieren.
4. Zeitbasierte Orders: Präzision im Millisekundenbereich
Es gibt Situationen, in denen du eine Order nicht "zum Preis X", sondern "zur Zeit T" ausführen musst. Klingt seltsam? Es ist tatsächlich eine ganze Klasse von Strategien.
Anwendungsfälle
Funding-Rate-Arbitrage. Bei Perpetual-Futures wird das Funding alle 8 Stunden gezahlt (00:00, 08:00, 16:00 UTC bei Binance). Beträgt die Funding-Rate = +0,1 %, musst du im Moment der Abrechnung short sein. Strategie: einige Sekunden vor der Abrechnung einen Short eröffnen, das Funding kassieren, die Position schließen. Das Timing ist entscheidend - eine Sekunde Verzögerung bedeutet verpasstes Funding.
Session-Eröffnungen/-Schließungen. Auf traditionellen Märkten und einigen Krypto-Derivaten gibt es feste Sessions. Die Eröffnungsauktion (NYSE, CME) ist der Moment, in dem die Liquidität ihren Höhepunkt erreicht. Eine Order 100 ms vor der Auktion zu platzieren, ist ein Edge.
Nachrichtenbasierte Ausführung. Inflationsdaten werden zu einem festgelegten Zeitpunkt veröffentlicht. Der Algorithmus parst die Zahl aus einem News-Feed und platziert innerhalb von 50 ms eine Order. Hier wird zeitbasierte Ausführung mit ereignisgesteuerter Logik kombiniert.
Implementierung
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);
Wichtiger Vorbehalt: Die Präzision einer zeitbasierten Order wird nicht durch deinen Code begrenzt, sondern durch die Netzwerklatenz zur Börse. Beträgt dein Ping zur API 50 ms, wird selbst ein perfektes Busy-Wait ein Delta von 50 ms haben. Für ernsthaftes HFT wird Co-Location verwendet - der Server steht physisch direkt neben der Matching-Engine der Börse.
5. Virtuelle/synthetische Orders: Die Unsichtbaren in deinem System
Virtuelle Orders: Orders existieren nur im Speicher des Bots, bis der Trigger auslöst
Dies ist vielleicht das am meisten unterschätzte Werkzeug im Arsenal des Algo-Traders. Eine virtuelle Order (auch synthetische Order genannt) ist eine Order, die nur in deinem System existiert. Sie wird nicht an die Börse gesendet, bis eine Auslösebedingung erfüllt ist (typischerweise - der Preis erreicht ein bestimmtes Niveau).
Wie es funktioniert
- Dein Algorithmus entscheidet: "Ich will BTC bei 40.000 $ kaufen"
- Statt eine Limit-Order an die Börse zu senden, erstellt er eine virtuelle Order im Speicher
- Abonniert den WebSocket-Preisstrom
- Wenn Bid/Ask 40.000 $ erreicht - sendet er eine echte Market- oder Limit-Order an die Börse
Warum virtuelle Orders wichtig sind
Kein Informationsleck. Deine Order ist im Orderbuch unsichtbar. Niemand - weder andere Trader noch HFT-Algorithmen noch die Börse selbst - kennt deine Absichten bis zum Moment der Ausführung. Das verschiebt das Kräfteverhältnis grundlegend.
Front-Running-Schutz. Auf Krypto-Börsen, besonders auf weniger transparenten, besteht begründeter Verdacht, dass Informationen über große Limit-Orders für Front-Running genutzt werden können (dazu gibt es sogar Studien). Virtuelle Orders eliminieren dieses Risiko.
Grid-Bots. Ein klassischer Grid-Bot platziert ein Gitter aus 50-200 Orders auf verschiedenen Preisniveaus. Wenn du sie alle an die Börse sendest - sind das 200 Orders im Buch, die: (a) für alle sichtbar sind, (b) das Order-Limit der Börse aufbrauchen (typischerweise 200-300 offene Orders pro Konto), (c) wenn sich der Preis stark bewegt, alle ausgeführt werden und du mit einer riesigen Position endest. Virtuelle Orders lösen alle drei Probleme.
Fallende Messer fangen. Strategie: virtuelle Kauforders auf den Niveaus -5 %, -10 %, -15 % unter dem aktuellen Preis platzieren. Fällt der Markt - werden die Orders schrittweise ausgelöst. Fällt er nicht - riskierst du nichts und belegst keine Order-Slots der Börse.
TypeScript-Implementierung
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
);
}
}
Fallstricke virtueller Orders
-
Latenzlücke. Zwischen dem Moment, in dem du den Preis siehst, und dem Moment, in dem die echte Order die Börse erreicht, vergeht Zeit. In einem volatilen Markt kann der Preis in diesen 20-100 ms davonfliegen. Lösung: eine leicht aggressive Limit-Order senden (mit Puffer).
-
Verpasste Ausführungen. Wenn der Preis dein Niveau in einem einzigen Tick "durchstach" (Flash Crash) und zurückprallte - reagierst du möglicherweise nicht rechtzeitig. Eine reguläre Limit-Order im Buch wäre ausgeführt worden; eine virtuelle - nicht.
-
State-Management. Virtuelle Orders leben im Speicher. Stürzt der Prozess ab - sind die Orders verloren. Lösung: persistenter Speicher (Redis, SQLite, Datei) mit Wiederherstellung beim Neustart.
6. Bedingte/Smart-Orders: Order-Kombinatorik
Wenn eine einzelne Order nicht ausreicht, kombinieren Trader sie zu bedingten Konstruktionen. Einige werden nativ an Börsen unterstützt, andere programmatisch implementiert.
OCO (One Cancels Other)
Zwei Orders sind verknüpft: Wenn eine ausgeführt wird - wird die andere automatisch storniert. Klassisches Beispiel: Du bist in einer Long-Position und willst sowohl einen Take-Profit als auch einen Stop-Loss setzen. Was zuerst auslöst - das andere muss storniert werden.
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
Eine dreiteilige Konstruktion: eine primäre Einstiegsorder + OCO für den Ausstieg (Take-Profit + Stop-Loss). Im Grunde ein vollständiger Trade-Lebenszyklus in einem einzigen Aufruf:
- Einstieg: Limit-Kauforder
- Take-Profit: Limit-Verkaufsorder (darüber)
- Stop-Loss: Stop-Market-Verkaufsorder (darunter)
Wenn der Einstieg ausgeführt wird, werden TP und SL automatisch platziert. Wenn eine der beiden ausgeführt wird - wird die andere storniert.
If-Then-Logik
Die flexibelste Option - Order-Ketten mit beliebigen Bedingungen:
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},
},
]
}
]
Solche Konstruktionen werden von keiner Börse nativ unterstützt - nur programmatische Implementierung. Das ist einer der Gründe, warum Algotrading-Systeme unweigerlich ihre eigene Order-Management-Schicht entwickeln.
7. Wie Market Maker spezialisierte Ordertypen nutzen
Market Making ist ein eigenes Universum, und das Order-Toolkit passt dazu. Die Aufgabe eines Market Makers ist es, kontinuierlich Bid und Ask zu quotieren, am Spread zu verdienen und dabei Adverse Selection zu minimieren (die Situation, in der ein informierter Trader gegen dich handelt).
Post-only als Must-Have
Für einen Market Maker ist Post-only keine Option - es ist eine Anforderung. Wenn deine Order versehentlich als Taker ausgeführt wird - zahlst du statt eines Maker-Rebates eine Taker-Gebühr. Über Tausende Orders pro Tag ist das katastrophal.
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
Auf einigen Börsen (Kraken, Bitfinex) sind Hidden-Orders verfügbar - sie erscheinen nicht im Orderbuch, sind aber an der Börse und nehmen am Matching teil. Der Kompromiss: Du zahlst Taker-Gebühren auch als Maker, gewinnst aber Anonymität.
Für einen Market Maker ist das ein Werkzeug für das Inventory-Management: Hat sich eine große Position angesammelt, kannst du eine Hidden-Order platzieren, um sie aufzulösen, ohne deine Absicht dem Markt preiszugeben.
Pegged-Orders
Eine an das beste Bid/Ask gekoppelte Order. Bei Coinbase Advanced Trade kannst du beispielsweise eine Order platzieren, die automatisch dem besten Bid folgt und immer an der Spitze der Warteschlange sitzt. Das ist eine native nachziehende Order auf Börsenebene - aber sie ist bei weitem nicht universell verfügbar.
Bulk-Order-Management
Professionelle Market Maker nutzen Batch-APIs, um Dutzende Orders in einer einzigen HTTP-Anfrage gleichzeitig zu stornieren und zu platzieren. Bei Binance ist das batchOrders, bei Bybit - place-batch-order. Das reduziert Latenz und Rate-Limit-Druck.
8. Vergleichstabelle der Ordertypen
| Ordertyp | Ausführungsgarantie | Preisgarantie | Sichtbar im Buch | Nativ an Börsen | Implementierungsaufwand |
|---|---|---|---|---|---|
| Market | Ja | Nein | Nein (sofort) | Ja | Keiner |
| Limit | Nein | Ja | Ja | Ja | Keiner |
| Stop-Market | Ja (nach Auslösung) | Nein | Nein | Ja | Keiner |
| Stop-Limit | Nein | Ja | Nein (bis zur Auslösung) | Ja | Keiner |
| Trailing-Stop | Ja (nach Auslösung) | Nein | Nein | Teilweise | Gering |
| Iceberg | Nein | Ja | Teilweise | Teilweise | Mittel |
| Post-only | Nein | Ja | Ja | Ja | Keiner |
| TWAP | Nein (abhängig von Slices) | Nein | Teilweise | Nein | Mittel |
| VWAP | Nein | Nein | Teilweise | Nein | Hoch |
| Nachziehende Limit | Höher als Limit | Teilweise | Ja (aktuelle Order) | Nein | Mittel |
| Zeitbasiert | Abhängig vom Typ | Abhängig vom Typ | Nein (bis Zeit T) | Nein | Gering |
| Virtuell/Synthetisch | Niedriger als Limit | Abhängig vom Typ | Nein | Nein | Mittel |
| OCO | Ja (eine von zwei) | Teilweise | Ja (beide) | Teilweise | Mittel |
| Bracket | Ja | Teilweise | Ja | Selten | Hoch |
| Hidden | Nein | Ja | Nein | Selten | Keiner |
| Pegged | Nein | Dynamisch | Ja | Sehr selten | Hoch (falls programmatisch) |
Fazit: Die Order als Baustein der Strategie
Ordertypen sind nicht nur "Schaltflächen in einer Oberfläche". Sie sind fundamentale Primitive, aus denen die Ausführungsschicht jedes Handelssystems aufgebaut wird. Der Unterschied zwischen "Strategie ist im Backtesting profitabel" und "Strategie ist in Produktion profitabel" liegt oft genau hier - darin, wie genau du Orders an die Börse sendest.
Ein paar praktische Erkenntnisse:
- Beginne mit Standard-Orders, stelle sicher, dass du die Feinheiten verstehst (Stop-Limit vs Stop-Market, IOC vs FOK). Die meisten Fehler passieren hier.
- Virtuelle Orders sind ein Must-Have für Grid-Bots. Wenn du mehr als 50 Orders platzierst - sende sie nicht alle an die Börse.
- Nachziehen wird gebraucht, wenn die Fill-Rate wichtiger ist als der Preis. Aber setze immer max_chase_distance - sonst kannst du sehr weit abdriften.
- Zeitbasierte Ausführung ist eine Nische, aber mächtig für Funding-Arb und ereignisgesteuerte Strategien.
- Eine eigene Order-Management-Schicht ist für jedes ernsthafte Algotrading-System unvermeidlich. Börsennative Ordertypen reichen nicht aus.
Wenn du ein Handelssystem baust und tiefer einsteigen willst - schau dir unsere Artikel über die Warteschlangenposition im Orderbuch, WebSocket-Methoden in CCXT und Funding-Rate-Arbitrage an.
Referenzen und Quellen
- CCXT Library - eine einheitliche Bibliothek für die Arbeit mit Krypto-Börsen, die 100+ Börsen unterstützt
- Binance API Documentation - Dokumentation der Binance-Ordertypen
- Bybit API v5 - Bybit-Dokumentation, einschließlich 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 - Materialien zur Schätzung der Warteschlangenposition
- Trading Technologies (TT) - eine professionelle Plattform mit fortgeschrittenen Ordertypen
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.