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.