Appuyez sur ESC pour fermer

Comment Utiliser les Bots Krypto pour Générer des Profits (2026)

Automatisation du trading

Plongeons dans les mécanismes de l'automatisation du trading sans illusion : découvrons quels types de bots génèrent réellement des profits sur le marché crypto, où se cachent les risques invisibles et comment coder leur logique.

1. Architecture des stratégies : ce qui fonctionne vraiment en pratique

Seuls trois types principaux de systèmes automatisés survivent sur le marché. Le reste n'est que du marketing d'exchange conçu pour collecter des frais.

  • Grid Trading : Idéal pour les marchés en range (latéraux). Le bot place une grille d'ordres limites d'achat sous le prix actuel et de vente au-dessus, et engrange des profits grâce aux oscillations à l'intérieur du canal.
  • Dollar-Cost Averaging (Bots DCA) : Une stratégie d'accumulation. Le bot achète un actif par parts égales à intervalles réguliers ou lors d'une baisse de prix d'un pourcentage défini. Fonctionne sur le marché spot pour un portefeuille à long terme.
  • Arbitrage : Capturer les différences de prix entre les marchés spot de différentes plateformes ou le spread entre futures et spot (funding rate arbitrage). Requiert une faible latence (low latency) et une API optimisée.

2. Les mathématiques des Grid bots : pourquoi 80% des utilisateurs perdent leur dépôt

La plupart des gens lancent une grille sur le plus haut d'un altcoin volatil avec les paramètres par défaut. Le résultat est toujours le même : une cassure brutale du range à la baisse, le bot solde toute la devise de base à des prix dérisoires, et le portefeuille se retrouve lesté d'un actif « mort » affichant 60% de moins-value.

Pour éviter cela, le calcul des paramètres obéit à des règles strictes :

Comparaison des approches de configuration de grille :

  • Sélection d'actif : Erreur de débutant — un memecoin survendu à la volatilité folle. Approche pro — des cryptos du top 20 à forte liquidité et aux niveaux de support solides (BTC, ETH).
  • Type de grille : Erreur de débutant — arithmétique (pas de prix fixes en dollars). Approche pro — géométrique (pas en pourcentage égaux, essentiel pour un pourcentage de rendement stable).
  • Plage (Range) : Erreur de débutant — un couloir étroit à 5% du prix actuel. Approche pro — une large plage couvrant le range historique des 3 à 6 derniers mois.

3. Implémentation pratique : code du module de grid trading

Vous trouverez ci-dessous un extrait de script Python fonctionnel utilisant la bibliothèque ccxt. C'est le cœur de la logique de calcul pour générer des ordres de grille à pas géométrique. J'explique chaque partie dans les commentaires.

"""
Planificateur de grille d'ordres limit pour Binance Spot (mode simulation/dry-run).
Modèle de grille
--------------
Les niveaux L0 < L1 < ... < Ln forment n cellules [L_k, L_{k+1}].
Chaque cellule contient exactement un ordre actif à tout moment :
- L_k < prix  -> BUY à L_k. Une fois exécuté, un SELL est placé à L_{k+1}
                 pour la quantité reçue moins les frais en devise de base.
- L_k >= prix -> SELL à L_{k+1}. Nécessite un stock initial de devise de base
                 (doit être acheté au marché au préalable).
Le niveau égal au prix actuel appartient à la cellule SELL (non ignoré).
Le script n'envoie RIEN sur l'exchange.
"""
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")
# --------------------------------------------------------------------------
# Utilitaires Decimal
# --------------------------------------------------------------------------
def dec(value: Any, name: str) -> Decimal:
    try:
        result = Decimal(str(value))
    except (InvalidOperation, ValueError, TypeError) as exc:
        raise ValueError(f"{name}: valeur numérique invalide") from exc
    if not result.is_finite():
        raise ValueError(f"{name}: la valeur doit être finie")
    return result
def text(value: Decimal) -> str:
    """Decimal -> chaîne sans exposant ni zéros superflus."""
    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
# --------------------------------------------------------------------------
# Règles (filtres) de l'instrument Binance Spot
# --------------------------------------------------------------------------
@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]]
    # Fenêtre de moyenne pour les filtres de pourcentage ; 0 = utilise le dernier prix.
    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:
    """Dans les filtres Binance, 0 signifie 'restriction désactivée'."""
    if value is None:
        return None
    result = dec(value, name)
    return result if result > 0 else None
