Crypto Trading Bot Python Binance

Crypto Trading Bot Python Binance

You've done your homework, you've scoured through mounds of historical data and backtested every possible strategy. But how do you actually implement it and get your computer to place the trades for you? That's what we'll be doing here with Python for the Binance exchange.

The bot I'll be building here is a simple bot that monitors the RSI for a particular currency pair, buys when it reaches its entry threshold, and sells when it reaches the exit level. It never takes on more than one trade at a time. And holds until its condition is met (forever if necessary).

What is python-binance?

Python-binance is an unofficial wrapper for the Binance API. It massively simplifies a lot of the work involved in communicating with the Binance servers.

It also deals with authentication headaches for us. When placing orders through the plain Binance API we have to sign our requests with an extra HMAC SHA256 signature. Python-binance deals with all of this for us.

Functions like client.order_market_buy let us send market buy/sell orders in a single line of code. get_historical_klines does the same for fetching historical data.

Ultimately if you want maximum security and control, you'll want to use the plain old Binance API. I'd recommend starting with Python-Binance, and when you feel more confident you can decide whether you want to make the switch.

Get your API keys

In order to get the Binance server to place orders for us, it needs to uniquely identify our account and authenticate that we're the account holder. Your API keys are basically just passwords that we use to authenticate ourselves with Binance.

You'll get an API KEY and a SECRET KEY. Make sure to write them down somewhere safe, it'll only show you the secret key once. If you lose it you'll have to generate another pair of keys.

The Binance testnet is freely available to anyone with a Github account. The testnet is useful because it doesn't use real money and we can easily test our bot. It's very easy to convert to a real trading bot once you're happy with the results.

If you want to grab your real, live API keys, you can check out this tutorial from Binance.

Bot Architecture

We're going to construct this bot in a modular fashion as a series of smaller functions. That way we can easily debug errors and make additions/extensions as we get more confident in bot development.

Roughly speaking, we'll need the following components:

  • Data Source
  • Indicator Calculation
  • Error/Trade Logging
  • Order Placement
  • Main Loop

Most of those being self-explanatory. The main loop is responsible for orchestrating the other functions, and running the bot in an infinite loop until we decide to stop it.

File Structure

To get started you'll want to create two files in your new project folder

1.
2├── bot.py
3└── .env

bot.py contains our actual Python code, we'll get to work on that in a minute. .env contains our API keys.

It's good practice to separate them out like this so that you don't accidentally leak your API keys when you show your code to your buddy/the internet.

Your .env file should look something like this:

1API_KEY=mejBW0r8h45Kc1lJ5xK0ASQHaGxVazi3kSqKUA
2SECRET_KEY=5I2XlPNAKMWLEsH7euVMsI9fS7LVGVYnweHIlF7Iz5

We'll use a module called python-decouple to bring these into our Python script.

Authorizing our Binance Client

Now that we're all setup, let's open our bot.py file and get to work. We'll first want to import our packages

1from decouple import config
2from binance.client import Client

Make sure to install python-binance and python-decouple first with pip.

Our next step is to instantiate our client, which we can do with

1client = Client(config("API_KEY"), config("SECRET_KEY"), testnet=True)

Note the testnet=True. You'll obviously want to get rid of that if you're trading live. If binance.com is unavailable in your country and you use another TLD like binance.us, you'll have to change the tld parameter to the appropriate value.

Checking our balance

You can check how much of a given asset you have in your account with

1balance = client.get_asset_balance(asset='BTC')
2print(balance)
1{'asset': 'BTC', 'free': '1.16000000', 'locked': '0.00000000'}

The testnet accounts are loaded up with a bunch of different currencies. Note that Binance regularly resets the balance of the testnet account, approximately once per month, so you'll have to keep track of your gains yourself.

Fetching Candlestick / Kline data

Python-binance makes retrieving price data a cinch. We'll start out by defining our function and making the call to Binance

1def fetch_klines(asset):
2    # fetch 1 minute klines for the last day up until now
3    klines = client.get_historical_klines(asset, 
4        Client.KLINE_INTERVAL_1MINUTE, "1 hour ago UTC")

Client.KLINE_INTERVAL_1MINUTE represents the timeframe of the candlestick data that we'll be retrieving. Other timeframes are fairly intuitive, for example Client.KLINE_INTERVAL_1WEEK.

Our third argument gives the time range of the data we're after. We only need a few bars to calculate the RSI so I'm just grabbing one minute bars from the last hour. Changing this to "1 day ago UTC". Would do the same thing for a full day of candles.

This would be slower to download, which is why I don't do it here. You can experiment to see how many bars it'll let you download in a single request. Generally better safe than sorry here, you can always make multiple requests and stitch the results together.

Printing klines should give you something like:

1[[1642861860000, '35487.12000000', '35502.19000000',
2 '35453.59000000', '35453.59000000', '0.67249100', 1642861919999,
3 '23858.74339149', 59, '0.26766000', '9499.37069213', '0'], ...

Every sub-list in this list of lists represents a single candlestick. You can see the format of the response on the official documentation. For the purposes of this tutorial, we just need the closing price and the time of the candlestick. That's going to be the first and fifth element of each list.

To extract that and simplify our lives a little bit, we'll use this list comprehension

1klines = [ [x[0],x[4]] for x in klines ]
2print(klines)
1[[1642862220000, '35560.11000000'], [1642862280000, '35586.79000000'], [1642862340000, '35546.80000000'], ...

Much nicer, we'll also want to convert the closing price from string to float values, otherwise we won't be able to do any numerical calculation on them. We can just change the previous line to

1klines = pd.DataFrame(klines, columns = ["time","price"])
2klines["time"] = pd.to_datetime(klines["time"], unit = "ms")

We also convert the time column to a human readable pandas datetime format, rather than an integer.

At this point your function should look like:

1def fetch_klines(asset):
2    # fetch 1 minute klines for the last day up until now
3    klines = client.get_historical_klines(asset, 
4        Client.KLINE_INTERVAL_1MINUTE, "1 hour ago UTC")
5    klines = [ [x[0],float(x[4])] for x in klines ]
6    klines = pd.DataFrame(klines, columns = ["time","price"])
7    klines["time"] = pd.to_datetime(klines["time"], unit = "ms")
8    return klines

Make sure to import pandas as pd at the beginning of your script. Calling print(fetch_klines("BTCUSDT")) at the bottom of your script should now return your neatly-organized closing price data

1time                    price
20  2022-01-22 14:47:00  35533.45
31  2022-01-22 14:48:00  35497.26
42  2022-01-22 14:49:00  35427.01
53  2022-01-22 14:50:00  35406.54
6                          ...

Calculating Indicator Values

Now that we've got our price data we just need to run it through pandas_ta to calculate our RSI values.

1def get_rsi(asset):
2    klines = fetch_klines(asset)
3    klines["rsi"]=ta.rsi(close=klines["price"], length = 14)
4
5    return klines["rsi"].iloc[-1]

Make sure to add import pandas_ta as ta somewhere at the top of your script! When called this function will return the most recent RSI value for a particular asset.

Logging

We're going to need a method of writing logs down to disk in case something goes wrong. Having a written timestamped log of all errors that occur makes bug fixing so much easier

 1def log(msg):
 2    print(f"LOG: {msg}")
 3    if not os.path.isdir("logs"):
 4        os.mkdir("logs")
 5
 6    now = datetime.datetime.now()
 7    today = now.strftime("%Y-%m-%d")
 8    time = now.strftime("%H:%M:%S")
 9    with open(f"logs/{today}.txt", "a+") as log_file:
10        log_file.write(f"{time} : {msg}\n")

This handy little function will take in a msg as a string, and write it down to todays log file, which are kept in a logs folder in the same directory as the bot. You'll want to import datetime in order for this to work.

We also want to keep track of our trades so that we can judge how well our bot is performing. Since the Binance test network gets regularly reset we can't rely on the portfolio value in there for an idea of how well we're doing.

 1def trade_log(sym, side, price, amount):
 2    log(f"{side} {amount} {sym} for {price} per")
 3    if not os.path.isdir("trades"):
 4        os.mkdir("trades")
 5
 6    now = datetime.datetime.now()
 7    today = now.strftime("%Y-%m-%d")
 8    time = now.strftime("%H:%M:%S")
 9
10
11    if not os.path.isfile(f"trades/{today}.csv"):
12        with open(f"trades/{today}.csv", "w") as trade_file:
13            trade_file.write("sym,side,amount,price\n")
14
15    with open(f"trades/{today}.csv", "a+") as trade_file:
16        trade_file.write(f"{sym},{side},{amount},{price}\n")

Logging trades is basically the same as logging errors, just that we're writing down to a .csv file so that we can easily import our trades into Pandas later.

Keeping Track of Our Trades

Our strategy involves only buying one lot of our asset and then holding it until we sell. We want to ensure that this happens even if we turn off the bot or it breaks for some reason. The way I'm going to implement this is to write down a bot_account.json to disk that says whether the bot is currently looking to buy or looking to sell.

 1def create_account():
 2
 3    account = {
 4            "is_buying":True,
 5            "assets":{},
 6            }
 7
 8    with open("bot_account.json", "w") as f:
 9        f.write(json.dumps(account))
10
11def is_buying():
12    if os.path.isfile("bot_account.json"):
13
14        with open("bot_account.json") as f:
15            account = json.load(f)
16            if "is_buying" in account:
17                return account["is_buying"]
18            else:
19                return True
20
21    else:
22        create_account()
23        return True

Implementing Trading Logic

Now that we've got most of our infrastructure done, we need a method that will actually execute a trade for us. We want to give it the asset we want, whether to buy or sell, and the quantity that we wish to purchase.

 1def do_trade(account,client, asset, side, quantity):
 2
 3    if side == "buy":
 4        order = client.order_market_buy(
 5            symbol=asset,
 6            quantity=quantity)
 7
 8        account["is_buying"] = False
 9
10    else:
11        order = client.order_market_sell(
12            symbol=asset,
13            quantity=quantity)
14
15        account["is_buying"] = True
16
17    order_id = order["orderId"]
18
19    while order["status"] != "FILLED":
20
21        order = client.get_order(
22            symbol=asset,
23            orderId=order_id)
24
25        time.sleep(1)
26
27    price_paid = sum([ float(fill["price"]) * float(fill["qty"]) \
28            for fill in order["fills"]])
29
30    trade_log(asset, side, price_paid, quantity)
31
32    with open("bot_account.json","w") as f:
33        f.write(json.dumps(account))

Note that I also pass in the account file, which when serialized in Python is just a dictionary, and the Binance client object which it can use to interact with the broker.

The logic is fairly simple, if we are buying, do a market buy for the given quantity, then set is_buying to be False. For selling do the inverse.

1order_id = order["orderId"]
2
3    while order["status"] != "FILLED":
4
5        order = client.get_order(
6            symbol=asset,
7            orderId=order_id)
8
9        time.sleep(1)

This little section just grabs the order_id of our order, then keeps checking the status of the order until it's filled. We do this because we need to know how much we actually payed before we record it in the log. It'll also quickly show us if something's wrong with our ordering logic, as it'll never do anything. Make sure to import time here or you'll have trouble!

Main Loop

Finally we're ready to put everything together.

 1rsi = get_rsi(asset)
 2old_rsi = rsi
 3asset = "BTCUSDT"
 4
 5while True:
 6
 7    try:
 8        if not os.path.exists("bot_account.json"):
 9            create_account()
10
11        with open("bot_account.json") as f:
12            account = json.load(f)
13
14
15        old_rsi = rsi
16        rsi = get_rsi(asset)
17
18        if account["is_buying"]:
19
20            if rsi < entry and old_rsi > entry:
21                do_trade(account, client, asset, "buy", 0.01)
22
23        else:
24
25            if rsi > exit and old_rsi < exit:
26                do_trade(account, client, asset, "sell", 0.01)
27        
28        print(rsi)
29        time.sleep(10)
30
31    except Exception as e:
32        log("ERROR: " + str(e))

We have two rsi variables here. The reason for that is to make sure that we're only trading when we have a crossover, not just at any time we're below the entry or above the exit.

This way we ensure that we've just crossed the line. So on the entry, old_rsi has to be above our entry whilst rsi has to be below it, implying that we've just crossed the threshold. Vice versa on the exit.

If you run bot.py complete with all the functions we've defined so far you should be up and running!

You've successfully built a Binance trading bot. Let it run for a few hours and see if you can find any errors before letting it run rampant with your real wallets.

If you want to take this project further you'll want to build in some more robust exception handling, and look into streaming data through a websocket rather than relying on the API, as it's a little slow.

Video Tutorial

If you'd prefer a video tutorial, you can check out this free course on our youtube channel: