siman
2/10/2018 - 12:08 AM

kraken-minimal-trader

kraken-minimal-trader

const delay = require('delay');
const createDebug = require('debug');

const debug = createDebug('withRetry');
const getDelayForRetry = retry => 1000 * Math.pow(2, retry) + Math.random() * 100;

const withRetry = (target, maxRetries = 5, shouldRetry = () => true) => {
  return async (...args) => {
    for (let retry = 0; retry < maxRetries - 1; retry++) {
      try {
        return await target(...args);
      } catch (error) {
        debug(error);

        if (!shouldRetry(error)) {
          break;
        }
      }

      await delay(getDelayForRetry(retry));
    }

    return target(...args);
  };
};

module.exports = withRetry;
module.exports = function safely(fn, ...args) {
  return Promise.resolve()
  .then(!args ? fn : () => fn(...args))
  .then(result => [undefined, result])
  .catch(error => [error, undefined]);
}
const assert = require('assert');
const querystring = require('querystring');
const { createHmac, createHash } = require('crypto');
const request = require('superagent');
const { reduce } = require('lodash');
const safely = require('./safely');
const withRetry = require('./withRetry');

const {
  KRAKEN_API_KEY,
  KRAKEN_API_SECRET,
} = process.env;

assert(KRAKEN_API_KEY, 'KRAKEN_API_KEY is required');
assert(KRAKEN_API_SECRET, 'KRAKEN_API_SECRET is required');

const KRAKEN_BASE_URL = 'https://api.kraken.com';

const isNonceError = error => error.message.match(/EAPI:Invalid nonce/);

const withKrakenRetry = fn => {
  return withRetry(fn, 10, error => isNonceError(error));
};

const krakenGet = withKrakenRetry(async (path) => {
  console.log(path);
  const response = await request(`${KRAKEN_BASE_URL}${path}`).retry();
  const { body } = response;
  const { error, result } = body;

  if (error && error.length) {
    const wrappedError = new Error(`Kraken returned error: ${error[0]}`);
    throw wrappedError;
  }

  return result;
});

const apiSecretBuffer = new Buffer(KRAKEN_API_SECRET, 'base64');

const krakenPost = withKrakenRetry(async (path, fields) => {
  const nonce = (+new Date() * 1e3).toString();
  const requestBody = querystring.stringify(Object.assign({ nonce }, fields));
  const hash = createHash('sha256').update(nonce).update(requestBody).digest();
  const signature = createHmac('sha512', apiSecretBuffer).update(path).update(hash).digest('base64');

  const response = await request
    .post(`https://api.kraken.com${path}`)
    .set('API-Key', KRAKEN_API_KEY)
    .set('API-Sign', signature)
    .set('Content-Type', 'application/x-www-form-urlencoded')
    .set('Content-Length', requestBody.length)
    .send(requestBody)
    .retry();

  const { body } = response;
  const { error } = body;

  if (error && error.length) {
    throw new Error(`Kraken error: ${error[0]}`);
  }

  const { result } = body;
  assert(result);
  return result;
});

// returns { id, }
async function placeOrder(order) {
  const {
    symbol,
    side,
    price,
    size,
    internalId,
    postOnly = true,
    expiresIn = 90,
  } = order;

  assert(side && size && price);
  assert(side === 'buy' || side === 'sell');
  assert(symbol);
  assert(price);
  assert(size);

  const pair = symbol;
  assert(pair, `Unknown symbol ${symbol}`);

  try {
    const { txid } = await krakenPost('/0/private/AddOrder', {
      pair,
      type: side,
      ordertype: 'limit',
      price: parseFloat(price).toFixed(6),
      volume: size.toString(),
      ...(postOnly ? { oflags: 'post' } : {}),
      ...(expiresIn ? { expiretm: `+${expiresIn}` } : {}),
    });

    if (!txid.length) {
      throw new Error('Failed to place order');
    }

    return txid[0];
  } catch (error) {
    // TODO: Error handling
    throw error;
  }
}

// returns true or false
async function cancelOrder(id) {
  assert.equal(typeof id, 'string');

  let result;

  try {
    result = await krakenPost('/0/private/CancelOrder', {
      txid: id,
    });
  } catch (error) {
    if (error.message.match(/EOrder:Unknown order/)) {
      return false;
    }

    throw error;
  }

  const { count } = result;

  if (!count) {
    throw new Error(`Failed to cancel order ${id}`);
  }

  return !!count;
}

async function fetchMyOpenOrders() {
  const [error, result] = await safely(() => krakenPost('/0/private/OpenOrders', {
    trades: false,
  }));

  if (error) {
    throw error;
  }

  const orders = reduce(result.open || {}, (prev, item, id) => {
    const { pair } = item.descr;

    return {
      ...prev,
      [id]: {
        id,
        symbol: pair,
        side: item.descr.type,
        price: +item.descr.price,
        size: +item.vol - +item.vol_exec,
        createdAt: +new Date(item.opentm * 1e3),
      },
    };
  }, {});

  return orders;
}

async function fetchOrderBook(pair, count) {
  assert.equal(typeof pair, 'string');
  assert(count === undefined || typeof count === 'number');

  const result = await krakenGet(`/0/public/Depth?pair=${pair}`);
  const { bids, asks } = result[Object.keys([result][0])];

  return {
    bids,
    asks,
  };
}

async function fetchMyBalances() {
  const [error, result] = await safely(() => krakenPost('/0/private/Balance'));

  if (error) {
    throw error;
  }

  return reduce(result, (prev, balance, currency) => {
    return {
      ...prev,
      [currency]: +balance,
    }
  }, {});
}

module.exports = {
  placeOrder,
  cancelOrder,
  fetchMyBalances,
  fetchMyOpenOrders,
  fetchOrderBook,
};
#!/usr/bin/env node
const assert = require('assert');
const { delay } = require('bluebird');
const BigNumber = require('bignumber.js');
const kraken = require('./kraken');

const {
  fetchMyOpenOrders,
  fetchOrderBook,
  placeOrder,
  cancelOrder,
} = kraken;

const {
  KMT_SIZE = 0.01,
  KMT_MARGIN = 0.05,
  KMT_INTERVAL = 10e3,
  KMT_SYMBOL,
  KMT_SHORT_SYMBOL,
  KMT_SIDE,
} = process.env;

assert(KMT_SYMBOL, 'KMT_SYMBOL is required');
assert(KMT_SHORT_SYMBOL, 'KMT_SHORT_SYMBOL is required');
assert(KMT_SIDE, 'KMT_SIDE is required');
assert(['buy', 'sell'].includes(KMT_SIDE), KMT_SIDE);

const bn = (...args) => new BigNumber(...args);

const cancelAllOrders = async () => {
  const openOrders = await fetchMyOpenOrders();

  for (const orderId of Object.keys(openOrders)) {
    const order = openOrders[orderId];
    if (order.symbol !== KMT_SHORT_SYMBOL) { continue; }
    await cancelOrder(orderId);
  }
};

const tick = async () => {
  const orderBook = await fetchOrderBook(KMT_SYMBOL);
  let price;

  if (KMT_SIDE === 'buy') {
    const [top] = orderBook.bids;
    const [topPrice] = top;
    price = bn(topPrice).mul(bn(1).minus(KMT_MARGIN));
  } else {
    const [top] = orderBook.asks;
    const [topPrice] = top;
    price = bn(topPrice).mul(bn(1).plus(KMT_MARGIN));
  }

  const size = +KMT_SIZE;

  const order = {
    symbol: KMT_SYMBOL,
    price: price.toFixed(5),
    size: size.toFixed(3),
    side: KMT_SIDE,
  };

  await cancelAllOrders();

  console.log(`${order.side} ${order.size} ${KMT_SYMBOL} @ ${order.price}`);

  try {
    const orderId = await placeOrder(order);
    console.log(orderId);
  } catch (error) {
    if (error.message.match(/EOrder:Insufficient funds/)) {
      console.log('error: insufficient funds');
      return;
    }
    throw error;
  }
};

(async () => {
  const loop = async () => {
    while (true) {
      await tick();
      await delay(+KMT_INTERVAL);
    }
  };

  try {
    await loop();
  } catch (error) {
    console.error('Unhandled error:');
    console.error(error.stack);
    console.error('Trying to cancel all');
    await cancelAllOrders();
    process.exit(1);
  }
})();