bitstamp-kiss

is a Bitstamp API v2 wrapper with the joy of kiss literate programming

API | Annotated source | License

NPM version No deps JavaScript Style Guide KLP

API

The following methods are implemented:

Annotated source

See Bitstamp API as a reference.

// This code is generated by command: npm run markdown2code

Dependencies

Required dependencies are all core packages.

const crypto = require('crypto')
const https = require('https')
const querystring = require('querystring')

Environment

Customer id is your Bitstamp username. You can get API key and secret going to Account > Settings > API Access.

const BITSTAMP_APIKEY = process.env.BITSTAMP_APIKEY
const BITSTAMP_APISECRET = process.env.BITSTAMP_APISECRET
const BITSTAMP_CUSTOMERID = process.env.BITSTAMP_CUSTOMERID

Utils

coerceTick

Convert raw tick Object<String> into numeric values.

function coerceTick (tick) {
  return {
    high: parseFloat(tick.high),
    last: parseFloat(tick.last),
    timestamp: parseInt(tick.timestamp),
    bid: parseFloat(tick.bid),
    vwap: parseFloat(tick.vwap),
    volume: parseFloat(tick.volume),
    low: parseFloat(tick.low),
    ask: parseFloat(tick.ask),
    open: parseFloat(tick.open)
  }
}

getNonce

Get a unique progressive value. Current UTC timestamp is used, as usual. It is also to return value in milliseconds, to make the nonce unique.

function getNonce () {
  const now = new Date()

  const nonce = new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds()).getTime()

  return nonce + now.getUTCMilliseconds()
}

getSignature

function getSignature (nonce) {
  const hmac = crypto.createHmac('sha256', BITSTAMP_APISECRET)

  const message = nonce + BITSTAMP_CUSTOMERID + BITSTAMP_APIKEY

  hmac.update(message)

  const signature = hmac.digest('hex').toUpperCase()

  return signature
}

limitTo5Decimals

Truncate value to avoid Bitstamp API errors on sell limit order.

function limitTo5Decimals (value) {
  const decimals = value.toString().split('.')[1]

  if (decimals && decimals.length > 5) {
    return value.toFixed(5)
  } else {
    return value
  }
}

limitTo8Decimals

Truncate value to avoid Bitstamp API error:

Ensure that there are no more than 8 decimal places.

function limitTo8Decimals (value) {
  const decimals = value.toString().split('.')[1]

  if (decimals && decimals.length > 8) {
    return value.toFixed(8)
  } else {
    return value
  }
}

Public API

publicRequest

function publicRequest (path, next) {
  https.get(`https://www.bitstamp.net/api/${path}`, (response) => {
    const statusCode = response.statusCode

    if (statusCode !== 200) {
      const error = new Error(`Request failed with ${statusCode}`)

      response.resume()

      next(error)
    }

    response.setEncoding('utf8')

    let responseJSON = ''

    response.on('data', chunk => { responseJSON += chunk })

    response.on('end', () => {
      const responseData = JSON.parse(responseJSON)

      if (responseData.status === 'error') {
        const error = new Error(responseJSON)

        next(error)
      } else {
        next(null, responseData)
      }
    })
  }).on('error', next)
}

orderBook

Returns a JSON dictionary like the ticker call, with the calculated values being from within an hour.

function orderBook (currencyPair, next) {
  publicRequest(`v2/order_book/${currencyPair}/`, next)
}

exports.orderBook = orderBook

ticker

Returns data for the given currency pair.

/**
 * @param {String} currencyPair
 * @param {Function} next
 *
 * @returns {Object} tick
 * @returns {Number} tick.last Last currency price.
 * @returns {Number} tick.high Last 24 hours price high.
 * @returns {Number} tick.low Last 24 hours price low.
 * @returns {Number} tick.vwap Last 24 hours [volume weighted average price](https://en.wikipedia.org/wiki/Volume-weighted_average_price).
 * @returns {Number} tick.volume Last 24 hours volume.
 * @returns {Number} tick.bid Highest buy order.
 * @returns {Number} tick.ask Lowest sell order.
 * @returns {Number} tick.timestamp Unix timestamp date and time.
 * @returns {Number} tick.open First price of the day.
 */

function ticker (currencyPair, next) {
  publicRequest(`v2/ticker/${currencyPair}/`, (err, data) => {
    if (err) return next(err)

    next(null, coerceTick(data))
  })
}

exports.ticker = ticker

hourlyTicker

Returns a JSON dictionary like the ticker call, with the calculated values being from within an hour.

function hourlyTicker (currencyPair, next) {
  publicRequest(`/v2/ticker_hour/${currencyPair}/`, (err, data) => {
    if (err) return next(err)

    next(null, coerceTick(data))
  })
}

