website/content/blog/build-your-first-crypto-trading-bot.mdx
Every trading bot starts with the same four moves: connect to an exchange, load its markets, read some data, place an order. This tutorial does exactly that — and because CCXT ships the same API in six languages, every step below is shown in all of them. Pick your tab and stay in it.
Before writing any code:
setSandboxMode(true) — shown below.npm install ccxt
pip install ccxt
composer require ccxt/ccxt
dotnet add package ccxt
go get github.com/ccxt/ccxt/go/v4
// build.gradle.kts
implementation("io.github.ccxt:ccxt:4.5.64")
loadMarkets() downloads the exchange's tradable symbols, precision rules, and limits. Call
it once at startup; everything else builds on it.
import ccxt from 'ccxt';
const exchange = new ccxt.binance({
apiKey: process.env.API_KEY,
secret: process.env.SECRET,
});
exchange.setSandboxMode(true); // testnet — remove only when you mean it
await exchange.loadMarkets();
console.log(`${exchange.id}: ${Object.keys(exchange.markets).length} markets`);
import os
import ccxt
exchange = ccxt.binance({
'apiKey': os.environ['API_KEY'],
'secret': os.environ['SECRET'],
})
exchange.set_sandbox_mode(True) # testnet — remove only when you mean it
exchange.load_markets()
print(exchange.id, len(exchange.markets), 'markets')
<?php
$exchange = new \ccxt\binance([
'apiKey' => getenv('API_KEY'),
'secret' => getenv('SECRET'),
]);
$exchange->set_sandbox_mode(true); // testnet — remove only when you mean it
$exchange->load_markets();
echo $exchange->id, ': ', count($exchange->markets), " markets\n";
using ccxt;
var exchange = new Binance();
exchange.apiKey = Environment.GetEnvironmentVariable("API_KEY");
exchange.secret = Environment.GetEnvironmentVariable("SECRET");
exchange.setSandboxMode(true); // testnet — remove only when you mean it
await exchange.LoadMarkets();
Console.WriteLine($"{exchange.id}: markets loaded");
package main
import (
"fmt"
"os"
ccxt "github.com/ccxt/ccxt/go/v4"
)
func main() {
exchange := ccxt.NewBinance(nil)
exchange.ApiKey = os.Getenv("API_KEY")
exchange.Secret = os.Getenv("SECRET")
exchange.SetSandboxMode(true) // testnet — remove only when you mean it
exchange.LoadMarkets()
fmt.Println(exchange.Id, "markets loaded")
}
import io.github.ccxt.exchanges.Binance;
var exchange = new Binance();
exchange.apiKey = System.getenv("API_KEY");
exchange.secret = System.getenv("SECRET");
exchange.setSandboxMode(true); // testnet — remove only when you mean it
exchange.loadMarkets();
System.out.println("markets loaded");
CCXT's rate limiter is on by default — requests are automatically spaced out to respect the exchange's limits, so a simple loop won't get your IP banned.
const ticker = await exchange.fetchTicker('BTC/USDT');
console.log('last price:', ticker.last);
const balance = await exchange.fetchBalance();
console.log('free USDT:', balance['USDT'].free);
ticker = exchange.fetch_ticker('BTC/USDT')
print('last price:', ticker['last'])
balance = exchange.fetch_balance()
print('free USDT:', balance['USDT']['free'])
$ticker = $exchange->fetch_ticker('BTC/USDT');
echo 'last price: ', $ticker['last'], "\n";
$balance = $exchange->fetch_balance();
echo 'free USDT: ', $balance['USDT']['free'], "\n";
var ticker = await exchange.FetchTicker("BTC/USDT");
Console.WriteLine($"last price: {ticker.last}");
var balance = await exchange.FetchBalance();
Console.WriteLine($"free USDT: {balance["USDT"].Free}");
ticker, err := exchange.FetchTicker("BTC/USDT")
if err != nil {
panic(err)
}
fmt.Println("last price:", *ticker.Last)
balance, err := exchange.FetchBalance()
if err != nil {
panic(err)
}
fmt.Println("free USDT:", *balance.Free["USDT"])
var ticker = exchange.fetchTicker("BTC/USDT");
System.out.println("last price: " + ticker.last);
var balance = exchange.fetchBalance((java.util.Map<String, Object>) null);
System.out.println(balance);
Notice the symbol: BTC/USDT is CCXT's unified symbol format. You write the same symbol
on every exchange — CCXT translates it to whatever the exchange calls it internally
(BTCUSDT, XBTUSDT, BTC-USDT, ...).
We place a buy order 20% below the market so it won't fill while you experiment, then cancel it.
const price = exchange.priceToPrecision('BTC/USDT', ticker.last * 0.8);
const amount = 0.001;
const order = await exchange.createOrder('BTC/USDT', 'limit', 'buy', amount, price);
console.log('placed:', order.id);
await exchange.cancelOrder(order.id, 'BTC/USDT');
console.log('canceled');
price = exchange.price_to_precision('BTC/USDT', ticker['last'] * 0.8)
amount = 0.001
order = exchange.create_order('BTC/USDT', 'limit', 'buy', amount, price)
print('placed:', order['id'])
exchange.cancel_order(order['id'], 'BTC/USDT')
print('canceled')
$price = $exchange->price_to_precision('BTC/USDT', $ticker['last'] * 0.8);
$amount = 0.001;
$order = $exchange->create_order('BTC/USDT', 'limit', 'buy', $amount, $price);
echo 'placed: ', $order['id'], "\n";
$exchange->cancel_order($order['id'], 'BTC/USDT');
echo "canceled\n";
var price = (double)ticker.last * 0.8;
var amount = 0.001;
var order = await exchange.CreateOrder("BTC/USDT", "limit", "buy", amount, price);
Console.WriteLine($"placed: {order.id}");
await exchange.CancelOrder(order.id, "BTC/USDT");
Console.WriteLine("canceled");
price := *ticker.Last * 0.8
amount := 0.001
order, err := exchange.CreateOrder("BTC/USDT", "limit", "buy", amount,
ccxt.WithCreateOrderPrice(price))
if err != nil {
panic(err)
}
fmt.Println("placed:", *order.Id)
_, err = exchange.CancelOrder(*order.Id, ccxt.WithCancelOrderSymbol("BTC/USDT"))
if err != nil {
panic(err)
}
fmt.Println("canceled")
double price = ticker.last * 0.8;
double amount = 0.001;
var order = exchange.createOrder("BTC/USDT", "limit", "buy", amount, price, null);
System.out.println("placed: " + order.id);
exchange.cancelOrder(order.id, "BTC/USDT", null);
System.out.println("canceled");
Two details that separate toy scripts from real bots, both visible here:
priceToPrecision / amountToPrecision round values to what the
exchange actually accepts. Sending 26418.377777 raw gets your order rejected.cancelOrder. Many exchanges require it, and unified code
should assume it's needed.You now have the full loop: connect, read, act, undo. From here: