News are the talk of the day, or more specifically building a Binance trading bot that buys or sells based on news sentiment. The cryptocurrency market is still quite young compared to the stock market, and therefore news are especially influential in the crypto world.
I’m not sure about you, but I panic sold a few times, as well as hype-bought – a strategy also known as Buy High Sell Low. Me building and testing crypto bots is a rational response to my emotional trading tactics. I have made good manual trades though believe it or not, and this blog is about taking the best strategies that I have tested, read or thought about and turn them into efficient crypto trading bots.
In the previous blog, we covered how to analyse the daily crypto news sentiment by surfing the web in search of articles that match our keywords, and today we’re going to use that strategy in order to create a fully functional Python crypto trading bot for Binance.
Just a note here though, during the building and testing of this bot, I realised that the two APIs used in the last article don’t really fit so well in the scope of this bot so I have opted for a slightly different, more robust solution, and I ended up rewriting most of the code from the previous blog post.
Parameters and prerequisites of our Binance trading bot
Let’s clearly define what the bot will do and what you’re going to need to complete this project.
The bot will:
pull and analyse the last headline from the top 100 crypto news sites
provide an overview on the most mentioned coin across all the headlines
analyse the sentiment of each headline and categorise the output by coin
Place a Buy order if the compound sentiment is positive and there are at least 10 articles mentioning a particular coin
This is just a general overview, I will go through each of the points in more detail as we make our way through the code
You will need:
a Binance account
a GitHub account
Binance Testnet API keys
a few python libraries
some knowledge of Python
Generating Binance Testnet keys
With all of that out of the way, the first thing you’ll want to do is generate an API key and and API secret for the Binance Testnet. The Testnet allows you to test your bot in a safe environment and without having to use real cryptocurrency. Later on if you’re happy with your testing and would like to use it one live account you will simply replace the testnet keys with the mainnet ones.
If you don’t have a Binance account go ahead and create one. After you have created your Binance account you will need to go over to testnet.binance.vision in order to create a testnet account.
Click on Log In with GitHub and authenticate with your GitHub account, or start by creating a GitHub account if you don’t have one. After logging in, click on Generate HMAC_SHA256 Key.
Give your key a description and click Generate.
Now it’s important that you SAVE your API Key and Secret displayed as for security reasons, this is the only time you will be able to see them. Simply regenerate the keys if you haven’t saved them.
The code
Initial set up
If you don’t already have Python installed on your machine, head over to python.org and select a version compatible with your operating system. After Python has been installed on your computer, open up your cmd or terminal to install Pypi – this will allow you to download the Python modules that we need in order to set your Binance bot up.
Install Pip on Windows
Simply run the following command in Windows to install the pip installer
python get-pip.py
Install Pip on Mac
To install the pip installer on mac you’ll need to run both commands below. Make sure to replace python3.x with the version of Python that you have installed.
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.pypython3.x get-pip.py
Install Pip on Linux (debian/ubuntu)
apt install python3-pip
Install modules with Pip
That’s it, we’re now only one step away from pure Python joy, the last set-up step is to install the required modules using the pip installer. So, in your cmd or terminal, run the following commands. On Mac, you may need to replace pip with pip3like so: pip3 install requests.
pip install requests
pip install numpy
pip install python-binance
pip install nltk
Importing modules
A few modules are needed in order to achieve the desired functionality. I have included comments above each import so you can understand what they do and why they’re needed, but for now simply copy this into your Python compiler. If you don’t have a compiler, I recommend Atom.
# import for environment variables and waiting
import os, time
# used to parse XML feeds
import xml.etree.ElementTree as ET
# get the XML feed and pass it to Element tree
import requests
# date modules that we'll most likely need
from datetime import date, datetime, timedelta
# used to grab the XML url list from a CSV file
import csv
# numpy for sums and means
import numpy as np
# nlp library to analyse sentiment
import nltk
from nltk.sentiment import SentimentIntensityAnalyzer
# needed for the binance API
from binance.client import Client
from binance.enums import *
from binance.exceptions import BinanceAPIException, BinanceOrderException
# used for binance websocket
from binance.websockets import BinanceSocketManager
from twisted.internet import reactor
# used for executing the code
from itertools import count
Connecting to the Binance Testnet
It’s time to use our API key and secret to connect to the Binance Testnet, so that we can place trades and view the trade history. In order to keep my API keys private, I have created environment variables in my OS and I’m calling them below using the os.getenv() function. If you’re not planning to share the code and anonymize your keys, simply input your keys as a string: api_key = ‘YOUR_API_KEY’
# get binance key and secret from environment variables
api_key = os.getenv('binance_api_stalkbot_testnet')
api_secret = os.getenv('binance_secret_stalkbot_testnet')
# Authenticate with the client
client = Client(api_key, api_secret)
# The API URL is manually changed in the library to work on the testnet
client.API_URL = 'https://testnet.binance.vision/api'
Creating user input variables
With the connection to the Binance testnet, it’s time to define user-configurable variables for you Binance trading bot. These can be edited without too much technical skills and pose little risk of breaking the code.
############################################
# USER INPUT VARIABLES LIVE BELOW #
# You may edit those to configure your bot #
############################################
# select what coins to look for as keywords in articles headlines
# The key of each dict MUST be the symbol used for that coin on Binance
# Use each list to define keywords separated by commas: 'XRP': ['ripple', 'xrp']
# keywords are case sensitive
keywords = {
'XRP': ['ripple', 'xrp', 'XRP', 'Ripple', 'RIPPLE'],
'BTC': ['BTC', 'bitcoin', 'Bitcoin', 'BITCOIN'],
'XLM': ['Stellar Lumens', 'XLM'],
'BCH': ['Bitcoin Cash', 'BCH'],
'ETH': ['ETH', 'Ethereum'],
'BNB' : ['BNB', 'Binance Coin'],
'LTC': ['LTC', 'Litecoin']
}
# The Buy amount in the PAIRING symbol, by default USDT
# 100 will for example buy the equivalent of 100 USDT in Bitcoin.
QUANTITY = 100
# define what to pair each coin to
# AVOID PAIRING WITH ONE OF THE COINS USED IN KEYWORDS
PAIRING = 'USDT'
# define how positive the news should be in order to place a trade
# the number is a compound of neg, neu and pos values from the nltk analysis
# input a number between -1 and 1
SENTIMENT_THRESHOLD = 0.5
# define the minimum number of articles that need to be analysed in order
# for the sentiment analysis to qualify for a trade signal
# avoid using 1 as that's not representative of the overall sentiment
MINUMUM_ARTICLES = 10
# define how often to run the code (check for new + try to place trades)
# in minutes
REPEAT_EVERY = 60
############################################
# END OF USER INPUT VARIABLES #
# Edit with care #
############################################
keywords represent the coins you want the Bot to look for in each article headline
QUANTITY represents the amount to buy, by default in USDT
PAIRING allows you to change the pairing for example from BTCUSD to BTCBNB – If you do choose BNB as the paired coin, make sure to adjust the Quantity as 100 BNB is a lot more than 100 USDT.
SENTIMENT_THRESHOLD represents a compound value of positive, negative and neutral sentiments for each headline. Adjust this depending on how you want the bot to trade. A smaller value means that the average headline sentiment doesn’t have to be overwhelmingly positive in order to place a trade.
MINUMUM_ARTICLES determines the minimum amount of mentions for a coin across all headlines in order to trigger a trade. This is to ensure we get an accurate sentiment reading by analysing multiple headlines.
REPEAT_EVERY tells your Binance bot how often to run, check for news and decide whether to trade or not.
Connecting to a Binance websocket
In order to keep track of the current price, we need to get this from Binance. It can be done with the REST api as a request, or preferably through a websocket. Websockets stream live data to your code, so you don’t have to call them every time you need to get the live price for a coin.
# current price of CRYPTO pulled through the websocketCURRENT_PRICE = {} def # current price of CRYPTO pulled through the websocket
CURRENT_PRICE = {}
def ticker_socket(msg):
'''Open a stream for financial information for CRYPTO'''
if msg['e'] != 'error':
global CURRENT_PRICE
CURRENT_PRICE['{0}'.format(msg['s'])] = msg['c']
else:
print('error')
# connect to the websocket client and start the socket
bsm = BinanceSocketManager(client)
for coin in keywords:
conn_key = bsm.start_symbol_ticker_socket(coin+PAIRING, ticker_socket)
bsm.start()
The live price for each of our coins defined in keywords will live in CURRENT_PRICE under their own dictionary. So if you want to access the current price of bitcoin you would use CURRENT_PRICE[‘BTCUSDT’].
Calculate trading volume
By default, Binance will expect you to input the volume of your trade in the coin that you’re trading on. So for example, if you’re trading BTCUSDT, the Binance API expects the volume to be in BTC. This is tricky when you work with multiple coins and can lead to uncontrolled spend. The next function will help you to convert from your QUANTITY variable USDT to BTC.
def calculate_volume():
'''Calculate the amount of CRYPTO to trade in USDT'''
while CURRENT_PRICE == {}:
print('Connecting to the socket...')
time.sleep(3)
else:
volume = {}
for coin in CURRENT_PRICE:
volume[coin] = float(QUANTITY / float(CURRENT_PRICE[coin]))
volume[coin] = float('{:.6f}'.format(volume[coin]))
return volume
Import a list of crypto sites
I have used Feedspot’s top 100 Cryptocurrency blogs as news sources to be scraped – or so I thought. I actually ended up using Feedspot’s top 100 Bitcoin RSS Feeds, so right now there is some bias towards bitcoin. Luckily it’s easy to add more sources to the list as they are saved in a CSV file.
# load the csv file containg top 100 crypto feeds
# want to scan other websites?
# Simply add the RSS Feed url to the Crypto feeds.csv file
with open('Crypto feeds.csv') as csv_file:
# open the file
csv_reader = csv.reader(csv_file)
# remove any headers
next(csv_reader, None)
# create empty list
feeds = []
# add each row cotaining RSS url to feeds list
for row in csv_reader:
feeds.append(row[0])
Returning the last headline for each news source
The next function will return the latest headline in a dictionary format. It’s currently iterating through every rss feed one by one in a for loop. The operation takes about 2 minutes to complete. This can be improved by enabling multiprocessing, so I will probably update this in a future version.
# TO DO - run this in multiple processes to speed up the scraping
def get_headlines():
'''Returns the last headline for each link in the CSV file'''
# add headers to the request for ElementTree. Parsing issues occur without headers
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0'
}
headlines = {'source': [], 'title': [], 'pubDate' : [] }
for feed in feeds:
try:
# grab the XML for each RSS feed
r = requests.get(feed, headers=headers, timeout=7)
# define the root for our parsing
root = ET.fromstring(r.text)
# identify the last Headline
channel = root.find('channel/item/title').text
pubDate = root.find('channel/item/pubDate').text
# append the source and the title
headlines['source'].append(feed)
# append the publication date
headlines['pubDate'].append(pubDate)
# some jank to ensure no alien characters are being passed
headlines['title'].append(channel.encode('UTF-8').decode('UTF-8'))
print(channel)
except:
print(f'Could not parse {feed}')
return headlines
Categorising the headlines
Now that all the headlines are returned, we need to organise them in their own dictionary, based on the keywords found. So every time a headline matches one of the keywords in our keywords variable, that headline will be appended to a dictionary that looks like this: categorised_headlines[BTC] = [‘Headline 1’, ‘headline 2’, ‘headline 3’].
def categorise_headlines():
'''arrange all headlines scaped in a dictionary matching the coin's name'''
# get the headlines
headlines = get_headlines()
categorised_headlines = {}
# this loop will create a dictionary for each keyword defined
for keyword in keywords:
categorised_headlines['{0}'.format(keyword)] = []
# keyword needs to be a loop in order to be able to append headline to the correct dictionary
for keyword in keywords:
# looping through each headline is required as well
for headline in headlines['title']:
# appends the headline containing the keyword to the correct dictionary
if any(key in headline for key in keywords[keyword]):
categorised_headlines[keyword].append(headline)
return categorised_headlines
Determining the sentiment
The first function will analyse the sentiment of each headline, while the second one will categorise the results in a dictionary for each coin that was mentioned at least once in the headlines. The sentiment is analysed by using the Sentiment Intensity Analyser in the nltk module – a great Python library for neuro-linguistic analysis. You may have to run nltk.download() in your Python compiler once, otherwise you might get an error.
def analyse_headlines():
'''Analyse categorised headlines and return NLP scores'''
sia = SentimentIntensityAnalyzer()
categorised_headlines = categorise_headlines()
sentiment = {}
for coin in categorised_headlines:
if len(categorised_headlines[coin]) > 0:
# create dict for each coin
sentiment['{0}'.format(coin)] = []
# append sentiment to dict
for title in categorised_headlines[coin]:
sentiment[coin].append(sia.polarity_scores(title))
return sentiment
def compile_sentiment():
'''Arranges every compound value into a list for each coin'''
sentiment = analyse_headlines()
compiled_sentiment = {}
for coin in sentiment:
compiled_sentiment[coin] = []
for item in sentiment[coin]:
# append each compound value to each coin's dict
compiled_sentiment[coin].append(sentiment[coin][sentiment[coin].index(item)]['compound'])
return compiled_sentiment
Calculating the average compound sentiment
Now that you’ve returned and compiled each result based on the coin keyword found, it’s time we crunch the numbers for each coin. We’re going to use numpy in order to automatically calculate the mean from all the values in our lists. So instead of having an array with multiple compound values, for each coin we will only have an average of all these values. We’re also going to return the number of headlines analysed so that our Binance bot knows how many headlines were taken into the calculation of the average.
def compound_average():
'''Calculates and returns the average compoud sentiment for each coin'''
compiled_sentiment = compile_sentiment()
headlines_analysed = {}
for coin in compiled_sentiment:
headlines_analysed[coin] = len(compiled_sentiment[coin])
# calculate the average using numpy if there is more than 1 element in list
compiled_sentiment[coin] = np.array(compiled_sentiment[coin])
# get the mean
compiled_sentiment[coin] = np.mean(compiled_sentiment[coin])
# convert to scalar
compiled_sentiment[coin] = compiled_sentiment[coin].item()
return compiled_sentiment, headlines_analysed
Buy logic and request
This is it, we now have the headlines, the sentiment, the price and the coins we’re looking for. All we need to do now is check that the compound_sentiment is over the threshold that we have defined, and that the number of articles is higher than 10. If those two conditions match, we create and send a trading request to the Binance API.
If an order has been successful, the Binance bot will confirm in a message that says: “Order 1234 has been placed on BTCUSDT with 0.0012500 at 17/04/2021 and bought at 62,500”. There is currently no logging apart from what is stored on Binance, in the future all the information could be stored in a CSV file.
def buy():
'''Check if the sentiment is positive and keyword is found for each handle'''
compiled_sentiment, headlines_analysed = compound_average()
volume = calculate_volume()
for coin in compiled_sentiment:
# check if the sentiment and number of articles are over the given threshold
if compiled_sentiment[coin] > SENTIMENT_THRESHOLD and headlines_analysed[coin] > MINUMUM_ARTICLES:
# check the volume looks correct
print(f'preparing to buy {coin} with {volume} USDT at {CURRENT_PRICE[coin+PAIRING]}')
# create test order before pushing an actual order
test_order = client.create_test_order(symbol=coin+PAIRING, side='BUY', type='MARKET', quantity=volume[coin+PAIRING])
# try to create a real order if the test orders did not raise an exception
try:
buy_limit = client.create_order(
symbol=coin+PAIRING,
side='BUY',
type='MARKET',
quantity=volume[coin+PAIRING]
)
#error handling here in case position cannot be placed
except BinanceAPIException as e:
print(e)
except BinanceOrderException as e:
print(e)
# run the else block if the position has been placed and return some info
else:
# retrieve the last order
order = client.get_all_orders(symbol=coin+PAIRING, limit=1)
# convert order timsestamp into UTC format
time = order[0]['time'] / 1000
utc_time = datetime.fromtimestamp(time)
# grab the price of CRYPTO the order was placed at for reporting
bought_at = CURRENT_PRICE[coin+PAIRING]
# print order condirmation to the console
print(f"order {order[0]['orderId']} has been placed on {coin} with {order[0]['origQty']} at {utc_time} and bought at {bought_at}")
return bought_at, order
else:
print(f'Sentiment not positive enough for {coin}, or not enough headlines analysed: {compiled_sentiment[coin]}, {headlines_analysed[coin]}')
Putting it all together
You did it. You followed along through over 300 lines of code and now you have a working Binance trading bot that places Buy positions based on analysed news sentiment. All you need to do now, is ensure the code will execute at a pre-defined interval. By default the script will run once every hour, you can change the frequency in the REPEAT_EVERY variable. If you’re using Atom to compile your code you can Run it by pressing Ctrl+Shift+B, other compilers will have different shortcuts.
if name == 'main':
print('Press Ctrl-Q to stop the script')
for i in count():
buy()
print(f'Iteration {i}')
time.sleep(60 * REPEAT_EVERY)
Considerations
The list of articles can be improved to make it less biased
Multiprocessing would help improve the performance
The default compound threshold seems to be quite high in order to place any trades, experiment with the value of the SENTIMENT_THRESHOLD
Please see the GitHub Repo here for the source code!
Thank you for taking the time to go though this guide, and I hope that it was useful. If you liked this guide, please don't forget to like this article. If you want to see more guides like this, tip me to enable me to write more content like this!