← Alle Insights

asyncio für Trading-Bots: Event-Loop, WebSockets, Order-Streams.

Ein Trading-Bot, der mehrere Märkte gleichzeitig beobachtet, auf WebSocket-Ticks reagiert und parallel Orders rausschickt, läuft sauber nur mit asyncio. Threads sind in Python zu teuer, Multiprocessing zu schwerfällig. Aber asyncio richtig zu nutzen ist anspruchsvoll — und ein einziger blockierender Aufruf killt den ganzen Bot.

Warum asyncio für Trading.

Trading-Workloads sind I/O-gebunden: Sie warten auf Marktdaten, Order-Confirmations, REST-Responses. Genau hier glänzt asyncio. Statt für jeden WebSocket einen Thread zu starten (und unter dem GIL keinen echten Parallelismus zu bekommen), läuft alles auf einer Event-Loop. Ein Single-Thread-Bot kann problemlos 20 Symbole parallel tracken — bei minimaler CPU-Last.

Wichtig: asyncio ist nicht schneller für CPU-Arbeit. Wer Indikatoren oder ML-Modelle on-the-fly berechnet, muss diese Last in einen Worker-Pool auslagern (siehe weiter unten). asyncio koordiniert nur das Warten — die Rechenarbeit muss anderswo passieren.

Grundstruktur eines async-Bots.

import asyncio
import websockets
import json

class TradingBot:
    def __init__(self, symbols):
        self.symbols = symbols
        self.prices = {}
        self.order_queue = asyncio.Queue()

    async def market_data_feed(self, symbol):
        url = f'wss://stream.exchange.com/ws/{symbol}@trade'
        async for ws in websockets.connect(url):
            try:
                async for msg in ws:
                    data = json.loads(msg)
                    self.prices[symbol] = float(data['p'])
                    await self.on_tick(symbol, data)
            except websockets.ConnectionClosed:
                continue  # Auto-Reconnect

    async def on_tick(self, symbol, data):
        # Strategie-Logik
        if self.should_trade(symbol):
            await self.order_queue.put({'symbol': symbol, 'side': 'buy'})

    async def order_worker(self):
        while True:
            order = await self.order_queue.get()
            await self.send_order(order)

    async def run(self):
        feeds = [self.market_data_feed(s) for s in self.symbols]
        await asyncio.gather(*feeds, self.order_worker())

bot = TradingBot(['BTCUSDT', 'ETHUSDT', 'SOLUSDT'])
asyncio.run(bot.run())

Diese Struktur trennt drei Verantwortlichkeiten: Datenempfang (pro Symbol ein Feed), Strategie-Logik (on_tick) und Order-Versand (separater Worker, der aus einer Queue konsumiert). Sauberer Code, klare Verantwortlichkeiten, und jedes Element kann unabhängig getestet werden.

Die häufigsten Fallen.

Falle 1: Blockierende Aufrufe

Der gefährlichste Fehler ist eine synchrone Funktion, die in einer Coroutine aufgerufen wird. time.sleep(1) blockiert den gesamten Event-Loop — während dieser Sekunde verarbeitet der Bot weder Marktdaten noch Orders. Genauso requests.get, pandas.read_sql oder ein synchroner File-Read.

Faustregel: in async-Code keine synchronen I/O-Bibliotheken. Stattdessen httpx oder aiohttp für HTTP, asyncpg oder aiomysql für Datenbanken, aiofiles für Files.

Falle 2: Unhandled Exceptions

Eine Exception in einem Task, die nicht awaitet wird, verschwindet still — der Task stoppt, der Rest läuft weiter, und Sie merken erst nach Stunden, dass ein Feed tot ist. Lösung: Tasks immer mit Exception-Handlern wrappen.

import asyncio
import logging

async def safe_task(coro, name):
    try:
        await coro
    except asyncio.CancelledError:
        raise
    except Exception as e:
        logging.exception(f'Task {name} crashed: {e}')
        # Optional: Alarm an Telegram / Pagerduty

async def main():
    tasks = [
        asyncio.create_task(safe_task(feed_btc(), 'btc')),
        asyncio.create_task(safe_task(feed_eth(), 'eth')),
        asyncio.create_task(safe_task(order_worker(), 'orders')),
    ]
    await asyncio.gather(*tasks)

Falle 3: CPU-Bound im Event-Loop

Indikator-Berechnung, ML-Inference, große DataFrame-Operationen — alles, was mehr als ein paar Millisekunden CPU braucht, blockiert den Loop. Bei einem Tick alle 10 ms ist eine 50-ms-Berechnung katastrophal. Lösung: asyncio.to_thread oder ein expliziter ProcessPoolExecutor.

import asyncio
from concurrent.futures import ProcessPoolExecutor

executor = ProcessPoolExecutor(max_workers=4)

def heavy_signal_calc(prices):
    # ML-Modell, große Rolling-Stats, etc.
    return signal

async def on_tick(symbol, tick):
    loop = asyncio.get_running_loop()
    signal = await loop.run_in_executor(
        executor, heavy_signal_calc, prices_history[symbol]
    )
    if signal > 0.7:
        await place_order(symbol)

WebSocket-Reconnects und Heartbeats.

Produktiv läuft kein WebSocket 24/7 ohne Disconnect. Netzwerk-Schluckauf, Server- Restarts, Exchange-Wartung — Reconnects sind die Norm. Die Standard-Bibliothek websockets kann das, wenn man den Generator-Modus nutzt (async for ws in websockets.connect(url)). Wichtig ist außerdem ein eigener Heartbeat: viele Exchanges schließen idle Connections nach 60 Sekunden.

Zweiter wichtiger Punkt: nach einem Reconnect ist Ihr lokaler Order-State möglicherweise veraltet. Best Practice ist ein Resync-Schritt nach jedem Reconnect, der die offenen Orders und Positionen vom Exchange neu abholt.

Backpressure und Queue-Limits.

Wenn der Markt verrückt spielt, kommen mehr Ticks rein, als Sie verarbeiten können. Eine unbegrenzte Queue füllt sich, der Speicher wächst, irgendwann crasht der Bot. asyncio.Queue(maxsize=N) setzt eine Obergrenze. Bei vollem Buffer können Sie ältere Ticks droppen, was für die meisten Trading-Strategien sinnvoller ist als veraltete Daten zu verarbeiten.

import asyncio

tick_queue = asyncio.Queue(maxsize=1000)

async def producer(ws):
    async for msg in ws:
        try:
            tick_queue.put_nowait(msg)
        except asyncio.QueueFull:
            # Drop oldest, put new
            try:
                tick_queue.get_nowait()
            except asyncio.QueueEmpty:
                pass
            tick_queue.put_nowait(msg)

Testing und Monitoring.

Async-Code ist schwerer zu testen als synchroner. pytest-asyncio hilft, aber die Disziplin ist eine andere: Sie müssen Mocks für WebSockets bauen, Timing- Effekte simulieren, Reconnect-Szenarien durchspielen. Eine Investition, die sich lohnt — ein Bug in einem Live-Trading-Bot kostet schnell vierstellig.

Im Produktivbetrieb sind zwei Metriken kritisch: Event-Loop-Lag (wie lange dauert es, bis ein Task scheduled wird?) und Queue-Tiefe (wie weit hinkt die Verarbeitung hinter den Inputs hinterher?). Beides sollte permanent geloggt werden — am besten nach Prometheus oder Influx.

Wann asyncio nicht passt.

Drei Fälle, in denen asyncio nicht der richtige Hammer ist: erstens reine Backtests — hier ist Vektorisierung in Pandas/Polars schneller; zweitens ultra-low-latency-Setups, wo Python überhaupt nicht mehr passt und Sie zu C++, Rust oder MQL5 wechseln müssen; drittens triviale Bots, die alle paar Minuten eine REST-API aufrufen — da reicht ein simpler Sync-Loop.

Fazit.

asyncio ist das richtige Werkzeug für Trading-Bots mittlerer Komplexität: mehrere Feeds, Multi-Asset, REST + WebSocket parallel. Die Lernkurve ist real — die Mental-Models von „Was darf ich awaiten?" und „Was blockiert den Loop?" brauchen Zeit. Aber wer einmal einen sauberen async-Bot gebaut hat, schreibt nie wieder Trading-Code mit Threads.

Sie planen einen Trading-Bot mit Multi-Asset-Anbindung? Erstgespräch buchen — wir bauen die Architektur, die nicht in der ersten Live-Woche umkippt.