-
-
Save siman/d1059ee5aebadba2b9ff5decc6aa6e1b to your computer and use it in GitHub Desktop.
kraken-minimal-trader
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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); | |
} | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module.exports = function safely(fn, ...args) { | |
return Promise.resolve() | |
.then(!args ? fn : () => fn(...args)) | |
.then(result => [undefined, result]) | |
.catch(error => [error, undefined]); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment