Drücken Sie ESC, um zu schließen

Trading-Bots für Krypto-Profite Richten Nutzen (2026)

Trading-Automatisierung

Schauen wir uns die Mechanik der Trading-Automatisierung ohne Illusionen an: Welche Bot-Typen werfen im Krypto-Markt tatsächlich Rendite ab, wo lauern versteckte Risiken und wie schreibt man die Logik dahinter?

1. Strategie-Architektur: Was in der Praxis wirklich funktioniert

Nur drei Haupttypen automatisierter Systeme überleben auf dem Markt. Der Rest ist reines Exchange-Marketing, um Handelsgebühren einzusammeln.

  • Grid Trading (Raster-Trading): Ideal für Seitwärtsmärkte (Ranges). Der Bot platziert eine Leiter aus Limit-Kaufaufträgen unter dem aktuellen Preis und Limit-Verkaufsaufträgen darüber. Er verdient an den Schwankungen innerhalb dieses Korridors.
  • Cost-Averaging (DCA-Bots): Eine reine Akkumulationsstrategie. Der Bot kauft einen Vermögenswert in gleichen Teilen in festen zeitlichen Abständen oder bei Kursrücksetzern um einen definierten Prozentsatz. Läuft auf dem Spot-Markt für ein langfristiges Portfolio.
  • Arbitrage: Das Ausnutzen von Preisunterschieden zwischen Spot-Märkten verschiedener Börsen oder des Spreads zwischen Futures und Spot (Funding Rate Arbitrage). Erfordert gering Latenz (Low Latency) und optimierte APIs.

2. Die Mathematik hinter Grid-Bots: Warum 80 % ihr Kapital verbrennen

Die meisten starten ein Grid bei einem volatilen Altcoin auf dem absoluten Allzeithoch mit Standardeinstellungen. Das Ergebnis ist immer dasselbe: ein heftiger Ausbruch aus der Range nach unten, der Bot hat die gesamte Basiswährung zu Spottpreisen verkauft und das Portfolio sitzt auf einem "toten" Asset mit 60 % Drawdown.

Um das zu verhindern, basiert die Parametrisierung auf strengen Regeln:

Vergleich der Grid-Konfigurationsansätze:

  • Asset-Auswahl: Anfängerfehler — ein gehypter Memecoin mit wilder Volatilität. Profi-Ansatz — Top-20-Coins mit hoher Liquidität und starken Unterstützungslinien (BTC, ETH).
  • Grid-Typ: Anfängerfehler — arithmetisch (gleiche Dollar-Abstände). Profi-Ansatz — geometrisch (gleiche prozentuale Abstände, entscheidend für eine stabile Renditeprozentzahl).
  • Range (Spanne): Anfängerfehler — ein enger Korridor von 5 % zum aktuellen Preis. Profi-Ansatz — eine breite Spanne, die die historische Seitwärtsphase der letzten 3 bis 6 Monate abdeckt.

3. Praktische Umsetzung: Code des Grid-Trading-Moduls

Unten ist ein funktionierendes Python-Skriptfragment unter Verwendung der ccxt-Bibliothek zu sehen. Dies ist der Kern der Berechnungslogik zur Generierung von Grid-Orders mit geometrischen Schritten. In den Kommentaren erkläre ich, was was macht.

"""
Limit-Order-Grid-Planer für Binance Spot (Dry-Run-Modus).
Grid-Modell
-----------
Die Level L0 < L1 < ... < Ln bilden n Zellen [L_k, L_{k+1}].
In jeder Zelle befindet sich zu jedem Zeitpunkt genau ein aktiver Order:
- L_k < Preis -> BUY bei L_k. Nach Ausführung wird ein SELL bei L_{k+1} platziert
                 (abzüglich der Gebühr in der Basiswährung).
- L_k >= Preis -> SELL bei L_{k+1}. Hierfür wird ein anfänglicher Bestand an
                 Basiswährung benötigt (muss vorab zum Marktpreis gekauft werden).
Das Level, das dem aktuellen Preis entspricht, gehört zur SELL-Zelle (wird nicht übersprungen).
Das Skript sendet NIEMALS etwas an die Börse.
"""
from __future__ import annotations
import argparse
import logging
import sys
from dataclasses import dataclass
from decimal import (
    ROUND_CEILING,
    ROUND_FLOOR,
    ROUND_HALF_UP,
    Decimal,
    InvalidOperation,
    localcontext,
)
from typing import Any
import ccxt
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger("grid_planner")
ZERO = Decimal("0")
ONE = Decimal("1")
# --------------------------------------------------------------------------
# Decimal-Hilfsfunktionen
# --------------------------------------------------------------------------
def dec(value: Any, name: str) -> Decimal:
    try:
        result = Decimal(str(value))
    except (InvalidOperation, ValueError, TypeError) as exc:
        raise ValueError(f"{name}: ungültiger numerischer Wert") from exc
    if not result.is_finite():
        raise ValueError(f"{name}: Wert muss endlich sein")
    return result
def text(value: Decimal) -> str:
    """Decimal -> String ohne Exponent und überflüssige Nullen."""
    return format(value.normalize(), "f")
def floor_to_step(value: Decimal, step: Decimal) -> Decimal:
    return (value / step).to_integral_value(rounding=ROUND_FLOOR) * step