exports.hourlyTicker = hourlyTicker

transactions

/**
 * @param {currencyPair}
 * @param {String} time interval from which we want the transactions to be returned. Possible values are minute, hour (default) or day.
 * @params {Function} next
 */

function transactions (currencyPair, time, next) {
  const path = `v2/transactions/${currencyPair}/?time=${time}`

  publicRequest(path, next)
}

exports.transactions = transactions

Private API

These calls will be executed on the account (Sub or Main), to which the used API key is bound to.

privateRequest

function privateRequest (path, params, next) {
  const nonce = getNonce()
  const signature = getSignature(nonce)

  const requestData = querystring.stringify(Object.assign({}, params, {
    key: BITSTAMP_APIKEY,
    signature,
    nonce
  }))

  const requestOptions = {
    hostname: 'www.bitstamp.net',
    port: 443,
    path: `/api/${path}`,
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Content-Length': requestData.length,
      'Accept': 'application/json'
    }
  }

  const request = https.request(requestOptions, (response) => {
    const statusCode = response.statusCode

    if (statusCode !== 200) {
      const error = new Error(`Request failed with ${statusCode}`)

      response.resume()

      next(error)
    }

    response.setEncoding('utf8')

    let responseJSON = ''

    response.on('data', chunk => { responseJSON += chunk })

    response.on('end', () => {
      const responseData = JSON.parse(responseJSON)

      if (responseData.status === 'error') {
        const error = new Error(responseJSON)

        next(error)
      } else {
        next(null, responseData)
      }
    })
  })

  request.on('error', next)

  request.write(requestData)

  request.end()
}

accountBalance

This API call is cached for 10 seconds.

/**
 * @param {Function} next callback
 * @returns {Object} balance
 *
 * Balance
 *
 * @returns {Number} balance.usd_balance
 * @returns {Number} balance.btc_balance
 * @returns {Number} balance.eur_balance
 * @returns {Number} balance.xrp_balance
 * @returns {Number} balance.bch_balance
 * @returns {Number} balance.eth_balance
 * @returns {Number} balance.ltc_balance
 *
 * Reserved.
 *
 * @returns {Number} balance.usd_reserved
 * @returns {Number} balance.btc_reserved
 * @returns {Number} balance.eur_reserved
 * @returns {Number} balance.xrp_reserved
 * @returns {Number} balance.bch_reserved
 * @returns {Number} balance.eth_reserved
 * @returns {Number} balance.ltc_reserved
 *
 * Available for trading.
 *
 * @returns {Number} balance.usd_available
 * @returns {Number} balance.btc_available
 * @returns {Number} balance.eur_available
 * @returns {Number} balance.xrp_available
 * @returns {Number} balance.bch_available
 * @returns {Number} balance.eth_available
 * @returns {Number} balance.ltc_available
 *
 * Customer trading fees.
 *
 * @returns {Number} balance.bchbtc_fee
 * @returns {Number} balance.bcheur_fee
 * @returns {Number} balance.bchusd_fee
 * @returns {Number} balance.btceur_fee
 * @returns {Number} balance.btcusd_fee
 * @returns {Number} balance.ethbtc_fee
 * @returns {Number} balance.etheur_fee
 * @returns {Number} balance.ethusd_fee
 * @returns {Number} balance.eurusd_fee
 * @returns {Number} balance.ltcbtc_fee
 * @returns {Number} balance.ltceur_fee
 * @returns {Number} balance.ltcusd_fee
 * @returns {Number} balance.xrpbtc_fee
 * @returns {Number} balance.xrpeur_fee
 * @returns {Number} balance.xrpusd_fee
 */

function accountBalance (next) {
  privateRequest('v2/balance/', {}, (err, data) => {
    if (err) return next(err)

    next(null, {
      usd_balance: parseFloat(data.usd_balance),
      btc_balance: parseFloat(data.btc_balance),
      eur_balance: parseFloat(data.eur_balance),
      xrp_balance: parseFloat(data.xrp_balance),
      bch_balance: parseFloat(data.bch_balance),
      eth_balance: parseFloat(data.eth_balance),
      ltc_balance: parseFloat(data.ltc_balance),
      usd_reserved: parseFloat(data.usd_reserved),
      btc_reserved: parseFloat(data.btc_reserved),
      eur_reserved: parseFloat(data.eur_reserved),
      xrp_reserved: parseFloat(data.xrp_reserved),
      bch_reserved: parseFloat(data.bch_reserved),
      eth_reserved: parseFloat(data.eth_reserved),
      ltc_reserved: parseFloat(data.ltc_reserved),
      usd_available: parseFloat(data.usd_available),
      btc_available: parseFloat(data.btc_available),
      eur_available: parseFloat(data.eur_available),
      xrp_available: parseFloat(data.xrp_available),
      bch_available: parseFloat(data.bch_available),
      eth_available: parseFloat(data.eth_available),
      ltc_available: parseFloat(data.ltc_available),
      bchbtc_fee: parseFloat(data.bchbtc_fee),
      bcheur_fee: parseFloat(data.bcheur_fee),
      bchusd_fee: parseFloat(data.bchusd_fee),
      btceur_fee: parseFloat(data.btceur_fee),
      btcusd_fee: parseFloat(data.btcusd_fee),
      ethbtc_fee: parseFloat(data.ethbtc_fee),
      etheur_fee: parseFloat(data.etheur_fee),
      ethusd_fee: parseFloat(data.ethusd_fee),
      eurusd_fee: parseFloat(data.eurusd_fee),
      ltcbtc_fee: parseFloat(data.ltcbtc_fee),
      ltceur_fee: parseFloat(data.ltceur_fee),
      ltcusd_fee: parseFloat(data.ltcusd_fee),
      xrpbtc_fee: parseFloat(data.xrpbtc_fee),
      xrpeur_fee: parseFloat(data.xrpeur_fee),
      xrpusd_fee: parseFloat(data.xrpusd_fee)
    })
  })
}

exports.accountBalance = accountBalance

buyLimitOrder

This call will be executed on the account (Sub or Main), to which the used API key is bound to.

/**
 * @param {currencyPair}
 * @param {Object} param
 * @param {Number} param.amount
 * @param {Number} param.price
 * @param {Number} [param.limit_price] Optional: if the order gets executed, a new sell order will be placed, with "limit_price" as its price.
 * @param {Function} next callback
 * @returns {Object} response
 * @returns {Number} response.id Order ID.
 * @returns {String} response.datetime
 * @returns {String} response.type 0 (buy) or 1 (sell).
 * @returns {Number} response.price
 * @returns {Number} response.amount
 */
function buyLimitOrder (currencyPair, param, next) {
  const params = {
    amount: limitTo5Decimals(param.amount),
    price: limitTo5Decimals(param.price)
  }

  if (param.limit_price) {
    if (param.limit_price <= param.price) {
      next(new Error('limit_price <= price'))
    }

    params.limit_price = limitTo5Decimals(param.limit_price)
  }

  privateRequest(`v2/buy/${currencyPair}/`, params, (err, data) => {
    if (err) return next(err)

    next(null, {
      id: parseInt(data.id),
      datetime: data.datetime,
      type: data.type,
      price: parseFloat(data.price),
      amount: parseFloat(data.amount)
    })
  })
}

exports.buyLimitOrder = buyLimitOrder

buyMarketOrder

By placing a market order you acknowledge that the execution of your order depends on the market conditions and that these conditions may be subject to sudden changes that cannot be foreseen.

/**
 * @param {currencyPair}
 * @param {Number} amount
 * @param {Function} next callback
 * @returns {Object} response
 * @returns {Number} response.id Order ID.
 * @returns {String} response.datetime
 * @returns {String} response.type 0 (buy) or 1 (sell).
 * @returns {Number} response.price
 * @returns {Number} response.amount
 */
function buyMarketOrder (currencyPair, amount, next) {
  const params = {
    amount: limitTo8Decimals(amount)
  }

  privateRequest(`v2/buy/market/${currencyPair}/`, params, (err, data) => {
    if (err) return next(err)

    next(null, {
      id: parseInt(data.id),
      datetime: data.datetime,
      type: data.type,
      price: parseFloat(data.price),
      amount: parseFloat(data.amount)
    })
  })
}

exports.buyMarketOrder = buyMarketOrder

sellLimitOrder

This call will be executed on the account (Sub or Main), to which the used API key is bound to.

Note that daily_order param is not supported, since Bistamp API complains with error

Both limit_price and any optional parameter cannot be set.

/**
 * @param {currencyPair}
 * @param {Object} param
 * @param {Number} param.amount
 * @param {Number} param.price
 * @param {Number} [param.limit_price] Optional: if the order gets executed, a new buy order will be placed, with "limit_price" as its price.
 * @param {Function} next callback
 * @returns {Object} response
 * @returns {Number} response.id Order ID.
 * @returns {String} response.datetime
 * @returns {String} response.type 0 (buy) or 1 (sell).
 * @returns {Number} response.price
 * @returns {Number} response.amount
 */
function sellLimitOrder (currencyPair, param, next) {
  const params = {
    amount: limitTo5Decimals(param.amount),
    price: limitTo5Decimals(param.price)
  }

  if (param.limit_price) {
    if (param.limit_price >= param.price) {
      next(new Error('limit_price >= price'))
    }

    params.limit_price = limitTo5Decimals(param.limit_price)
  }

  privateRequest(`v2/sell/${currencyPair}/`, params, (err, data) => {
    if (err) return next(err)

    next(null, {
      id: parseInt(data.id),
      datetime: data.datetime,
      type: data.type,
      price: parseFloat(data.price),
      amount: parseFloat(data.amount)
    })
  })
}

exports.sellLimitOrder = sellLimitOrder

sellMarketOrder

By placing a market order you acknowledge that the execution of your order depends on the market conditions and that these conditions may be subject to sudden changes that cannot be foreseen. This call will be executed on the account (Sub or Main), to which the used API key is bound to.

/**
 * @param {currencyPair}
 * @param {Number} amount
 * @param {Function} next callback
 * @returns {Object} response
 * @returns {Number} response.id Order ID.
 * @returns {String} response.datetime
 * @returns {String} response.type 0 (buy) or 1 (sell).
 * @returns {Number} response.price
 * @returns {Number} response.amount
 */
function sellMarketOrder (currencyPair, amount, next) {
  const params = {
    amount: limitTo8Decimals(amount)
  }

  privateRequest(`v2/sell/market/${currencyPair}/`, params, (err, data) => {
    if (err) return next(err)

    next(null, {
      id: parseInt(data.id),
      datetime: data.datetime,
      type: data.type,
      price: parseFloat(data.price),
      amount: parseFloat(data.amount)
    })
  })
}

exports.sellMarketOrder = sellMarketOrder

userTransactions

Returns a descending list of transactions, represented as dictionaries.

For example, to get latest 100 BTC/USD market trade transactions

bitstamp.userTransactions('btcusd', 0, 100, 'desc', (err, transactions) => {
  if (err) throw err

  const marketTradeTransactions = transactions.filter(
    ({type}) => type === '2'
  )

  console.log(marketTradeTransactions)
})

The currencyPair parameter can be also all to get transactions for all currency pairs.

/**
 * @param {currencyPair}
 * @param {Number} offset to skip that many transactions before returning results (default: 0).
 * @param {Number} limit result to that many transactions (default: 100; maximum: 1000).
 * @param {Number} sort Sorting by date and time: asc - ascending; desc - descending (default: desc).
 * @param {Function} next callback
 *
 * @returns {Array} transactions
 *
 * Every transaction has the following properties:
 *
 * @prop {String} datetime
 * @prop {Number} id
 * @prop {String} type 0 - deposit; 1 - withdrawal; 2 - market trade; 14 - sub account transfer
 * @prop {Number} usd
 * @prop {Number} eur
 * @prop {Number} btc
 * @prop {Number} xrp
 * @prop {Number} btc_usd exchange rate (if available)
 * @prop {Number} xrp_usd exchange rate (if available)
 * @prop {Number} btc_eur exchange rate (if available)
 * @prop {Number} xrp_eur exchange rate (if available)
 * @prop {Number} fee
 * @prop {Number} order_id
 */

function userTransactions (currencyPair, offset, limit, sort, next) {
  const params = { offset, limit, sort }

  const path = currencyPair === 'all' ? 'v2/user_transactions/' : `v2/user_transactions/${currencyPair}/`

  privateRequest(path, params, (err, data) => {
    if (err) return next(err)

    next(null, data.map(data => {
      const { datetime, id, type } = data

      const transaction = { datetime, id, type }

      if (data.fee) transaction.fee = parseFloat(data.fee)
      if (data.order_id) transaction.order_id = data.order_id

      const currencies = ['btc', 'eur', 'xrp', 'usd']

      currencies.forEach(currency => {
        if (data[currency]) transaction[currency] = parseFloat(data[currency])
      })

      const exchangeRateLabel = Object.keys(data).find(key => (
        key.length === 7 && key.charAt(3) === '_'
      ))

      transaction[exchangeRateLabel] = parseFloat(data[exchangeRateLabel])

      const currency1 = exchangeRateLabel.substring(0, 3)
      const currency2 = exchangeRateLabel.substring(4)

      transaction[currency1] = parseFloat(data[currency1])
      transaction[currency2] = parseFloat(data[currency2])

      return transaction
    }))
  })
}

exports.userTransactions = userTransactions

Helpers

computeFee

/**
 * @param {Number} value
 *
 * @returns {Number} fee
 */
function computeFee (value) {
  const percentage = 0.25

  const fee = value * percentage / 100 // 0.25%

  return Math.ceil(fee * 100) / 100 // rounded to two decimals
}

exports.computeFee = computeFee

License

MIT