def load_rules(market: dict) -> SymbolRules:
    """Lit les filtres une seule fois. Tout problème ici est une erreur de configuration."""
    info = market.get("info") or {}
    status = info.get("status")
    if status is not None and status != "TRADING":
        raise RuntimeError(f"Statut de la paire {status}, attendu TRADING")
    if info.get("isSpotTradingAllowed") is False:
        raise RuntimeError("Le trading Spot est désactivé pour cette paire")
    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 introuvable")
    if not lot_filter:
        raise RuntimeError("Binance LOT_SIZE introuvable")
    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 et stepSize doivent être supérieurs à zéro")
    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"Valeurs avgPriceMins divergentes dans les filtres : {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,
    )
# --------------------------------------------------------------------------
# Grille
# --------------------------------------------------------------------------
def align_bounds(
    lower: Decimal, upper: Decimal, tick: Decimal
) -> tuple[Decimal, Decimal]:
    """Les bornes sont alignées vers l'intérieur de la plage."""
    lower = ceil_to_step(lower, tick)
    upper = floor_to_step(upper, tick)
    if lower >= upper:
        raise ValueError("La plage est vide après alignement sur le tickSize")
    return lower, upper
def calculate_grid_levels(
    lower_bound: Decimal,
    upper_bound: Decimal,
    grid_count: int,
    tick: Decimal,
) -> list[Decimal]:
    """Grille géométrique : grid_count intervalles, grid_count + 1 niveaux."""
    if not isinstance(grid_count, int) or isinstance(grid_count, bool):
        raise ValueError("grid_count doit être un nombre entier")
    if grid_count < 1:
        raise ValueError("grid_count doit être d'au moins 1")
    if min(lower_bound, upper_bound, tick) <= 0:
        raise ValueError("Les bornes et le tickSize doivent être supérieurs à zéro")
    if lower_bound >= upper_bound:
        raise ValueError("lower_bound doit être inférieur à upper_bound")
    lower, upper = align_bounds(lower_bound, upper_bound, tick)
    with localcontext() as ctx:
        ctx.prec = 50
        ratio = (upper / lower) ** (ONE / Decimal(grid_count))
        # Chaque niveau est calculé depuis la borne inférieure : l'erreur ne s'accumule pas.
        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(
            "Les niveaux se chevauchent ou l'ordre est rompu après arrondi au tick. "
            "Réduisez grid_count ou élargissez la plage."
        )
    return levels
def check_grid_economics(
    levels: list[Decimal],
    fee_rate: Decimal,
    extra_cost_rate: Decimal,
) -> None:
    """
    Filtre approximatif : le rendement net d'une cellule, frais inclus
    sur les deux jambes, doit dépasser la marge de frais.
    Ce n'est pas une garantie de rentabilité.
    """
    if fee_rate < 0 or extra_cost_rate < 0:
        raise ValueError("Les frais et la marge de coûts ne peuvent pas être < 0")
    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"Pas de grille trop serré : {text(low)} -> {text(high)} ; "
                f"rendement net {net_return:.4%}, "
                f"requis > {extra_cost_rate:.4%}"
            )
# --------------------------------------------------------------------------
# Validation d'ordre selon les filtres de l'exchange
# --------------------------------------------------------------------------
def validate_order(
    rules: SymbolRules,
    side: str,
    price: Decimal,
    amount: Decimal,
    avg_price: Decimal | None = None,
) -> Decimal:
    """Renvoie le montant notionnel de l'ordre ou lève une ValueError."""
    if side not in ("buy", "sell"):
        raise ValueError("side doit être buy ou sell")
    if price <= 0 or amount <= 0:
        raise ValueError(
            f"{side.upper()} @ {text(price)} : le prix et la quantité doivent être > 0 "
            f"(amount={text(amount)})"
        )
    if price % rules.tick != 0:
        raise ValueError(f"Le prix {text(price)} n'est pas un multiple du tickSize")
    if amount % rules.step != 0:
        raise ValueError(f"La quantité {text(amount)} n'est pas un multiple du stepSize")
    if price < rules.min_price:
        raise ValueError(f"Prix {text(price)} < minPrice {text(rules.min_price)}")
    if rules.max_price is not None and price > rules.max_price:
        raise ValueError(f"Prix {text(price)} > maxPrice {text(rules.max_price)}")
    if amount < rules.min_qty:
        raise ValueError(
            f"Quantité {text(amount)} < minQty {text(rules.min_qty)}"
        )
    if rules.max_qty is not None and amount > rules.max_qty:
        raise ValueError(
            f"Quantité {text(amount)} > maxQty {text(rules.max_qty)}"
        )
    cost = price * amount
    if cost < rules.min_notional:
        raise ValueError(
            f"Notionnel {text(cost)} < minNotional {text(rules.min_notional)}"
        )
    if rules.max_notional is not None and cost > rules.max_notional:
        raise ValueError(
            f"Notionnel {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} : prix {text(price)} hors limites "
                    f"{text(minimum)}–{text(maximum)}"
                )
    return cost
# --------------------------------------------------------------------------
# Plan de grille
# --------------------------------------------------------------------------
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]]:
    """
    Construit les ordres par cellule. Renvoie (ordres, cellules_ignorées).
    Les frais en devise de base réduisent la quantité du SELL associé.
    """
    if budget_per_level <= 0:
        raise ValueError("Le budget par niveau doit être supérieur à zéro")
    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:
                # Cellule BUY : achat à low, vente à 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)
                # Le SELL associé sera placé plus tard, le filtre de pourcentage
                # sera vérifié par l'exchange lui-même, donc on ne transmet pas avg_price ici.
                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:
                # Cellule SELL : la quantité est calculée à partir du prix du SELL lui-même,
                # pour que le notionnel ne dépasse pas le budget par niveau.
                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"cellule {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:
    """
    Estimation des besoins en capitaux. C'est une ESTIMATION avec marge de slippage,
    et non une garantie : le prix réel et les frais lors de l'achat initial
    peuvent différer.
    """
    buys = [o for o in orders if o["side"] == "buy"]
    sells = [o for o in orders if o["side"] == "sell"]
    # L'exécution d'un ordre BUY consomme au plus son notionnel : les frais sont prélevés
    # sur l'actif reçu (BTC) ou en BNB, jamais en USDT.
    quote_for_buys = sum((o["notional"] for o in buys), ZERO)
    base_net_needed = sum((o["amount"] for o in sells), ZERO)
    # Achat initial au marché (taker). Si les frais sont en base, il faut acheter
    # davantage pour qu'il reste base_net_needed après déduction des frais.
    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:
        # Les ordres limit (BUY, SELL associés et initiaux) utilisent le taux maker,
        # l'achat initial au marché utilise le taux taker. Un ordre limit qui traverse
        # immédiatement le carnet s'exécutera comme taker : cette estimation ne le détecte pas.
        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,
    }
# --------------------------------------------------------------------------
# Intégration de l'exchange
# --------------------------------------------------------------------------
def restrict_to_spot(exchange) -> None:
    """Demande à ccxt de charger uniquement les marchés spot (le format d'option dépend de la version)."""
    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]:
    """Renvoie (bid, ask, last)."""
    ticker = exchange.fetch_ticker(symbol)
    if ticker.get("bid") is None or ticker.get("ask") is None:
        raise RuntimeError(f"{symbol} : l'exchange n'a pas renvoyé de bid/ask")
    bid = dec(ticker["bid"], "bid")
    ask = dec(ticker["ask"], "ask")
    if bid <= 0 or ask <= 0 or bid > ask:
        raise RuntimeError(f"{symbol} : bid/ask invalides {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:
    """
    Prix de référence pour PERCENT_PRICE*. Reproduit la règle Binance :
    avgPriceMins == 0 -> dernier prix, sinon moyenne sur avgPriceMins minutes.
    La fenêtre renvoyée par /avgPrice est comparée à celle du filtre.
    La vérification finale sera de toute façon effectuée par l'exchange lors de l'envoi de l'ordre.
    """
    if rules.avg_price_mins == 0:
        if last is None:
            raise RuntimeError("Le filtre requiert le prix last, mais l'exchange ne l'a pas renvoyé")
        return last
    response = exchange.publicGetAvgPrice({"symbol": market["id"]})
    price = dec(response["price"], "avgPrice")
    if price <= 0:
        raise RuntimeError("Binance a renvoyé un avgPrice invalide")
    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"La fenêtre avgPrice ({mins} min) ne correspond pas à avgPriceMins "
            f"du filtre ({rules.avg_price_mins} min) : impossible de reproduire la vérification"
        )
    return price
def resolve_rate(
    market: dict,
    key: str,
    explicit: Decimal | None,
    fee_in_bnb: bool,
) -> Decimal:
    """
    Taux de frais : explicite, sinon issu du marché (maker/taker), sinon 0.1%.
    La réduction pour paiement en BNB n'est PAS supposée : le taux réel du compte
    doit être transmis explicitement, sinon l'estimation est conservatrice (sans réduction).
    """
    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(
            "Taux %s non défini explicitement : utilisation de %s sans réduction BNB. "
            "Veuillez transmettre le taux réel de votre compte via "
            "--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:
    """
    Pour le futur module d'exécution : quantité du SELL associé après
    l'exécution réelle d'un BUY.
    executed_qty — quantité BRUTE achetée (executedQty de l'ordre,
                   avant déduction des frais).
    fills        — exécutions de l'ordre au format ccxt : chaque élément possède
                   fill["fee"] = {"cost": ..., "currency": ...} ou None.
    Seuls les frais prélevés en devise de base sont déduits : les frais en BNB
    ou en devise de cotation ne réduisent pas la quantité d'actif.
    free_balance — solde disponible de devise de base, non réservé
                   pour d'autres ordres (géré par le code appelant).
    """
    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("La quantité n'est pas positive après déduction des frais")
    amount = floor_to_step(min(net, free_balance), rules.step)
    validate_order(rules, "sell", price, amount)
    return amount
# --------------------------------------------------------------------------
# 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="Montant notionnel maximal d'un ordre unique en devise de cotation",
    )
    parser.add_argument(
        "--fee-rate", default=None,
        help="Frais maker (fraction), par défaut maker du marché",
    )
    parser.add_argument(
        "--taker-fee-rate", default=None,
        help="Frais taker pour l'achat initial, par défaut taker du marché",
    )
    parser.add_argument(
        "--fee-in-bnb", action="store_true",
        help="Les frais sont payés en BNB plutôt qu'en actif reçu",
    )
    parser.add_argument("--extra-cost-rate", default="0.0005")
    parser.add_argument(
        "--slippage", default="0.002",
        help="Marge de slippage pour l'achat initial (fraction)",
    )
    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} doit être compris dans l'intervalle [0, 1)")
    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"Paire de trading introuvable : {symbol}")
        market = exchange.market(symbol)
        if market.get("spot") is False:
            raise RuntimeError(f"{symbol} n'est pas un marché spot")
        if market.get("active") is False:
            raise RuntimeError(f"Paire de trading inactive : {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"Le prix actuel {text(mid)} est hors de la plage de grille "
                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("Ignoré : %s", message)
        if not orders:
            raise RuntimeError("Aucun ordre n'a passé la validation")
        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("Paire %s introuvable : impossible d'estimer les frais en BNB", bnb_symbol)
        summary = summarize_plan(
            orders, fee_rate, taker_rate, fee_in_base, ask, slippage, bnb_price
        )
        print(f"Paire :      {symbol}")
        print(f"Bid / Ask :  {text(bid)} / {text(ask)}  (mid {text(mid)})")
        print(f"Tick / Step: {text(rules.tick)} / {text(rules.step)}")
        print(f"Grille :     {text(levels[0])} – {text(levels[-1])}, "
              f"cellules : {len(levels) - 1}")
        print(f"Frais :      maker {fee_rate:.4%}, taker {taker_rate:.4%} "
              f"({'en actif reçu' if fee_in_base else 'en BNB'})")
        if avg_price is not None:
            print(f"Prix de référence filtres : {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"  -> après exécution 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"Ordres BUY :  {summary['buy_orders']}, "
              f"requis ~{money(summary['quote_for_buys'])} {quote}")
        print(f"Ordres SELL : {summary['sell_orders']}, "
              f"réserve nécessaire {text(summary['base_needed_for_sells'])} {base}")
        print(f"Achat initial ~{money(summary['initial_buy_cost_estimate'])} "
              f"{quote} (au ask, frais taker, slippage {slippage:.2%})")
        print(f"Total ~{money(summary['total_quote_estimate'])} {quote} "
              f"(estimation, pas une garantie de solde suffisant)")
        if not fee_in_base:
            line = (f"Frais en BNB : ~{money(summary['bnb_fee_quote_equivalent'])} "
                    f"{quote} en équivalent")
            if summary["bnb_needed"] is not None:
                line += f", ~{summary['bnb_needed']:.4f} BNB"
            print(line + " (cycle de vie complet de tous les ordres)")
        print()
        print("Mode simulation : aucun ordre n'a été envoyé à l'exchange.")
    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("Erreur exchange/réseau : %s", exc)
        return 1
    except (ValueError, RuntimeError) as exc:
        logger.error("Erreur de calcul : %s", exc)
        return 1
    return 0
if __name__ == "__main__":
    sys.exit(main())

Enregistrez et nommez le fichier, par exemple grid_planner.py. Exécution : python grid_planner.py --grid-count 20 --budget-per-level 100 --fee-in-bnb --fee-rate 0.00075 --taker-fee-rate 0.00075

4. Risques cachés et pièges de l'automatisation : ce que les exchanges vous cachent

Les équipes marketing des exchanges adorent exhiber de superbes courbes de rendement basées sur des backtests historiques. En réalité, le trading algorithmique se heurte à des verrous techniques capables de cramer votre capital bien plus vite qu'une erreur humaine.

  • Glissement de prix (Slippage) en forte volatilité : Quand le marché défonce un niveau de support, l'carnet d'ordres se vide en quelques millisecondes. Le bot tente de placer ou d'exécuter un ordre au marché, mais l'ordre se fait exécuter au pire prix possible. Sur des altcoins peu liquides, le slippage peut engloutir une semaine de profits de grid en une seule bougie d'impulsion.
  • La toxicité des frais (Maker vs Taker) : Une stratégie de grid repose sur des ordres limit (mode Maker), où les exchanges vous versent souvent des rebates ou appliquent des frais minimes. Mais si la logique du bot bascule en panic sell au marché (Stop-Loss ou cassure violente), ce sont les frais Taker exorbitants qui s'appliquent, flinguant instantanément la rentabilité théorique de votre stratégie.
  • L'illusion des backtests : Les données historiques ne prennent jamais en compte la latence réseau réelle (le ping vers l'API de l'exchange) ni les files d'attente du moteur d'appariement (matching engine). Un bot affichant +300% sur des historiques 2024–2025 peut très bien finir dans le rouge en conditions réelles sur un marché flat à cause des délais d'exécution.

5. Protéger son capital : la checklist de risk management pour vos bots

Il est hors de question de lancer un algorithme et de partir en vacances. L'automatisation exige une infrastructure de monitoring en béton :

Règles de sécurité et de contrôle :

  • Kill-Switch strict : Le code ou la configuration de votre terminal doit intégrer un stop-loss absolu sur l'ensemble du capital alloué au bot. Si la valeur globale du portefeuille (base + quote) chute par exemple de 15 %, le bot annule immédiatement tous les ordres limit et sécurise la perte.
  • Isolation du capital : Ne gardez jamais les fonds de trading du bot sur le même sous-compte que vos investissements long terme. Utilisez des clés API isolées avec l'option de retrait désactivée : seul le trading doit être autorisé.
  • Monitoring via alertes Telegram : Configurez l'envoi des notifications critiques directement sur votre messagerie plutôt que de compter sur l'interface de l'exchange. Chaque session interrompue, erreur API ou ordre d'urgence exécuté doit être consigné instantanément.

En résumé

Un bot de trading est un bistouri entre les mains d'un chirurgien, pas un pilotage automatique d'avion. Il automatise la discipline, mais ne remplace pas un cerveau. Commencez avec de petits montants en grid spot sur des paires liquides, testez votre logique face à la latence réelle, et gardez en tête que le pire ennemi de vos profits n'est pas le marché, mais vos propres paramètres sans gestion du risque.

L'Académie EXMON vous rappelle : une infrastructure solide et un contrôle strict des risques passent toujours en premier. Les vrais gagnants sont ceux qui savent stopper leur algorithme au bon moment.

Résumer cet article de blog avec :

FAQ

Les systèmes algorithmiques génèrent des gains en exploitant les inefficacités de marché à court terme, en exécutant des grilles d'ordres ou en automatisant le DCA sans subir de biais émotionnel. La rentabilité nette dépend directement du contrôle strict du risque, de la latence des connexions API (REST et WebSocket) et de l'optimisation des types d'ordres pour minimiser les frais taker et le glissement de prix lors des pics de volatilité.

Le cassage violent d'un canal de consolidation constitue la défaillance majeure des algorithmes de grille. Lorsque le cours franchit les bornes définies, le moteur d'exécution accumule une position vendeuse ou acheteuse défavorable sur un actif en chute, provoquant un tirage vers le bas massif du capital en l'absence de paramètres de Stop-Loss stricts et de suivi dynamique.

Une fréquence d'exécution élevée draine la marge bénéficiaire au profit du moteur d'appariement des plateformes en raison de la grille tarifaire taker, de l'élargissement du spread bid-ask et du slippage négatif. Les stratégies quantitatives doivent impérativement intégrer dans leur espérance mathématique les taux maker/taker, le temps de réponse réseau et le taux de financement (funding rate) des contrats dérivés.
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

...

Partager votre avis

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués *