Tekan ESC untuk menutup

Cara Gunakan Bot Trading untuk Profit Kripto (2026)

Automasi Trading

Mari kita bedah mekanik di balik automasi trading tanpa ilusi, cari tahu bot jenis apa yang benar-benar cuan di market crypto, di mana letak risiko tersembunyi, dan bagaimana cara merancang logika kodenya.

1. Arsitektur Strategi: Apa yang Benar-Benar Efektif di Lapangan

Hanya ada tiga jenis sistem terotomasi yang bisa bertahan di market. Selebihnya cuma trik marketing exchange buat ngeruk fee transaksi.

  • Grid Trading: Cocok banget buat market sideways (konsolidasi). Bot bakal masang tangga limit order buat beli di bawah harga sekarang dan jual di atasnya, lalu profit dari fluktuasi di dalam rentang koridor tersebut.
  • Dollar-Cost Averaging (Bot DCA): Strategi akumulasi murni. Bot nyicil aset dalam porsi yang sama di interval waktu tertentu atau pas harga turun di persentase tertentu. Cocok dipakai di market spot buat portofolio jangka panjang.
  • Arbitrase: Nangkep selisih harga spot antar exchange atau spread antara market futures dan spot (funding rate arbitrage). Butuh low latency dan API yang di-optimize secara maksimal.

2. Matematika Grid Bot: Kenapa 80% User Ludes Dananya

Kebanyakan orang langsung nyalain grid bot pas altcoin volatil lagi di puncak (all-time high) pakai settingan default. Hasilnya selalu sama: jebol ke bawah, bot terpaksa jual semua aset dasarnya di harga murah, dan portofolio nyangkut dengan floating loss sampai 60%.

Biar nggak kejadian kayak gitu, kalkulasi parameternya harus ikutan aturan ketat:

Perbandingan Pendekatan Setup Grid:

  • Pemilihan Aset: Kesalahan pemula – milih memecoin hype dengan volatilitas liar. Pendekatan pro – pakai koin top-20 dengan likuiditas tinggi dan level support kuat (BTC, ETH).
  • Tipe Grid: Kesalahan pemula – grid aritmatika (jarak dolar sama rata). Pendekatan pro – grid geometris (jarak persentase sama rata, krusial banget buat persentase yield yang stabil).
  • Rentang (Range): Kesalahan pemula – koridor sempit cuma 5% dari harga sekarang. Pendekatan pro – rentang lebar yang bisa meng-cover tren sideways historis selama 3–6 bulan terakhir.

3. Implementasi Praktis: Kode Modul Grid Trading

Di bawah ini ada potongan script Python fungsional pakai library ccxt. Ini adalah core logic untuk ngehasilin order grid dengan jarak geometris. Di bagian komentar udah gua jelasin fungsi masing-masing barisnya.

"""
Perencana grid limit order untuk Binance Spot (mode dry-run).
Model Grid
----------
Level L0 < L1 < ... < Ln membentuk n cell [L_k, L_{k+1}].
Di tiap cell hanya ada tepat satu order aktif:
- L_k < harga -> BUY di L_k. Setelah terisi, SELL dipasang di L_{k+1}
                 sebesar jumlah yang diterima dikurangi fee mata uang dasar.
- L_k >= harga -> SELL di L_{k+1}. Membutuhkan cadangan awal mata uang dasar
                 (harus dibeli lewat market order terlebih dahulu).
Level yang sama dengan harga saat ini masuk ke cell SELL (tidak dilewati).
Script ini SAMA SEKALI TIDAK mengirim apa pun ke 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")
# --------------------------------------------------------------------------
# Utilitas Decimal
# --------------------------------------------------------------------------
def dec(value: Any, name: str) -> Decimal:
    try:
        result = Decimal(str(value))
    except (InvalidOperation, ValueError, TypeError) as exc:
        raise ValueError(f"{name}: nilai numerik tidak valid") from exc
    if not result.is_finite():
        raise ValueError(f"{name}: nilai harus berhingga")
    return result
def text(value: Decimal) -> str:
    """Decimal -> string tanpa eksponen dan nol berlebih."""
    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
# --------------------------------------------------------------------------
# Aturan (Filter) Pair 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]]
    # Window rata-rata untuk filter persentase; 0 = menggunakan harga terakhir.
    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:
    """Dalam filter Binance, 0 berarti 'batasan dinonaktifkan'."""
    if value is None:
        return None
    result = dec(value, name)
    return result if result > 0 else None
def load_rules(market: dict) -> SymbolRules:
    """Membaca filter sekali saja. Kendala di sini berarti kesalahan konfigurasi."""
    info = market.get("info") or {}
    status = info.get("status")
    if status is not None and status != "TRADING":
        raise RuntimeError(f"Status pair adalah {status}, ekspetasinya TRADING")
    if info.get("isSpotTradingAllowed") is False:
        raise RuntimeError("Trading Spot dinonaktifkan untuk pair ini")
    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 tidak ditemukan")
    if not lot_filter:
        raise RuntimeError("Binance LOT_SIZE tidak ditemukan")
    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 dan stepSize harus lebih besar dari nol")
    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"Nilai avgPriceMins pada filter berbeda-beda: {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]:
    """Batas diratakan ke arah dalam range."""
    lower = ceil_to_step(lower, tick)
    upper = floor_to_step(upper, tick)
    if lower >= upper:
        raise ValueError("Range kosong setelah disejajarkan dengan tickSize")
    return lower, upper
def calculate_grid_levels(
    lower_bound: Decimal,
    upper_bound: Decimal,
    grid_count: int,
    tick: Decimal,
) -> list[Decimal]:
    """Grid geometris: grid_count interval, grid_count + 1 level."""
    if not isinstance(grid_count, int) or isinstance(grid_count, bool):
        raise ValueError("grid_count harus berupa integer")
    if grid_count < 1:
        raise ValueError("grid_count minimal harus 1")
    if min(lower_bound, upper_bound, tick) <= 0:
        raise ValueError("Batas dan tickSize harus lebih besar dari nol")
    if lower_bound >= upper_bound:
        raise ValueError("lower_bound harus lebih kecil dari 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))
        # Setiap level dihitung dari batas bawah: mencegah akumulasi error.
        levels = [
            round_to_step(lower * (ratio ** i), tick)
            for i in range(grid_count + 1)
        ]
    levels[0] = lower
    levels[-1] = upper
    if any(a >= b for a, b in zip(levels, levels[1:])):
        raise ValueError(
            "Level beririsan atau urutan rusak setelah pembulatan tick. "
            "Kurangi grid_count atau lebarkan range."
        )
    return levels
def check_grid_economics(
    levels: list[Decimal],
    fee_rate: Decimal,
    extra_cost_rate: Decimal,
) -> None:
    """
    Filter aproksimasi: yield bersih cell dengan memperhitungkan fee
    di kedua sisi harus melebihi buffer biaya.
    Ini bukan garansi profitabilitas.
    """
    if fee_rate < 0 or extra_cost_rate < 0:
        raise ValueError("Fee dan buffer biaya tidak boleh < 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"Step grid terlalu sempit: {text(low)} -> {text(high)}; "
                f"yield bersih {net_return:.4%}, "
                f"butuh di atas {extra_cost_rate:.4%}"
            )
# --------------------------------------------------------------------------
# Validasi Order Berdasarkan Filter Exchange
# --------------------------------------------------------------------------
def validate_order(
    rules: SymbolRules,
    side: str,
    price: Decimal,
    amount: Decimal,
    avg_price: Decimal | None = None,
) -> Decimal:
    """Mengembalikan nilai notional order atau memunculkan ValueError."""
    if side not in ("buy", "sell"):
        raise ValueError("side harus bernilai buy atau sell")
    if price <= 0 or amount <= 0:
        raise ValueError(
            f"{side.upper()} @ {text(price)}: harga dan jumlah harus > 0 "
            f"(amount={text(amount)})"
        )
    if price % rules.tick != 0:
        raise ValueError(f"Harga {text(price)} bukan kelipatan tickSize")
    if amount % rules.step != 0:
        raise ValueError(f"Jumlah {text(amount)} bukan kelipatan stepSize")
    if price < rules.min_price:
        raise ValueError(f"Harga {text(price)} < minPrice {text(rules.min_price)}")
    if rules.max_price is not None and price > rules.max_price:
        raise ValueError(f"Harga {text(price)} > maxPrice {text(rules.max_price)}")
    if amount < rules.min_qty:
        raise ValueError(
            f"Jumlah {text(amount)} < minQty {text(rules.min_qty)}"
        )
    if rules.max_qty is not None and amount > rules.max_qty:
        raise ValueError(
            f"Jumlah {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}: harga {text(price)} di luar rentang "
                    f"{text(minimum)}–{text(maximum)}"
                )
    return cost
# --------------------------------------------------------------------------
# Perencana 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]]:
    """
    Membangun order lintas cell. Mengembalikan (orders, skipped_cells).
    Fee mata uang dasar mengurangi jumlah SELL pasangannya.
    """
    if budget_per_level <= 0:
        raise ValueError("Budget per level harus lebih besar dari nol")
    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:
                # Cell BUY: beli di low, jual di 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)
                # SELL pasangan akan dipasang nanti, filter persentase akan
                # divalidasi langsung oleh exchange, jadi avg_price tidak diteruskan di sini.
                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:
                # Cell SELL: jumlah dihitung dari harga SELL itu sendiri,
                # memastikan notional tidak melebihi budget level.
                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"cell {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:
    """
    Estimasi kebutuhan modal. Ini adalah ESTIMASI dengan buffer slippage,
    bukan garansi: harga eksekusi aktual dan fee pembelian awal bisa berbeda.
    """
    buys = [o for o in orders if o["side"] == "buy"]
    sells = [o for o in orders if o["side"] == "sell"]
    # Eksekusi order BUY menyerap tidak lebih dari notional: fee diambil
    # dari aset yang diterima (BTC) atau dari BNB, bukan dari USDT.
    quote_for_buys = sum((o["notional"] for o in buys), ZERO)
    base_net_needed = sum((o["amount"] for o in sells), ZERO)
    # Pembelian awal market (taker). Saat fee dalam aset dasar, kita perlu membeli
    # lebih banyak agar base_net_needed tetap terpenuhi setelah dikurangi fee.
    taker_base_fee = taker_fee_rate if fee_in_base else ZERO
    base_gross = base_net_needed / (ONE - taker_base_fee)
    initial_notional = base_gross * ask * (ONE + slippage)
    bnb_fee_quote = ZERO
    bnb_needed = None
    if not fee_in_base:
        # Limit order (BUY, SELL pasang dan awal) menggunakan fee maker,
        # pembelian awal market menggunakan fee taker. Limit order yang langsung
        # menembus orderbook akan tereksekusi sebagai taker: estimasi ini tidak mencakup hal tersebut.
        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,
    }
# --------------------------------------------------------------------------
# Integrasi Exchange
# --------------------------------------------------------------------------
def restrict_to_spot(exchange) -> None:
    """Instruksikan ccxt untuk mengambil market spot saja (format opsi tergantung versi)."""
    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]:
    """Mengembalikan (bid, ask, last)."""
    ticker = exchange.fetch_ticker(symbol)
    if ticker.get("bid") is None or ticker.get("ask") is None:
        raise RuntimeError(f"{symbol}: exchange tidak mengembalikan 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 tidak valid {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:
    """
    Harga referensi untuk PERCENT_PRICE*. Mereplikasi aturan Binance:
    avgPriceMins == 0 -> harga terakhir, jika tidak maka rata-rata dari avgPriceMins menit.
    Window yang dikembalikan /avgPrice dicocokkan dengan window dari filter.
    Pemeriksaan akhir tetap divalidasi oleh exchange saat submit order.
    """
    if rules.avg_price_mins == 0:
        if last is None:
            raise RuntimeError("Filter membutuhkan harga last, tapi exchange tidak mengembalikannya")
        return last
    response = exchange.publicGetAvgPrice({"symbol": market["id"]})
    price = dec(response["price"], "avgPrice")
    if price <= 0:
        raise RuntimeError("Binance mengembalikan avgPrice yang tidak valid")
    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"Window avgPrice ({mins} min) tidak cocok dengan avgPriceMins "
            f"filter ({rules.avg_price_mins} min): tidak dapat mereplikasi pemeriksaan"
        )
    return price
def resolve_rate(
    market: dict,
    key: str,
    explicit: Decimal | None,
    fee_in_bnb: bool,
) -> Decimal:
    """
    Fee rate: eksplisit, atau dari market (maker/taker), default ke 0.1%.
    Diskon fee BNB TIDAK diasumsikan: tier fee akun aktual harus
    diteruskan secara eksplisit, jika tidak estimasi bersifat konservatif (tanpa diskon).
    """
    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(
            "Fee rate %s tidak di-set secara eksplisit: menggunakan %s tanpa diskon BNB. "
            "Teruskan rate akun aktual Anda melalui "
            "--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:
    """
    Untuk modul eksekusi masa depan: jumlah SELL pasangan setelah eksekusi BUY aktual.
    executed_qty — jumlah kotor yang dibeli (executedQty order, sebelum fee).
    fills        — eksekusi order format ccxt: masing-masing berisi
                   fill["fee"] = {"cost": ..., "currency": ...} atau None.
    Hanya fee yang dipotong dalam mata uang dasar yang dikurangi: fee dalam BNB
    atau mata uang quote tidak mengurangi jumlah aset.
    free_balance — saldo bebas mata uang dasar yang tidak dicadangkan untuk order lain
                   (caller bertanggung jawab memastikan ini).
    """
    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("Jumlah bersih tidak positif setelah pemotongan fee")
    amount = floor_to_step(min(net, free_balance), rules.step)
    validate_order(rules, "sell", price, amount)
    return amount
# --------------------------------------------------------------------------
# Main Execution
# --------------------------------------------------------------------------
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="Nilai notional maksimum dari satu order dalam mata uang quote",
    )
    parser.add_argument(
        "--fee-rate", default=None,
        help="Rate fee maker (fraksi), default mengikuti rate maker market",
    )
    parser.add_argument(
        "--taker-fee-rate", default=None,
        help="Rate fee taker untuk pembelian awal, default mengikuti rate taker market",
    )
    parser.add_argument(
        "--fee-in-bnb", action="store_true",
        help="Fee dibayar menggunakan BNB alih-alih aset yang diterima",
    )
    parser.add_argument("--extra-cost-rate", default="0.0005")
    parser.add_argument(
        "--slippage", default="0.002",
        help="Buffer slippage pembelian awal (fraksi)",
    )
    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} harus berada dalam rentang [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"Trading pair tidak ditemukan: {symbol}")
        market = exchange.market(symbol)
        if market.get("spot") is False:
            raise RuntimeError(f"{symbol} bukan market spot")
        if market.get("active") is False:
            raise RuntimeError(f"Trading pair tidak aktif: {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"Harga saat ini {text(mid)} berada di luar rentang 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("Dilewati: %s", message)
        if not orders:
            raise RuntimeError("Tidak ada order yang lolos validasi")
        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("Pair %s tidak ditemukan: estimasi penggunaan BNB dilewati", bnb_symbol)
        summary = summarize_plan(
            orders, fee_rate, taker_rate, fee_in_base, ask, slippage, bnb_price
        )
        print(f"Pair:        {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"cell: {len(levels) - 1}")
        print(f"Fee Rate:    maker {fee_rate:.4%}, taker {taker_rate:.4%} "
              f"({'dalam aset yang diterima' if fee_in_base else 'dalam BNB'})")
        if avg_price is not None:
            print(f"Harga referensi filter: {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"  -> saat terisi 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"Order BUY:   {summary['buy_orders']}, "
              f"butuh ~{money(summary['quote_for_buys'])} {quote}")
        print(f"Order SELL:  {summary['sell_orders']}, "
              f"cadangan dibutuhkan {text(summary['base_needed_for_sells'])} {base}")
        print(f"Pembelian awal ~{money(summary['initial_buy_cost_estimate'])} "
              f"{quote} (pada ask, fee taker, slippage {slippage:.2%})")
        print(f"Total ~{money(summary['total_quote_estimate'])} {quote} "
              f"(estimasi, bukan garansi kecukupan saldo)")
        if not fee_in_base:
            line = (f"Fee BNB: ~{money(summary['bnb_fee_quote_equivalent'])} "
                    f"{quote} ekuivalen")
            if summary["bnb_needed"] is not None:
                line += f", ~{summary['bnb_needed']:.4f} BNB"
            print(line + " (siklus penuh dari semua order)")
        print()
        print("Mode dry-run: order TIDAK dikirim ke 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("Error exchange/jaringan: %s", exc)
        return 1
    except (ValueError, RuntimeError) as exc:
        logger.error("Error kalkulasi: %s", exc)
        return 1
    return 0
if __name__ == "__main__":
    sys.exit(main())

Simpan dan beri nama file, contohnya grid_planner.py. Cara Jalankan: python grid_planner.py --grid-count 20 --budget-per-level 100 --fee-in-bnb --fee-rate 0.00075 --taker-fee-rate 0.00075

4. Risiko Tersembunyi dan Jebakan Otomatisasi yang Dirahasiakan Exchange

Tim marketing exchange hobi banget mamerin grafik profit bot yang kelihatan manis dari hasil backtest historis. Padahal di dunia nyata, algo trading bakal nabrak tembok teknis yang bisa bikin modal amblas jauh lebih cepat dibanding kesalahan manual manusia.

  • Slippage Parah Saat Volatilitas Tinggi: Begitu pasar jebol level support, likuiditas di order book hilang dalam hitungan milidetik. Bot bakal berusaha pasang atau eksekusi market order, tapi hasilnya malah dapet harga terburuk. Di altcoin yang tipis, slippage kayak gini bisa ngeruk profit grid seminggu cuma dalam sekali impuls pergerakan harga.
  • Tokoitaz Fee (Maker vs Taker): Strategi grid itu pada dasarnya ngandelin limit order — alias zona Maker, tempat exchange ngasih rebate atau potongan fee paling murah. Tapi kalau logika bot lu tiba-tiba panic sell dan jualan secara instan (karena Stop-Loss atau breakdown), lu bakal kena fee Taker yang sangat mahal dan langsung merusak kalkulasi matematika strategi lu.
  • Ilusi Hasil Backtest: Data historis nggak pernah memperhitungkan latency jaringan yang sebenarnya (ping ke API exchange) dan antrean di matching engine. Bot yang kelihatan mentereng dengan profit +300% di backtest periode 2024–2025 bisa aja malah keleponan dan minus pas live trading di pasar crab akibat delay eksekusi.

5. Cara Amankan Modal: Checklist Risk Management Wajib Buat Bot

Lu nggak bisa asal nyalain algoritma terus ditinggal liburan santai. Otomatisasi butuh sistem monitoring dan infrastruktur yang ketat:

Aturan Keamanan dan Kontrol Kendali:

  • Kill-Switch Absolut: Harus ada fitur stop-loss ketat buat seluruh modal yang dipegang bot, baik di kodenya langsung maupun di settingan terminal. Kalau nilai portofolio gabungan dari koin base dan quote anjlok katakanlah 15%, bot harus langsung nyabut semua limit order dan mengunci kerugian saat itu juga.
  • Isolasi Alokasi Modal: Jangan pernah nyampurin modal trading bot di akun atau sub-akun yang sama dengan dana tabungan jangka panjang lu. Pake API Key terisolasi dan pastiin opsi withdrawal dimatikan — cuma izinkan akses untuk trading saja.
  • Monitoring via Telegram Alert: Setting notifikasi penting biar terkirim langsung ke aplikasi pesan kayak Telegram, bukan cuma nungguin notif di UI exchange. Setiap kali ada sesi terputus, API error, atau eksekusi order darurat, lu harus dapet infonya secara real-time.

Kesimpulannya

Trading bot itu ibarat pisau bedah di tangan dokter spesialis, bukan fitur autopilot di pesawat. Dia bertugas mengotomatisasi disiplin lu, bukan menggantikan fungsi otak. Mulailah dari nominal kecil pake spot grid di pasangan koin yang likuid, tes logikanya terhadap delay di kondisi nyata, dan ingat: musuh utama profit lu bukanlah market, melainkan settingan bot lu sendiri yang mengabaikan manajemen risiko.

Catatan dari Akademi EXMON: Infrastruktur yang solid dan manajemen risiko yang ketat adalah prioritas utama. Cuan sejati diraih oleh trader yang tahu kapan harus mematikan algoritmanya.

Rangkum postingan blog ini dengan:

FAQ

Sistem perdagangan algoritmik menghasilkan keuntungan dengan memanfaatkan efisiensi harga jangka pendek, mengeksekusi parameter grid, dan menjalankan strategi akumulasi DCA tanpa gangguan bias emosional. Keberhasilan profitabilitas bergantung pada manajemen risiko yang ketat, latensi koneksi API (REST dan WebSocket), serta pemilihan tipe order yang tepat untuk menekan biaya taker dan slippage saat volatilitas pasar melonjak.

Breakout harga yang menembus batas rentang konsolidasi menjadi penyebab utama kegagalan algoritma grid. Ketika harga menembus batas bawah atau atas yang telah dikonfigurasi, mesin eksekusi akan terus menumpuk posisi rugi pada aset yang turun atau menjual simpanan aset terlalu dini, sehingga memicu penurunan modal yang signifikan jika parameter Stop-Loss dan Trailing tidak diaktifkan.

Frekuensi transaksi yang tinggi mengikis profit bersih akibat akumulasi struktur biaya taker, pelebaran spread bid-ask pada buku order, dan slippage eksekusi. Setiap strategi kuantitatif wajib memperhitungkan margin keuntungan bersih setelah dikurangi tarif maker/taker, latensi jaringan, serta biaya pendanaan (funding rate) pada kontrak derivatif agar tidak menghasilkan nilai harapan matematis yang negatif.
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

...

Sampaikan pemikiran Anda

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *