-
Notifications
You must be signed in to change notification settings - Fork 9
/
index.js
738 lines (653 loc) · 27.3 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
const _ = require('lodash');
const coinifyCurrency = require('@coinify/currency');
const Error = require('./lib/ErrorHelper');
const Request = require('./lib/RequestHelper');
const {
convertFromKrakenCurrencyCode,
convertToKrakenCurrencyCode,
convertFromKrakenTransaction,
convertFromKrakenTrade
} = require('./lib/ApiResponseConverter');
const consoleLogLevel = require('console-log-level');
const constants = require('./lib/constants');
const Kraken = function (settings) {
this.key = settings.key;
this.secret = settings.secret;
this.otp = settings.otp;
this.host = settings.host || constants.HOST;
this.timeout = settings.timeout || constants.REQUEST_TIMEOUT;
this.logger = settings.logger || consoleLogLevel({ level: process.env.LOG_LEVEL || 'debug' });
const normalizedSupportedCurrencies = settings.supportedCurrencies || constants.SUPPORTED_BASE_CURRENCIES;
this.supportedCurrencies = normalizedSupportedCurrencies.map(convertToKrakenCurrencyCode);
};
/* ================= API endpoints exposed methods ================= */
function validateCurrenciesConstructPair(baseCurrency, quoteCurrency) {
if (!baseCurrency || !quoteCurrency) {
return {
error: Error.create('Missing base currency or quote currency',
Error.MODULE_ERROR, null)
};
}
baseCurrency = baseCurrency.toUpperCase();
quoteCurrency = quoteCurrency.toUpperCase();
if (!_.includes(constants.SUPPORTED_BASE_CURRENCIES, baseCurrency) || !_.includes(constants.SUPPORTED_QUOTE_CURRENCIES, quoteCurrency)) {
return {
error: Error.create(`Kraken only supports ${constants.SUPPORTED_BASE_CURRENCIES.join(', ')} as base and ${constants.SUPPORTED_QUOTE_CURRENCIES.join(', ')} as quote currencies.`,
Error.MODULE_ERROR, null)
};
}
/* Kraken returns XBT as BTC. It accepts both variations, but returns XBT only. */
const baseCurrencyKraken = baseCurrency === 'BTC' ? 'XBT' : baseCurrency;
let pair;
let inversePair = false;
const concatPair = `${baseCurrency}${quoteCurrency}`;
// Map to inconsistent Kraken pairs
if ([ 'BTC', 'ETH' ].includes(baseCurrency)) {
pair = `X${baseCurrencyKraken}Z${quoteCurrency}`;
} else if ([ 'USDT' ].includes(baseCurrency) && [ 'USD' ].includes(quoteCurrency)) {
pair = `${baseCurrencyKraken}Z${quoteCurrency}`;
}else if (constants.INVERSE_CURRENCY_PAIRS[concatPair]){
const inverseBase = constants.INVERSE_CURRENCY_PAIRS[concatPair].slice(0, quoteCurrency.length);
const inverseQuote = constants.INVERSE_CURRENCY_PAIRS[concatPair].slice(quoteCurrency.length);
inversePair = true;
pair = `X${inverseBase}Z${inverseQuote}`;
} else {
pair = `${baseCurrencyKraken}${quoteCurrency}`;
}
// const inversePair = constants.INVERSE_CURRENCY_PAIRS[`${quoteCurrency}${baseCurrency}`] || false;
return {
baseCurrency,
quoteCurrency,
baseCurrencyKraken,
pair,
inversePair
};
}
/**
* Returns the current order book of Kraken in a custom organized look
*
* @param {string} baseCurrency The currency code (3 chars) of the base currency of the order book
* @param {string} quoteCurrency The currency code (3 chars) of the quote currency of the order book
* @param {function} callback Returns the customized Order Book data object
* orderBook:
* {
* baseCurrency: "BTC", // The currency of baseAmount
* quoteCurrency: "USD", // The currency to determine the price <quoteCurrency>/baseCurrency>
* asks: [ // List of entries with bitcoins for sale, sorted by lowest price first
* {
* price: 450.65,
* baseAmount: 44556677 // 0.44556677 BTC for sale
* }
* // ... more ask entries
* ],
* bids: [ // List of entries for buying bitcoins, sorted by most highest price first
* {
* price: 450.31,
* baseAmount: 33445566 // Someone wants to buy 0.33445566 BTC
* }
* // ... more bid entries
* ]
* }
*/
Kraken.prototype.getOrderBook = function (baseCurrency, quoteCurrency, callback) {
const currencies = validateCurrenciesConstructPair(baseCurrency, quoteCurrency);
if (currencies.error) {
return callback(currencies.error);
}
Request.get(this, 'Depth', {
qs: { pair: currencies.pair }
}, (err, res) => {
if (err) {
return callback(err);
}
// This happened, so lets fix it for kraken
if (res['BTC/USD']) {
res.XXBTZUSD = res['BTC/USD'];
}
if (!res[currencies.pair]) {
return callback(Error.create(`Currency pair: ${currencies.pair} is not in response`,
Error.MODULE_ERROR, null));
}
/* Declare the orderBook object with the currency pair */
const orderBook = {
baseCurrency: currencies.baseCurrency,
quoteCurrency: currencies.quoteCurrency,
bids: [],
asks: []
};
/* Organize the Order Book values in a custom way */
const convertRawEntry = function convertRawEntry(entry) {
const price = parseFloat(entry[0]);
const baseAmount = coinifyCurrency.toSmallestSubunit(parseFloat(entry[1]), currencies.baseCurrency);
if (currencies.inversePair) {
return {
price: 1 / price,
baseAmount: coinifyCurrency.toSmallestSubunit(baseAmount * price, currencies.quoteCurrency)
};
}
return { price, baseAmount };
};
const rawBids = res[currencies.pair].bids || [];
const rawAsks = res[currencies.pair].asks || [];
/* Declare and assign the organized bids and asks to the orderBook object */
orderBook.bids = rawBids.map(convertRawEntry);
orderBook.asks = rawAsks.map(convertRawEntry);
return callback(null, orderBook);
});
};
/**
* Returns ticker information for a given currency pair.
*
* @param {string} baseCurrency The currency code (3 chars) of the base currency of the ticker
* @param {string} quoteCurrency The currency code (3 chars) of the quote currency of the ticker
* @param {function} callback Returns the customized Order Book data object
* {
* baseCurrency: 'BTC',
* quoteCurrency: 'USD',
* bid: 649.89,
* ask: 650.12,
* lastPrice: 649.97,
* high24Hours: 652.55,
* low24Hours: 634.98,
* vwap24Hours: 647.37,
* volume24Hours: 1234567890 // 12.3456789 BTC
* }
*/
Kraken.prototype.getTicker = function (baseCurrency, quoteCurrency, callback) {
const currencies = validateCurrenciesConstructPair(baseCurrency, quoteCurrency);
if (currencies.error) {
return callback(currencies.error);
}
Request.get(this, 'Ticker', {
qs: { pair: currencies.pair }
}, (err, res) => {
if (err) {
return callback(err);
}
/*
* Kraken returns a response with the following format:
* {
* <pair_name>: {
* a: [<price>, <whole lot volume>, <lot volume>], // ask
* b: [<price>, <whole lot volume>, <lot volume>], // bid
* c: [<price>, <lot volume>], // last trade closed
* v: [<today>, <last 24 hours>], // volume
* p: [<today>, <last 24 hours>], // volume weighted average price
* t: [<today>, <last 24 hours>], // number of trades
* l: [<today>, <last 24 hours>], // low
* h: [<today>, <last 24 hours>], // high
* o: <price> // today's opening price
* }
* }
*/
const result = res[currencies.pair];
if(currencies.inversePair){
return callback(null, {
baseCurrency: currencies.baseCurrency, // We do not flip the input pair as it is the correct direction
quoteCurrency: currencies.quoteCurrency,
bid: 1 / parseFloat(result.a[0]), // flip bid to ask
ask: 1 / parseFloat(result.b[0]), // flip ask to bid
lastPrice: 1 / parseFloat(result.c[0]),
high24Hours: 1 / parseFloat(result.l[1]), // flip low to high
low24Hours: 1 / parseFloat(result.h[1]), // flip high to low
vwap24Hours: 1 / parseFloat(result.p[1]),
volume24Hours: coinifyCurrency.toSmallestSubunit(result.v[1], currencies.quoteCurrency)
});
}
return callback(null, {
baseCurrency: currencies.baseCurrency,
quoteCurrency: currencies.quoteCurrency,
bid: parseFloat(result.b[0]),
ask: parseFloat(result.a[0]),
lastPrice: parseFloat(result.c[0]),
high24Hours: parseFloat(result.h[1]),
low24Hours: parseFloat(result.l[1]),
vwap24Hours: parseFloat(result.p[1]),
volume24Hours: coinifyCurrency.toSmallestSubunit(result.v[1], currencies.baseCurrency)
});
});
};
/**
* Returns the available and total balance amounts of the account.
*
* @param {function} callback Returns the customized balance object
* balance: {
* available: {
* USD: <int subunit amount>,
* EUR: <int subunit amount>,
* BTC: <int subunit amount>
* }
* total: {
* USD: <int subunit amount>,
* EUR: <int subunit amount>,
* BTC: <int subunit amount>
* }
* }
*/
Kraken.prototype.getBalance = function (callback) {
/* Firstly, get the total balance values */
Request.post(this, 'Balance', null, (err, resBalance) => {
if (err) {
return callback(err);
}
/* Secondly, get the open orders */
Request.post(this, 'OpenOrders', {}, (err, resOrders) => {
if (err) {
return callback(err);
}
try {
// Get currencies we want to store balance for and convert to subunits
const total = _(resBalance)
.mapKeys((amount, currency) => convertFromKrakenCurrencyCode(currency))
.pickBy((amount, currency) => coinifyCurrency.isValidCurrency(currency))
.mapValues((amount, currency) => coinifyCurrency.toSmallestSubunit(amount, currency))
.value();
const toSubtractFromTotal = _(_.cloneDeep(total)).mapValues(() => 0).value();
/*
* Loop through the open orders (reserved) and accumulate amounts to be subtracted from the total balance, so
* that we calculate how much is the available balance
*/
_.forEach(resOrders.open, (order) => {
const baseCurrency = convertFromKrakenCurrencyCode(order.descr.pair.slice(0, 3));
const quoteCurrency = order.descr.pair.slice(3);
/*
* The crypto currency is always the base and fiat is the quote in the orders. Therefore, for SELL orders we
* want to see the volume we are selling (the `vol` property) and reserve it. And for BUY orders we want to see
* how much we are buying for (the `descr.price` property) and reserve it.
*/
switch (order.descr.type) {
case 'sell':
toSubtractFromTotal[baseCurrency] += coinifyCurrency.toSmallestSubunit(order.vol, baseCurrency);
break;
case 'buy':
toSubtractFromTotal[quoteCurrency] += coinifyCurrency.toSmallestSubunit(order.descr.price, quoteCurrency);
break;
}
});
const available = _(_.cloneDeep(total)).mapValues((amount, currency) => amount - toSubtractFromTotal[currency]).value();
return callback(null, {
available,
total
});
} catch (err) {
return callback(err);
}
});
});
};
/**
* Fetches a trade object which contains the status and an array of the transactions to that trade.
* Constructs and returns an object with trade currency pair and accummulated amounts from all transactions
* of the trade.
*
* @param {object} tradeToQuery An object that contains data about the trade to be fetched. Must have at least the
* following structure:
* trade:
* {
* raw: {
* txid: <string> the_trade_id,
* <string> order_type
* }
* }
* @param {function} callback Returns the found and customized trade object:
* {
* type: 'limit',
* state: 'closed',
* baseAmount: -200000000, // Sold 2.00000000 BTC...
* quoteAmount: 74526, // ... for 745.26 USD
* baseCurrency: 'BTC' // coinifyCurrency of the baseAmount
* quoteCurrency: 'USD' // coinifyCurrency of the quoteAmount
* feeAmount: 11, // We paid 0.11 USD to the exchange as commission for the order
* feeCurrency: 'USD', // coinifyCurrency of the feeAmount
* raw: {...}, // Exchange-specific object
* }
*/
Kraken.prototype.getTrade = function (tradeToQuery, callback) {
if (!tradeToQuery || !callback) {
return callback(Error.create('Trade object is a required parameter.', Error.MODULE_ERROR, null));
}
if (!tradeToQuery.raw) {
return callback(Error.create('\'trade.raw\' is a required property.', Error.MODULE_ERROR, null));
}
const txid = getTransactionIdFromTrade(tradeToQuery);
Request.post(this, 'QueryTrades', {
txid
}, (err, resTrade) => {
/*
* 'QueryTrades' returns only the 'closed' Trades, whereas we want to check for trades in both 'closed' and 'open'
* "Invalid order" error means tha provided 'txid' is of non-existent Trade - therefore check if it's still an
* 'open' order.
*/
if (err && err.errorMessages && _.find(err.errorMessages, msg => msg === 'EOrder:Invalid order')) {
Request.post(this, 'QueryOrders', {
txid
}, (err, resOrder) => {
if (err) {
return callback(err);
}
/*
* Kraken returns a response, from QueryTrades endpoint, with the following format (listing only properties
* that we actually use):
*
* { 'OEX7R7-ID6ZP-KATRCD':
* {
* status: 'open',
* descr:
* { pair: 'XBTEUR',
* type: 'buy',
* ordertype: 'limit',
* price: '1.000',
* order: 'buy 4.00000000 XBTEUR @ limit 1.000'
* },
* vol: '4.00000000',
* fee: '0.00000'
* }
*/
return callback(null, constructTradeObject(resOrder, 'order', txid));
});
} else {
if (err) {
return callback(err);
}
/*
* Kraken returns a response, from QueryTrades endpoint, with the following format (listing only properties
* that we actually use):
*
* { 'TEBCPN-YCQ7U-PQSUJF': // ID of the Trade that we are querying
* {
* pair: 'XXBTZEUR', // coinifyCurrency pair of the Trade: 'X<baseCurrency>Z<quoteCurrency>'
* type: 'sell', // type of the trade (sell/buy)
* ordertype: 'market', // type of the order (market/limit)
* price: '507.00000', // the price it's been sold/bought for, denominated in quoteCurrency
* fee: '0.01318', // commission fee paid for the order execution, denominated in quoteCurrency
* vol: '0.01000000' // the volume that has been bough/sold, denominated in baseCurrency
* }
* }
*/
return callback(null, constructTradeObject(resTrade, 'trade', txid));
}
});
};
/* Extracts the transaction id from the trade object
*
* @param {object} trade object returned by placeTrade or getTrade method.
* @returns {string} transactionId
*/
function getTransactionIdFromTrade(trade) {
const raw = trade.raw;
if (raw.hasOwnProperty('txid')) {
return raw.txid[0];
}
return Object.getOwnPropertyNames(raw)[0];
}
/**
* Constructs and returns the desired custom formatted object.
*
* When a trade - all data needed is contained withing the main sub-object of the response: `res[txid]`.
* When an open order - some of the values are withing the main sub-object, and others are withing a sub-sub-object:
* `res[txid].descr`
* That is why an `entityType` is passed as an argument and throughout the function body, different values are picked
* from different sub-objects, for `!isTrade` (non-trade) response data.
*
* @param {object} responseData The response object itthis
* @param {string} entityType The type: 'trade' or 'order'
* @param {string} tradeId The trade ID
* @returns See return result of getTrade() endpoint
*/
function constructTradeObject(responseData, entityType, tradeId) {
const isTrade = entityType === 'trade', // TRUE if it's a Trade, FALSE if it's an Open Order
tradeValues = entityType === 'trade' ? responseData[tradeId] : responseData[tradeId].descr;
let baseCurrency = isTrade ? tradeValues.pair.slice(1, 4) : tradeValues.pair.slice(0, 3);
const quoteCurrency = isTrade ? tradeValues.pair.slice(5) : tradeValues.pair.slice(3);
baseCurrency = baseCurrency === 'XBT' ? 'BTC' : baseCurrency;
const baseAmount = coinifyCurrency.toSmallestSubunit(parseFloat(responseData[tradeId].vol), baseCurrency);
let state = isTrade ? 'closed' : responseData[tradeId].status;
// Rename canceled to cancelled
if (state === 'canceled') {
state = 'cancelled';
}
//Kraken returns quote amount and fee amount as 0 for open orders, we would like it to be null
let quoteAmount = null;
let feeAmount = null;
if (state !== 'open') {
quoteAmount = coinifyCurrency.toSmallestSubunit(parseFloat(responseData[tradeId].cost), quoteCurrency);
quoteAmount = tradeValues.type === constants.TYPE_BUY_ORDER ? -quoteAmount : quoteAmount;
feeAmount = coinifyCurrency.toSmallestSubunit(parseFloat(responseData[tradeId].fee), quoteCurrency);
}
return {
externalId: tradeId,
type: tradeValues.ordertype,
state,
baseAmount: tradeValues.type === constants.TYPE_SELL_ORDER ? -baseAmount : baseAmount,
quoteAmount,
baseCurrency,
quoteCurrency,
feeAmount,
feeCurrency: quoteCurrency,
raw: responseData
};
}
/**
* Returns a list of transactions objects, starting from the latest one, descending, fetched from your Kraken
* account.
* If the `latestTransaction` is provided, then fetch the transactions from the provided one, onwards.
* Otherwise, return ALL transactions.
*
* @param {object} latestTransaction The transaction object, onwards from which to start fetching deposits. Must have
* a raw.time attribute which represents a unix timestamp in seconds
* @param {function} callback Returns the found transactions, sorted by earliest first
*/
Kraken.prototype.listTransactions = function (latestTransaction, callback) {
// Allow for listTransactions(callback) shorthand
if (typeof latestTransaction === 'function') {
callback = latestTransaction;
latestTransaction = undefined;
}
// If a latestTransaction is given, use that to not get transactions earlier than that one.
// We have to truncate the unix timestamp in order to be sure that latestTransaction is included in the response
const start = latestTransaction ? Math.trunc(latestTransaction.raw.time) : null;
// Get withdrawals
return this._listTransactionsRecursive('withdrawal', start, [], (err, withdrawals) => {
if (err) {
return callback(err);
}
// Get deposits
return this._listTransactionsRecursive('deposit', start, [], (err, deposits) => {
if (err) {
return callback(err);
}
// Join deposits and withdrawals into a single array
const transactions = withdrawals.concat(deposits);
// Sort transactions by earliest first
const transactionsSorted = _.sortBy(transactions, [ 'raw.time' ]);
// Return sorted transactions to caller
return callback(null, transactionsSorted);
});
});
};
/**
* Recursively calls the Ledgers endpoint until all ledger entries have been fetched
*
* @param {string} type Type of ledger entry to query for. 'withdrawal' or 'deposit'
* @param {float|null} start Unix timestamp of earliest entry to query for
* @param {array} knownTransactions Transactions already received from previous calls
* @param {function} callback Yields a list of transactions, sorting is undefined
* @private
*/
Kraken.prototype._listTransactionsRecursive = function (type, start, knownTransactions, callback) {
// Perform POST request to Ledgers endpoint
const postData = {
type: type,
ofs: knownTransactions.length
};
if (start) {
postData.start = start;
}
return Request.post(this, 'Ledgers', postData, (err, response) => {
// Relay error, if any
if (err) {
return callback(err);
}
// Check that the response contains ledger
if (!_.isObject(response.ledger)) {
const error = Error.create('Unexpected response from Ledgers endpoint', Error.EXCHANGE_SERVER_ERROR);
error.responseBody = JSON.stringify(response);
return callback(error);
}
try {
// Construct transaction objects for each ledger entry
const transactions = _.values(response.ledger).map(convertFromKrakenTransaction);
// Merge our newly converted transactions with the ones from previous calls
const allTransactions = transactions.concat(knownTransactions);
// Decide how to progress: Continue if there are more ledger entries, otherwise just stop now.
if (_.isEmpty(response.ledger)) {
// No more ledger entries to be found. Let's just return them now!
return callback(null, allTransactions);
}
// There are more entries to be found, let's call this function recursively and add the results to our list
return this._listTransactionsRecursive(type, start, allTransactions, callback);
} catch (err) {
return callback(err);
}
});
};
/**
* Place a limit BUY or SELL trade (order), depending on the sign of the baseAmount provided.
* SELL if amount is negative
* BUY if amount is positive
*
* @param {int} baseAmount The amount in base currency to buy or sell on the exchange; If negative amount,
* place sell limit order. If positive amount, place buy limit order. Denominated in
* smallest sub-unit of the base currency
* @param {number} limitPrice The minimum/maximum rate that you want to sell/buy for. If baseAmount is negative, this
* is the minimum rate to sell for. If baseAmount is positive, this is the maximum rate to
* buy for. limitPrice must always strictly positive
* @param {string} baseCurrency The exchange's base currency. For Kraken it is always BTC or BSV
* @param {string} quoteCurrency The exchange's quote currency. For Kraken it is always USD
* @param {function} callback Returns the customized data object of the placed trade object data
*/
Kraken.prototype.placeTrade = function (baseAmount, limitPrice, baseCurrency, quoteCurrency, callback) {
const currencies = validateCurrenciesConstructPair(baseCurrency, quoteCurrency);
if (currencies.error) {
return callback(currencies.error);
}
if (!baseAmount || isNaN(baseAmount) || baseAmount === 0) {
return callback(Error.create('The base amount must be a number and larger or smaller than 0.', Error.MODULE_ERROR, null));
}
if (!limitPrice || isNaN(limitPrice) || limitPrice < 0) {
return callback(Error.create('The limit price must be a positive number.', Error.MODULE_ERROR, null));
}
/*
* API call fails if there is more than 1 decimails of precision
* So we round to nearest decimal
*/
limitPrice = Math.round(10 * limitPrice) / 10;
/* Decide whether to place a BUY or a SELL trade */
const orderType = baseAmount < 0 ? constants.TYPE_SELL_ORDER : constants.TYPE_BUY_ORDER,
amountSubUnit = Math.abs(baseAmount);
/*
* The amount passed to the method is denominated in smallest sub-unit, but Kraken API requires
* the amount to be in main unit, so we convert it.
*/
const amountMainUnit = coinifyCurrency.fromSmallestSubunit(amountSubUnit, currencies.baseCurrency);
const params = {
pair: currencies.pair,
type: orderType,
ordertype: 'limit',
price: limitPrice,
volume: amountMainUnit
};
Request.post(this, 'AddOrder', params, (err, res) => {
if (err) {
return callback(err);
}
/* Construct the custom trade response object */
const trade = {
externalId: res.txid[0],
type: 'limit',
state: 'open',
baseAmount: baseAmount,
baseCurrency: currencies.baseCurrency,
quoteCurrency: currencies.quoteCurrency,
limitPrice: limitPrice,
raw: res
};
return callback(null, trade);
});
};
/**
* Lists Trade History for a given period.
*
* @typedef {Function} CallbackFn
*
* @param {Error|null} error
* @param {{
* externalId: string;
* timestamp: string;
* state: 'closed' | string;
* baseCurrency: string;
* baseAmount: number;
* feeAmount: number;
* quoteCurrency: string;
* quoteAmount: number;
* type: 'buy' | 'sell';
* orderType: string;
* raw: any;
* }[]} trade - An array of trade details.
*
* @returns {void}
*/
/**
* @param {Date} fromDateTime The start DateTime
* @param {Date} toDateTime The end DateTime
* @param {CallbackFn} callback Returns the customized data object of the trades in the given period
*/
Kraken.prototype.listTradeHistoryForPeriod = function (fromDateTime, toDateTime, callback) {
if (Object.prototype.toString.call(fromDateTime) !== '[object Date]' || Object.prototype.toString.call(toDateTime) !== '[object Date]') {
return callback(Error.create('fromDateTime and toDateTime must be an instance of Date.', Error.MODULE_ERROR, null), null);
}
Request.post(this, 'TradesHistory', {
type: 'all',
trades: false,
start: fromDateTime.getTime() / 1000,
end: toDateTime.getTime() / 1000
}, (err, res) => {
if (err) {
return callback(err, null);
}
const { trades } = res;
if (!trades || typeof trades !== 'object') {
return callback(Error.create('Invalid response from kraken trades endpoint.', Error.MODULE_ERROR, res), null);
}
if (Object.keys(trades).length === 0) {
return callback(null, []);
}
const validTrades = [];
for (const [ tradeId, trade ] of Object.entries(trades)) {
if (trade.time < fromDateTime.getTime() / 1000 || trade.time > toDateTime.getTime() / 1000) {
continue;
}
const [ err, converted ] = convertFromKrakenTrade(tradeId, trade);
if (err && !converted) {
this.logger.info('Cannot convert kraken trade to internal trade', { tradeId, trade });
continue;
}
validTrades.push(converted);
}
callback(null, validTrades);
});
};
Kraken.prototype.listTrades = async function (latestTrade = null) {
return new Promise((resolve, reject) => {
const latestTxDate = latestTrade && latestTrade.createTime
? latestTrade.createTime
: new Date(0);
this.listTradeHistoryForPeriod(latestTxDate, new Date(), (error, trades) => {
if (error) {
return reject(error);
}
resolve(trades || []);
});
});
};
module.exports = Kraken;