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.