├── sold-assets.json ├── holding-assets.json ├── bot.js ├── package.json ├── binance.js ├── functions ├── getPrices.js ├── getExchangeConfig.js ├── scan.js ├── helpers.js ├── buy.js └── sell.js ├── config.env.example ├── LICENSE ├── app.js ├── constants.js ├── .gitignore └── README.md /sold-assets.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /holding-assets.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: './config.env' }); 2 | const app = require('./app'); 3 | app.listen(process.env.PORT); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binance-volatility-trading-bot-js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node bot.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/21jake/Binance-volatility-trading-bot-JS.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/21jake/Binance-volatility-trading-bot-JS/issues" 17 | }, 18 | "homepage": "https://github.com/21jake/Binance-volatility-trading-bot-JS#readme", 19 | "dependencies": { 20 | "dotenv": "^9.0.1", 21 | "node-binance-api": "^0.12.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /binance.js: -------------------------------------------------------------------------------- 1 | const { TESTNET_URLS, MAINNET_URLS, TEST_MODE } = require('./constants'); 2 | const Binance = require('node-binance-api'); 3 | const { returnTimeLog } = require('./functions/helpers'); 4 | 5 | const { API_KEY_TEST, API_SECRET_TEST, API_KEY_MAIN, API_SECRET_MAIN } = process.env; 6 | 7 | const binance = new Binance().options({ 8 | verbose: !TEST_MODE, 9 | urls: TEST_MODE ? TESTNET_URLS : MAINNET_URLS, 10 | APIKEY: TEST_MODE ? API_KEY_TEST : API_KEY_MAIN, 11 | APISECRET: TEST_MODE ? API_SECRET_TEST : API_SECRET_MAIN, 12 | }); 13 | 14 | if (!TEST_MODE) { 15 | console.log( 16 | `${returnTimeLog()} You're using the bot on the mainnet with real money, be cautious and don't start with too much quantity!!` 17 | ); 18 | } 19 | 20 | module.exports = binance; 21 | -------------------------------------------------------------------------------- /functions/getPrices.js: -------------------------------------------------------------------------------- 1 | const { FIATS } = require('../constants'); 2 | const binance = require('../binance'); 3 | const { returnTimeLog } = require('./helpers'); 4 | 5 | const getPrices = async () => { 6 | try { 7 | let data = await binance.prices(); 8 | const output = {}; 9 | for (const coin in data) { 10 | if (coin.includes(process.env.PAIR_WITH) && !coin.includes(FIATS) && !coin.match(/UP|DOWN/g)) { 11 | output[coin] = { 12 | price: data[coin], 13 | time: new Date().getTime(), 14 | }; 15 | } 16 | } 17 | return output; 18 | } catch (error) { 19 | console.log( 20 | `${returnTimeLog()} There was an error getting prices: ${error.body || JSON.stringify(error)}` 21 | ); 22 | } 23 | }; 24 | 25 | module.exports = getPrices; 26 | -------------------------------------------------------------------------------- /config.env.example: -------------------------------------------------------------------------------- 1 | CREATE A NEW config.env FILE WITH THE FORMAT IS AS BELOW 2 | 3 | PORT=5000 4 | 5 | # BINANCE CONFIGURATION 6 | API_KEY_TEST = 'YOUR_API_KEY' 7 | API_SECRET_TEST = 'YOUR_API_KEY' 8 | API_KEY_MAIN = 'YOUR_API_KEY' 9 | API_SECRET_MAIN = 'YOUR_API_KEY' 10 | 11 | # PRICE CHECKING INTERVAL, MEASURED IN MINUTES 12 | INTERVAL = 5 13 | 14 | # SAFE MODE SCAN INTERVAL (CHECK PRICE MORE FREQUENTLY AFTER THE ASSET IS BOUGHT) 15 | SCAN_INTERVAL = 1 16 | 17 | # SET USDT AS THE DEFAULT PAIR-ER 18 | PAIR_WITH = 'USDT' 19 | 20 | # THE MAXIMUM AMOUNT THE BOT CAN SPEND 21 | QUANTITY = 50 22 | 23 | # MINIMUM AMOUNT TO PLACE AN ORDER, BINANCE WOULDN'T ALLOW A LOWER QUANTITY. 24 | MIN_QUANTITY = 11 25 | 26 | # IF THE PRICE OF A COIN INCREASES BY THIS PERCENTAGE, THE BOT PROCEEDS TO BUY IT. DEFAULT TO 3 27 | VOLATILE_TRIGGER = 3 28 | 29 | # TAKE PROFIT THRESHOLD 30 | TP_THRESHOLD = 6 31 | 32 | # STOP LOSS THRESHOLD 33 | SL_THRESHOLD = 3 34 | 35 | # SOMETIMES BINANCE WOULDN'T LET YOU SELL 100% OF THE ASSET. 36 | # IF YOU HAVE BNBs TO PAY FOR TRANSACTIONS, YOU CAN SET THIS TO 100 (%) 37 | ACTUAL_SELL_RATIO = 99.5 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 21jake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | require('./functions/getExchangeConfig'); 3 | const { handleBuy } = require('./functions/buy'); 4 | const { handleSell } = require('./functions/sell'); 5 | const getPrices = require('./functions/getPrices'); 6 | const { sleep, detectVolatiles, returnTimeLog } = require('./functions/helpers'); 7 | const safeScan = require('./functions/scan'); 8 | const { SAFE_MODE } = require('./constants'); 9 | 10 | const app = https.createServer(); 11 | 12 | const { INTERVAL, SCAN_INTERVAL } = process.env; 13 | const intervalInMs = INTERVAL * 60000; 14 | const scanIntervalInMs = SCAN_INTERVAL * 60000; 15 | 16 | const main = async () => { 17 | try { 18 | const initialPrices = await getPrices(); 19 | while (initialPrices['BTCUSDT'].time > new Date().getTime() - intervalInMs) { 20 | console.log(`${returnTimeLog()} Wait for the bot to gather data to check price volatility...`); 21 | await sleep(intervalInMs); 22 | } 23 | const lastestPrice = await getPrices(); 24 | const volatiles = detectVolatiles(initialPrices, lastestPrice); 25 | await handleSell(lastestPrice); 26 | await handleBuy(volatiles); 27 | } catch (error) { 28 | console.log(`${returnTimeLog()} Error in excuting main function: ${error || JSON.stringify(error)}`); 29 | } 30 | }; 31 | 32 | main(); 33 | setInterval(main, intervalInMs); 34 | 35 | if (SAFE_MODE) { 36 | setInterval(safeScan, scanIntervalInMs); 37 | } 38 | 39 | module.exports = app; 40 | -------------------------------------------------------------------------------- /functions/getExchangeConfig.js: -------------------------------------------------------------------------------- 1 | const binance = require('../binance'); 2 | const { FIATS } = require('../constants'); 3 | const { writeFile } = require('fs').promises; 4 | const { returnTimeLog } = require('./helpers'); 5 | 6 | const formatExchangeConfig = (data) => { 7 | let minimums = {}; 8 | for (let obj of data.symbols) { 9 | if (obj.symbol.includes(process.env.PAIR_WITH) && !FIATS.includes(obj.symbol)) { 10 | let filters = { status: obj.status }; 11 | for (let filter of obj.filters) { 12 | if (filter.filterType == 'MIN_NOTIONAL') { 13 | filters.minNotional = filter.minNotional; 14 | } else if (filter.filterType == 'PRICE_FILTER') { 15 | filters.minPrice = filter.minPrice; 16 | filters.maxPrice = filter.maxPrice; 17 | filters.tickSize = filter.tickSize; 18 | } else if (filter.filterType == 'LOT_SIZE') { 19 | filters.stepSize = filter.stepSize; 20 | filters.minQty = filter.minQty; 21 | filters.maxQty = filter.maxQty; 22 | } 23 | } 24 | filters.orderTypes = obj.orderTypes; 25 | filters.icebergAllowed = obj.icebergAllowed; 26 | minimums[obj.symbol] = filters; 27 | } 28 | } 29 | return minimums; 30 | }; 31 | 32 | const getExchangeConfig = () => { 33 | return new Promise((resolve, reject) => { 34 | binance.exchangeInfo((err, data) => { 35 | if (err) { 36 | reject(err); 37 | } else { 38 | resolve(data); 39 | } 40 | }); 41 | }); 42 | }; 43 | 44 | module.exports = (async () => { 45 | try { 46 | const data = await getExchangeConfig(); 47 | const formatedData = formatExchangeConfig(data); 48 | await writeFile('exchange-config.json', JSON.stringify(formatedData, null, 4)); 49 | } catch (error) { 50 | console.log(`${returnTimeLog()} Error in getting exchange config: ${error}`); 51 | } 52 | })(); 53 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | const MARKET_FLAG = { type: 'MARKET' }; 2 | 3 | /* List of pairs to exclude 4 | by default we're excluding the most popular fiat pairs 5 | and some margin keywords, as we're only working on the SPOT account 6 | */ 7 | const FIATS = ['EURUSDT', 'GBPUSDT', 'JPYUSDT', 'USDUSDT', 'DOWN', 'UP']; 8 | const TESTNET_URLS = { 9 | base: 'https://testnet.binance.vision/api/', 10 | combineStream: 'wss://testnet.binance.vision/stream?streams=', 11 | stream: 'wss://testnet.binance.vision/ws/', 12 | }; 13 | const MAINNET_URLS = { 14 | base: 'https://api.binance.com/api/', 15 | combineStream: 'wss://stream.binance.com:9443/stream?streams=', 16 | stream: 'wss://stream.binance.com:9443/ws/', 17 | }; 18 | 19 | // Set TEST_MODE = false to switch to the mainnet to trade with REAL money 20 | const TEST_MODE = true; 21 | 22 | /* 23 | Every time an asset hits the TP, the bot doesn't sell it immediately. 24 | The SL and TP threshold of that asset is increased. 25 | If an asset hits the SL, we sell (In fact, we just sell at SL). 26 | 27 | For example, BTCUSDT is bought at 100. TP is 106 (6%) and SL is 97 (3%). 28 | When it hits 106, the TP is adjusted to ~109 and SL is ~103. 29 | Whenever it hits SL (97 or 103...), the bot sells. 30 | 31 | Disable this feature by setting "TRAILING_MODE" below to false 32 | */ 33 | const TRAILING_MODE = true; 34 | 35 | /* 36 | SAFE MODE description: To avoid the rapid ups and immediate downs within the next interval, 37 | after an asset is bought, the bot scan to check the asset price every 1 minute 38 | (1 is the default value, you can change the SCAN_INTERVAL in the config.env file). 39 | If the asset price hits SL threshold during that 1 minute, the bot will proceed to sell the asset. 40 | Turning off the scan by setting "SAFE_MODE" below to false 41 | */ 42 | const SAFE_MODE = true; 43 | 44 | module.exports = { MARKET_FLAG, FIATS, TESTNET_URLS, MAINNET_URLS, TEST_MODE, TRAILING_MODE, SAFE_MODE }; 45 | -------------------------------------------------------------------------------- /functions/scan.js: -------------------------------------------------------------------------------- 1 | const { readPortfolio, returnTimeLog, getBinanceConfig } = require('./helpers'); 2 | const { sell, handleSellData } = require('./sell'); 3 | const getPrices = require('./getPrices'); 4 | 5 | const safeScan = async () => { 6 | try { 7 | const portfolio = await readPortfolio(); 8 | if (!portfolio.length) { 9 | console.log( 10 | `${returnTimeLog()} Scanning: Current portfolio is empty, waiting for asset(s) to be bought first...` 11 | ); 12 | } else { 13 | for (const asset of portfolio) { 14 | const { symbol } = asset; 15 | const lastestAssetPrice = await getAssetPrice(symbol); 16 | await sellAssetIfHitSL(asset, lastestAssetPrice); 17 | } 18 | } 19 | } catch (error) { 20 | console.log(`${returnTimeLog()} Error in scanning ${error}`); 21 | } 22 | }; 23 | 24 | const sellAssetIfHitSL = async (asset, lastestAssetPrice) => { 25 | try { 26 | const { SL_Threshold, symbol } = asset; 27 | if (lastestAssetPrice <= SL_Threshold) { 28 | const exchangeConfig = await getBinanceConfig(); 29 | const sellData = await sell(exchangeConfig, asset); 30 | await handleSellData(sellData, lastestAssetPrice, asset); 31 | console.log( 32 | `${returnTimeLog()} ${symbol} price hasn hit SL threshold during this scan and the asset is sold` 33 | ); 34 | } else { 35 | console.log( 36 | `${returnTimeLog()} ${symbol} price hasn't hit SL threshold during this scan and the asset is kept` 37 | ); 38 | } 39 | } catch (error) { 40 | throw `Error in selling asset when it hits SL threshold: ${error}`; 41 | } 42 | }; 43 | 44 | const getAssetPrice = async (symbol) => { 45 | try { 46 | const lastestPrice = await getPrices(); 47 | const assetPrice = lastestPrice[symbol].price; 48 | return assetPrice; 49 | } catch (error) { 50 | throw `Error in getting asset price: ${error}`; 51 | } 52 | }; 53 | 54 | module.exports = safeScan; 55 | -------------------------------------------------------------------------------- /functions/helpers.js: -------------------------------------------------------------------------------- 1 | const { readFile, writeFile } = require('fs').promises; 2 | 3 | const returnPercentageOfX = (x, percentage) => { 4 | return (percentage * x) / 100; 5 | }; 6 | const sleep = (ms) => { 7 | return new Promise((resolve) => setTimeout(resolve, ms)); 8 | }; 9 | 10 | const removeDuplicates = (array) => { 11 | return [...new Set(array)]; 12 | }; 13 | 14 | const detectVolatiles = (initialPrices, lastestPrices) => { 15 | const volatiles = []; 16 | for (const coin in initialPrices) { 17 | const changePercentage = 18 | ((lastestPrices[coin]['price'] - initialPrices[coin]['price']) / initialPrices[coin]['price']) * 100; 19 | if (changePercentage >= process.env.VOLATILE_TRIGGER) { 20 | const formatedChange = Number(changePercentage).toFixed(2); 21 | console.log( 22 | `${returnTimeLog()} The price of ${coin} has increased ${formatedChange}% 23 | within last ${process.env.INTERVAL} minutes...` 24 | ); 25 | volatiles.push(coin); 26 | } 27 | } 28 | return removeDuplicates(volatiles); 29 | }; 30 | 31 | const returnTimeLog = () => `[${new Date().toLocaleString()}] `; 32 | 33 | const readPortfolio = async () => { 34 | try { 35 | return JSON.parse(await readFile('holding-assets.json')); 36 | } catch (error) { 37 | throw `Error reading portfolio: ${error}`; 38 | } 39 | }; 40 | 41 | const savePortfolio = async (data) => { 42 | try { 43 | await writeFile('holding-assets.json', JSON.stringify(data, null, 4), { flag: 'w' }); 44 | } catch (error) { 45 | throw `Error saving portfolio: ${error}`; 46 | } 47 | }; 48 | 49 | const getBinanceConfig = async () => { 50 | try { 51 | return JSON.parse(await readFile('exchange-config.json')); 52 | } catch (error) { 53 | throw `Error getting exchange config: ${error}`; 54 | } 55 | }; 56 | 57 | module.exports = { 58 | returnPercentageOfX, 59 | sleep, 60 | getBinanceConfig, 61 | detectVolatiles, 62 | returnTimeLog, 63 | readPortfolio, 64 | savePortfolio, 65 | }; 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Config file 107 | config.env 108 | exchange-config.json -------------------------------------------------------------------------------- /functions/buy.js: -------------------------------------------------------------------------------- 1 | const { readFile } = require('fs').promises; 2 | const binance = require('../binance'); 3 | const { MARKET_FLAG } = require('../constants'); 4 | const { 5 | returnPercentageOfX, 6 | returnTimeLog, 7 | readPortfolio, 8 | savePortfolio, 9 | getBinanceConfig, 10 | } = require('./helpers'); 11 | 12 | const { VOLATILE_TRIGGER, INTERVAL, QUANTITY, MIN_QUANTITY, TP_THRESHOLD, SL_THRESHOLD } = process.env; 13 | 14 | const calculatePortfolioValue = (portfolio) => { 15 | let value = 0; 16 | if (portfolio.length) { 17 | portfolio.forEach(({ quantity, bought_at }) => { 18 | value += quantity * bought_at; 19 | }); 20 | } 21 | return value; 22 | }; 23 | 24 | const buy = async (coin, quantity) => { 25 | try { 26 | const orderData = await binance.marketBuy(coin, quantity, (flags = MARKET_FLAG)); 27 | return orderData; 28 | } catch (error) { 29 | throw `Error in executing buy function: ${error.body || JSON.stringify(error)}`; 30 | } 31 | }; 32 | 33 | const calculateBuyingQuantity = async (symbol, length, portfolio) => { 34 | try { 35 | const exchangeConfig = await getBinanceConfig(); 36 | const { stepSize } = exchangeConfig[symbol]; 37 | const currentPortfolioValue = calculatePortfolioValue(portfolio); 38 | 39 | // The budget is splited equally for each order 40 | let allowedAmountToSpend = QUANTITY / length; 41 | 42 | /* Generally the bot will not spend 100% (only like 98-99%) of the budget because the the actual quantity is rounded down 43 | Do not buy if current portolio value is greater than 90% of the orignal quantity */ 44 | if (currentPortfolioValue >= returnPercentageOfX(QUANTITY, 90)) { 45 | throw `Current portfolio value exceeds the initial quantity, waiting for the current asset(s) to be sold first...`; 46 | } 47 | 48 | /* 49 | In case the allowed amount smaller than the min qty, proceed to buy the with the min qty 50 | For example in an interval, there are 4 coins to buy and the budget is 30... 51 | since you can't buy with 30/4 = 7.5 USDT, the allowed amount is increased to 11 52 | In this case, only the first two coins in this batch will be bought at 11 USDT each, 8 USDT won't be spent 53 | */ 54 | if (allowedAmountToSpend < MIN_QUANTITY) { 55 | allowedAmountToSpend = MIN_QUANTITY; 56 | } 57 | 58 | const price = await binance.prices(symbol); 59 | const quantity = allowedAmountToSpend / price[symbol]; 60 | const quantityBasedOnStepSize = await binance.roundStep(quantity, stepSize); 61 | return quantityBasedOnStepSize; 62 | } catch (error) { 63 | throw `Error in calculating quantity: ${JSON.stringify(error)}`; 64 | } 65 | }; 66 | 67 | const handleBuy = async (volatiles) => { 68 | if (volatiles.length) { 69 | for (const symbol of volatiles) { 70 | try { 71 | const portfolio = await readPortfolio(); 72 | const quantity = await calculateBuyingQuantity(symbol, volatiles.length, portfolio); 73 | const purchaseData = await buy(symbol, quantity); 74 | const { price } = purchaseData.fills[0]; 75 | const orderData = { 76 | symbol, 77 | quantity, 78 | orderId: purchaseData.orderId, 79 | bought_at: Number(price), 80 | TP_Threshold: Number(price) + returnPercentageOfX(Number(price), TP_THRESHOLD), 81 | SL_Threshold: Number(price) - returnPercentageOfX(Number(price), SL_THRESHOLD), 82 | purchase_time: new Date().toLocaleString(), 83 | purchase_time_unix: new Date().getTime(), 84 | updated_at: new Date().toLocaleString(), 85 | }; 86 | portfolio.push(orderData); 87 | console.log(`${returnTimeLog()} Successfully place an order: ${JSON.stringify(orderData)}`); 88 | await savePortfolio(portfolio); 89 | } catch (error) { 90 | console.log( 91 | `${returnTimeLog()} Error in executing buying volatiles function: ${ 92 | error.body || JSON.stringify(error) 93 | }` 94 | ); 95 | } 96 | } 97 | } else { 98 | console.log( 99 | `${returnTimeLog()} No coin has risen more than ${VOLATILE_TRIGGER}% in the last ${INTERVAL} minutes` 100 | ); 101 | } 102 | }; 103 | 104 | module.exports = { handleBuy }; 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Binance-volatility-trading-bot-JS 2 | 3 | I take the idea from this project. The original bot is written in Python. I quite like it so I re-write it in Java Script to fix some issues and add some improvements. Shoutout to CyberPunkMetalHead for such an awesome contribution 4 | 5 | Here's the main functions of the bot: 6 | 7 | 1. Listen to the prices on Binance for every interval of 4 minutes. 8 | 2. Figure out which coin's price has increase by 3% in each interval and proceed to buy. 9 | 3. Track the bought assets' prices every interval, sell at 6% profit or 3% stop loss. 10 | 4. If you choose to keep holding the asset when it hits TP, that's possible in [TRAILING_MODE](#trailing-desc) 11 | 5. Record sold assets so users can see which coins were sold at what profit, so they can refine variables and automatically exclude some pairs when trading. 12 | 13 | All the of the variables: Budget, Interval, Take profit or Stop loss thresholds, The change in price to trigger the buy function... are configurable by the users 14 | 15 | # Installation 16 | 17 | 1. Requirements: 18 | 23 | 24 | 2. Download the project here 25 | 3. Open the terminal at the root folder, run 26 | 27 | > npm install 28 | 29 | to install necessary packages 30 | 31 | 4. Create a new config.env file based on the config.env.example file at the root folder. Place your configurations there. Again, whoever has this file can place orders from your account 32 |
33 | To retrieve your Binance API Key and Secret on both Testnet and Mainnet, I find no better guide than this one over here 34 | 35 | 5. The bot is default to run on the Testnet. If you want to switch to Mainnet, set the TEST_MODE constant (in the constants.js file) to false 36 | 37 | > const TEST_MODE = false; 38 | 39 | 6. Finally, to start the script, open your terminal and run 40 | 41 | > npm run start 42 | 43 | 7. To stop the bot, hit Ctrl + C combination in the terminal 44 | 45 | # Notes 46 | 47 | 1. Create a config.env file in the root folder and place your configurations there. For the love of God don't expose this file since it contains your API keys. 48 | 2. If you set the budget (QUANTITY) of 50 USDT, the bot will not spend more than 50 USDT on trading (It checks the current portfolio first before making the purchase decision). 49 | 3. If the inital QUANTITY is 50 USDT and there are 2 coins to buy in that interval, the bot allocates 25 USDT for each coin order. 50 | 4. If the inital QUANTITY is 50 USDT and there is one asset worths 30 USDT in the portfolio, the bot will spend 20 USDT for following orders. 51 | 5. The bot is default to sell 99.5% of the bought amount. The reason is sometimes you can't sell 100% of an asset on Binance. If you have some BNBs to pay for transactions then you can set the 99.5% ratio to 100%. This is configurable. 52 | 6. Generally, you better place an order with at least 11 USDT to be accepted by Binance. 53 | 7. TRAILING MODE DESCRIPTION:
54 | This mode runs by default. 55 | Every time an asset hits the TP, the bot doesn't sell it immediately. 56 | The SL and TP threshold of that asset is increased. 57 | If an asset hits the SL, we sell (In fact, we just sell at SL). 58 | 59 | For example, BTCUSDT is bought at 100. TP is 106 (6%) and SL is 97 (3%). 60 | When it hits 106, the TP is adjusted to ~109 and SL is ~103. 61 | Whenever it hits SL (97 or 103...), the bot sells. 62 | 63 | Disable this feature by setting "TRAILING_MODE" (in the constants.js file) to false 64 | 65 | 8. SAFE_MODE DESCRIPTION:
66 | This mode runs by default. 67 | To avoid the rapid ups and immediate downs within the next interval, 68 | after an asset is bought, the bot scan to check the asset price every 1 minute 69 | (1 is the default value, you can change the SCAN_INTERVAL in the config.env file). 70 | If the asset price hits SL threshold during that 1 minute, the bot will proceed to sell the asset. 71 | 72 | Disable this feature by setting "SAFE_MODE" (in the constants.js file) to false 73 | 74 | # Contribution 75 | 76 | If you run into some issues or have some suggestions, feel free to open an issue at the project's repo. I would be more than happy to read/approve some pull requests :). 77 | -------------------------------------------------------------------------------- /functions/sell.js: -------------------------------------------------------------------------------- 1 | const binance = require('../binance'); 2 | const { readFile, writeFile } = require('fs').promises; 3 | const { 4 | returnPercentageOfX, 5 | returnTimeLog, 6 | savePortfolio, 7 | readPortfolio, 8 | getBinanceConfig, 9 | } = require('./helpers'); 10 | 11 | const { MARKET_FLAG, TRAILING_MODE, TEST_MODE } = require('../constants'); 12 | const { TP_THRESHOLD, SL_THRESHOLD } = process.env; 13 | 14 | const sell = async (exchangeConfig, { symbol, quantity }) => { 15 | try { 16 | const { stepSize } = exchangeConfig[symbol]; 17 | const actualQty = returnPercentageOfX(quantity, process.env.ACTUAL_SELL_RATIO); 18 | const roundedQty = await binance.roundStep(actualQty, stepSize); 19 | const sellData = await binance.marketSell(symbol, roundedQty, (flags = MARKET_FLAG)); 20 | return sellData; 21 | } catch (error) { 22 | throw `Error in selling ${quantity} of ${symbol}: ${error.body || JSON.stringify(error)}`; 23 | } 24 | }; 25 | 26 | const saveSuccessOrder = async (order, coinRecentPrice) => { 27 | try { 28 | const successOrders = JSON.parse(await readFile('sold-assets.json')); 29 | const displayProfit = ((coinRecentPrice - order.bought_at) / order.bought_at) * 100; 30 | 31 | const successOrder = { 32 | ...order, 33 | sell_time: new Date().toLocaleString(), 34 | sell_at: Number(coinRecentPrice), 35 | profit: `${displayProfit.toFixed(2)}%`, 36 | }; 37 | successOrders.push(successOrder); 38 | await writeFile('sold-assets.json', JSON.stringify(successOrders, null, 4), { flag: 'w' }); 39 | const { symbol, profit } = successOrder; 40 | console.log( 41 | `${returnTimeLog()} The asset ${symbol} has been sold sucessfully at the profit of ${profit} and recorded in sold-assets.json` 42 | ); 43 | } catch (error) { 44 | throw `Error in saving success order: ${error}`; 45 | } 46 | }; 47 | 48 | const handleSellData = async (sellData, coinRecentPrice, order) => { 49 | try { 50 | const { symbol, TP_Threshold, SL_Threshold, quantity } = order; 51 | if (TEST_MODE ? sellData.status : sellData.status === 'FILLED') { 52 | if (coinRecentPrice >= TP_Threshold) { 53 | console.log(`${returnTimeLog()} ${symbol} price has hit TP threshold`); 54 | } else if (coinRecentPrice <= SL_Threshold) { 55 | console.log(`${returnTimeLog()} ${symbol} price has hit SL threshold`); 56 | } 57 | await saveSuccessOrder(order, coinRecentPrice); 58 | await removeSymbolFromPortfolio(symbol); 59 | } else { 60 | console.log( 61 | `${returnTimeLog()} Sell order: ${quantity} of ${symbol} not executed properly by Binance, waiting for another chance to sell...` 62 | ); 63 | } 64 | } catch (error) { 65 | throw `Error in handling sell data ${error.body || JSON.stringify(error)}`; 66 | } 67 | }; 68 | 69 | const changeOrderThresholds = async ({ symbol }, coinRecentPrice) => { 70 | try { 71 | const orders = await readPortfolio(); 72 | const updatedOrders = orders.map((order) => { 73 | if (order.symbol !== symbol) { 74 | return order; 75 | } else { 76 | // The TP threshold achieved will act as the base for new TP_Threshold and SL_Threshold 77 | const updatedOrder = { 78 | ...order, 79 | updated_at: new Date().toLocaleString(), 80 | TP_Threshold: Number(coinRecentPrice) + returnPercentageOfX(Number(coinRecentPrice), TP_THRESHOLD), 81 | SL_Threshold: Number(coinRecentPrice) - returnPercentageOfX(Number(coinRecentPrice), SL_THRESHOLD), 82 | }; 83 | return updatedOrder; 84 | } 85 | }); 86 | await savePortfolio(updatedOrders); 87 | console.log( 88 | `${returnTimeLog()} The ${symbol} has hit TP threshold and we continue to hold as TRAILING MODE activated` 89 | ); 90 | } catch (error) { 91 | throw `Error in changing order thresholds: ${error}`; 92 | } 93 | }; 94 | 95 | const handlePriceHitThreshold = async (exchangeConfig, order, coinRecentPrice) => { 96 | try { 97 | const { TP_Threshold, SL_Threshold } = order; 98 | 99 | // In TRAILING_MODE, only sell if the asset's price hits SL 100 | if (TRAILING_MODE && coinRecentPrice >= TP_Threshold) { 101 | await changeOrderThresholds(order, coinRecentPrice); 102 | } else if (!TRAILING_MODE || coinRecentPrice <= SL_Threshold) { 103 | const sellData = await sell(exchangeConfig, order); 104 | await handleSellData(sellData, coinRecentPrice, order); 105 | } 106 | } catch (error) { 107 | throw `Error in handling price hitting threshold: ${error.body || JSON.stringify(error)}`; 108 | } 109 | }; 110 | 111 | const handleSell = async (lastestPrice) => { 112 | const orders = await readPortfolio(); 113 | if (orders.length) { 114 | const exchangeConfig = await getBinanceConfig(); 115 | orders.forEach(async (order) => { 116 | try { 117 | const { symbol, TP_Threshold, SL_Threshold, quantity } = order; 118 | const { price: coinRecentPrice } = lastestPrice[symbol]; 119 | 120 | if (coinRecentPrice >= TP_Threshold || coinRecentPrice <= SL_Threshold) { 121 | await handlePriceHitThreshold(exchangeConfig, order, coinRecentPrice); 122 | } else { 123 | console.log( 124 | `${returnTimeLog()} ${symbol} price hasn't hit SL or TP threshold, continue to wait...` 125 | ); 126 | } 127 | } catch (error) { 128 | console.log(`${returnTimeLog()} Error in excuting sell function: ${JSON.stringify(error)}`); 129 | } 130 | }); 131 | } else { 132 | console.log(`${returnTimeLog()} The portfolio is currently empty, wait for the chance to sell...`); 133 | } 134 | }; 135 | 136 | const removeSymbolFromPortfolio = async (symbol) => { 137 | try { 138 | const orders = await readPortfolio(); 139 | const updatedOrders = orders.filter((order) => order.symbol !== symbol); 140 | await savePortfolio(updatedOrders); 141 | } catch (error) { 142 | console.log(`${returnTimeLog()} Error in removing symbol from portfolio: ${error}`); 143 | } 144 | }; 145 | 146 | module.exports = { handleSell, sell, handleSellData }; 147 | --------------------------------------------------------------------------------