DEV Community

Sascha Becker
Sascha Becker

Posted on • Originally published at Medium on

A cryptocurrency trading bot that doesn’t scam you

Cryptocurrency trading bots are becoming the next big thing people think they need. And so they buy one. So did I.

This is not a copy & pasta example. It’s more a guideline on how to achieve what you want

Step 0: Learn throwing money away the hard way

Investing in a third party cryptocurrency trading bot makes you feel you did the right thing. You can become finally rich by doing nothing. The bot will make money for you, right? Wrong!

You just gave away money to buy this bot to make the developer rich. And he does nothing, except some updates here and there. So in that way he does make money with a cryptocurrency bot.

But you are still not rich. Dang it! What to do?

Step 1: Write your own

The only bot you should trust comes from a codebase you build. It comes from a logic and pattern you invented. Don’t let anyone tell you what’s right.

I coded one and you can, too! So let’s get started. (Spoiler: I’m not rich, yet)

You can also clone my github repo if you want.

https://github.com/TeamWertarbyte/crypto-trading-bot

Step 2: Find a good cryptocurrency trading market

This is the hardest part. Find a good place for your bot to trade on. Good starting points are either bittrex or kraken. The both have good APIs to work on.

https://bittrex.com/Home/Api

Kraken | Buy, Sell and Margin Trade Bitcoin (BTC) and Ethereum (ETH) - API

Create an account and let’s get to the next step.

Step 3: Define the bot environment

For my bot I stuck with bittrex.com It has a solid API and good response times. They also provide a websocket for live updates.

The framework I’m using for this example is node js. But you can choose whatever you like. I wanted to have it run via docker-compose in the command line. So node js was a good pick.

The API can be accessed via a npm package like

dparlevliet/node.bittrex.api

Or you write your own REST client to access bittrex. The documentation for the API in bittrex is quite outdated. It is for version 1.1 which is in use for quite some time now. Bittrex also accepts calls from, the not yet official released, version 2.0. The mentioned package for node does a pretty good job implementing the shiny new 2.0 calls. Use that!

Step 4: Define your trade logic

As you want to crunch the numbers you some market indicators. For me it was the EMA (Exponential Moving Average) value. You can read more about other values here:

Market Indicators [ChartSchool]

I chose two market indicators on an hour basis.

  • (RED) EMA with a period of 50
  • (BLUE) EMA with a period of 15

Alone they are not very effective. But look at those points where they cross. They are the ideal entry or exit points for buy or sell orders.

So my trade logic was clear

Buy if EMA 15 is greater than EMA 50 and sell if EMA 50 is greater than EMA 15. This way my money is always in altcoins that are raising and once they fall sell it back to bitcoin.

This is just one out of dizillion combinations. Try what’s best for you. I’m working on an AI that detects the best market indicators and creates new ones but that will be another post.

Step 5: Docker

Your bot has to run 24/7 otherwise you won’t be profitable. So I wrapped my bot in a docker compose which runs forever.

Dockerfile:

FROM node:latest

# Create app directory
RUN mkdir - p / usr / src / app
WORKDIR / usr / src / app

# Install app dependencies
COPY package.json / usr / src / app/  
RUN npm install

# Bundle app source
COPY. / usr / src / app

RUN npm run build
CMD ["npm", "start"]

docker-compose.yml

version:'2'
services:  
 server:  
 build:.
restart:'unless-stopped'

Step 6: The loop

Let’s define the loop and its configs. I will explain every loop action step by step so be patient. Our config object will grow over time. Our constructor creates three internal variables.

  • bittrex — The access point to the bittrex api
  • currencies — Holds our populated coins, its values, market indicators and everything else coin related
  • bitcoin — It’s just the extracted bitcoin coin from the currencies

I use bluebird as my promise library because of concurrency control.

const now = require('performance-now')
const Promise = require('bluebird')
const log = require('fancy-log')

Promise.config({
 cancellation: true  
})

const configs = {
 cancelOpenOrdersOnStart: true,
 refreshTimeout: 180000 // 3 minutes
}

const INVEST\_HOLD\_OR\_REJECT = {
 HOLD: 'HOLD',
 INVEST: 'INVEST',
 REJECT: 'REJECT',
 NONE: 'NONE'
}

export default class Watcher {
 constructor (bittrex) {
this.bittrex = bittrex
this.currencies = []
this.bitcoin = {}
 }

async watch () {
const start = now()
 log.info(`## Started emma watch ##`)

 configs.cancelOpenOrdersOnStart && await this.cancelOldOrders()

await this.fetchMarkets()
await this.fetchBalances()

await this.injectMarketSummaries( this.currencies)
await this.filterCurrenciesByMarkets(configs.markets)
await this.injectCandleData( this.currencies)
await this.injectMarketIndicators( this.currencies)
await this.countEmaCrossPointTicks( this.currencies)
await this.injectClosedOrderHistory( this.currencies)

await this.injectMarketSummaries( this.currencies)
await this.evaluateInvestHoldOrReject( this.currencies)

await this.rejectBadInvestments( this.currencies)
await this.invest( this.currencies)

 log.info(`## Finished ${(now() - start).toFixed(5)} ms ##`)
 setTimeout(() =\> this.watch(), configs.refreshTimeout)
 }
}

Step 6.1: Cancel old orders

Orders made by our bot may not always go through. So we have to cancel old orders if they won’t be accepted.

async cancelOldOrders () {
const start = now()
const openOrders = await this.bittrex.getOpenOrders()

if (openOrders.length \> 0) {
for ( let openOrder of openOrders) {
const elapsedHours = ( new Date() - new Date(openOrder.Opened)) / 1000 / 60 / 60
if (elapsedHours \> configs.maxHoursToHoldOrder) {
 log.info(`Cancel ${openOrder.OrderType} on ${openOrder.Exchange} du to older than ${configs.maxHoursToHoldOrder} hours`)
await this.bittrex.cancel({uuid: openOrder.OrderUuid})
 }
 }
 log.info(`Canceled old orders ${(now() - start).toFixed(5)} ms`)
 }
}

I added a config variable to control elapsed hours to hold an order.

const configs = {
 cancelOpenOrdersOnStart: true ,
 maxHoursToHoldBalance: 168, // 7 days
 refreshTimeout: 180000, // 3 minutes
}

Step 6.2: Populate the currencies

We fetch the current markets and filter only the BTC markets. I don’t support ETH markets for now.

async fetchMarkets () {
const start = now()
this.currencies = await this.bittrex.getMarkets()

 // remove other market than BTC for now
this.currencies = this.currencies.filter(c =\> c.BaseCurrency === 'BTC')
 log.info(`Fetched currencies ${(now() - start).toFixed(5)} ms`)
}

Then we need to inject the current balances of the coins to see their actual value from our wallet. I also separate bitcoin at this stage.

async fetchBalances () {
const start = now()
const balances = await this.bittrex.getBalances()
for ( let currency of this.currencies) {
const balance = balances.find((b) =\> b.Currency === currency.MarketCurrency)
if (balance) {
 currency.balance = balance
 } else {
 currency.balance = null  
}
 }

this.bitcoin = balances.find((b) =\> b.Currency === 'BTC')

 log.info(`Fetched balances ${(now() - start).toFixed(5)} ms`)
}

Step 6.3: Get market live data

To get get BID, ASK and LATEST we need to inject the market summaries into our currencies.

async injectMarketSummaries (data) {
const start = now()

const marketSummaries = await this.bittrex.getMarketSummaries({\_: Date.now()})

await Promise.map(data, async (d) =\> {
const marketSummary = marketSummaries.find((ms) =\> d.BaseCurrency === ms.Market.BaseCurrency && d.MarketCurrency === ms.Market.MarketCurrency)
 d.marketSummary = marketSummary.Summary
 })

 log.info(`Injected market summaries ${(now() - start).toFixed(5)} ms`)
}

Step 6.4: Filter your currencies

I added specific markets where I want to trade on.

const configs = {
 cancelOpenOrdersOnStart: true ,
 maxHoursToHoldOrder: 0.5,
 refreshTimeout: 180000, // 3 minutes
 markets: [
 'ADA',
 'BCC',
 'DASH'
 ]
}

You can filter also by volume or a specific BTC value. Or you just don’t filter and trade with all available coin.

async filterCurrenciesByMarkets (markets) {
const start = now()

this.currencies = this.currencies.filter(c =\> markets.includes(c.MarketCurrency) || (c.balance && c.balance.Available \> 0))

 log.info(`Filtered currencies by selected markets ${(now() - start).toFixed(5)} ms`)
}

Step 6.5: Light the candles

To crunch some market indicators we need the candles. A candle describes a value for the selected market in a timeframe with a given step interval.

http://globaltrendtraders.com/technical-analysis/technical-analysis-of-stock-trends-1-chart-types/

You can read more about candles here:

Technical Analysis of Stock Trends #1 - Chart Types

async injectCandleData (data) {
const start = now()
const \_ = Date.now()

const USDT\_BTC = await this.bittrex.getCandles({
 marketName: 'USDT-BTC',
 tickInterval: configs.tickInterval,
 \_
 })

await Promise.map(data, async (d) =\> {
 d.candles = await this.bittrex.getCandles({
 marketName: `BTC-${d.MarketCurrency}`,
 tickInterval: configs.tickInterval,
 \_
 })

 d.candles = d.candles.map((c, i) =\> this.parseCandleData(c, USDT\_BTC[i]))
 }, {concurrency: 15})

 log.info(`Injected candle data ${(now() - start).toFixed(5)} ms`)
}

parseCandleData (d, USDT\_BTC) {
return {
 date: parseDate(d.T),
 open: USDT\_BTC.O \* d.O,
 high: USDT\_BTC.H \* d.H,
 low: USDT\_BTC.L \* d.L,
 close: USDT\_BTC.C \* d.C,
 volume: d.V
 }
}

The step interval is a new config variable called tickInterval. You can go as low as five minutes or as long as several hours. I went with an hour as it’s more robust and I don’t want fast trading.

const configs = {
 cancelOpenOrdersOnStart: true ,
 maxHoursToHoldOrder: 0.5,
 refreshTimeout: 180000, // 3 minutes
 tickInterval: 'hour',
 markets: [
 'ADA',
 'BCC',
 'DASH'
 ]
}

Also not that I multiply the values with USDT_BTC. Bittrex always displays a BTC value. As I don’t want to trade on a given BTC instead on the actual USD value of the currency I have to calculate a bit.

You can skip the USDT_BTC part if you want to trade on raw BTC value curves.

Step 6.6: Crunch the numbers

It’s time to get our market indicators. But wait! You don’t have to calculate it by yourself. there’s a package for that. Install react stockcharts together with react and you got the math for free.

rrag/react-stockcharts

I want to use EMA. We also need a time and date parser.

import { timeParse } from'd3-time-format'
import { ema } from'react-stockcharts/lib/indicator'

const parseDate = timeParse('%Y-%m-%dT%H:%M:%S')

First let’s inject our market indicators.

async injectMarketIndicators (data) {
const start = now()

await Promise.map(data, async (d) =\> {
 d.keyFigures = await this.calculateMarketIndicators(d.candles)
 })

 log.info(`Calculated key figures ${(now() - start).toFixed(5)} ms`)
}

calculateMarketIndicators (data) {
const ema50 = ema()
 .id(0)
 .options({windowSize: 50})
 .merge((d, c) =\> {d.ema50 = c})
 .accessor(d =\> d.ema50)

const ema15 = ema()
 .id(1)
 .options({windowSize: 15})
 .merge((d, c) =\> {d.ema15 = c})
 .accessor(d =\> d.ema15)

return ema50(ema15(data))
}

The windowSize defines the timespan to calculate the indicators from.

Step 6.7: Count steps since last EMA crossing

As an EMA Crossing I define a point in time where my two defined EMA values (EMA50 and EMA15) crossed. Like a break even for cryptocurrencies.

async countEmaCrossPointTicks (data) {
const start = now()

await Promise.map(data, async (d) =\> {
const lastKeyFigures = d.keyFigures[d.keyFigures.length - 1]
const lastEmaDifference = lastKeyFigures.ema50 - lastKeyFigures.ema15

 d.positiveTicks = 0
 d.negativeTicks = 0
for ( let i = d.keyFigures.length - 1; i \> 0; i--) {
const keyFigures = d.keyFigures[i]

if (lastEmaDifference \> 0 && keyFigures.ema50 - keyFigures.ema15 \> 0) {
 d.negativeTicks++
 } else if (lastEmaDifference \< 0 && keyFigures.ema50 - keyFigures.ema15 \< 0) {
 d.positiveTicks++
 } else {
break  
}
 }
 })

 log.info(`Counted ticks since last ema crossing ${(now() - start).toFixed(5)} ms`)
}

Every currency now has either a positive or a negative step counter. This tells me how very good or very bad it would be to invest in this currency at this given point in time.

Step 6.8: Inject closed order history

In the past I made decisions on how much profit I made since I bought that coin. Or how much loss I gained since last buy order. Currently I don’t use that information.

async injectClosedOrderHistory (data) {
const start = now()

await Promise.map(data, async (d) =\> {
 d.orderHistory = await this.bittrex.getOrderHistory({
 market: `BTC-${d.MarketCurrency}`
 })
 }, {concurrency: 10})

 log.info(`Injected closed orders ${(now() - start).toFixed(5)} ms`)
}

Every currency now has a orderHistory array filled with old orders. Bittrex does sadly only serve orders from the last 30 days or 500 entries in total. So use that with caution.

Step 6.9: Refresh market history

As we calculate and crunch the numbers time has passed by and the latest currency value might wrong at this time. So we refresh it in the loop.

await this.injectMarketSummaries( this.currencies)

Step 6.10: Decide where to invest and what to sell

This will be the hardest part. When is it perfect to invest in a currency? When is it better to reject a current invest and start over? When do you do nothing and wait?

I decided to only look on my EMA crossings and how long the are now. That’s what my counted ticks are for. I also added a minimum tick counter value config variable to prevent invest hopping (buy, sell, buy, sell in a short time).

const configs = {
 cancelOpenOrdersOnStart: true ,
 maxEffectiveLoss: 60, // 60%
 maxHoursToHoldOrder: 0.5,
 refreshTimeout: 180000, // 3 minutes
 tickInterval: 'hour',
 markets: [
 'ADA',
 'BCC',
 'DASH'
 ],
 minimumEmaTicks: 5
}

I also introduced a maximum loss cap to reject invest that fall way to low.

async evaluateInvestHoldOrReject (data) {
const start = now()

await Promise.map(data, async (d) =\> {
const lastKeyFigures = d.keyFigures[d.keyFigures.length - 1]

 d.holdOrReject = INVEST\_HOLD\_OR\_REJECT.HOLD

if (d.balance && d.balance.Available \> 0) {
if (lastKeyFigures.ema15 \< lastKeyFigures.ema50 && d.negativeTicks \>= configs.minimumEmaTicks) {
 log.info(`Will reject ${d.MarketCurrency} due to ${d.negativeTicks} negative ema ticks`)
 d.holdOrReject = INVEST\_HOLD\_OR\_REJECT.REJECT
 } else if (d.balance.Available \* d.marketSummary.Bid \< configs.btcPerInvest \* (1 - configs.maxEffectiveLoss / 100)) {
 log.info(`Will reject ${d.MarketCurrency} due to falling below ${configs.btcPerInvest} btc`)
 d.holdOrReject = INVEST\_HOLD\_OR\_REJECT.REJECT
 }
 } else if (!d.balance || (d.balance && d.balance.Available === 0)) {
if (lastKeyFigures.ema15 \> lastKeyFigures.ema50 && d.positiveTicks \>= configs.minimumEmaTicks) {
 log.info(`Will invest in ${d.MarketCurrency} due to ${d.positiveTicks} positive ema ticks. EMA15 ${lastKeyFigures.ema15} \> EMA50 ${lastKeyFigures.ema50}`)
 d.holdOrReject = INVEST\_HOLD\_OR\_REJECT.INVEST
 }
 }
 })
 log.info(`Evaluated invest hold or reject ${(now() - start).toFixed(5)} ms`)
}

Step 6.11: Reject bad investments

As you evaluated investments as bad they will be sold. So let’s do so.

