Cryptocurrency Trading Bot With Golang

How To Make A Cryptocurrency Trading Bot With Golang

Posted on
A Quick Disclaimer: I'm a programmer and have very little experience trading. Please do your own research before using the algorithm I create for the cryptocurrency trading bot or any other algorithm. Only trade with money you can afford to lose.

In this guide I will show you how to create  a cryptocurrency trading bot with Go. I highly recommend an IDE such as Goland but you can use a simple text editor if you would like. Feel free to take a look at my guide on how to develop with Go on Linux if you don’t have Go installed.

With that out of the way we can move on to the good part. I will be creating my own index, a very simple index, which goes up or down depending on the quantity of sell and buy orders placed. Basically I will only look at orders that are 2{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a} above or below the latest fill order price and use those to make an index. I will use Bittrex for trading as it offers a lot of trade pairs and will focus on BTC-VTC as I believe in both coins and would like to hold either no matter what my bot does.  My index is basically order rate relative to last price multiplied by the number of VTC the order is for. If the latest price is 0.000100 and a buy order for 10000 VTC is placed at 0.000102 my index would go up by 10200 points.

(0.000102/0.000100)*10000 = 10200

This is a really simple index and someone bidding 10000 VTC 2{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a} higher than latest fill price should maybe have a larger impact? However, to keep things simple I decided to just go with this implementation. If someone was to sell 10000 for 2{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a} less than latest price my index would decrease by 10200 in the same way.

Ah and did I mention that my index resets every minute? I wanted my bot to make minute by minute decisions so I reset the index to 0 every minute.  But enough details, let’s get started with some code.

I used a library called go-bittrex but the current version does not support websockets which will give us real time data. There is a fork of go-bittrex that implements a function for websockets and I have decided to go with that instead. In addition to this library you will also need an API key and secret which you will find on the Settings page under the menu API Keys. I activated everything except Withdraw capabilities just in case my keys get compromised. Once you have your keys we can take a look at the example code on the go-bittrex project page:

package main

import (
	"fmt"
	"github.com/toorop/go-bittrex"
)

const (
	API_KEY    = "YOUR_API_KEY"
	API_SECRET = "YOUR_API_SECRET"
)

func main() {
	// Bittrex client
	bittrex := bittrex.New(API_KEY, API_SECRET)

	// Get markets
	markets, err := bittrex.GetMarkets()
	fmt.Println(err, markets)
}

If you run the code above you should see a long list of all the key-pairs.

[{LTC BTC Litecoin Bitcoin 0.01435906 BTC-LTC true false https://bittrexblobstorage.blob.core.windows.net/public/6defbc41-582d-47a6-bb2e-d0fa88663524.png} {DOGE...

Now, it might look a bit weird and that’s because we are simply using print on our object called markets. If we actually took the time to loop through our markets we could take the pieces we want and format it in a better, more readable way. Let’s try to get our account balances and format it a bit better. Make sure you have replaced your api keys with YOUR_API_KEY and YOUR_API_SECRET in the script above. We will replace the code under //Get markets with our own code. My changed code looks like this:

// Get markets
// Get balances
balances, err := bittrex.GetBalances()
if err == nil {
   for _, b := range balances {
      fmt.Println(b.Currency, b.Available, "/", b.Balance, " Deposit:", b.CryptoAddress)
   }
} else {
   fmt.Println(err)
}

This code will loop through all the balances in our account and print the cryptocurrency, how much we have available for trading, how much we have in total and our deposit address if there is one. If you don’t see any balances it could be because you don’t have anything in your account or you might have the wrong api keys. The output for my account looks like the following:

ARK 0 / 0 Deposit: 
BTC 0.01160229 / 0.01160229 Deposit: 1KqNRbFyi9b2Ej248BMnskCcR82Vdenk7a
BTG 0 / 0 Deposit: GYi1VdAJ892Dwkx5JgRHeXUyMk9PYhBLWW
CVC 0 / 0 Deposit: 
LTC 0 / 0 Deposit: LKPnskMDSsKyPMyPibGDVcFgQiqJme1igR
NAV 0 / 0 Deposit: 
QRL 0 / 0 Deposit: 
RISE 0 / 0 Deposit: 
SC 0 / 0 Deposit: 
SNT 0 / 0 Deposit: 
SYS 0 / 0 Deposit: 
UBQ 0 / 0 Deposit: 
VTC 522.26812636 / 522.26812636 Deposit: 
XVG 0 / 0 Deposit:

You can take a look at the bittrex object to see what other methods are available, I doubt I have had a chance to explore them all.

Goland Hints for Cryptocurrency Trading Bot

The Code of a Cryptocurrency Trading Bot

When I set up my trading bot I added a few constants that work with my index to trigger sells or buys and also limits how much I need to gain or lose before selling, buying or cancelling an order. Here are my constants:

const (
   API_KEY       = ""
   API_SECRET    = ""
   BUY_STRING    = "BTC"
   SELL_STRING   = "VTC"
   MARKET_STRING = BUY_STRING + "-" + SELL_STRING
   MIN_GAIN      = 0.02
   MAX_LOSS      = 0.02
   ORDER_RANGE      = 0.02

   BUY_TRIGGER    = 10000.0
   SELL_TRIGGER   = -10000.0
   ORDER_VARIANCE = 0.02
)

I also need some variables that will keep track of balances, orders, prices, my index, if I have an open order and so on. I also added a bool called readyToRun which is set to false for the first 60 seconds as my script gathers data and it also disables all trading if I lose connection to Bittrex. I also added a highIndex and a lowIndex to get an idea for what the max and min are in one minute for my index.

var (
   balances     []bittrex.Balance
   orders       []bittrex.Order
   ticker       = bittrex.Ticker{}
   lastPrice    float64
   lastBuyPrice = 0.00
   buySellIndex = 0.00
   openOrder    = false
   readyToRun   = false

   highIndex = 0.00
   lowIndex  = 0.00
)

The main function is what is initially called when you start running my program. This function will call all other functions.

func main() {

   // Bittrex client
   bittrexClient := bittrex.New(API_KEY, API_SECRET)

   go updateStats(bittrexClient)

   // A Simple Trading Strategy
   // We create our own buy/sell index, this resets with every buy and sell
   // If we buy and incur a 2{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a} loss we sell at current ask
   // If we buy and make at least a 2{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a} profit and our index is sell we sell
   // If we place an order and it does not clear and market moves -+2{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a} we cancel
   // Every trade has a 0.25{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a} fee

   ch := make(chan bittrex.ExchangeState, 16)

   go subscribeMarket(bittrexClient, ch)

   for st := range ch {
      // Order placed
      for _, b := range st.Buys {
         //log.Println("Buy: ", b.Quantity, " for ", b.Rate, " as ", b.Type)
         quantity, _ := b.Quantity.Float64()
         rate, _ := b.Rate.Float64()
         calculateIndex(true, quantity, rate)
      }
      for _, s := range st.Sells {
         //log.Println("Sell: ", s.Quantity, " for ", s.Rate, " as ", s.Type)
         quantity, _ := s.Quantity.Float64()
         rate, _ := s.Rate.Float64()
         calculateIndex(false, quantity, rate)
      }
      // Order actually fills
      for _, f := range st.Fills {
         //log.Println("Fill: ", f.Quantity, " for ", f.Rate, " as ", f.OrderType)
         // We could say that lastPrice is technically the fill price
         lastPrice, _ = f.Rate.Float64()
      }
      log.Printf("BuySellIndex:     {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f\n", buySellIndex)
      decideBuySell(bittrexClient)
   }
}

Here we create a new bittrex client with our api keys and then use this to perform further actions. We call updateStats as a goroutine, this means it will run asynchronously with the rest of our program. To learn more about goroutines I would recommend checking out Go By Example. The rest of the code will be executed no matter what happens with updateStats. The next thing we do is to make a channel to handle messages received from Bittrex over our websocket connection. The subscribeMarket function will set up a websocket connection that uses the channel that was created and our for loop at the end of the script will handle any incoming messages. The messages I care about are buys, sells and fills. Buys and sells are just orders, a fill is when one of these orders goes through making it “official” so to speak. I use the buys and sells to calculate my index using the calculateIndex function. Any fill is used to update or lastPrice so that we are always acting with the most recent price when deciding to buy or sell. Finally we decide if we should buy or sell anything.

func subscribeMarket(b *bittrex.Bittrex, ch chan bittrex.ExchangeState) {
      log.Println("Connecting to:", MARKET_STRING)
      err := b.SubscribeExchangeUpdate(MARKET_STRING, ch, nil)
      if err != nil {
         log.Println("Error:", err)
      }
      log.Println("Reconnecting....")
      go subscribeMarket(b, ch)
}

The above code handles our websocket connection via the bittrex-go library. If it should end or error for some reason we try reconnecting. This function is poorly written but works. I would sometimes lose the websocket connection and might need to come back and update this function if I figure out how to reconnect in a better way.

To buy or to Sell?

func decideBuySell(b *bittrex.Bittrex) {
   if openOrder {
      // Should we close the open order?
      for _, o := range orders {
         ppu, _ := o.PricePerUnit.Float64()
         log.Printf("Order percent: {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f\n", ppu/lastPrice)
         if ppu/lastPrice > (1.00+ORDER_VARIANCE) || ppu/lastPrice < (1.00-ORDER_VARIANCE) {
            log.Println("Canceled order: ", o.OrderUuid)
            b.CancelOrder(o.OrderUuid)
            // We assume we only have one order at a time
         }
      }
   }
   // If we have no open order should we buy or sell?
   if !openOrder {
      if buySellIndex > BUY_TRIGGER {
         log.Println("BUY TRIGGER ACTIVE!")
         for _, bals := range balances {
            bal, _ := bals.Balance.Float64()
            if BUY_STRING == bals.Currency {
               //log.Printf("Bal: {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}s == {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}s\n", bal/lastPrice, SELL_STRING, bals.Currency)
            }
            if bal > 0.01 && BUY_STRING == bals.Currency && lastPrice > 0.00 {
               // Place buy order
               log.Printf("Placed buy order of {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}s at {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.8f\n=================================================\n", (bal/lastPrice)-5, BUY_STRING, lastPrice)
               order, err := b.BuyLimit(MARKET_STRING, decimal.NewFromFloat((bal/lastPrice)-5), decimal.NewFromFloat(lastPrice))
               if err != nil {
                  log.Println("ERROR ", err)
               } else {
                  log.Println("Confirmed: ", order)
               }
               lastBuyPrice = lastPrice
               openOrder = true
            }
         }
      } else if buySellIndex < SELL_TRIGGER {
         log.Println("SELL TRIGGER ACTIVE!")
         for _, bals := range balances {
            bal, _ := bals.Balance.Float64()
            if SELL_STRING == bals.Currency {
               //allow := "false"
               //if allowSell() {
               // allow = "true"
               //}
               //log.Printf("Bal: {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}s == {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}s && {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}s\n", bal, BUY_STRING, bals.Currency, allow)
            }
            if bal > 0.01 && SELL_STRING == bals.Currency && lastPrice > 0.00 && allowSell() {
               // Place sell order
               log.Printf("Placed sell order of {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}s at {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.8f\n=================================================\n", bal, BUY_STRING, lastPrice)
               order, err := b.SellLimit(MARKET_STRING, decimal.NewFromFloat(bal), decimal.NewFromFloat(lastPrice))
               if err != nil {
                  log.Println("ERROR ", err)
               } else {
                  log.Println("Confirmed: ", order)
               }
               openOrder = true
            }
         }
      }
   }
}
func allowSell() bool {
   if lastBuyPrice > 0 {
      gain := lastPrice / lastBuyPrice
      if gain < (1.00 - MAX_LOSS) {
         return true
      }
      if gain < (1.00 + MIN_GAIN) {
         return false
      }
   }
   return true
}

The functions above is a big part of the simple algorithm I created. The first part of the function checks if we have an open order. I decided that if I place an order and the market moves +- 2{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a} then I want to cancel any order because it will most likely never fill. The 2{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a} might be a bit high but it works well as a safety measure as most of my orders tend to fill quickly. Buying or selling is only triggered if my index hits 10000 or -10000 and then some additional checks are run. I implemented the allowSell function to make sure that my bot only sells if there is a gain or loss of more than the Bittrex trading fee of 0.25{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}. In my code I set the margin to be at least +-2{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.

func calculateIndex(buy bool, q float64, r float64) {
   // q is quantity VTC
   // r is the rate
   percent := 0.00
   // Calculate percentage of rate
   if r > 0 && q > 0 && lastPrice > 0 && readyToRun {
      percent = lastPrice / r
      if buy {
         //log.Printf("Buy percent: {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f\n", percent)
         //log.Printf("Buy quantity: {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f\n", q)
         if percent > (1.00 - ORDER_RANGE) && percent < (1.00 + ORDER_RANGE) {
            buySellIndex = buySellIndex + (percent * q)
         }
      } else {
         //log.Printf("Sell percent: {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f\n", percent)
         //log.Printf("Sell quantity: {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f\n", q)
         if percent > (1.00 - ORDER_RANGE) && percent < (1.00 + ORDER_RANGE) {
            percent = percent - 2.00 // Reverse percent, lower is higher
            buySellIndex = buySellIndex + (percent * q)
         }
      }
   }
   if buySellIndex > highIndex {
      highIndex = buySellIndex
   }
   if buySellIndex < lowIndex {
      lowIndex = buySellIndex
   }
   // Reset really high or low numbers due to startup
   if highIndex > 5000000.00 || lowIndex < -5000000.00 {
      highIndex = 0.00
      lowIndex = 0.00
      buySellIndex = 0.00
   }
}

Here is the calculateIndex function which does exactly what it says. It is what my bot uses to decide to buy or sell. At the bottom I implemented a reset for my indexes as I would sometimes see very high or low values. This happend when launching my program as a lot of orders come through the websocket right away. This is also why my program waits 60 seconds to start. My index is calculated every time there is a message via the websocket. This can happen several times per second and on each update my program will decide to buy/sell. This allows the bot to be very responsive and act instantly.

func updateStats(b *bittrex.Bittrex) {
   var err error = nil
   for {
      go func(b *bittrex.Bittrex) {
         balances, err = b.GetBalances()
         orders, err = b.GetOpenOrders(MARKET_STRING)
         ticker, err = b.GetTicker(MARKET_STRING)
         if err != nil {
            log.Println("Error:", err)
            // Pause calculations in case of error
            readyToRun = false
         }

         log.Printf("====================================\n")
         log.Printf("Last price:       {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}v\n", ticker.Last)
         log.Printf("Index:           {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f\n", buySellIndex)
         log.Printf("High Index:       {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f\n", highIndex)
         log.Printf("Low Index:           {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}.4f\n", lowIndex)
         lastPrice, _ = ticker.Last.Float64()
         buySellIndex = 0.00

         log.Printf("Bid:         {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}v\n", ticker.Bid)
         log.Printf("Ask:         {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}v\n", ticker.Ask)

         // Do we have an open order?
         openOrder = len(orders) > 0

         for _, o := range orders {
            log.Println("Pending order: ", o.OrderType, " Quanitity: ", o.QuantityRemaining, "/", o.Quantity, " Price: ", o.PricePerUnit)
         }

         // Where do we have balances
         for _, b := range balances {
            bal, _ := b.Balance.Float64()
            if bal > 0.00 {
               log.Printf("{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}s:          {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}v {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}s {25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a}v\n", b.Currency, b.Available, "/", b.Balance)
            }
         }
         log.Printf("====================================\n")

      }(b)
      <-time.After(60 * time.Second)
      // Wait 60 to init and collect data
      readyToRun = true
   }
}

Finally we have the updateStats function. This function loops every 60 seconds and updates the orders, balances, last price and resets my index to 0. I also configured it to print out useful data for tweaking my variables or debugging the application.

All of the code I have shared with you above is available on Github and is MIT licensed so that you are free to take the code and modify it for your own needs. I also recommend testing some of the calculations in a scratch file, I made an example scratch file that I used to make sure my calculations did what I thought they would.

This seemed like a good topic to write about. I hope my example will help someone else learn. I would love to hear your feedback, criticism and hopefully improvements to the code in the comments below.

Thanks for reading!

Update: Turns out the first cryptocurrency trading bot I ever made was not profitable at all. Initially it made some good trades but then it kept losing money. I tried to tweak my current algorithm and ran it for a week loosing 30{25926e1ae733c694be1fca7aad2c08379e63fd61e8d02131996d4c61b85a2b0a} or about 300 euro. At the moment I shut the bot down and have plans to make a new, better trading bot in the future.

Comments

3 thoughts on “How To Make A Cryptocurrency Trading Bot With Golang

Got Something To Say?

Your email address will not be published. Required fields are marked *