UNPKG

@proton/ccxt

Version:

A JavaScript / TypeScript / Python / C# / PHP cryptocurrency trading library with support for 130+ exchanges

298 lines (294 loc) 12 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); // ------------------------------------------------------------------------ // // 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 TRUNCATE = 0; // rounding mode const ROUND = 1; const ROUND_UP = 2; const ROUND_DOWN = 3; const DECIMAL_PLACES = 2; // digits counting mode const SIGNIFICANT_DIGITS = 3; const TICK_SIZE = 4; const NO_PADDING = 5; // zero-padding mode const PAD_WITH_ZERO = 6; 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) { if (x === undefined) return undefined; if (typeof x !== 'number') return x.toString(); const s = x.toString(); if (Math.abs(x) < 1.0) { const n_e = s.split('e-'); const n = n_e[0].replace('.', ''); const e = parseInt(n_e[1]); const neg = (s[0] === '-'); if (e) { x = (neg ? '-' : '') + '0.' + (new Array(e)).join('0') + n.substring(neg); return x; } } else { const parts = s.split('e'); if (parts[1]) { let e = parseInt(parts[1]); const m = parts[0].split('.'); let part = ''; if (m[1]) { e -= m[1].length; part = m[1]; } return m[0] + part + (new Array(e + 1)).join('0'); } } return s; } //----------------------------------------------------------------------------- // expects non-scientific notation const truncate_regExpCache = []; const 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(); }; const truncate = (num, precision = 0) => parseFloat(truncate_to_string(num, precision)); function precisionFromString(str) { // support string formats like '1e-4' if (str.indexOf('e') > -1) { const numStr = str.replace(/\de/, ''); return parseInt(numStr) * -1; } // support integer formats (without dot) like '1', '10' etc [Note: bug in decimalToPrecision, so this should not be used atm] // if (str.indexOf ('.') === -1) { // return str.length * -1 // } // default strings like '0.0001' const split = str.replace(/0+$/g, '').split('.'); return (split.length > 1) ? (split[1].length) : 0; } /* ------------------------------------------------------------------------ */ const decimalToPrecision = (x, roundingMode, numPrecisionDigits, countingMode = DECIMAL_PLACES, paddingMode = NO_PADDING) => { if (countingMode === TICK_SIZE) { if (typeof numPrecisionDigits === 'string') { numPrecisionDigits = parseFloat(numPrecisionDigits); } if (numPrecisionDigits <= 0) { throw new Error('TICK_SIZE cant be used with negative or zero numPrecisionDigits'); } } if (numPrecisionDigits < 0) { 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, 22, 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); const isNegative = str[0] === '-'; const strStart = isNegative ? 1 : 0; const 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; const DOT = 46; const ZERO = 48; const ONE = (ZERO + 1); const FIVE = (ZERO + 5); const 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; let digitsStart = -1; // significant digits let 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 let 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 const readEnd = (digitsEnd < afterDot) ? (afterDot) : digitsEnd; // 12(3)000 ----> 123000( ) /* Compute various sub-ranges */ const nSign = (signNeeded ? 1 : 0); // (-)123.456 const nBeforeDot = (nSign + (afterDot - readStart)); // (-123).456 const nAfterDot = Math.max(readEnd - afterDot, 0); // -123.(456) const actualLength = (readEnd - readStart); // -(123.456) const desiredLength = (paddingMode === NO_PADDING) ? (actualLength) // -(123.456) : (precisionEnd - readStart); // -(123.456 ) const pad = Math.max(desiredLength - actualLength, 0); // -123.456( ) const padStart = (nBeforeDot + 1 + nAfterDot); // -123.456( ) const padEnd = (padStart + pad); // -123.456 ( ) const 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); }; function omitZero(stringNumber) { if (stringNumber === undefined || stringNumber === '') { return undefined; } if (parseFloat(stringNumber) === 0) { return undefined; } return stringNumber; } /* ------------------------------------------------------------------------ */ exports.DECIMAL_PLACES = DECIMAL_PLACES; exports.NO_PADDING = NO_PADDING; exports.PAD_WITH_ZERO = PAD_WITH_ZERO; exports.ROUND = ROUND; exports.ROUND_DOWN = ROUND_DOWN; exports.ROUND_UP = ROUND_UP; exports.SIGNIFICANT_DIGITS = SIGNIFICANT_DIGITS; exports.TICK_SIZE = TICK_SIZE; exports.TRUNCATE = TRUNCATE; exports.decimalToPrecision = decimalToPrecision; exports.numberToString = numberToString; exports.omitZero = omitZero; exports.precisionConstants = precisionConstants; exports.precisionFromString = precisionFromString; exports.truncate = truncate; exports.truncate_to_string = truncate_to_string;