Sentiment based trading-bot on Binance using NodeJS
Yes! In this article we are going to build a customizable trading bot, that can buy and sell Bitcoins on Binance based on the tweet sentiment whenever Elon musk tweets.
Question : Can you tell us the motivation behind this bot?
Answer : Sure, why not?!
Can you see the price movement? They’re calling it, the “Musk Effect”.
Before we jump into the implementation part, let’s get prepared for it.
📝 Prerequisites
-
NodeJS : To run this bot, you need to have NodeJS installed on your machine. If you haven’t done it yet, use this link.
-
Twitter Developer Account: For getting the Bearer token to make valid requests to Twitter API.
-
Binance Testnet Keys: The keys are required to place orders programatically on the Binance Testnet exchange. It can be obtained by signing in with your GitHub account.
🧘 Preparation
Let’s create a new directory for this bot and switch our current directory to it. I’m gonna call it the “musk-bot”.
mkdir musk-bot && cd musk-bot
Initialize a new project by running npm init .
The dependencies used in this project is as follows:
-
axios: To make http requests to the Twitter API
-
dotenv: To read values of the environment variables
-
node-binance-api: To place orders on the Binance Exchange (testnet)
-
sentiment: To retrieve the sentiment of the tweet.
We need to install the dependencies required.
npm install axios dotenv node-binance-api sentiment --save
To increase some consistency in the bot, I decided storing the trade logs and last processed tweetId in the filesystem. So we need to create two more files in the project directory.
touch trade.logs tweetId.txt
Sweet!👏 Now it’s time for the implementation.
💡 Algorithm
The way our bot works is pretty simple and straightforward.
-
First it fetches up-to 10 tweets from the last processed tweet. When we start the bot for the first time, it fetches the last 10 tweets from the given twitter user Id.
-
We will maintain an array of words which is required to check which tweets are needed for processing.The tweets without that words are neglected. In our case the word array will be something like
['btc', 'bitcoin']
. There can be some hyperlinks in the tweets that contains the words present in the array, so we will remove the hyper links from the tweet using Regex before checking it against the array of words. -
Extract the sentiment score from the tweet. If neutral or it doesn’t break the threshold, we ignore it.
-
If the tweet breaks the positive/negative threshold score (can be defined in a file, check implementation for more details), we will place a buy/sell order based on the sentiment.
-
Then we will record the trade log and the lastProcessedTweetId in a file.
-
If all the 10 tweets doesn't contain any tweets related to the words in word array, then we simply update the lastProcessedTweetId to the most recent tweet present in the tweet array returned by the Twitter API (ie.,
lastProcessedTweetId = tweets[0].id
)and wait for the next iteration (sliding window technique).
⌛️Implementation
Create a new file called app.js
in the project root directory and add the following contents.
//Require the dependencies
var Sentiment = require("sentiment");
require("dotenv").config();
const axios = require("axios");
const fs = require("fs");
const placeOrder = require("./helpers/placeOrder");
const { twitterUserId, shouldInclude, interval } = require("./config");
// For calling the Twitter API
const options = {
headers: {
Authorization: "Bearer " + process.env.BEARER_TOKEN,
},
};
//Orders will be placed only if the sentiment score breaks threshold
const POSITIVE_THRESHOLD = 1;
const NEGATIVE_THRESHOLD = -1;
// Returns the sentiment score
async function getScore(text) {
var sentiment = new Sentiment();
var result = sentiment.analyze(text);
return result.score;
}
let lastProcessedId = 0;
//Used to start processing from last processed tweet.
// To make sure that the bot doesn't miss any tweets when it is offline.
function loadLatest() {
try {
const dataFromFs = fs.readFileSync("./tweetId.txt", "utf8");
//console.log(dataFromFs);
lastProcessedId = dataFromFs;
console.log("Loaded last processed Tweet ID");
} catch (error) {
console.error("error reading from file", error.message);
}
}
async function startProcessing() {
console.log("Scanning tweets...");
//Get 10 tweets from last processed tweet id.
let tweets = await axios.get(
`https://api.twitter.com/2/users/${twitterUserId}/tweets?exclude=retweets&since_id=${lastProcessedId}&max_results=10`,
options
);
console.log(tweets.data.meta.result_count, "new tweet(s) found!");
if (tweets.data.meta.result_count == 0) return;
tweets = tweets.data.data;
//To store the processed tweets
let results = [];
for (let i = 0; i < tweets.length; i++) {
let obj = {};
const tweet = tweets[i];
let text = tweet.text.replace(/(?:https?):\/\/[\n\S]+/g, ""); // remove links
if (new RegExp(shouldInclude.join("|")).test(text.toLowerCase())) {
obj = {
text: text,
};
let score = await getScore(text);
let sentiment =
score >= POSITIVE_THRESHOLD
? "POSITIVE"
: score <= NEGATIVE_THRESHOLD
? "NEGATIVE"
: "NEUTRAL";
obj["sentiment"] = sentiment;
obj["tweetId"] = tweet.id;
if (sentiment != "NEUTRAL") results.push(obj);
if (results.length) break;
}
}
console.log("Results", JSON.stringify(results, null, 4));
if (results.length) {
//Execute trade
results[0].sentiment == "POSITIVE" ? placeOrder("BUY") : placeOrder("SELL");
//Update the latest tweet ID
fs.writeFileSync("./tweetId.txt", results[0].tweetId);
} else {
fs.writeFileSync("./tweetId.txt", tweets[tweets.length].tweetId);
}
}
const start = async function () {
console.log("Bot started...");
setInterval(async () => {
loadLatest();
await startProcessing();
}, 1000 * 60 * interval);
};
start();
Now we need to create the configuration files to customize this bot. Create a file called config.js
to store the basic configurations of this bot.
module.exports = {
BASE: "BTC",
QUOTE: "USDT",
//buys BTC using 30% of USDT balance
BUY_PERCENT: 30,
//sells 35% of BTC
SELL_PERCENT: 35,
//bot checks for these words in the tweets
shouldInclude: ["btc", "bitcoin"],
//UserId of the twitter user. It can be obtained from this [API](https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-by-username-username)
twitterUserId: "elon_musk_id_goes_here",
//Frequency to check for new tweets (in minutes)
interval : 2
};
As mentioned this bot is customizable. It is not restricted only to trade bitcoin or to poll the tweets of Elon Musk. You can change these parameters as per your requirements.
Now let’s create another file .env to store the environment variables.
BEARER_TOKEN = <TWITTER_BEARER_TOKEN>
BINANCE_API_KEY= <BINANCE_TESTNET_API_KEY>
BINANCE_SECRET= <BINANCE_TESTNET_SECRET_KEY>
Almost there! We need to create a helper function to place orders on the Binance testnet exchange. Create a new folder called helpers inside the project directory and add the following contents to a new file: placeOrder.js
const Binance = require("node-binance-api");
const fs = require("fs");
const { BASE, QUOTE, BUY_PERCENT, SELL_PERCENT } = require("../config");
//Binance configuration to trade on TESTNET
const binance = new Binance().options({
APIKEY: process.env.BINANCE_API_KEY,
APISECRET: process.env.BINANCE_SECRET,
verbose: true,
urls: {
base: "https://testnet.binance.vision/api/", // remove this to trade on mainnet
},
});
module.exports = async (action) => {
{
let balances;
const MARKET = BASE + QUOTE; //In our case the market is BTCUSDT
//fetch current market price
let cmp = await binance.prices(MARKET);
cmp = cmp[MARKET];
switch (action) {
case "BUY":
balances = await binance.balance();
// Fetch USDT balance
let quoteBalance = balances[QUOTE].available;
//Calculate 30% of USDT balance to buy BTC
let buyValue = parseFloat(quoteBalance) * (BUY_PERCENT / 100);
let buyQuantity = parseFloat(buyValue / cmp).toFixed(5);
if (buyQuantity < 0)
return { error: true, message: `Insufficient ${QUOTE} balance` };
//Place Buy order at the market price
binance.marketBuy(MARKET, buyQuantity, (error, response) => {
if (error) {
console.log(
"Oops, Couldn't place order at the moment",
error.message || error.title
);
return { error: true };
}
let log = `${new Date().toLocaleString()} - Bought ${buyQuantity} ${BASE} for ${buyValue} ${QUOTE}. OrderID : ${
response.orderId
}\n`;
// Record the transaction for future reference
fs.appendFileSync("./trade.logs", log);
console.log(log);
return { error: false };
});
break;
case "SELL":
balances = await binance.balance();
//Fetch the BTC balance
let baseBalance = balances[BASE].available;
//Calculate 35% percent of total BTC holdings
let sellQuantity = parseFloat(baseBalance) * (SELL_PERCENT / 100);
if (sellQuantity < 0)
return { error: true, message: `Insufficient ${BASE} balance` };
//Place market sell order
binance.marketSell(
MARKET,
parseFloat(sellQuantity).toFixed(5),
(error, response) => {
if (error) {
console.log(
"Oops, Couldn't place order at the moment",
error.message || error.title
);
return { error: true };
}
log = `${new Date().toLocaleString()} - Sold ${sellQuantity} ${BASE}. OrderID : ${
response.orderId
}\n`;
// record the transaction
fs.appendFileSync("./trade.logs", log);
console.log(log);
return { error: false };
}
);
break;
default:
return { error: true, message: "Invalid action" };
}
}
};
The above script will work based on the specified option.
- If the action passed to the function is BUY then, the script will calculate the BUY_PERCENT (its 30 in our case), from the total USDT balance and places a MARKET BUY order in the given market (BTCUSDT).
- If the action is SELL due to negative sentiment, then the script sells around SELL PERCENT (35%) of the total holdings for the market price.
😋 It’s time for Live Action
For the testing purpose I added my Twitter userId in the config.js file and started the bot by running the node app command.
Simultaneously I posted a positive tweet stating that bitcoin is great .The output is as follows:
trades.log file had the following trade data inserted.
25/06/2021, 00:06:02 - Bought 0.04739 BTC for 1656.069314602 USDT. OrderID : 5005343
Cool ✨ Later, I posted some negative and neutral tweets.The results were perfect!
You can see that our bot had ignored the tweet that had a neutral sentiment and placed a Sell Order for the negative tweet. It works as expected.
I also tried with Elon Musk’s account as well for making sure that the bot calculates the sentiment as required.For that I was required to make some configurations to get some old tweets from his account and process them with our bot. Here are the sentiment results from our BOT.
Results [
{
"text": "#Bitcoin 💔 ",
"sentiment": "NEGATIVE" // <--
}
]
...
Results [
{
"text": '[@Cointelegraph](http://twitter.com/Cointelegraph) This is inaccurate. Tesla only sold ~10% of holdings to confirm BTC could be liquidated easily without moving market.\n' +
'\n' + 'When there’s confirmation of reasonable (~50%) clean energy usage by miners with positive future trend, Tesla will resume allowing Bitcoin transactions.',
"sentiment": 'POSITIVE' // <--
}
]
...
You can see that the sentiments are properly assessed by our bot 💥
Source code for this bot can be found here on GitHub.
👋 End Notes
Yes I agree that this bot is very simple and as always, there is a plenty of room for improvement. But the main goal is to showcase how we can leverage Twitter API and place orders using Binance API. This bot is absolutely fun, and it helps us to learn something new and interesting. You can try to change the config file and play around with this bot. If you find any issues or wish to share your thoughts please let me know.
Happy coding! 🔥🔥