def ceil_to_step(value: Decimal, step: Decimal) -> Decimal:
    return (value / step).to_integral_value(rounding=ROUND_CEILING) * step
def round_to_step(value: Decimal, step: Decimal) -> Decimal:
    return (value / step).to_integral_value(rounding=ROUND_HALF_UP) * step
# --------------------------------------------------------------------------
# Binance-Spot-Instrumentenregeln (Filter)
# --------------------------------------------------------------------------
@dataclass(frozen=True)
class SymbolRules:
    tick: Decimal
    step: Decimal
    min_price: Decimal
    max_price: Decimal | None
    min_qty: Decimal
    max_qty: Decimal | None
    min_notional: Decimal
    max_notional: Decimal | None
    percent_general: tuple[Decimal, Decimal] | None  # (down, up)
    percent_by_side: dict[str, tuple[Decimal, Decimal]]
    # Mittelungsfenster für Prozentfilter; 0 = letzter Preis wird verwendet.
    avg_price_mins: int | None
    @property
    def has_percent_filters(self) -> bool:
        return self.percent_general is not None or bool(self.percent_by_side)
def _positive_or_none(value: Any, name: str) -> Decimal | None:
    """In Binance-Filtern bedeutet 0 „Einschränkung deaktiviert“."""
    if value is None:
        return None
    result = dec(value, name)
    return result if result > 0 else None
def load_rules(market: dict) -> SymbolRules:
    """Liest die Filter einmalig ein. Jedes Problem hier ist ein Konfigurationsfehler."""
    info = market.get("info") or {}
    status = info.get("status")
    if status is not None and status != "TRADING":
        raise RuntimeError(f"Paar-Status ist {status}, erwartet wurde TRADING")
    if info.get("isSpotTradingAllowed") is False:
        raise RuntimeError("Spot-Trading ist für dieses Paar deaktiviert")
    filters = {
        item.get("filterType"): item
        for item in info.get("filters", [])
        if item.get("filterType")
    }
    price_filter = filters.get("PRICE_FILTER")
    lot_filter = filters.get("LOT_SIZE")
    if not price_filter:
        raise RuntimeError("Binance PRICE_FILTER nicht gefunden")
    if not lot_filter:
        raise RuntimeError("Binance LOT_SIZE nicht gefunden")
    tick = dec(price_filter["tickSize"], "PRICE_FILTER.tickSize")
    step = dec(lot_filter["stepSize"], "LOT_SIZE.stepSize")
    if tick <= 0 or step <= 0:
        raise RuntimeError("tickSize und stepSize müssen größer als Null sein")
    min_notional = ZERO
    max_notional = None
    for filter_type in ("MIN_NOTIONAL", "NOTIONAL"):
        item = filters.get(filter_type)
        if not item:
            continue
        if item.get("minNotional") is not None:
            min_notional = max(
                min_notional,
                dec(item["minNotional"], f"{filter_type}.minNotional"),
            )
        if item.get("maxNotional") is not None:
            max_notional = _positive_or_none(
                item["maxNotional"], f"{filter_type}.maxNotional"
            )
    percent_general = None
    item = filters.get("PERCENT_PRICE")
    if item:
        percent_general = (
            dec(item["multiplierDown"], "PERCENT_PRICE.multiplierDown"),
            dec(item["multiplierUp"], "PERCENT_PRICE.multiplierUp"),
        )
    percent_by_side: dict[str, tuple[Decimal, Decimal]] = {}
    item = filters.get("PERCENT_PRICE_BY_SIDE")
    if item:
        percent_by_side["buy"] = (
            dec(item["bidMultiplierDown"], "bidMultiplierDown"),
            dec(item["bidMultiplierUp"], "bidMultiplierUp"),
        )
        percent_by_side["sell"] = (
            dec(item["askMultiplierDown"], "askMultiplierDown"),
            dec(item["askMultiplierUp"], "askMultiplierUp"),
        )
    avg_mins_values = set()
    for key in ("PERCENT_PRICE", "PERCENT_PRICE_BY_SIDE"):
        item = filters.get(key)
        if item and item.get("avgPriceMins") is not None:
            avg_mins_values.add(int(dec(item["avgPriceMins"], "avgPriceMins")))
    if len(avg_mins_values) > 1:
        raise RuntimeError(
            f"Unterschiedliche avgPriceMins in Filtern: {sorted(avg_mins_values)}"
        )
    avg_price_mins = avg_mins_values.pop() if avg_mins_values else None
    return SymbolRules(
        tick=tick,
        step=step,
        min_price=dec(price_filter.get("minPrice", "0"), "minPrice"),
        max_price=_positive_or_none(price_filter.get("maxPrice"), "maxPrice"),
        min_qty=dec(lot_filter.get("minQty", "0"), "LOT_SIZE.minQty"),
        max_qty=_positive_or_none(lot_filter.get("maxQty"), "LOT_SIZE.maxQty"),
        min_notional=min_notional,
        max_notional=max_notional,
        percent_general=percent_general,
        percent_by_side=percent_by_side,
        avg_price_mins=avg_price_mins,
    )
# --------------------------------------------------------------------------
# Grid
# --------------------------------------------------------------------------
def align_bounds(
    lower: Decimal, upper: Decimal, tick: Decimal
) -> tuple[Decimal, Decimal]:
    """Die Grenzen werden nach innen an den Bereich angepasst."""
    lower = ceil_to_step(lower, tick)
    upper = floor_to_step(upper, tick)
    if lower >= upper:
        raise ValueError("Bereich ist nach Ausrichtung an tickSize leer")
    return lower, upper
def calculate_grid_levels(
    lower_bound: Decimal,
    upper_bound: Decimal,
    grid_count: int,
    tick: Decimal,
) -> list[Decimal]:
    """Geometrisches Grid: grid_count Intervalle, grid_count + 1 Level."""
    if not isinstance(grid_count, int) or isinstance(grid_count, bool):
        raise ValueError("grid_count muss eine Ganzzahl sein")
    if grid_count < 1:
        raise ValueError("grid_count muss mindestens 1 sein")
    if min(lower_bound, upper_bound, tick) <= 0:
        raise ValueError("Grenzen und tickSize müssen größer als Null sein")
    if lower_bound >= upper_bound:
        raise ValueError("lower_bound muss kleiner als upper_bound sein")
    lower, upper = align_bounds(lower_bound, upper_bound, tick)
    with localcontext() as ctx:
        ctx.prec = 50
        ratio = (upper / lower) ** (ONE / Decimal(grid_count))
        # Jedes Level wird von der unteren Grenze aus berechnet: Keine Fehlerkumulierung.
        levels = [
            round_to_step(lower * (ratio ** i), tick)
            for i in range(grid_count + 1)
        ]
    levels[0] = lower
    levels[-1] = upper
    if any(a >= b for a, b in zip(levels, levels[1:])):
        raise ValueError(
            "Level haben sich nach Tick-Rundung überschneidet oder Reihenfolge verletzt. "
            "Reduzieren Sie grid_count oder erweitern Sie den Bereich."
        )
    return levels
def check_grid_economics(
    levels: list[Decimal],
    fee_rate: Decimal,
    extra_cost_rate: Decimal,
) -> None:
    """
    Approximativer Filter: Die Nettorendite einer Zelle unter Berücksichtigung
    der Gebühren an beiden Belegen muss den Kostenpuffer übersteigen.
    Dies ist keine Rentabilitätsgarantie.
    """
    if fee_rate < 0 or extra_cost_rate < 0:
        raise ValueError("Gebühr und Kostenpuffer dürfen nicht < 0 sein")
    keep = (ONE - fee_rate) ** 2
    for low, high in zip(levels, levels[1:]):
        net_return = (high / low) * keep - ONE
        if net_return <= extra_cost_rate:
            raise ValueError(
                f"Grid-Schrittweite zu klein: {text(low)} -> {text(high)}; "
                f"Nettorendite {net_return:.4%}, "
                f"erforderlich ist mehr als {extra_cost_rate:.4%}"
            )
# --------------------------------------------------------------------------
# Order-Validierung anhand der Börsenfilter
# --------------------------------------------------------------------------
def validate_order(
    rules: SymbolRules,
    side: str,
    price: Decimal,
    amount: Decimal,
    avg_price: Decimal | None = None,
) -> Decimal:
    """Gibt den Nominalwert des Orders zurück oder wirft ValueError."""
    if side not in ("buy", "sell"):
        raise ValueError("side muss buy oder sell sein")
    if price <= 0 or amount <= 0:
        raise ValueError(
            f"{side.upper()} @ {text(price)}: Preis und Menge müssen > 0 sein "
            f"(amount={text(amount)})"
        )
    if price % rules.tick != 0:
        raise ValueError(f"Preis {text(price)} ist kein Vielfaches von tickSize")
    if amount % rules.step != 0:
        raise ValueError(f"Menge {text(amount)} ist kein Vielfaches von stepSize")
    if price < rules.min_price:
        raise ValueError(f"Preis {text(price)} < minPrice {text(rules.min_price)}")
    if rules.max_price is not None and price > rules.max_price:
        raise ValueError(f"Preis {text(price)} > maxPrice {text(rules.max_price)}")
    if amount < rules.min_qty:
        raise ValueError(
            f"Menge {text(amount)} < minQty {text(rules.min_qty)}"
        )
    if rules.max_qty is not None and amount > rules.max_qty:
        raise ValueError(
            f"Menge {text(amount)} > maxQty {text(rules.max_qty)}"
        )
    cost = price * amount
    if cost < rules.min_notional:
        raise ValueError(
            f"Nominalwert {text(cost)} < minNotional {text(rules.min_notional)}"
        )
    if rules.max_notional is not None and cost > rules.max_notional:
        raise ValueError(
            f"Nominalwert {text(cost)} > maxNotional {text(rules.max_notional)}"
        )
    if avg_price is not None:
        bounds = []
        if rules.percent_general:
            bounds.append(("PERCENT_PRICE", *rules.percent_general))
        if side in rules.percent_by_side:
            bounds.append(("PERCENT_PRICE_BY_SIDE", *rules.percent_by_side[side]))
        for name, down, up in bounds:
            minimum, maximum = avg_price * down, avg_price * up
            if not minimum <= price <= maximum:
                raise ValueError(
                    f"{name}: Preis {text(price)} außerhalb des Bereichs "
                    f"{text(minimum)}–{text(maximum)}"
                )
    return cost
# --------------------------------------------------------------------------
# Grid-Plan
# --------------------------------------------------------------------------
def build_grid_plan(
    levels: list[Decimal],
    current_price: Decimal,
    rules: SymbolRules,
    budget_per_level: Decimal,
    fee_rate: Decimal,
    fee_in_base: bool,
    avg_price: Decimal | None,
) -> tuple[list[dict], list[str]]:
    """
    Erstellt Orders pro Zelle. Gibt (Orders, übersprungene_Zellen) zurück.
    Die Gebühr in der Basiswährung reduziert die Menge des korrespondierenden SELLs.
    """
    if budget_per_level <= 0:
        raise ValueError("Budget pro Level muss größer als Null sein")
    base_fee = fee_rate if fee_in_base else ZERO
    orders: list[dict] = []
    skipped: list[str] = []
    for k, (low, high) in enumerate(zip(levels, levels[1:])):
        try:
            if low < current_price:
                # BUY-Zelle: Kaufen bei low, Verkaufen bei high.
                amount = floor_to_step(budget_per_level / low, rules.step)
                cost = validate_order(rules, "buy", low, amount, avg_price)
                sell_amount = floor_to_step(amount * (ONE - base_fee), rules.step)
                # Das korrespondierende SELL wird später platziert; der Prozentfilter
                # wird dann direkt von der Börse überprüft, weshalb hier kein avg_price übergeben wird.
                validate_order(rules, "sell", high, sell_amount)
                orders.append({
                    "cell": k,
                    "side": "buy",
                    "price": low,
                    "amount": amount,
                    "notional": cost,
                    "on_fill_sell_price": high,
                    "on_fill_sell_amount": sell_amount,
                })
            else:
                # SELL-Zelle: Die Menge wird direkt vom SELL-Preis aus berechnet,
                # damit der Nominalwert das Level-Budget nicht überschreitet.
                amount = floor_to_step(budget_per_level / high, rules.step)
                sell_cost = validate_order(
                    rules, "sell", high, amount, avg_price
                )
                orders.append({
                    "cell": k,
                    "side": "sell",
                    "price": high,
                    "amount": amount,
                    "notional": sell_cost,
                })
        except ValueError as exc:
            skipped.append(f"Zelle {k} [{text(low)}–{text(high)}]: {exc}")
    return orders, skipped
def summarize_plan(
    orders: list[dict],
    fee_rate: Decimal,
    taker_fee_rate: Decimal,
    fee_in_base: bool,
    ask: Decimal,
    slippage: Decimal,
    bnb_price: Decimal | None,
) -> dict:
    """
    Abschätzung des Kapitalbedarfs. Dies ist eine SCHÄTZUNG mit Slippage-Puffer,
    keine Garantie: Tatsächlicher Preis und Gebühren beim initialen Kauf können abweichen.
    """
    buys = [o for o in orders if o["side"] == "buy"]
    sells = [o for o in orders if o["side"] == "sell"]
    # Ein BUY-Order zieht maximal den Nominalwert (notional) ab: Die Gebühr wird
    # vom erhaltenen Asset (BTC) oder von BNB abgezogen, niemals von USDT.
    quote_for_buys = sum((o["notional"] for o in buys), ZERO)
    base_net_needed = sum((o["amount"] for o in sells), ZERO)
    # Initialer Marktkauf (Taker). Wenn die Gebühr in der Basiswährung anfällt,
    # muss mehr gekauft werden, damit nach Gebühren genau base_net_needed übrig bleibt.
    taker_base_fee = taker_fee_rate if fee_in_base else ZERO
    base_gross = base_net_needed / (ONE - taker_base_fee)
    initial_notional = base_gross * ask * (ONE + slippage)
    bnb_fee_quote = ZERO
    bnb_needed = None
    if not fee_in_base:
        # Limit-Orders (BUY, korrespondierende und initiale SELLs) werden als Maker gerechnet,
        # der initiale Marktkauf als Taker. Ein Limit-Order, der das Orderbuch
        # sofort kreuzt, wird als Taker ausgeführt: Diese Schätzung erfasst dies nicht.
        buy_leg = sum((o["notional"] for o in buys), ZERO)
        paired_sell_leg = sum(
            (o["on_fill_sell_price"] * o["on_fill_sell_amount"] for o in buys),
            ZERO,
        )
        sell_leg = sum((o["notional"] for o in sells), ZERO)
        bnb_fee_quote = (
            (buy_leg + paired_sell_leg + sell_leg) * fee_rate
            + initial_notional * taker_fee_rate
        )
        if bnb_price is not None:
            bnb_needed = bnb_fee_quote / bnb_price
    return {
        "buy_orders": len(buys),
        "sell_orders": len(sells),
        "quote_for_buys": quote_for_buys,
        "base_needed_for_sells": base_net_needed,
        "initial_buy_cost_estimate": initial_notional,
        "total_quote_estimate": quote_for_buys + initial_notional,
        "bnb_fee_quote_equivalent": bnb_fee_quote,
        "bnb_needed": bnb_needed,
    }
# --------------------------------------------------------------------------
# Börsen-Integration
# --------------------------------------------------------------------------
def restrict_to_spot(exchange) -> None:
    """Weist ccxt an, nur Spot-Märkte zu laden (Optionsformatversionsabhängig)."""
    fetch_markets = exchange.options.get("fetchMarkets")
    if isinstance(fetch_markets, list):
        exchange.options["fetchMarkets"] = ["spot"]
    elif isinstance(fetch_markets, dict) and "types" in fetch_markets:
        exchange.options["fetchMarkets"] = {**fetch_markets, "types": ["spot"]}
def get_ticker_prices(
    exchange, symbol: str
) -> tuple[Decimal, Decimal, Decimal | None]:
    """Gibt (bid, ask, last) zurück."""
    ticker = exchange.fetch_ticker(symbol)
    if ticker.get("bid") is None or ticker.get("ask") is None:
        raise RuntimeError(f"{symbol}: Börse hat keine bid/ask zurückgegeben")
    bid = dec(ticker["bid"], "bid")
    ask = dec(ticker["ask"], "ask")
    if bid <= 0 or ask <= 0 or bid > ask:
        raise RuntimeError(f"{symbol}: ungültige bid/ask {bid}/{ask}")
    last = ticker.get("last")
    return bid, ask, (dec(last, "last") if last is not None else None)
def fetch_percent_reference_price(
    exchange, market: dict, rules: SymbolRules, last: Decimal | None
) -> Decimal:
    """
    Referenzpreis für PERCENT_PRICE*. Reproduziert die Binance-Regel:
    avgPriceMins == 0 -> letzter Preis, andernfalls Durchschnitt der letzten avgPriceMins Minuten.
    Das von /avgPrice zurückgegebene Fenster wird mit dem Filter-Fenster abgeglichen.
    Die finale Validierung erfolgt ohnehin durch die Börse bei der Orderaufgabe.
    """
    if rules.avg_price_mins == 0:
        if last is None:
            raise RuntimeError("Für den Filter wird ein Last-Preis benötigt, aber die Börse hat keinen geliefert")
        return last
    response = exchange.publicGetAvgPrice({"symbol": market["id"]})
    price = dec(response["price"], "avgPrice")
    if price <= 0:
        raise RuntimeError("Binance hat einen ungültigen avgPrice zurückgegeben")
    mins = response.get("mins")
    if (
        rules.avg_price_mins is not None
        and mins is not None
        and int(mins) != rules.avg_price_mins
    ):
        raise RuntimeError(
            f"avgPrice-Fenster ({mins} Min.) stimmt nicht mit avgPriceMins "
            f"des Filters ({rules.avg_price_mins} Min.) überein: Überprüfung kann nicht repliziert werden"
        )
    return price
def resolve_rate(
    market: dict,
    key: str,
    explicit: Decimal | None,
    fee_in_bnb: bool,
) -> Decimal:
    """
    Gebührensatz: Explizit angegeben oder vom Markt (maker/taker), standardmäßig 0.1%.
    Ein BNB-Rabatt wird NICHT vorausgesetzt: Der tatsächliche Kontosatz
    muss explizit übergeben werden, da die Schätzung sonst konservativ (ohne Rabatt) bleibt.
    """
    if explicit is not None:
        return explicit
    value = market.get(key)
    rate = dec(value, key) if value is not None else Decimal("0.001")
    if fee_in_bnb:
        logger.warning(
            "Satz %s nicht explizit angegeben: Verwende %s ohne BNB-Rabatt. "
            "Übergeben Sie den tatsächlichen Kontosatz über "
            "--fee-rate / --taker-fee-rate.",
            key, rate,
        )
    return rate
def paired_sell_amount(
    executed_qty: Decimal,
    fills: list[dict],
    base_asset: str,
    free_balance: Decimal,
    price: Decimal,
    rules: SymbolRules,
) -> Decimal:
    """
    Für zukünftige Ausführungsmodule: Menge des korrespondierenden SELLs nach
    der tatsächlichen Ausführung eines BUY.
    executed_qty — BRUTTO-Kaufmenge (executedQty der Order, vor Gebührenabzug).
    fills        — Order-Ausführungen im ccxt-Format: Jedes enthält
                   fill["fee"] = {"cost": ..., "currency": ...} oder None.
    Es werden nur Gebühren abgezogen, die in der Basiswährung erhoben wurden: Eine Gebühr in BNB
    oder in der Kurswährung verringert die Asset-Menge nicht.
    free_balance — Verfügbares Guthaben der Basiswährung, das nicht für andere
                   Orders reserviert ist (dafür ist der aufrufende Code verantwortlich).
    """
    fee_in_base = ZERO
    for fill in fills:
        fee = fill.get("fee")
        if (
            fee
            and fee.get("currency") == base_asset
            and fee.get("cost") is not None
        ):
            fee_in_base += dec(fee["cost"], "fee.cost")
    net = executed_qty - fee_in_base
    if net <= 0:
        raise ValueError("Menge nach Gebührenabzug nicht positiv")
    amount = floor_to_step(min(net, free_balance), rules.step)
    validate_order(rules, "sell", price, amount)
    return amount
# --------------------------------------------------------------------------
# Hauptprogramm (main)
# --------------------------------------------------------------------------
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    parser.add_argument("--symbol", default="BTC/USDT")
    parser.add_argument("--lower", default="50000")
    parser.add_argument("--upper", default="70000")
    parser.add_argument("--grid-count", type=int, default=20)
    parser.add_argument(
        "--budget-per-level",
        default="100",
        help="Maximaler Nominalwert eines einzelnen Orders in Kurswährung",
    )
    parser.add_argument(
        "--fee-rate", default=None,
        help="Maker-Gebühr (Anteil), standardmäßig Maker-Gebühr des Marktes",
    )
    parser.add_argument(
        "--taker-fee-rate", default=None,
        help="Taker-Gebühr für den initialen Kauf, standardmäßig Taker-Gebühr des Marktes",
    )
    parser.add_argument(
        "--fee-in-bnb", action="store_true",
        help="Gebühr wird in BNB statt im empfangenen Asset bezahlt",
    )
    parser.add_argument("--extra-cost-rate", default="0.0005")
    parser.add_argument(
        "--slippage", default="0.002",
        help="Slippage-Puffer für den initialen Kauf (Anteil)",
    )
    return parser.parse_args(argv)
def optional_rate(value: str | None, name: str) -> Decimal | None:
    if value is None:
        return None
    result = dec(value, name)
    if not ZERO <= result < ONE:
        raise ValueError(f"{name} muss im Bereich [0, 1) liegen")
    return result
def run(args: argparse.Namespace) -> None:
    symbol = args.symbol
    lower_bound = dec(args.lower, "lower")
    upper_bound = dec(args.upper, "upper")
    budget = dec(args.budget_per_level, "budget_per_level")
    extra_cost_rate = dec(args.extra_cost_rate, "extra_cost_rate")
    slippage = optional_rate(args.slippage, "slippage")
    explicit_fee = optional_rate(args.fee_rate, "fee_rate")
    explicit_taker = optional_rate(args.taker_fee_rate, "taker_fee_rate")
    exchange = ccxt.binance({
        "enableRateLimit": True,
        "timeout": 15000,
        "options": {"defaultType": "spot"},
    })
    def money(value: Decimal) -> str:
        return text(value.quantize(Decimal("0.01")))
    try:
        restrict_to_spot(exchange)
        exchange.load_markets()
        if symbol not in exchange.markets:
            raise ValueError(f"Handelspaar nicht gefunden: {symbol}")
        market = exchange.market(symbol)
        if market.get("spot") is False:
            raise RuntimeError(f"{symbol} ist kein Spot-Markt")
        if market.get("active") is False:
            raise RuntimeError(f"Handelspaar ist inaktiv: {symbol}")
        rules = load_rules(market)
        fee_in_base = not args.fee_in_bnb
        fee_rate = resolve_rate(market, "maker", explicit_fee, args.fee_in_bnb)
        taker_rate = resolve_rate(
            market, "taker", explicit_taker, args.fee_in_bnb
        )
        bid, ask, last = get_ticker_prices(exchange, symbol)
        mid = (bid + ask) / Decimal("2")
        levels = calculate_grid_levels(
            lower_bound, upper_bound, args.grid_count, rules.tick
        )
        if not levels[0] <= mid <= levels[-1]:
            raise ValueError(
                f"Aktueller Preis {text(mid)} außerhalb des Grid-Bereichs "
                f"{text(levels[0])}–{text(levels[-1])}"
            )
        check_grid_economics(levels, fee_rate, extra_cost_rate)
        avg_price = (
            fetch_percent_reference_price(exchange, market, rules, last)
            if rules.has_percent_filters
            else None
        )
        orders, skipped = build_grid_plan(
            levels=levels,
            current_price=mid,
            rules=rules,
            budget_per_level=budget,
            fee_rate=fee_rate,
            fee_in_base=fee_in_base,
            avg_price=avg_price,
        )
        for message in skipped:
            logger.warning("Übersprungen: %s", message)
        if not orders:
            raise RuntimeError("Kein Order hat die Validierung bestanden")
        bnb_price = None
        if args.fee_in_bnb:
            bnb_symbol = f"BNB/{market['quote']}"
            if bnb_symbol in exchange.markets:
                b, a, _ = get_ticker_prices(exchange, bnb_symbol)
                bnb_price = (b + a) / Decimal("2")
            else:
                logger.warning("Paar %s nicht gefunden: BNB-Verbrauch kann nicht geschätzt werden", bnb_symbol)
        summary = summarize_plan(
            orders, fee_rate, taker_rate, fee_in_base, ask, slippage, bnb_price
        )
        print(f"Paar:        {symbol}")
        print(f"Bid / Ask:   {text(bid)} / {text(ask)}  (mid {text(mid)})")
        print(f"Tick / Step: {text(rules.tick)} / {text(rules.step)}")
        print(f"Grid:        {text(levels[0])} – {text(levels[-1])}, "
              f"Zellen: {len(levels) - 1}")
        print(f"Gebühren:    Maker {fee_rate:.4%}, Taker {taker_rate:.4%} "
              f"({'in erhaltenem Asset' if fee_in_base else 'in BNB'})")
        if avg_price is not None:
            print(f"Filter-Referenzpreis: {text(avg_price)} "
                  f"(avgPriceMins={rules.avg_price_mins})")
        print()
        for order in orders:
            line = (
                f"{order['side'].upper():4} {text(order['amount'])} "
                f"@ {text(order['price'])}  (~{money(order['notional'])})"
            )
            if order["side"] == "buy":
                line += (
                    f"  -> nach Ausführung SELL "
                    f"{text(order['on_fill_sell_amount'])} "
                    f"@ {text(order['on_fill_sell_price'])}"
                )
            print(line)
        quote, base = market["quote"], market["base"]
        print()
        print(f"BUY-Orders:  {summary['buy_orders']}, "
              f"benötigt ~{money(summary['quote_for_buys'])} {quote}")
        print(f"SELL-Orders: {summary['sell_orders']}, "
              f"benötigter Vorrat {text(summary['base_needed_for_sells'])} {base}")
        print(f"Initialer Kauf ~{money(summary['initial_buy_cost_estimate'])} "
              f"{quote} (zum Ask, Taker-Gebühr, Slippage {slippage:.2%})")
        print(f"Gesamt ~{money(summary['total_quote_estimate'])} {quote} "
              f"(Schätzung, keine Garantie für ausreichendes Guthaben)")
        if not fee_in_base:
            line = (f"Gebühren in BNB: ~{money(summary['bnb_fee_quote_equivalent'])} "
                    f"{quote} im Äquivalent")
            if summary["bnb_needed"] is not None:
                line += f", ~{summary['bnb_needed']:.4f} BNB"
            print(line + " (vollständiger Lebenszyklus aller Orders)")
        print()
        print("Dry-Run-Modus: Es wurden KEINE Orders an die Börse gesendet.")
    finally:
        exchange.close()
def main(argv: list[str] | None = None) -> int:
    args = parse_args(argv)
    try:
        run(args)
    except ccxt.BaseError as exc:
        logger.error("Börsen-/Netzwerkfehler: %s", exc)
        return 1
    except (ValueError, RuntimeError) as exc:
        logger.error("Berechnungsfehler: %s", exc)
        return 1
    return 0
if __name__ == "__main__":
    sys.exit(main())

4. Versteckte Risiken und Automatisierungsfallen, über die Exchanges schweigen

Exchange-Marketer lieben es, schicke Bot-Renditekurven basierend auf historischen Backtests zu präsentieren. In der Realität stößt der algorithmische Handel jedoch auf technische Hürden, die das Guthaben schneller verbrennen können als jeder menschliche Fehlklick.

  • Slippage bei hoher Volatilität: Wenn der Markt ein Support-Level durchbricht, trocknet das Orderbuch innerhalb von Millisekunden aus. Der Bot versucht, eine Market-Order zu platzieren oder auszuführen, doch der Deal wird zum Worst-Case-Preis gefüllt. Bei illiquiden Altcoins kann Slippage den gesamten Grid-Wochengewinn in einem einzigen Impuls auffressen.
  • Gebührentoxizität (Maker vs. Taker): Ein Grid baut auf Limit-Orders auf — das ist Maker-Territory, wo Exchanges oft Rebates zahlen oder minimale Gebühren berechnen. Doch wenn die Bot-Logik in einen Notverkauf per Market-Order kippt (Stop-Loss oder Panik bei einem Breakout), greifen saftige Taker-Gebühren, die die Strategie-Mathematik komplett zerstören.
  • Die Backtest-Illusion: Historische Daten berücksichtigen weder die reale Netzwerklatenz (Ping zur Exchange-API) noch die Matching-Engine-Warteschlangen. Ein Bot, der in historischen 2024–2025er Tests +300 % abgeliefert hat, kann im Live-Betrieb in einem Seitwärtsmarkt allein durch Ausführungsverzögerungen ins Minus rutschen.

5. Kapital absichern: Die Risikomanagement-Checkliste für Bots

Man kann keinen Algorithmus starten und einfach in den Urlaub fahren. Automatisierung erfordert eine rigorose Monitoring-Infrastruktur:

Sicherheits- und Kontrollregeln:

  • Eiserner Kill-Switch: Im Code oder in den Terminal-Einstellungen muss ein absoluter Stop-Loss für das gesamte Bot-Kapital hinterlegt sein. Sinkt der Gesamtwert aus Basis- und Quote-Währung beispielsweise um 15 %, zieht der Bot alle Limit-Orders und realisiert den Verlust.
  • Kapitalisolation: Bewahren Sie das Handelskapital des Bots niemals auf demselben Subkonto wie Ihre langfristigen Hauptreserven auf. Nutzen Sie isolierte API-Keys mit deaktivierter Auszahlungsfunktion — ausschließlich Trading ist erlaubt.
  • Monitoring via Telegram-Alerts: Richten Sie den Versand kritischer Benachrichtigungen nicht über die Exchange-Oberfläche ein, sondern direkt in den Messenger. Jede abgebrochene Session, jeder API-Fehler und jede ausgeführte Notfall-Order muss sofort gemeldet werden.

Fazit

Ein Trading-Bot ist das Skalpell in der Hand eines Chirurgen, kein Autopilot im Flugzeug. Er automatisiert die Disziplin, ersetzt aber kein Gehirn. Beginnen Sie mit kleinen Summen auf Spot-Grids bei liquiden Instrumenten, testen Sie die Logik unter realen Latenzbedingungen und denken Sie daran: Der größte Feind des Profits ist nicht der Markt, sondern Ihre eigenen Einstellungen ohne Risikomanagement.

Die EXMON Academy erinnert: Eine verlässliche Infrastruktur und strikte Risikokontrolle stehen immer an erster Stelle. Wer gewinnt, ist derjenige, der weiß, wann er den Algorithmus stoppen muss.

4. Versteckte Risiken und Automatisierungsfallen, über die Exchanges schweigen

Exchange-Marketer lieben es, schicke Bot-Renditekurven basierend auf historischen Backtests zu präsentieren. In der Realität stößt der algorithmische Handel jedoch auf technische Hürden, die das Guthaben schneller verbrennen können als jeder menschliche Fehlklick.

  • Slippage bei hoher Volatilität: Wenn der Markt ein Support-Level durchbricht, trocknet das Orderbuch innerhalb von Millisekunden aus. Der Bot versucht, eine Market-Order zu platzieren oder auszuführen, doch der Deal wird zum Worst-Case-Preis gefüllt. Bei illiquiden Altcoins kann Slippage den gesamten Grid-Wochengewinn in einem einzigen Impuls auffressen.
  • Gebührentoxizität (Maker vs. Taker): Ein Grid baut auf Limit-Orders auf — das ist Maker-Territory, wo Exchanges oft Rebates zahlen oder minimale Gebühren berechnen. Doch wenn die Bot-Logik in einen Notverkauf per Market-Order kippt (Stop-Loss oder Panik bei einem Breakout), greifen saftige Taker-Gebühren, die die Strategie-Mathematik komplett zerstören.
  • Die Backtest-Illusion: Historische Daten berücksichtigen weder die reale Netzwerklatenz (Ping zur Exchange-API) noch die Matching-Engine-Warteschlangen. Ein Bot, der in historischen 2024–2025er Tests +300 % abgeliefert hat, kann im Live-Betrieb in einem Seitwärtsmarkt allein durch Ausführungsverzögerungen ins Minus rutschen.

5. Kapital absichern: Die Risikomanagement-Checkliste für Bots

Man kann keinen Algorithmus starten und einfach in den Urlaub fahren. Automatisierung erfordert eine rigorose Monitoring-Infrastruktur:

Sicherheits- und Kontrollregeln:

  • Eiserner Kill-Switch: Im Code oder in den Terminal-Einstellungen muss ein absoluter Stop-Loss für das gesamte Bot-Kapital hinterlegt sein. Sinkt der Gesamtwert aus Basis- und Quote-Währung beispielsweise um 15 %, zieht der Bot alle Limit-Orders und realisiert den Verlust.
  • Kapitalisolation: Bewahren Sie das Handelskapital des Bots niemals auf demselben Subkonto wie Ihre langfristigen Hauptreserven auf. Nutzen Sie isolierte API-Keys mit deaktivierter Auszahlungsfunktion — ausschließlich Trading ist erlaubt.
  • Monitoring via Telegram-Alerts: Richten Sie den Versand kritischer Benachrichtigungen nicht über die Exchange-Oberfläche ein, sondern direkt in den Messenger. Jede abgebrochene Session, jeder API-Fehler und jede ausgeführte Notfall-Order muss sofort gemeldet werden.

Fazit

Ein Trading-Bot ist das Skalpell in der Hand eines Chirurgen, kein Autopilot im Flugzeug. Er automatisiert die Disziplin, ersetzt aber kein Gehirn. Beginnen Sie mit kleinen Summen auf Spot-Grids bei liquiden Instrumenten, testen Sie die Logik unter realen Latenzbedingungen und denken Sie daran: Der größte Feind des Profits ist nicht der Markt, sondern Ihre eigenen Einstellungen ohne Risikomanagement.

Die EXMON Academy erinnert: Eine verlässliche Infrastruktur und strikte Risikokontrolle stehen immer an erster Stelle. Wer gewinnt, ist derjenige, der weiß, wann er den Algorithmus stoppen muss.

Diesen Blogbeitrag zusammenfassen mit:

FAQ

Algorithmatische Handelssysteme erzielen Gewinne durch die Ausnutzung kurzfristiger Marktineffizienzen, die Ausführung von Grid-Parametern und die Automatisierung von DCA-Strategien ohne emotionale Fehlentscheidungen. Die Netto-Profitabilität hängt direkt von striktem Risikomanagement, latenzarmen REST- und WebSocket-API-Verbindungen sowie der Wahl richtiger Ordertypen ab, um Taker-Gebühren und Slippage bei hoher Volatilität zu minimieren.

Ein dynamischer Ausbruch aus einer Konsolidierungsspanne stellt das primäre Verlustszenario für Grid-Algorithmen dar. Durchbricht der Kurs die definierten Support- oder Resistance-Grenzen, akkumuliert die Ausführungs-Engine fortlaufend Verlustpositionen in einem fallenden Asset oder veräußert Bestände zu früh, was ohne harte Stop-Loss-Orders und Trailing-Parameter zu massiven Depoteinbußen führt.

Eine hohe Ausführungsfrequenz schmälert die Nettomarge zugunsten der Matching-Engine der Börse durch Taker-Gebührenstrukturen, breite Bid-Ask-Spreads und negative Slippage. Quantitative Handelsstrategien müssen den erwarteten mathematischen Vorteil stets nach Abzug von Maker/Taker-Raten, Netzwerklatenzen und Funding-Rates bei Derivatekontrakten berechnen, um einen negativen Erwartungswert zu vermeiden.
Martyn Borkowski

I am a crypto trader specializing in digital assets and blockchain markets.

My focus is on identifying opportunities, managing risk, and optimizing strategies to achieve consistent growth in the fast-evolving world of cryptocurrency.

Verification & Professional Profiles: X Profile

...

Diskussion beitreten

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *