Beta 7 with some repo names changed.

This commit is contained in:
Luis Fernando Molina committed 2020-11-15 13:05:19 +01:00
1 parent 0f7b40cca7
commit 963c19493a
3408 files changed
+462217 -15516

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
'use strict';
module.exports = {
"env": {
"node": true,
"es6": true,
"browser": true,
},
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "script",
},
"rules": {
"semi": "off",
"no-unused-vars": ["off"],
"quotes": ["off", "single"],
"quote-props": "off",
"func-call-spacing": ["error", "always"],
"one-var": "off",
"indent": ["warn", 4, { "ignoredNodes": ["ObjectPattern"] }],
"comma-style": "off",
"no-multi-spaces": "off",
"comma-dangle": "off",
"spaced-comment": "off",
"camelcase": "off",
"padded-blocks": "off",
"multiline-comment-style": "off",
"curly": "off",
"rest-spread-spacing": "off",
"no-multiple-empty-lines": "off",
"no-undef-init": "off",
"no-useless-return": "off",
"no-console": "off",
"operator-linebreak": "off",
"key-spacing": "off",
"brace-style": "off",
"padding-line-between-statements": ["off",
{"blankLine": "never", "prev":"*", "next": "*" },
{"blankLine": "always", "prev":"directive", "next": "*" },
{"blankLine": "always", "prev":"*", "next": "cjs-export" },
],
},
}
+1456
View File
File diff suppressed because it is too large. Load diff
+44
View File
@@ -0,0 +1,44 @@
'use strict';
const errorHierarchy = {
'BaseError': {
'ExchangeError': {
'AuthenticationError': {
'PermissionDenied': {},
'AccountSuspended': {},
},
'ArgumentsRequired': {},
'BadRequest': {
'BadSymbol': {},
},
'BadResponse': {
'NullResponse': {},
},
'InsufficientFunds': {},
'InvalidAddress': {
'AddressPending': {},
},
'InvalidOrder': {
'OrderNotFound': {},
'OrderNotCached': {},
'CancelPending': {},
'OrderImmediatelyFillable': {},
'OrderNotFillable': {},
'DuplicateOrderId': {},
},
'NotSupported': {},
},
'NetworkError': {
'DDoSProtection': {
'RateLimitExceeded': {},
},
'ExchangeNotAvailable': {
'OnMaintenance': {},
},
'InvalidNonce': {},
'RequestTimeout': {},
},
},
};
module.exports = errorHierarchy;
+51
View File
@@ -0,0 +1,51 @@
'use strict';
const errorHierarchy = require ('./errorHierarchy.js')
/* ------------------------------------------------------------------------ */
function subclass (BaseClass, classes, namespace = {}) {
for (const [className, subclasses] of Object.entries (classes)) {
const Class = Object.assign (namespace, {
/* By creating a named property, we trick compiler to assign our class constructor function a name.
Otherwise, all our error constructors would be shown as [Function: Error] in the debugger! And
the super-useful `e.constructor.name` magic wouldn't work — we then would have no chance to
obtain a error type string from an error instance programmatically! */
[className]: class extends BaseClass {
constructor (message) {
super (message)
/* A workaround to make `instanceof` work on custom Error classes in transpiled ES5.
See my blog post for the explanation of this hack:
https://medium.com/@xpl/javascript-deriving-from-error-properly-8d2f8f315801 */
this.constructor = Class
this.__proto__ = Class.prototype
this.name = className
this.message = message
}
}
})[className]
subclass (Class, subclasses, namespace)
}
return namespace
}
/* ------------------------------------------------------------------------ */
module.exports = subclass (
// Root class
Error,
// Derived class hierarchy
errorHierarchy
)
+28
View File
@@ -0,0 +1,28 @@
'use strict';
/* ------------------------------------------------------------------------ */
const { unCamelCase } = require ('./functions/string')
const unCamelCasePropertyNames = x => {
for (const k in x) x[unCamelCase (k)] = x[k] // camel_case_method = camelCaseMethod
return x
}
/* ------------------------------------------------------------------------ */
module.exports = unCamelCasePropertyNames (Object.assign ({}
, require ('./functions/platform')
, require ('./functions/generic')
, require ('./functions/string')
, require ('./functions/type')
, require ('./functions/number')
, require ('./functions/encode')
, require ('./functions/crypto')
, require ('./functions/time')
, require ('./functions/throttle')
, require ('./functions/misc')
))
/* ------------------------------------------------------------------------ */
+147
View File
@@ -0,0 +1,147 @@
'use strict'
/* ------------------------------------------------------------------------ */
const CryptoJS = require ('../../static_dependencies/crypto-js/crypto-js')
const { capitalize } = require ('./string')
const { stringToBase64, utf16ToBase64, urlencodeBase64 } = require ('./encode')
const NodeRSA = require ('./../../static_dependencies/node-rsa/NodeRSA')
const { numberToLE } = require ('./encode')
const EC = require ('./../../static_dependencies/elliptic/lib/elliptic').ec
const { ArgumentsRequired } = require ('./../errors')
const BN = require ('../../static_dependencies/BN/bn.js')
/* ------------------------------------------------------------------------ */
const hash = (request, hash = 'md5', digest = 'hex') => {
const options = {}
if (hash === 'keccak') {
hash = 'SHA3'
options['outputLength'] = 256
}
const result = CryptoJS[hash.toUpperCase ()] (request, options)
return (digest === 'binary') ? result : result.toString (CryptoJS.enc[capitalize (digest)])
}
/* ............................................. */
const hmac = (request, secret, hash = 'sha256', digest = 'hex') => {
const result = CryptoJS['Hmac' + hash.toUpperCase ()] (request, secret)
if (digest) {
const encoding = (digest === 'binary') ? 'Latin1' : capitalize (digest)
return result.toString (CryptoJS.enc[capitalize (encoding)])
}
return result
}
/* ............................................. */
function rsa (request, secret, alg = 'RS256') {
const algos = {
'RS256': 'pkcs1-sha256',
'RS512': 'pkcs1-sha512',
}
if (!(alg in algos)) {
throw new ExchangeError (alg + ' is not a supported rsa signing algorithm.')
}
const algorithm = algos[alg]
let key = new NodeRSA (secret, {
'environment': 'browser',
'signingScheme': algorithm,
})
return key.sign (request, 'base64', 'binary')
}
/**
* @return {string}
*/
function jwt (request, secret, alg = 'HS256') {
const algos = {
'HS256': 'sha256',
'HS384': 'sha384',
'HS512': 'sha512',
};
const encodedHeader = urlencodeBase64 (stringToBase64 (JSON.stringify ({ 'alg': alg, 'typ': 'JWT' })))
const encodedData = urlencodeBase64 (stringToBase64 (JSON.stringify (request)))
const token = [ encodedHeader, encodedData ].join ('.')
const algoType = alg.slice (0, 2);
const algorithm = algos[alg]
let signature = undefined
if (algoType === 'HS') {
signature = urlencodeBase64 (hmac (token, secret, algorithm, 'base64'))
} else if (algoType === 'RS') {
signature = urlencodeBase64 (rsa (token, secret, alg))
}
return [ token, signature ].join ('.')
}
function ecdsa (request, secret, algorithm = 'p256', hashFunction = undefined, fixedLength = false) {
let digest = request
if (hashFunction !== undefined) {
digest = hash (request, hashFunction, 'hex')
}
const curve = new EC (algorithm)
let signature = curve.sign (digest, secret, 'hex', { 'canonical': true })
let counter = new BN ('0')
const minimum_size = new BN ('1').shln (8 * 31).sub (new BN ('1'))
while (fixedLength && (signature.r.gt (curve.nh) || signature.r.lte (minimum_size) || signature.s.lte (minimum_size))) {
signature = curve.sign (digest, secret, 'hex', { 'canonical': true, 'extraEntropy': counter.toArray ('le', 32)})
counter = counter.add (new BN ('1'))
}
return {
'r': signature.r.toString (16).padStart (64, '0'),
's': signature.s.toString (16).padStart (64, '0'),
'v': signature.recoveryParam,
}
}
/* ------------------------------------------------------------------------ */
const totp = (secret) => {
const dec2hex = s => ((s < 15.5 ? '0' : '') + Math.round (s).toString (16))
, hex2dec = s => parseInt (s, 16)
, leftpad = (s, p) => (p + s).slice (-p.length) // both s and p are short strings
const base32tohex = (base32) => {
let base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
let bits = ''
let hex = ''
for (let i = 0; i < base32.length; i++) {
let val = base32chars.indexOf (base32.charAt (i).toUpperCase ())
bits += leftpad (val.toString (2), '00000')
}
for (let i = 0; i + 4 <= bits.length; i += 4) {
let chunk = bits.substr (i, 4)
hex = hex + parseInt (chunk, 2).toString (16)
}
return hex
}
const getOTP = (secret) => {
secret = secret.replace (' ', '') // support 2fa-secrets with spaces like "4TDV WOGO" → "4TDVWOGO"
let epoch = Math.round (new Date ().getTime () / 1000.0)
let time = leftpad (dec2hex (Math.floor (epoch / 30)), '0000000000000000')
let hmacRes = hmac (CryptoJS.enc.Hex.parse (time), CryptoJS.enc.Hex.parse (base32tohex (secret)), 'sha1', 'hex')
let offset = hex2dec (hmacRes.substring (hmacRes.length - 1))
let otp = (hex2dec (hmacRes.substr (offset * 2, 8)) & hex2dec ('7fffffff')) + ''
otp = (otp).substr (otp.length - 6, 6)
return otp
}
return getOTP (secret)
}
/* ------------------------------------------------------------------------ */
module.exports = {
hash,
hmac,
jwt,
totp,
rsa,
ecdsa,
}
/* ------------------------------------------------------------------------ */
+71
View File
@@ -0,0 +1,71 @@
"use strict";
/* ------------------------------------------------------------------------ */
const CryptoJS = require ('../../static_dependencies/crypto-js/crypto-js')
const qs = require ('../../static_dependencies/qs/index')
const BN = require ('../../static_dependencies/BN/bn')
/* ------------------------------------------------------------------------ */
module.exports =
{ json: (data, params = undefined) => JSON.stringify (data)
, unjson: JSON.parse
, isJsonEncodedObject: object => (
(typeof object === 'string') &&
(object.length >= 2) &&
((object[0] === '{') || (object[0] === '['))
)
, stringToBinary (str) {
const arr = new Uint8Array (str.length)
for (let i = 0; i < str.length; i++) { arr[i] = str.charCodeAt (i); }
return CryptoJS.lib.WordArray.create (arr)
}
, stringToBase64: string => CryptoJS.enc.Latin1.parse (string).toString (CryptoJS.enc.Base64)
, utf16ToBase64: string => CryptoJS.enc.Utf16 .parse (string).toString (CryptoJS.enc.Base64)
, base64ToBinary: string => CryptoJS.enc.Base64.parse (string)
, base64ToString: string => CryptoJS.enc.Base64.parse (string).toString (CryptoJS.enc.Utf8)
, binaryToBase64: binary => binary.toString (CryptoJS.enc.Base64)
, base16ToBinary: string => CryptoJS.enc.Hex.parse (string)
, binaryToBase16: binary => binary.toString (CryptoJS.enc.Hex)
, binaryConcat: (...args) => args.reduce ((a, b) => a.concat (b))
, binaryConcatArray: (arr) => arr.reduce ((a, b) => a.concat (b))
, urlencode: object => qs.stringify (object)
, urlencodeWithArrayRepeat: object => qs.stringify (object, { arrayFormat: 'repeat' })
, rawencode: object => qs.stringify (object, { encode: false })
, encode: x => x
, decode: x => x
// Url-safe-base64 without equals signs, with + replaced by - and slashes replaced by underscores
, urlencodeBase64: base64string => base64string.replace (/[=]+$/, '')
.replace (/\+/g, '-')
.replace (/\//g, '_')
, numberToLE: (n, padding) => {
const hexArray = new BN (n).toArray ('le', padding)
return byteArrayToWordArray (hexArray)
}
, numberToBE: (n, padding) => {
const hexArray = new BN (n).toArray ('be', padding)
return byteArrayToWordArray (hexArray)
}
}
function byteArrayToWordArray(ba) {
const wa = []
for (let i = 0; i < ba.length; i++) {
wa[(i / 4) | 0] |= ba[i] << (24 - 8 * i)
}
return CryptoJS.lib.WordArray.create (wa, ba.length)
}
module.exports['byteArrayToWordArray'] = byteArrayToWordArray
/* ------------------------------------------------------------------------ */
+239
View File
@@ -0,0 +1,239 @@
"use strict";
/* ------------------------------------------------------------------------ */
const { isObject, isNumber, isDictionary, isArray } = require ('./type')
/* ------------------------------------------------------------------------ */
const keys = Object.keys
, values = x => !isArray (x) // don't copy arrays if they're already arrays!
? Object.values (x)
: x
, index = x => new Set (values (x))
, extend = (...args) => Object.assign ({}, ...args) // NB: side-effect free
, clone = x => isArray (x)
? Array.from (x) // clones arrays
: extend (x) // clones objects
/* ------------------------------------------------------------------------ */
module.exports =
{ keys
, values
, extend
, clone
, index
, ordered: x => x // a stub to keep assoc keys in order (in JS it does nothing, it's mostly for Python)
, unique: x => Array.from (index (x))
, arrayConcat: (a, b) => a.concat (b)
/* ............................................. */
, inArray (needle, haystack) {
return haystack.includes (needle)
}
, toArray (object) {
return Object.values (object)
}
, isEmpty (object) {
if (!object)
return true;
return (Array.isArray (object) ? object : Object.keys (object)).length < 1;
}
/* ............................................. */
, keysort (x, out = {}) {
for (const k of keys (x).sort ())
out[k] = x[k]
return out
}
/* ............................................. */
/*
Accepts a map/array of objects and a key name to be used as an index:
array = [
{ someKey: 'value1', anotherKey: 'anotherValue1' },
{ someKey: 'value2', anotherKey: 'anotherValue2' },
{ someKey: 'value3', anotherKey: 'anotherValue3' },
]
key = 'someKey'
Returns a map:
{
value1: { someKey: 'value1', anotherKey: 'anotherValue1' },
value2: { someKey: 'value2', anotherKey: 'anotherValue2' },
value3: { someKey: 'value3', anotherKey: 'anotherValue3' },
}
*/
, indexBy (x, k, out = {}) {
for (const v of values (x))
if (k in v)
out[v[k]] = v
return out
}
/* ............................................. */
/*
Accepts a map/array of objects and a key name to be used as a grouping parameter:
array = [
{ someKey: 'value1', anotherKey: 'anotherValue1' },
{ someKey: 'value1', anotherKey: 'anotherValue2' },
{ someKey: 'value3', anotherKey: 'anotherValue3' },
]
key = 'someKey'
Returns a map:
{
value1: [
{ someKey: 'value1', anotherKey: 'anotherValue1' },
{ someKey: 'value1', anotherKey: 'anotherValue2' },
]
value3: [
{ someKey: 'value3', anotherKey: 'anotherValue3' }
],
}
*/
, groupBy (x, k, out = {}) {
for (const v of values (x)) {
if (k in v) {
const p = v[k]
out[p] = out[p] || []
out[p].push (v)
}
}
return out
}
/* ............................................. */
/*
Accepts a map/array of objects, a key name and a key value to be used as a filter:
array = [
{ someKey: 'value1', anotherKey: 'anotherValue1' },
{ someKey: 'value2', anotherKey: 'anotherValue2' },
{ someKey: 'value3', anotherKey: 'anotherValue3' },
]
key = 'someKey'
value = 'value1'
Returns an array:
[
value1: { someKey: 'value1', anotherKey: 'anotherValue1' },
]
*/
, filterBy (x, k, value = undefined, out = []) {
for (const v of values (x))
if (v[k] === value)
out.push (v)
return out
}
/* ............................................. */
, sortBy: (array, // NB: MUTATES ARRAY!
key,
descending = false,
direction = descending ? -1 : 1) => array.sort ((a, b) =>
((a[key] < b[key]) ? -direction :
((a[key] > b[key]) ? direction : 0)))
/* ............................................. */
, flatten: function flatten (x, out = []) {
for (const v of x) {
if (isArray (v)) flatten (v, out)
else out.push (v)
}
return out
}
/* ............................................. */
, pluck: (x, k) => values (x)
.filter (v => k in v)
.map (v => v[k])
/* ............................................. */
, omit (x, ...args) {
if (!Array.isArray (x)) {
const out = clone (x)
for (const k of args) {
if (isArray (k)) { // omit (x, ['a', 'b'])
for (const kk of k) {
delete out[kk]
}
}
else delete out[k] // omit (x, 'a', 'b')
}
return out
}
return x
}
/* ............................................. */
, sum (...xs) {
const ns = xs.filter (isNumber) // leave only numbers
return (ns.length > 0)
? ns.reduce ((a, b) => a + b, 0)
: undefined
}
/* ............................................. */
, deepExtend: function deepExtend (...xs) {
let out = undefined
for (const x of xs) {
if (isDictionary (x)) {
if (!isObject (out)) {
out = {}
}
for (const k in x) {
out[k] = deepExtend (out[k], x[k])
}
} else {
out = x
}
}
return out
}
/* ------------------------------------------------------------------------ */
}
+131
View File
@@ -0,0 +1,131 @@
'use strict';
const { ROUND_UP, ROUND_DOWN } = require ('./number')
const { NotSupported } = require ('./../errors')
//-------------------------------------------------------------------------
// converts timeframe to seconds
const parseTimeframe = (timeframe) => {
const amount = timeframe.slice (0, -1)
const unit = timeframe.slice (-1)
let scale = undefined;
if (unit === 'y') {
scale = 60 * 60 * 24 * 365
} else if (unit === 'M') {
scale = 60 * 60 * 24 * 30
} else if (unit === 'w') {
scale = 60 * 60 * 24 * 7
} else if (unit === 'd') {
scale = 60 * 60 * 24
} else if (unit === 'h') {
scale = 60 * 60
} else if (unit === 'm') {
scale = 60
} else if (unit === 's') {
scale = 1
} else {
throw new NotSupported ('timeframe unit ' + unit + ' is not supported')
}
return amount * scale
}
const roundTimeframe = (timeframe, timestamp, direction = ROUND_DOWN) => {
const ms = parseTimeframe (timeframe) * 1000
// Get offset based on timeframe in milliseconds
const offset = timestamp % ms
return timestamp - offset + ((direction === ROUND_UP) ? ms : 0);
}
// given a sorted arrays of trades (recent last) and a timeframe builds an array of OHLCV candles
const buildOHLCVC = (trades, timeframe = '1m', since = -Infinity, limit = Infinity) => {
const ms = parseTimeframe (timeframe) * 1000;
const ohlcvs = [];
const [ timestamp, /* open */, high, low, close, volume, count ] = [ 0, 1, 2, 3, 4, 5, 6 ];
const oldest = Math.min (trades.length - 1, limit);
for (let i = 0; i <= oldest; i++) {
const trade = trades[i];
if (trade.timestamp < since)
continue;
const openingTime = Math.floor (trade.timestamp / ms) * ms; // shift to the edge of m/h/d (but not M)
const candle = ohlcvs.length - 1;
if (candle === -1 || openingTime >= ohlcvs[candle][timestamp] + ms) {
// moved to a new timeframe -> create a new candle from opening trade
ohlcvs.push ([
openingTime, // timestamp
trade.price, // O
trade.price, // H
trade.price, // L
trade.price, // C
trade.amount, // V
1, // count
]);
} else {
// still processing the same timeframe -> update opening trade
ohlcvs[candle][high] = Math.max (ohlcvs[candle][high], trade.price);
ohlcvs[candle][low] = Math.min (ohlcvs[candle][low], trade.price);
ohlcvs[candle][close] = trade.price;
ohlcvs[candle][volume] += trade.amount;
ohlcvs[candle][count]++;
} // if
} // for
return ohlcvs;
}
const extractParams = (string) => {
const re = /{([\w-]+)}/g
const matches = []
let match = re.exec (string)
while (match) {
matches.push (match[1])
match = re.exec (string)
}
return matches
}
const implodeParams = (string, params) => {
if (!Array.isArray (params)) {
const keys = Object.keys (params)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (!Array.isArray (params[key])) {
string = string.replace ('{' + key + '}', params[key])
}
}
}
return string
}
/* ------------------------------------------------------------------------ */
module.exports = {
aggregate (bidasks) {
const result = {}
for (let i = 0; i < bidasks.length; i++) {
const [ price, volume ] = bidasks[i];
if (volume > 0) {
result[price] = (result[price] || 0) + volume
}
}
return Object.keys (result).map (price => [parseFloat (price), parseFloat (result[price])])
},
parseTimeframe,
roundTimeframe,
buildOHLCVC,
ROUND_UP,
ROUND_DOWN,
implodeParams,
extractParams
}
/* ------------------------------------------------------------------------ */
+335
View File
@@ -0,0 +1,335 @@
'use strict'
/* ------------------------------------------------------------------------
NB: initially, I used objects for options passing:
decimalToPrecision ('123.456', { digits: 2, round: true, afterPoint: true })
...but it turns out it's hard to port that across different languages and it is also
probably has a performance penalty -- while it's a performance critical code! So
I switched to using named constants instead, as it is actually more readable and
succinct, and surely doesn't come with any inherent performance downside:
decimalToPrecision ('123.456', ROUND, 2, DECIMAL_PLACES) */
const ROUND = 0 // rounding mode
, TRUNCATE = 1
, ROUND_UP = 2
, ROUND_DOWN = 3
const DECIMAL_PLACES = 0 // digits counting mode
, SIGNIFICANT_DIGITS = 1
, TICK_SIZE = 2
const NO_PADDING = 0 // zero-padding mode
, PAD_WITH_ZERO = 1
const precisionConstants = {
ROUND,
TRUNCATE,
ROUND_UP,
ROUND_DOWN,
DECIMAL_PLACES,
SIGNIFICANT_DIGITS,
TICK_SIZE,
NO_PADDING,
PAD_WITH_ZERO,
}
/* ------------------------------------------------------------------------ */
// See https://stackoverflow.com/questions/1685680/how-to-avoid-scientific-notation-for-large-numbers-in-javascript for discussion
function numberToString (x) { // avoids scientific notation for too large and too small numbers
if (typeof x === 'string') return x
const s = x.toString ()
if (Math.abs (x) < 1.0) {
const e = parseInt (s.split ('e-')[1])
const neg = (s[0] === '-')
if (e) {
x *= Math.pow (10, e - 1)
x = (neg ? '-' : '') + '0.' + (new Array (e)).join ('0') + x.toString ().substring (neg ? 3 : 2)
return x
}
} else {
const parts = s.split ('e')
if (parts[1]) {
let e = parseInt (parts[1])
const m = parts[0].split ('.')
if (m[1]) {
e -= m[1].length
}
return m[0] + m[1] + (new Array (e + 1)).join ('0')
}
}
return s;
}
//-----------------------------------------------------------------------------
// expects non-scientific notation
const truncate_regExpCache = []
, truncate_to_string = (num, precision = 0) => {
num = numberToString (num)
if (precision > 0) {
const re = truncate_regExpCache[precision] || (truncate_regExpCache[precision] = new RegExp ("([-]*\\d+\\.\\d{" + precision + "})(\\d)"))
const [ , result] = num.toString ().match (re) || [null, num]
return result.toString ()
}
return parseInt (num).toString ()
}
, truncate = (num, precision = 0) => parseFloat (truncate_to_string (num, precision))
function precisionFromString (string) {
const split = string.replace (/0+$/g, '').split ('.')
return (split.length > 1) ? (split[1].length) : 0
}
/* ------------------------------------------------------------------------ */
const decimalToPrecision = (x, roundingMode
, numPrecisionDigits
, countingMode = DECIMAL_PLACES
, paddingMode = NO_PADDING) => {
if (numPrecisionDigits < 0) {
if (countingMode === TICK_SIZE) {
throw new Error (`TICK_SIZE cant be used with negative numPrecisionDigits`)
}
const toNearest = Math.pow (10, -numPrecisionDigits)
if (roundingMode === ROUND) {
return (toNearest * decimalToPrecision (x / toNearest, roundingMode, 0, countingMode, paddingMode)).toString ()
}
if (roundingMode === TRUNCATE) {
return (x - (x % toNearest)).toString ()
}
}
/* handle tick size */
if (countingMode === TICK_SIZE) {
const precisionDigitsString = decimalToPrecision (numPrecisionDigits, ROUND, 100, DECIMAL_PLACES, NO_PADDING)
const newNumPrecisionDigits = precisionFromString (precisionDigitsString)
let missing = x % numPrecisionDigits
// See: https://github.com/ccxt/ccxt/pull/6486
missing = Number (decimalToPrecision (missing, ROUND, 8, DECIMAL_PLACES, NO_PADDING));
const fpError = decimalToPrecision (missing / numPrecisionDigits, ROUND, Math.max (newNumPrecisionDigits, 8), DECIMAL_PLACES, NO_PADDING)
if (precisionFromString (fpError) !== 0) {
if (roundingMode === ROUND) {
if (x > 0) {
if (missing >= numPrecisionDigits / 2) {
x = x - missing + numPrecisionDigits
} else {
x = x - missing
}
} else {
if (missing >= numPrecisionDigits / 2) {
x = Number (x) - missing
} else {
x = Number (x) - missing - numPrecisionDigits
}
}
} else if (roundingMode === TRUNCATE) {
x = x - missing
}
}
return decimalToPrecision (x, ROUND, newNumPrecisionDigits, DECIMAL_PLACES, paddingMode);
}
/* Convert to a string (if needed), skip leading minus sign (if any) */
const str = numberToString (x)
, isNegative = str[0] === '-'
, strStart = isNegative ? 1 : 0
, strEnd = str.length
/* Find the dot position in the source buffer */
for (var strDot = 0; strDot < strEnd; strDot++)
if (str[strDot] === '.')
break
const hasDot = strDot < str.length
/* Char code constants */
const MINUS = 45
, DOT = 46
, ZERO = 48
, ONE = (ZERO + 1)
, FIVE = (ZERO + 5)
, NINE = (ZERO + 9)
/* For -123.4567 the `chars` array will hold 01234567 (leading zero is reserved for rounding cases when 099 → 100) */
const chars = new Uint8Array ((strEnd - strStart) + (hasDot ? 0 : 1))
chars[0] = ZERO
/* Validate & copy digits, determine certain locations in the resulting buffer */
let afterDot = chars.length
, digitsStart = -1 // significant digits
, digitsEnd = -1
for (var i = 1, j = strStart; j < strEnd; j++, i++) {
const c = str.charCodeAt (j)
if (c === DOT) {
afterDot = i--
} else if ((c < ZERO) || (c > NINE)) {
throw new Error (`${str}: invalid number (contains an illegal character '${str[i - 1]}')`)
} else {
chars[i] = c
if ((c !== ZERO) && (digitsStart < 0)) digitsStart = i
}
}
if (digitsStart < 0) digitsStart = 1
/* Determine the range to cut */
let precisionStart = (countingMode === DECIMAL_PLACES) ? afterDot // 0.(0)001234567
: digitsStart // 0.00(1)234567
, precisionEnd = precisionStart +
numPrecisionDigits
/* Reset the last significant digit index, as it will change during the rounding/truncation. */
digitsEnd = -1
/* Perform rounding/truncation per digit, from digitsEnd to digitsStart, by using the following
algorithm (rounding 999 → 1000, as an example):
step = i=3 i=2 i=1 i=0
chars = 0999 0999 0900 1000
memo = ---0 --1- -1-- 0--- */
let allZeros = true;
let signNeeded = isNegative;
for (let i = chars.length - 1, memo = 0; i >= 0; i--) {
let c = chars[i]
if (i !== 0) {
c += memo
if (i >= (precisionStart + numPrecisionDigits)) {
const ceil = (roundingMode === ROUND) &&
(c >= FIVE) &&
!((c === FIVE) && memo) // prevents rounding of 1.45 to 2
c = ceil ? (NINE + 1) : ZERO
}
if (c > NINE) { c = ZERO; memo = 1; }
else memo = 0
} else if (memo) c = ONE // leading extra digit (0900 → 1000)
chars[i] = c
if (c !== ZERO) {
allZeros = false
digitsStart = i
digitsEnd = (digitsEnd < 0) ? (i + 1) : digitsEnd
}
}
/* Update the precision range, as `digitsStart` may have changed... & the need for a negative sign if it is only 0 */
if (countingMode === SIGNIFICANT_DIGITS) {
precisionStart = digitsStart
precisionEnd = precisionStart + numPrecisionDigits
}
if (allZeros) {
signNeeded = false
}
/* Determine the input character range */
const readStart = ((digitsStart >= afterDot) || allZeros) ? (afterDot - 1) : digitsStart // 0.000(1)234 ----> (0).0001234
, readEnd = (digitsEnd < afterDot) ? (afterDot ) : digitsEnd // 12(3)000 ----> 123000( )
/* Compute various sub-ranges */
const nSign = (signNeeded ? 1 : 0) // (-)123.456
, nBeforeDot = (nSign + (afterDot - readStart)) // (-123).456
, nAfterDot = Math.max (readEnd - afterDot, 0) // -123.(456)
, actualLength = (readEnd - readStart) // -(123.456)
, desiredLength = (paddingMode === NO_PADDING)
? (actualLength) // -(123.456)
: (precisionEnd - readStart) // -(123.456 )
, pad = Math.max (desiredLength - actualLength, 0) // -123.456( )
, padStart = (nBeforeDot + 1 + nAfterDot) // -123.456( )
, padEnd = (padStart + pad) // -123.456 ( )
, isInteger = (nAfterDot + pad) === 0 // -123
/* Fill the output buffer with characters */
const out = new Uint8Array (nBeforeDot + (isInteger ? 0 : 1) + nAfterDot + pad)
// ------------------------------------------------------------------------------------------ // ---------------------
if (signNeeded) out[0] = MINUS // - minus sign
for (i = nSign, j = readStart; i < nBeforeDot; i++, j++) out[i] = chars[j] // 123 before dot
if (!isInteger) out[nBeforeDot] = DOT // . dot
for (i = nBeforeDot + 1, j = afterDot; i < padStart; i++, j++) out[i] = chars[j] // 456 after dot
for (i = padStart; i < padEnd; i++) out[i] = ZERO // 000 padding
/* Build a string from the output buffer */
return String.fromCharCode (...out)
}
// toWei / fromWei
function fromWei (amount, decimals = 18) {
if (amount === undefined) {
return amount
}
const exponential = Math.floor (amount).toExponential () // wei must be whole numbers
const [ n, exponent ] = exponential.split ('e')
const newExponent = parseInt (exponent) - decimals
return parseFloat (n + 'e' + newExponent)
}
function toWei (amount, decimals = 18) {
if (amount === undefined) {
return amount
}
const exponential = parseFloat (amount).toExponential ()
const [ n, exponent ] = exponential.split ('e')
const newExponent = parseInt (exponent) + decimals
return numberToString (Math.floor (parseFloat (n + 'e' + newExponent))) // wei must be whole numbers
}
/* ------------------------------------------------------------------------ */
module.exports = {
toWei,
fromWei,
numberToString,
precisionFromString,
decimalToPrecision,
truncate_to_string,
truncate,
precisionConstants,
ROUND,
TRUNCATE,
ROUND_UP,
ROUND_DOWN,
DECIMAL_PLACES,
SIGNIFICANT_DIGITS,
TICK_SIZE,
NO_PADDING,
PAD_WITH_ZERO,
}
/* ------------------------------------------------------------------------ */
+59
View File
@@ -0,0 +1,59 @@
'use strict';
const updateCachedOrders = function (openOrders, symbol) {
}
const updateOrders = function (oldOrders, newOrders, closeOpenOrders = false) {
for (let i = 0; i < newOrders.length; i++) {
let newOrder = newOrders[i];
}
// update local cache with open orders
for (let j = 0; j < openOrders.length; j++) {
const id = openOrders[j]['id'];
this.orders[id] = openOrders[j];
}
let openOrdersIndexedById = this.indexBy (openOrders, 'id');
let cachedOrderIds = Object.keys (this.orders);
let result = [];
for (let k = 0; k < cachedOrderIds.length; k++) {
// match each cached order to an order in the open orders array
// possible reasons why a cached order may be missing in the open orders array:
// - order was closed or canceled -> update cache
// - symbol mismatch (e.g. cached BTC/USDT, fetched ETH/USDT) -> skip
let id = cachedOrderIds[k];
let order = this.orders[id];
result.push (order);
if (!(id in openOrdersIndexedById)) {
// cached order is not in open orders array
// if we fetched orders by symbol and it doesn't match the cached order -> won't update the cached order
if (symbol !== undefined && symbol !== order['symbol'])
continue;
// order is cached but not present in the list of open orders -> mark the cached order as closed
if (order['status'] === 'open') {
order = this.extend (order, {
'status': 'closed', // likewise it might have been canceled externally (unnoticed by "us")
'cost': undefined,
'filled': order['amount'],
'remaining': 0.0,
});
if (order['cost'] === undefined) {
if (order['filled'] !== undefined)
order['cost'] = order['filled'] * order['price'];
}
this.orders[id] = order;
}
}
}
return result;
console.log (this.id, orders, openOrders, closedOrders, canceledOrders)
process.exit ()
}
// -----------------------------------------------------------------------------
module.exports = {
updateOrders,
}
+38
View File
@@ -0,0 +1,38 @@
"use strict";
// ----------------------------------------------------------------------------
// There's been a lot of messing with this code...
// The problem is to satisfy the following requirements:
// - properly detect isNode == true on server side and isNode == false in the browser (on client side)
// - make sure create-react-app, react-starter-kit and other react frameworks work
// - make sure it does not break the browserified version (when linked into a html from a cdn)
// - make sure it does not break the webpacking and babel-transpiled scripts
// - make sure it works in Electron
// - make sure it works with Angular.js
// - make sure it does not break other possible usage scenarios
const isBrowser = typeof window !== 'undefined'
const isElectron = typeof process !== 'undefined' &&
typeof process.versions !== 'undefined' &&
typeof process.versions.electron !== 'undefined'
const isWebWorker = typeof WorkerGlobalScope !== 'undefined' && (self instanceof WorkerGlobalScope)
const isWindows = typeof process !== 'undefined' && process.platform === "win32"
const isNode = !(isBrowser || isWebWorker)
const defaultFetch = typeof (fetch) === "undefined" ? require ('../../static_dependencies/fetch-ponyfill/fetch-node') ().fetch : fetch
// ----------------------------------------------------------------------------
module.exports = {
isBrowser,
isElectron,
isWebWorker,
isNode,
isWindows,
defaultFetch,
}
+21
View File
@@ -0,0 +1,21 @@
"use strict";
/* ------------------------------------------------------------------------ */
const uuid = a => a ? (a ^ Math.random () * 16 >> a / 4).toString (16)
: ([1e7]+-1e3+-4e3+-8e3+-1e11).replace (/[018]/g, uuid)
module.exports =
{ uuid
// hasFetchOHLCV → has_fetch_ohlcv; parseHTTPResponse → parse_http_response
, unCamelCase: s => s.match (/^[A-Z0-9_]+$/) ? s : (s.replace (/[a-z0-9][A-Z]/g, x => x[0] + '_' + x[1]).replace(/[A-Z0-9][A-Z0-9][a-z]/g, x => x[0] + '_' + x[1] + x[2]).toLowerCase ())
, capitalize: s => s.length
? (s.charAt (0).toUpperCase () + s.slice (1))
: s
, strip: s => s.replace(/^\s+|\s+$/g, '')
}
/* ------------------------------------------------------------------------ */
+64
View File
@@ -0,0 +1,64 @@
"use strict";
/* ------------------------------------------------------------------------ */
const { sleep
, now } = require ('./time')
/* ------------------------------------------------------------------------ */
module.exports = {
throttle: function throttle (cfg) {
let lastTimestamp = now ()
, numTokens = (cfg.numTokens !== undefined) ? cfg.numTokens : cfg.capacity
, running = false
, counter = 0
const queue = []
return Object.assign ((rateLimit, cost) => {
if (queue.length > cfg.maxCapacity)
throw new Error ('Backlog is over max capacity of ' + cfg.maxCapacity)
return new Promise (async (resolve, reject) => {
try {
queue.push ({ cost, resolve, reject })
if (!running) {
running = true
while (queue.length > 0) {
const hasEnoughTokens = cfg.capacity ? (numTokens > 0) : (numTokens >= 0)
if (hasEnoughTokens) {
if (queue.length > 0) {
let { cost, resolve, reject } = queue[0]
cost = (cost || cfg.defaultCost)
if (numTokens >= Math.min (cost, cfg.capacity)) {
numTokens -= cost
queue.shift ()
resolve ()
}
}
}
const t = now ()
, elapsed = t - lastTimestamp
lastTimestamp = t
numTokens = Math.min (cfg.capacity, numTokens + elapsed * (1 / rateLimit))
await sleep (cfg.delay)
}
running = false
}
} catch (e) {
reject (e)
}
})
}, cfg, { configure: newCfg => throttle (Object.assign ({}, cfg, newCfg)) })
}
}
/* ------------------------------------------------------------------------ */
+185
View File
@@ -0,0 +1,185 @@
'use strict';
/* ------------------------------------------------------------------------ */
const now = Date.now // TODO: figure out how to utilize performance.now () properly – it's not as easy as it does not return a unix timestamp...
const microseconds = () => now () * 1000 // TODO: utilize performance.now for that purpose
const milliseconds = now
const seconds = () => Math.floor (now () / 1000)
/* ------------------------------------------------------------------------ */
const setTimeout_original = setTimeout
const setTimeout_safe = (done, ms, setTimeout = setTimeout_original /* overrideable for mocking purposes */, targetTime = now () + ms) => {
/* The built-in setTimeout function can fire its callback earlier than specified, so we
need to ensure that it does not happen: sleep recursively until `targetTime` is reached... */
let clearInnerTimeout = () => {}
let active = true
const id = setTimeout (() => {
active = true
const rest = targetTime - now ()
if (rest > 0) {
clearInnerTimeout = setTimeout_safe (done, rest, setTimeout, targetTime) // try sleep more
} else {
done ()
}
}, ms)
return function clear () {
if (active) {
active = false // dunno if IDs are unique on various platforms, so it's better to rely on this flag to exclude the possible cancellation of the wrong timer (if called after completion)
clearTimeout (id)
}
clearInnerTimeout ()
}
}
/* ------------------------------------------------------------------------ */
class TimedOut extends Error {
constructor () {
const message = 'timed out'
super (message)
this.constructor = TimedOut
this.__proto__ = TimedOut.prototype
this.message = message
}
}
/* ------------------------------------------------------------------------ */
const iso8601 = (timestamp) => {
let _timestampNumber = undefined;
if (typeof timestamp === 'number') {
_timestampNumber = Math.floor (timestamp);
} else {
_timestampNumber = parseInt (timestamp, 10);
}
// undefined, null and lots of nasty non-numeric values yield NaN
if (Number.isNaN (_timestampNumber) || _timestampNumber < 0) {
return undefined;
}
// last line of defence
try {
return new Date (_timestampNumber).toISOString ();
} catch (e) {
return undefined;
}
}
const parse8601 = (x) => {
if (typeof x !== 'string' || !x) {
return undefined;
}
if (x.match (/^[0-9]+$/)) {
// a valid number in a string, not a date.
return undefined;
}
if (x.indexOf ('-') < 0 || x.indexOf (':') < 0) { // no date can be without a dash and a colon
return undefined;
}
// last line of defence
try {
const candidate = Date.parse (((x.indexOf ('+') >= 0) || (x.slice (-1) === 'Z')) ? x : (x + 'Z').replace (/\s(\d\d):/, 'T$1:'));
if (Number.isNaN (candidate)) {
return undefined;
}
return candidate;
} catch (e) {
return undefined;
}
}
const parseDate = (x) => {
if (typeof x !== 'string' || !x) {
return undefined;
}
if (x.indexOf ('GMT') >= 0) {
try {
return Date.parse (x);
} catch (e) {
return undefined;
}
}
return parse8601 (x);
}
const mdy = (timestamp, infix = '-') => {
infix = infix || ''
const date = new Date (timestamp)
const Y = date.getUTCFullYear ().toString ()
let m = date.getUTCMonth () + 1
let d = date.getUTCDate ()
m = m < 10 ? ('0' + m) : m.toString ()
d = d < 10 ? ('0' + d) : d.toString ()
return m + infix + d + infix + Y
}
const ymd = (timestamp, infix = '-') => {
infix = infix || ''
const date = new Date (timestamp)
const Y = date.getUTCFullYear ().toString ()
let m = date.getUTCMonth () + 1
let d = date.getUTCDate ()
m = m < 10 ? ('0' + m) : m.toString ()
d = d < 10 ? ('0' + d) : d.toString ()
return Y + infix + m + infix + d
}
const ymdhms = (timestamp, infix = ' ') => {
const date = new Date (timestamp)
const Y = date.getUTCFullYear ()
let m = date.getUTCMonth () + 1
let d = date.getUTCDate ()
let H = date.getUTCHours ()
let M = date.getUTCMinutes ()
let S = date.getUTCSeconds ()
m = m < 10 ? ('0' + m) : m
d = d < 10 ? ('0' + d) : d
H = H < 10 ? ('0' + H) : H
M = M < 10 ? ('0' + M) : M
S = S < 10 ? ('0' + S) : S
return Y + '-' + m + '-' + d + infix + H + ':' + M + ':' + S
}
module.exports =
{
now
, microseconds
, milliseconds
, seconds
, iso8601
, parse8601
, parseDate
, mdy
, ymd
, ymdhms
, setTimeout_safe
, sleep: ms => new Promise (resolve => setTimeout_safe (resolve, ms))
, TimedOut
, timeout: async (ms, promise) => {
let clear = () => {}
const expires = new Promise (resolve => (clear = setTimeout_safe (resolve, ms)))
try {
return await Promise.race ([promise, expires.then (() => { throw new TimedOut () })])
} finally {
clear () // fixes https://github.com/ccxt/ccxt/issues/749
}
}
}
/* ------------------------------------------------------------------------ */
+65
View File
@@ -0,0 +1,65 @@
"use strict";
/* ------------------------------------------------------------------------ */
const isNumber = Number.isFinite
, isInteger = Number.isInteger
, isArray = Array.isArray
, hasProps = o => ((o !== undefined) && (o !== null))
, isString = s => (typeof s === 'string')
, isObject = o => ((o !== null) && (typeof o === 'object'))
, isDictionary = o => (isObject (o) && !isArray (o))
, isStringCoercible = x => ((hasProps (x) && x.toString) || isNumber (x))
/* ............................................. */
const prop = (o, k) => (isObject (o) ? o[k] : undefined)
, prop2 = (o, k1, k2) => (!isObject (o) ? undefined : (((k1 in o) && (o[k1] !== null)) ? o[k1] : o[k2]))
/* ............................................. */
const asFloat = x => ((isNumber (x) || isString (x)) ? parseFloat (x) : NaN)
, asInteger = x => ((isNumber (x) || isString (x)) ? parseInt (x, 10) : NaN)
/* ............................................. */
module.exports = {
isNumber
, isInteger
, isArray
, isObject
, isString
, isStringCoercible
, isDictionary
, hasProps
, prop
, asFloat
, asInteger
, safeFloat: (o, k, $default, n = asFloat (prop (o, k))) => (isNumber (n) ? n : $default)
, safeInteger: (o, k, $default, n = asInteger (prop (o, k))) => (isNumber (n) ? n : $default)
, safeIntegerProduct: (o, k, $factor, $default, n = asInteger (prop (o, k))) => (isNumber (n) ? parseInt (n * $factor) : $default)
, safeTimestamp: (o, k, $default, n = asFloat (prop (o, k))) => (isNumber (n) ? parseInt (n * 1000) : $default)
, safeValue: (o, k, $default, x = prop (o, k)) => (hasProps (x) ? x : $default)
, safeString: (o, k, $default, x = prop (o, k)) => (isStringCoercible (x) ? String (x) : $default)
, safeStringLower: (o, k, $default, x = prop (o, k)) => (isStringCoercible (x) ? String (x).toLowerCase () : $default)
, safeStringUpper: (o, k, $default, x = prop (o, k)) => (isStringCoercible (x) ? String (x).toUpperCase () : $default)
// not using safeFloats with an array argument as we're trying to save some cycles here
// we're not using safeFloat3 either because those cases are too rare to deserve their own optimization
, safeFloat2: (o, k1, k2, $default, n = asFloat (prop2 (o, k1, k2))) => (isNumber (n) ? n : $default)
, safeInteger2: (o, k1, k2, $default, n = asInteger (prop2 (o, k1, k2))) => (isNumber (n) ? n : $default)
, safeIntegerProduct2: (o, k1, k2, $factor, $default, n = asInteger (prop2 (o, k1, k2))) => (isNumber (n) ? parseInt (n * $factor) : $default)
, safeTimestamp2: (o, k1, k2, $default, n = asFloat (prop2 (o, k1, k2))) => (isNumber (n) ? parseInt (n * 1000) : $default)
, safeValue2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (hasProps (x) ? x : $default)
, safeString2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (isStringCoercible (x) ? String (x) : $default)
, safeStringLower2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (isStringCoercible (x) ? String (x).toLowerCase () : $default)
, safeStringUpper2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (isStringCoercible (x) ? String (x).toUpperCase () : $default)
}
/* ------------------------------------------------------------------------ */