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);
}
})();