Deep dive into Pricewatch Bot

trading-bots
saturn-trading-bots

#1

Pricewatch Bot Review

Pricewatch bot is the first of the many bots that we have released for Saturn Network platform. Its logic is very simple - buy high sell low, and all the scary cryptography and blockchain stuff is abstracted away by powerful libraries like ethers.js and saturn.js. This allows even beginners to write bots and focus on what’s important - automating trading strategies and finding profitable patterns. We have taken care of everything else.

This post covers the anatomy of the bot. Hopefully after reading this material you will be more confident in implementing some strategies of your own. Or maybe try implementing some of the strategies that @sam has explained?

Files

  • example-config.json - this file is included strictly in illustrative purposes. The user must supply their own config.
  • package.json and yarn.lock - these files specify which dependencies these project relies on, and are mostly not for you, the reader, but for the computer that executes the code. If you are not familiar with node.js and javascript development you should look at this link.
  • pricewatch-bot.js - the actual logic of the bot. If you want to create your own bot this is the file that you will have to modify.

pricewatch-bot.js

Let’s review this file line by line.

#!/usr/bin/env node
const axios     = require('axios')
const chalk     = require('chalk')
const ethers    = require('ethers')
const moment    = require('moment')
const _         = require('lodash')
const program   = require('commander')
const Table     = require('easy-table')
const BigNumber = require('bignumber.js')
const Saturn    = require('@saturnnetwork/saturn.js').Saturn

Here we load the necessary libraries. Libraries are just code that somebody else wrote so you can use it later without reinventing the wheel. These libraries help us deal with datetime formats, pretty printing with colors, talking to Saturn API and the Ethereum and Ethereum Classic blockchains.

const version   = require('./package.json').version
const saturnApi = 'https://ticker.saturn.network/api/v2'
const epsilon   = new BigNumber('0.00005')

These three lines define some helpful constants that we shall use going forward. Namely,

  • version describes the version of the bot software
  • saturnApi specifies saturnApi URL
  • epsilon is simply a very small number

Next, we define some useful helper functions. In general, it is good practice to break down functionality into small little functions that do only one thing and that you can later compose together.

const pipeline = async (funcs) => {
  return await funcs.reduce((promise, func) => {
    return promise.then(result => {
      return func().then(Array.prototype.concat.bind(result))
    })
  }, Promise.resolve([]))
}

This function takes an array of asynchronous, long running functions, and executes them one by one preserving the order.

function getChainId(chain) {
  if (chain === 'ETC') { return 61 }
  if (chain === 'ETH') { return 1 }
  console.log('Unknown chainId for chain', chain)
  process.exit(1)
}

This function helps us calculate EIP155 chain id for supported networks.

function rpcNode(chain) {
  if (chain === 'ETC') { return 'https://etc-rpc.binancechain.io/' }
  if (chain === 'ETH') { return 'https://mainnet.infura.io/mew' }
  console.log('Unknown chainId for chain', chain)
  process.exit(1)
}

This function specifies which remote Ethereum full node we should be using to send signed transactions. Thank you MyEtherWallet and Binance!

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Javascript does not have a built-in sleep function. Lets write our own!

function makeSaturnClient(blockchain, program, wallet) {
  let rpcnode = rpcNode(blockchain)
  let chainId = getChainId(blockchain)
  let provider = new ethers.providers.JsonRpcProvider(rpcnode, { chainId: chainId, name: blockchain })

  let saturn
  if (blockchain === 'ETC') {
    saturn = new Saturn(saturnApi, { etc: wallet.connect(provider) })
  } else {
    saturn = new Saturn(saturnApi, { eth: wallet.connect(provider) })
  }

  return saturn
}

This function helps us instantiate the correct instance of Saturn.js client.

let tradesForToken = async function(token, blockchain, action, timestamp) {
  let url = `${saturnApi}/trades/by_timestamp/${blockchain}/0x0000000000000000000000000000000000000000/${token}/${timestamp}.json`
  let subset = action === "buy" ? "buys" : "sells"

  let trades = await axios.get(url)
  if (trades.status !== 200) {
    throw new Error(`API error while fetching trade info. Status code ${trades.status}`)
  }
  return trades.data[subset]
}

This function fetches the list of all trades for a given token that happened on Saturn Network after a given timestamp (i.e. within last hour).

let etherVolume = function(trades, wallet, action) {
  let filtered = _.filter(trades, (x) => {
    return (x.buyer === wallet.toLowerCase() || x.seller === wallet.toLowerCase())
  })
  if (filtered.length === 0) { return new BigNumber(0) }
  if (action === 'buy') {
    let amounts = _.map(filtered, (x) => new BigNumber(x.buytokenamount))
    return _.reduce(amounts, (x, y) => x.plus(y), new BigNumber(0))
  } else {
    let amounts = _.map(filtered, (x) => new BigNumber(x.selltokenamount))
    return _.reduce(amounts, (x, y) => x.plus(y), new BigNumber(0))
  }
}

This function calculates how much ether (ETC or ETH) have ben traded in the supplied trades.

let allowedLimit = async function(token, blockchain, action, wallet, limit) {
  let oneHourAgo = moment().subtract(1, 'hour').unix()
  let trades = await tradesForToken(token, blockchain, action, oneHourAgo)

  return new BigNumber(limit).minus(etherVolume(trades, wallet, action))
}

This function tells you how much more ether you can trade until the hour expires. etherVolume and tradesForToken combined together work to ensure the hourly limit of pricewatch bot is respected.

let orderInfo = async function(blockchain, tx) {
  let url = `${saturnApi}/orders/by_tx/${blockchain}/${tx}.json`

  let order = await axios.get(url)
  if (order.status !== 200) {
    throw new Error(`API error while fetching trade info. Status code ${trades.status}`)
  }
  let price = new BigNumber(order.data.price)
  let balance = new BigNumber(order.data.balance)
  return {
    price: price,
    tokenbalance: balance,
    etherbalance: price.times(balance)
  }
}

While it is possible to fetch order info using Saturn.js, here we query the API directly in order to fetch the order balance in both tokens and ether.

let tokenBalance = async function(blockchain, token, wallet) {
  let url = `${saturnApi}/tokens/balances/${blockchain}/${wallet}/${token}.json`

  let response = await axios.get(url)
  if (response.status !== 200) {
    throw new Error(`API error while fetching trade info. Status code ${trades.status}`)
  }

  return new BigNumber(response.data.balances.walletbalance)
}

This function lets the developer query for a token balance of a given address using a simple HTTP API without having to bother with all the weird blockchain setups and accounting for decimals. The heavy lifting is done by Saturn API.

let etherBalance = async function(blockchain, wallet) {
  let url = `${saturnApi}/tokens/balances/${blockchain}/${wallet}/0x0000000000000000000000000000000000000000.json`

  let response = await axios.get(url)
  if (response.status !== 200) {
    throw new Error(`API error while fetching trade info. Status code ${trades.status}`)
  }

  return new BigNumber(response.data.balances.walletbalance)
}

Similarly, this function lets the developer query for ether balance of a given address using a simple HTTP API without having to bother with all the weird blockchain setups and accounting for decimals. The heavy lifting is done by Saturn API.

Now that we have defined our helpers, let’s proceed with writing the beautiful CLI user interface. I am sure you would much prefer a graphical user interface, or a GUI. However please note that making a CLI interface is a relatively simple task these days, and making a GUI is obviously harder. Our project requires further funding in order to work on simple user interfaces. For now please enjoy what’s available, fund us or volunteer your efforts!

program
  .version(version, '-v, --version')
  .description('Watch a price of a given token on Saturn Network and auto buy/sell. More details are available at ' + chalk.underline.red('https://forum.saturn.network/t/saturn-trading-bot-guides/4046'))
  .option('-p, --pkey [pkey]', 'Private key of the wallet to use for trading')
  .option('-m, --mnemonic [mnemonic]', 'Mnemonic (i.e. from Saturn Wallet) of the wallet to use for trading')
  .option('-i, --walletid [walletid]', 'If using a mnemonic, choose which wallet to use. Default is Account 2 of Saturn Wallet / MetaMask.', 2)
  .option('-j, --json [json]', 'Trading bot config file')
  .option('-d, --delay [delay]', 'Polling delay in seconds', 60)
  .parse(process.argv)

Try executing npx @saturnnetwork/pricewatch-bot -h. The lines above use Commander.js written by one of the best javascript programmers in the world in order to create this beautiful help message and parse command line arguments.

if (!program.mnemonic && !program.pkey) {
  console.error('At least one of [pkey], [mnemonic] must be supplied')
  process.exit(1)
}

if (program.mnemonic && program.pkey) {
  console.error('Only one of [pkey], [mnemonic] must be supplied')
  process.exit(1)
}

Of course, some of the provided arguments may be wrong. We try to parse these arguments and, if we discover broken input, we try to inform the user about what went wrong.

let wallet
if (program.mnemonic) {
  let walletid = parseInt(program.walletid) - 1
  wallet = ethers.Wallet.fromMnemonic(program.mnemonic, `m/44'/60'/0'/0/${walletid}`)
} else {
  wallet = new ethers.Wallet(program.pkey)
}

Here, we create the wallet - we use the provided private key to create a bridge that will let us send transactions to the blockchain.

if (!program.json) {
  console.error('Must specify bot config .json file location')
  process.exit(1)
}

The bot needs a config file. Try to read it and error out if it is not specified.

console.log(chalk.green(`Loading pricewatch-bot v${version} ...`))
console.log(chalk.green(`Trading address: ${chalk.underline(wallet.address)}\nUsing the following strategies`))
let botconfig = require(program.json)
console.log(Table.print(botconfig, {
  houretherlimit: { name: 'Hourly ether limit' },
  action: { printer: function (val, width) {
      let text = val === "buy" ? chalk.green.bold(val) : chalk.red.bold(val)
      return width ? Table.padLeft(text, width) : text
    }},
  blockchain: { printer: function (val, width) {
      let text = val === "ETC" ? chalk.black.bgGreen.bold(val) : chalk.black.bgWhite.bold(val)
      return width ? Table.padLeft(text, width) : text
    }}
}))

These lines are responsible for printing the pretty table at the start of the bot.

At this point, everything is ready for the trade logic. For your own bot feel free to copypaste everything above, and modify some of the lines below.

The trading logic is written in a two-step format. First, look at the order book, at hour hourly limits, at our strategy, and figure out what actions need to be performed: order cancels, new trades, new orders. Then, in the second step, we use the pipeline function to execute the blockchain transactions one by one.

The whole trading logic is encapsulated in the function that we called trade. In the last line of the bot we instruct the computer to call the trade function every 60 seconds.

let trade = async function() {
  try {
    let schedule = []
    await Promise.all(_.map(botconfig, async (row) => {
      let saturn = makeSaturnClient(row.blockchain.toUpperCase(), program, wallet)
      let info = await saturn.query.getTokenInfo(row.token, row.blockchain)

schedule is initialized as an empty array and this is where we will store desired actions from step one. botconfig is the json file that bot user has specified. The rest of the lines read like English language and are fairly self-explanatory. Let me know if you are still confused though!

Going forward, we do some branching depending on whether your config says buy or sell. We will cover the buy branch in full detail. Reading the sell branch is left as an exercise to the reader.

      if (row.action === 'buy') {
        let threshold = new BigNumber(row.price)
        let bestBuyPrice = new BigNumber(info.best_sell_price)

Starts pretty straightforward. Let’s look at what our price threshold is, and at what is the best available price on the market is.

        if (bestBuyPrice.isLessThanOrEqualTo(threshold)) {
          schedule.push(async () => {
            console.log(chalk.yellow(`Buy opportunity for ${row.blockchain}::${row.token} @ ${chalk.green(info.best_sell_price)}`))

If best price on the market is less than the threshold price then the bot should attempt to buy!
schedule.push here refers to pushing the result of step one into the scedule array, that step two of the bot will execute. Notice that we push an async function with no arguments.

This function checks that all limits are respected, and if there is available money to trade it shall trade. Otherwise it will simply do nothing and will notify the user of what is going on.

            let tradeLimit = await allowedLimit(
              row.token.toLowerCase(),
              row.blockchain.toUpperCase(),
              row.action,
              wallet.address,
              row.houretherlimit
            )
            if (! tradeLimit.gt(epsilon)) {
              console.log(chalk.yellow(`Hourly trade limit reached. Skipping...`))
              return true
            }

All the helper functions in the beginning of our tutorial are finally being used! Here we check how much money is left from our hourly limit in order to be traded on the market and then we either trade or skip the opportunity based on our user’s desires.

            let order = await orderInfo(row.blockchain, info.best_sell_order_tx)

            let tradeEtherAmount = tradeLimit.gt(order.etherbalance) ?
              order.etherbalance : tradeLimit

            let walletBalance = await etherBalance(row.blockchain, wallet.address)
            if (! walletBalance.gt(epsilon)) {
              console.log(chalk.yellow(`Not enough ${row.blockchain} in your wallet to complete transaction. Skipping...`))
              return true
            }

All conditions are satisfied. The price is good, the hourly limit is good. What’s stopping us? At this point, only the bots balance. Make sure we have enough funds in the wallet before we proceed with executing a trade.

            tradeEtherAmount = tradeEtherAmount.gt(walletBalance) ? walletBalance : tradeEtherAmount

            let tokenAmount = tradeEtherAmount.dividedBy(order.price).toFixed(parseInt(info.decimals))
            let tx = await saturn[row.blockchain.toLowerCase()].newTrade(tokenAmount, info.best_sell_order_tx)

            console.log(chalk.yellow(`Attempting to buy ${tokenAmount} tokens\ntx: ${chalk.underline(tx)}`))

Finally! The price is good, the hourly limit is good, and we have money in the wallet. How to we execute a trade? Thanks to Saturn.js we can do this with 1 line of code.

            await saturn.query.awaitTradeTx(tx, saturn[row.blockchain.toLowerCase()])

That’s it! Was simple, wasn’t it! Now try to read the sell branch of the bot without my commentary and try to understand it.

          })
        }
      } else if (row.action === 'sell') {
        let threshold = new BigNumber(row.price)
        let bestSellPrice = new BigNumber(info.best_buy_price)

        if (bestSellPrice.isGreaterThanOrEqualTo(threshold)) {
          schedule.push(async () => {
            console.log(chalk.yellow(`Sell opportunity for ${row.blockchain}::${row.token} @ ${chalk.red(info.best_buy_price)}`))
            let tradeLimit = await allowedLimit(
              row.token.toLowerCase(),
              row.blockchain.toUpperCase(),
              row.action,
              wallet.address,
              row.houretherlimit
            )
            if (! tradeLimit.gt(epsilon)) {
              console.log(chalk.yellow(`Hourly trade limit reached. Skipping...`))
              return true
            }
            let order = await orderInfo(row.blockchain, info.best_buy_order_tx)

            let tradeEtherAmount = tradeLimit.gt(order.etherbalance) ?
              order.etherbalance : tradeLimit

            let tokenAmount = tradeEtherAmount.dividedBy(order.price).toFixed(parseInt(info.decimals))

            let walletBalance = await tokenBalance(row.blockchain, row.token, wallet.address)
            if (! walletBalance.gt(epsilon)) {
              console.log(chalk.yellow(`Not enough ${row.blockchain}::${row.token} in your wallet to complete transaction. Skipping...`))
              return true
            }

            tokenAmount = new BigNumber(tokenAmount).gt(walletBalance) ? walletBalance : tokenAmount
            let tx = await saturn[row.blockchain.toLowerCase()].newTrade(tokenAmount, info.best_buy_order_tx)

            console.log(chalk.yellow(`Attempting to sell ${tokenAmount} tokens\ntx: ${chalk.underline(tx)}`))
            await saturn.query.awaitTradeTx(tx, saturn[row.blockchain.toLowerCase()])
          })
        }
      } else {
        throw new Error(`Unknown action ${row.action}`)
      }
    }))

Isn’t it nice to learn new things? Think about what you have just accomplished - you now know how to write your own trading bot! And it was all free, you didn’t have to take out any student loans in order to learn this very much practical skill! If you want us to publish more learning materials consider participating on our HODL program.

Let’s wrap up the bot. First line in this block checks if the schedule is empty. If it is empty do nothing, if it is not empty then execute every trade on the blockchain, one by one.

The other lines in the block make sure that our bot restarts in case of a weird error (i.e. your internet went down or a blockchain node was not responding). It prints the error and the bot keeps on running.

    if (schedule.length) { await pipeline(schedule) }
  } catch(error) {
    console.error(error.message)
  }

The last thing the bot does is instruct the computer to run the bot logic again after a brief pause (default is 60 seconds). Too slow and your strategy will miss opportunities. Too fast and you’ll be needlessly spamming the network and you will be throttled. 15-60 seconds is optimal.

  setTimeout(trade, parseInt(program.delay) * 1000)
};

(async () => await trade())()

Congratulations! I am looking forward to seeing your new trading bot on the market. Got questions? Let us know in the comments below.

Happy Trading!


Any follow up on this article on bots?
How to use pricewatch bot
Saturn Trading Bot Guides
#2

oie_rlaxIL82LHaC


#3

RSI based on https://blog.saturn.network/how-does-the-rsi-indicator-work-for-crypto-trading/

    const _ = require('lodash'):

    let getRSI = (saturn, token, network = 'etc', periods = false) => {
      return new Promise((resolve, reject) => {
        let x = periods || 14;
        saturn.query.ohlcv(token, network || 'etc').then((ohlcvdata) => {
          let period = ohlcvdata.slice(x * -1);
          let avgOpen = _.meanBy(period, (p) => Number(p.open));
          let avgClose = _.meanBy(period, (p) => Number(p.close));
          let RS = avgClose / avgOpen;
          let RSI = 100 - (100 / (1 + RS));
          resolve({
            avgOpen, avgClose, RSI, x
          })
        })
      })
    }

use:

    let token = '0xac55641cbb734bdf6510d1bbd62e240c2409040f'
    getRSI(saturnjs,token,'etc',14).then(rsidata => console.log)

TypeScript if needed

    import _ from 'lodash'

    export default class Functions {
      public getRSI = (saturn, token: string, network?: string, periods?: number): Promise<object> => {
        return new Promise((resolve, reject) => {
          let x = periods || 14
          saturn.query.ohlcv(token, network || 'etc').then((ohlcvdata:any) => {
            let period: Array<any> = ohlcvdata.slice(x * -1)
            let avgOpen: number = _.meanBy(period, (p:any) => Number(p.open))
            let avgClose: number = _.meanBy(period, (p:any) => Number(p.close))
            let RS: number = avgClose / avgOpen
            let RSI: number = 100 - (100 / (1 + RS))
            resolve({
              avgOpen, avgClose, RSI, x
            })
          })
        })
      }
    }