Pressione ESC para fechar

Como Usar Bots de Trading para Lucrar em Crypto (2026)

Automação de Trading

Vamos abrir o jogo e analisar a mecânica por trás da automação de trading sem ilusões: quais tipos de bots realmente geram lucro no mercado cripto, onde ficam os riscos ocultos e como estruturar a lógica por trás deles.

1. Arquitetura de Estratégias: O Que Funciona de Verdade na Prática

Apenas três tipos principais de sistemas automatizados sobrevivem no mercado. O resto é puro marketing de corretora focado em farmar taxa de corretagem.

  • Grid Trading (Robô de Grade): Perfeito para mercados laterais (range-bound). O bot posiciona uma escada de ordens limite de compra abaixo do preço atual e de venda acima. Ele lucra com as oscilações dentro do corredor.
  • Dollar-Cost Averaging (Bots DCA): Uma estratégia de acumulação pura. O bot compra o ativo em partes iguais em intervalos de tempo fixos ou quando o preço cai por uma porcentagem específica. Funciona no spot para um portfólio de longo prazo.
  • Arbitragem: Capturar diferenças de preço entre mercados spot de diferentes corretoras ou o spread entre futuros e spot (funding rate arbitrage). Exige baixa latência (low latency) e uma API altamente otimizada.

2. A Matemática dos Grid Bots: Por Que 80% dos Usuários Perdem a Banca

A maioria das pessoas liga um grid bot no topo absoluto de uma altcoin volátil usando as configurações padrão. O resultado é sempre o mesmo: um rompimento forte da faixa para baixo, o bot desova toda a moeda base a preço de banana, e o portfólio fica entupido de um ativo "morto" com um drawdown de 60%.

Para evitar cair nessa armadilha, o cálculo de parâmetros segue regras rígidas:

Comparação de Abordagens de Configuração de Grid:

  • Seleção de Ativo: Erro de noob — meme coin hypado com volatilidade insana. Abordagem profissional — moedas do top 20 com alta liquidez e zonas de suporte sólidas (BTC, ETH).
  • Tipo de Grade: Erro de noob — aritmética (espaçamento fixo em dólares). Abordagem profissional — geométrica (espaçamento percentual igual, essencial para manter um rendimento percentual estável).
  • Faixa (Range): Erro de noob — um corredor estreito a 5% do preço atual. Abordagem profissional — uma faixa ampla que cubra o histórico de consolidação lateral dos últimos 3 a 6 meses.

3. Implementação Prática: Código do Módulo de Grid Trading

Abaixo está um trecho de script funcional em Python usando a biblioteca ccxt. Este é o núcleo da lógica de cálculo para gerar ordens de grade com espaçamento geométrico. Explico o que cada parte faz nos comentários.

"""
Planejador de Grid de Ordens Limitadas para Binance Spot (modo dry-run).
Modelo de Grid
--------------
Os níveis L0 < L1 < ... < Ln formam n células [L_k, L_{k+1}].
Em cada célula, há exatamente uma ordem ativa por vez:
- L_k <  preço  -> BUY em L_k. Após a execução, um SELL é posicionado em L_{k+1}
                 com a quantidade resultante descontada a taxa na moeda base.
- L_k >= preço -> SELL в L_{k+1}. Isso requer um estoque inicial de moeda base
                 (que precisa ser comprado a mercado com antecedência).
O nível igual ao preço atual pertence à célula SELL (não é pulado).
Este script NÃO envia absolutamente nada para a 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")
# --------------------------------------------------------------------------
# Utilitários Decimal
# --------------------------------------------------------------------------
def dec(value: Any, name: str) -> Decimal:
    try:
        result = Decimal(str(value))
    except (InvalidOperation, ValueError, TypeError) as exc:
        raise ValueError(f"{name}: valor numérico inválido") from exc
    if not result.is_finite():
        raise ValueError(f"{name}: o valor precisa ser finito")
    return result
def text(value: Decimal) -> str:
    """Decimal -> string sem notação científica e sem zeros desnecessários."""
    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
# --------------------------------------------------------------------------
# Regras do Instrumento Binance Spot (Filtros)
# --------------------------------------------------------------------------
@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]]
    # Janela de média para filtros percentuais; 0 = usa o último preço.
    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:
    """Nos filtros da Binance, 0 significa 'restrição desativada'."""
    if value is None:
        return None
    result = dec(value, name)
    return result if result > 0 else None
def load_rules(market: dict) -> SymbolRules:
    """Lê os filtros uma vez. Qualquer problema aqui é um erro de configuração."""
    info = market.get("info") or {}
    status = info.get("status")
    if status is not None and status != "TRADING":
        raise RuntimeError(f"O status do par é {status}, esperava-se TRADING")
    if info.get("isSpotTradingAllowed") is False:
        raise RuntimeError("O trading spot está desativado para este par")
    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("PRICE_FILTER da Binance não encontrado")
    if not lot_filter:
        raise RuntimeError("LOT_SIZE da Binance não encontrado")
    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 e stepSize devem ser maiores que zero")
    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"avgPriceMins divergentes nos filtros: {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]:
    """Os limites são ajustados para dentro do intervalo."""
    lower = ceil_to_step(lower, tick)
    upper = floor_to_step(upper, tick)
    if lower >= upper:
        raise ValueError("O intervalo ficou vazio após o alinhamento com o tickSize")
    return lower, upper
def calculate_grid_levels(
    lower_bound: Decimal,
    upper_bound: Decimal,
    grid_count: int,
    tick: Decimal,
) -> list[Decimal]:
    """Grid geométrico: grid_count intervalos, grid_count + 1 níveis."""
    if not isinstance(grid_count, int) or isinstance(grid_count, bool):
        raise ValueError("grid_count precisa ser um número inteiro")
    if grid_count < 1:
        raise ValueError("grid_count precisa ser pelo menos 1")
    if min(lower_bound, upper_bound, tick) <= 0:
        raise ValueError("Os limites e o tickSize devem ser maiores que zero")
    if lower_bound >= upper_bound:
        raise ValueError("lower_bound precisa ser menor que 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))
        # Cada nível é calculado a partir do limite inferior: sem acúmulo de erro.
        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(
            "Os níveis se sobrepuseram ou quebraram a ordem após o arredondamento para o tick. "
            "Diminua o grid_count ou aumente o intervalo."
        )
    return levels
def check_grid_economics(
    levels: list[Decimal],
    fee_rate: Decimal,
    extra_cost_rate: Decimal,
) -> None:
    """
    Filtro aproximado: considerando as taxas em ambas as pontas,
    o retorno líquido da célula precisa superar o colchão de custos.
    Isso não é garantia de lucro.
    """
    if fee_rate < 0 or extra_cost_rate < 0:
        raise ValueError("A taxa e o colchão de custos não podem ser < 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"Passo do grid muito pequeno: {text(low)} -> {text(high)}; "
                f"retorno líquido {net_return:.4%}, "
                f"exige mais de {extra_cost_rate:.4%}"
            )
# --------------------------------------------------------------------------
# Validação de Ordens pelos Filtros da Exchange
# --------------------------------------------------------------------------
def validate_order(
    rules: SymbolRules,
    side: str,
    price: Decimal,
    amount: Decimal,
    avg_price: Decimal | None = None,
) -> Decimal:
    """Retorna o valor notional da ordem ou dispara um ValueError."""
    if side not in ("buy", "sell"):
        raise ValueError("side deve ser buy ou sell")
    if price <= 0 or amount <= 0:
        raise ValueError(
            f"{side.upper()} @ {text(price)}: preço e quantidade devem ser > 0 "
            f"(amount={text(amount)})"
        )
    if price % rules.tick != 0:
        raise ValueError(f"O preço {text(price)} não é múltiplo de tickSize")
    if amount % rules.step != 0:
        raise ValueError(f"A quantidade {text(amount)} não é múltipla de stepSize")
    if price < rules.min_price:
        raise ValueError(f"Preço {text(price)} < minPrice {text(rules.min_price)}")
    if rules.max_price is not None and price > rules.max_price:
        raise ValueError(f"Preço {text(price)} > maxPrice {text(rules.max_price)}")
    if amount < rules.min_qty:
        raise ValueError(
            f"Quantidade {text(amount)} < minQty {text(rules.min_qty)}"
        )
    if rules.max_qty is not None and amount > rules.max_qty:
        raise ValueError(
            f"Quantidade {text(amount)} > maxQty {text(rules.max_qty)}"
        )
    cost = price * amount
    if cost < rules.min_notional:
        raise ValueError(
            f"Notional {text(cost)} < minNotional {text(rules.min_notional)}"
        )
    if rules.max_notional is not None and cost > rules.max_notional:
        raise ValueError(
            f"Notional {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}: preço {text(price)} fora do intervalo "
                    f"{text(minimum)}–{text(maximum)}"
                )
    return cost
# --------------------------------------------------------------------------
# Plano do Grid
# --------------------------------------------------------------------------
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]]:
    """
    Constrói as ordens por célula. Retorna (ordens, celulas_ignoradas).
    A taxa na moeda base reduz a quantidade do SELL emparelhado.
    """
    if budget_per_level <= 0:
        raise ValueError("O orçamento por nível precisa ser maior que zero")
    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:
                # Célula BUY: compra em low, vende em 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)
                # O SELL emparelhado será lançado depois; o filtro percentual
                # será validado pela própria exchange nesse momento, então não passamos avg_price aqui.
                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:
                # Célula SELL: a quantidade é calculada com base no preço do próprio SELL,
                # para que o notional não estoure o orçamento por nível.
                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"célula {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:
    """
    Estimativa de capital necessário. Trata-se de uma ESTIMATIVA com colchão para slippage,
    e não uma garantia: o preço real e as taxas na compra inicial podem variar.
    """
    buys = [o for o in orders if o["side"] == "buy"]
    sells = [o for o in orders if o["side"] == "sell"]
    # A ordem BUY gasta no máximo o notional: a taxa é cobrada
    # do ativo recebido (BTC) ou de BNB, nunca de USDT.
    quote_for_buys = sum((o["notional"] for o in buys), ZERO)
    base_net_needed = sum((o["amount"] for o in sells), ZERO)
    # Compra inicial a mercado (taker). Se a taxa for na base, é preciso
    # comprar mais para que, após o desconto, reste exatamente base_net_needed.
    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:
        # Ordens limit (BUY, SELLs emparelhados e iniciais) entram como maker,
        # e a compra inicial a mercado como taker. Uma ordem limit que atravesse
        # o book na hora será executada como taker: esta estimativa não pega isso.
        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,
    }
# --------------------------------------------------------------------------
# Integração com a Exchange
# --------------------------------------------------------------------------
def restrict_to_spot(exchange) -> None:
    """Solicitamos ao ccxt carregar apenas mercados spot (o formato da opção varia conforme a versão)."""
    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]:
    """Retorna (bid, ask, last)."""
    ticker = exchange.fetch_ticker(symbol)
    if ticker.get("bid") is None or ticker.get("ask") is None:
        raise RuntimeError(f"{symbol}: a exchange não retornou 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 inválidos {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:
    """
    Preço de referência para PERCENT_PRICE*. Replica a regra da Binance:
    avgPriceMins == 0 -> último preço, caso contrário, a média dos últimos avgPriceMins minutos.
    A janela retornada por /avgPrice é conferida com a janela do filtro.
    A checagem final será feita obrigatoriamente pela exchange ao enviar a ordem.
    """
    if rules.avg_price_mins == 0:
        if last is None:
            raise RuntimeError("O preço last é necessário para o filtro, mas a exchange não o retornou")
        return last
    response = exchange.publicGetAvgPrice({"symbol": market["id"]})
    price = dec(response["price"], "avgPrice")
    if price <= 0:
        raise RuntimeError("A Binance retornou um avgPrice inválido")
    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"A janela avgPrice ({mins} min) não bate com avgPriceMins "
            f"do filtro ({rules.avg_price_mins} min): impossível reproduzir a verificação"
        )
    return price
def resolve_rate(
    market: dict,
    key: str,
    explicit: Decimal | None,
    fee_in_bnb: bool,
) -> Decimal:
    """
    Taxa de corretagem: explícita, caso contrário pega do mercado (maker/taker), senão 0.1%.
    Desconto por pagar em BNB NÃO é assumido: a taxa real da conta
    deve ser repassada explicitamente, senão a estimativa fica conservadora (sem desconto).
    """
    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(
            "A taxa %s não foi informada explicitamente: usando %s sem o desconto de BNB. "
            "Passe a taxa real da sua conta através de "
            "--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:
    """
    Para o futuro módulo de execução: a quantidade do SELL emparelhado após
    a execução real de um BUY.
    executed_qty — quantidade COMPRADA BRUTA (executedQty da ordem, antes do desconto da taxa).
    fills        — execuções da ordem no formato ccxt: cada uma tem
                   fill["fee"] = {"cost": ..., "currency": ...} ou None.
    Apenas as taxas cobradas na moeda base são descontadas: taxas em BNB
    ou na moeda cotada não diminuem a quantidade do ativo.
    free_balance — saldo livre da moeda base que não está reservado
                   para outras ordens (o código chamador é responsável por isso).
    """
    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("A quantidade não é positiva após o desconto da taxa")
    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="Valor notional máximo de uma única ordem na moeda cotada",
    )
    parser.add_argument(
        "--fee-rate", default=None,
        help="Taxa de maker (fração), padrão é o maker do mercado",
    )
    parser.add_argument(
        "--taker-fee-rate", default=None,
        help="Taxa de taker para a compra inicial, padrão é o taker do mercado",
    )
    parser.add_argument(
        "--fee-in-bnb", action="store_true",
        help="A taxa é paga em BNB em vez do ativo recebido",
    )
    parser.add_argument("--extra-cost-rate", default="0.0005")
    parser.add_argument(
        "--slippage", default="0.002",
        help="Colchão de slippage para a compra inicial (fração)",
    )
    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} deve estar no intervalo [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"Par de negociação não encontrado: {symbol}")
        market = exchange.market(symbol)
        if market.get("spot") is False:
            raise RuntimeError(f"{symbol} não é um mercado spot")
        if market.get("active") is False:
            raise RuntimeError(f"O par de negociação está inativo: {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"O preço atual {text(mid)} está fora do intervalo do grid "
                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("Ignorado: %s", message)
        if not orders:
            raise RuntimeError("Nenhuma ordem passou na validação")
        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("Par %s não encontrado: impossível estimar o custo em BNB", bnb_symbol)
        summary = summarize_plan(
            orders, fee_rate, taker_rate, fee_in_base, ask, slippage, bnb_price
        )
        print(f"Par:         {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"células: {len(levels) - 1}")
        print(f"Taxas:       maker {fee_rate:.4%}, taker {taker_rate:.4%} "
              f"({'no ativo recebido' if fee_in_base else 'em BNB'})")
        if avg_price is not None:
            print(f"Preço de referência dos filtros: {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"  -> após execução 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"Ordens BUY:  {summary['buy_orders']}, "
              f"necessário ~{money(summary['quote_for_buys'])} {quote}")
        print(f"Ordens SELL: {summary['sell_orders']}, "
              f"reserva necessária {text(summary['base_needed_for_sells'])} {base}")
        print(f"Compra inicial ~{money(summary['initial_buy_cost_estimate'])} "
              f"{quote} (no ask, taxa taker, slippage {slippage:.2%})")
        print(f"Total ~{money(summary['total_quote_estimate'])} {quote} "
              f"(estimativa, sem garantia de saldo suficiente)")
        if not fee_in_base:
            line = (f"Taxas em BNB: ~{money(summary['bnb_fee_quote_equivalent'])} "
                    f"{quote} equivalentes")
            if summary["bnb_needed"] is not None:
                line += f", ~{summary['bnb_needed']:.4f} BNB"
            print(line + " (ciclo de vida completo de todas as ordens)")
        print()
        print("Modo dry-run: Nenhuma ordem foi enviada para a 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("Erro de exchange/rede: %s", exc)
        return 1
    except (ValueError, RuntimeError) as exc:
        logger.error("Erro de cálculo: %s", exc)
        return 1
    return 0
if __name__ == "__main__":
    sys.exit(main())

Salve e nomeie o arquivo, por exemplo, grid_planner.py. Execução: python grid_planner.py --grid-count 20 --budget-per-level 100 --fee-in-bnb --fee-rate 0.00075 --taker-fee-rate 0.00075

4. Riscos Ocultos e Armadilhas da Automação que as Exchanges Não Contam

Os marqueteiros de exchanges adoram exibir gráficos bonitos de rentabilidade de bots em backtests históricos. Na realidade, o trading algorítmico esbarra em barreiras técnicas capazes de queimar sua banca mais rápido do que um erro humano.

  • Slippage em Alta Volatilidade: Quando o mercado rompe um suporte, o book de ofertas esvazia em milissegundos. O bot tenta posicionar ou executar uma ordem a mercado, mas a operação é preenchida no pior preço. Em altcoins de baixa liquidez, um único candle de impulso pode devorar o lucro de uma semana inteira da sua grid.
  • Toxicidade de Taxas (Maker vs Taker): Uma grid é construída com ordens limitadas — o que é o território Maker, onde as exchanges frequentemente te pagam rebates ou cobram taxas mínimas. Mas se a lógica do bot entra em pânico e faz um despejo a mercado (Stop-Loss ou pânico no rompimento), entram em ação as taxas Taker salgadas, destruindo a matemática da estratégia.
  • A Ilusão dos Backtests: Dados históricos não consideram a latência real de rede (ping até a API da exchange) e as filas do motor de emparelhamento (matching engine). Um bot que bateu +300% em testes históricos de 2024–2025 pode facilmente operar no vermelho em um mercado lateral (sideways) na vida real devido a atrasos de execução.

5. Como Proteger Seu Capital: Checklist de Gestão de Risco para Bots

Não dá para rodar um algoritmo e sair de férias. A automação exige uma infraestrutura de monitoramento rígida:

Regras de Segurança e Controle:

  • Kill-Switch Rígido: No código ou nas configurações da sua plataforma deve existir um stop-loss absoluto para toda a banca alocada no bot. Se o valor combinado da moeda base e da cotação cair digamos 15%, o bot cancela todas as ordens limit e encerra a operação no prejuízo.
  • Isolamento de Capital: Nunca mantenha o capital de trading do bot na mesma subconta das suas economias de longo prazo. Use chaves API isoladas com a função de saque desativada — apenas ordens de compra e venda permitidas.
  • Monitoramento via Telegram Alerts: Configure o envio de notificações críticas diretamente para o seu mensageiro, em vez de depender da interface da exchange. Cada sessão encerrada, erro de API ou ordem de emergência executada deve ser reportada instantaneamente.

Resumo da Ópera

Um bot de trading é um bisturi nas mãos de um cirurgião, e não um piloto automático de avião. Ele automatiza a sua disciplina, mas não substitui o raciocínio. Comece com valores baixos em grids spot em ativos líquidos, teste a lógica sob latência real e lembre-se: o maior inimigo do lucro não é o mercado, mas sim os seus próprios parâmetros sem gestão de risco.

A Academia EXMON lembra: infraestrutura robusta e controle de risco vêm sempre em primeiro lugar. Quem ganha dinheiro de verdade é quem sabe a hora certa de parar o algoritmo.

Resumir este post do blog com:

FAQ

Sistemas de negociação algorítmica geram lucros explorando ineficiências de mercado de curto prazo, executando parâmetros de grade e automatizando estratégias de DCA sem viés emocional. A lucratividade líquida depende de controle rígido de risco, conexões API de baixa latência via REST e WebSocket, além da escolha correta de tipos de ordens para mitigar taxas de taker e slippage durante picos de volatilidade.

O rompimento violento de uma consolidação de preços representa a principal falha estrutural dos algoritmos de grade. Quando o preço ultrapassa os limites configurados de suporte ou resistência, o motor de execução acumula posições desfavoráveis em um ativo em queda ou vende ativos precocemente, gerando drawdown acentuado caso parâmetros rígidos de Stop-Loss e Trailing não estejam ativos.

A alta frequência de execução drena a margem de lucro operacional em favor do motor de correspondência da exchange devido à estrutura de taxas de taker, ao alargamento do spread entre compra e venda e ao slippage negativo. Estratégias quantitativas precisam obrigatoriamente calcular o retorno esperado após deduzir taxas de maker e taker, latência de rede e taxas de financiamento (funding rate) em contratos derivados para evitar uma expectativa matemática negativa.
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

...

Deixe seu parecer

O seu endereço de e-mail não será publicado. Campos obrigatórios estão marcados *