async rejectBadInvestments (data) {
const start = now()

await Promise.map(data, async (d) =\> {
if (d.holdOrReject === INVEST\_HOLD\_OR\_REJECT.REJECT) {
if (d.marketSummary.Bid \* d.balance.Available \>= configs.minimumSellBalanceInBTC && d.balance.Available \> d.MinTradeSize) {
try {
await this.bittrex.sellLimit({
 market: `${d.BaseCurrency}-${d.MarketCurrency}`,
 quantity: d.balance.Available,
 rate: (d.marketSummary.Bid - configs.rateSellBuyExtraBtc)
 })
 } catch (e) {
 log.info(e)
 }
 log.info(`${d.MarketCurrency} placed REJECT SELL order`)
 }
 }
 }, {concurrency: 20})
 log.info(`Rejected investments ${(now() - start).toFixed(5)} ms`)
}

I added configs.rateSellBuyExtraBtc to add a tiny amount of BTC to secure the sell and buy order. Also configs.minimumSellBalanceInBTC was added to check the bittrex sell threshold. Bittrex doesn’t allow orders smaller than that.

const configs = {
 cancelOpenOrdersOnStart: true ,
 minimumSellBalanceInBTC: 0.0005,
 maxEffectiveLoss: 60, // 60%
 maxHoursToHoldOrder: 0.5,
 refreshTimeout: 180000, // 3 minutes
 rateSellBuyExtraBtc: 0.0000000000001, // to secure buy or sell
 tickInterval: 'hour',
 markets: [
 'ADA',
 'BCC',
 'DASH'
 ],
 minimumEmaTicks: 3
}

Step 6.12: Invest all the bitcoin!

Now let’s throw all our remaining bitcoin value into currencies that seems worth it. I sorted my options by EMA ticks to invest in fresh risers first.

I also instroduced two new config variables.

const configs = {
 btcBuffer: 0.01, // btc that has to stay on the bank
 btcPerInvest: 0.005,
 cancelOpenOrdersOnStart: true ,
 minimumSellBalanceInBTC: 0.0005,
 maxEffectiveLoss: 60, // 60%
 maxHoursToHoldOrder: 0.5,
 refreshTimeout: 180000, // 3 minutes
 rateSellBuyExtraBtc: 0.0000000000001, // to secure buy or sell
 tickInterval: 'hour',
 markets: [
 'ADA',
 'BCC',
 'DASH'
 ],
 minimumEmaTicks: 3
}
  • btcPerInvest defines a fixed BTC value which will be used to make that order happen. You can also scale that value exponential based on your available bitcoin.
  • btcBuffer defines a fixed btc value which has to stay and will not used for investment. I like to have some btc left for my manual trades.
async invest (data) {
const start = now()

let buyCounter = 0

 // Sort by tick count from last ema crossing
 data.sort((a, b) =\> a.positiveTicks - b.positiveTicks)
for ( let d of data) {
if ( this.bitcoin.Available \> configs.btcPerInvest + configs.btcBuffer) {
if (d.holdOrReject === INVEST\_HOLD\_OR\_REJECT.INVEST) {
const quantity = (configs.btcPerInvest - 0.00000623) / d.marketSummary.Ask

if (quantity \> d.MinTradeSize) {
try {
await this.bittrex.buyLimit({
 market: `${d.BaseCurrency}-${d.MarketCurrency}`,
 quantity,
 rate: (d.marketSummary.Ask + configs.rateSellBuyExtraBtc)
 })
 buyCounter++
this.bitcoin.Available -= configs.btcPerInvest
 log.info(`Invested ${configs.btcPerInvest} bitcoin in ${d.MarketCurrency} for ${quantity} amount`)
 } catch (e) {
 log.info(e)
 }
 }
 }
 } else {
 log.info(`Not enough btc left to invest. Keep saving more!`)
break  
}
 }

 log.info(`Invested ${configs.btcPerInvest * buyCounter} btc in ${buyCounter} options ${(now() - start).toFixed(5)} ms`)
}

Step 7: Deploy

The bot is done. I push the code onto my server and start it with

$ docker-compose up --build -d

This will build and start my bot. Logs can be viewed via logs of docker-compose.

Step 8: Improve your bot

Your bot will make errors, you will make bad invest decisions based on certain market indicators. Learn from it and improve your code. To be hones, my bot lost nearly half my money before becoming stable and making actual profit again. Don’t give up.


Top comments (0)