UNPKG

@nktkas/hyperliquid

Version:

Hyperliquid API SDK for all major JS runtimes, written in TypeScript.

178 lines 7.39 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.formatPrice = formatPrice; exports.formatSize = formatSize; /** * Format price according to Hyperliquid [rules](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size): * - Maximum 5 significant figures * - Maximum 6 (for perp) or 8 (for spot) - `szDecimals` decimal places * - Integer prices are always allowed regardless of significant figures * * @param price - The price to format (as string or number). * @param szDecimals - The size decimals of the asset. * @param type - The market type: "perp" for perpetuals or "spot" for spot markets. Default is "perp". * * @throws {RangeError} If the formatted price is 0 * * @example * ```ts * import { formatPrice } from "@nktkas/hyperliquid/utils"; * const price = formatPrice("0.0000123456789", 0, "spot"); // → "0.00001234" * ``` */ function formatPrice(price, szDecimals, type = "perp") { price = price.toString().trim(); assertNumberString(price); // Integer prices are always allowed if (/^-?\d+$/.test(price)) return formatDecimalString(price); // Apply decimal limit: max 6 (perp) or 8 (spot) - szDecimals const maxDecimals = Math.max((type === "perp" ? 6 : 8) - szDecimals, 0); price = StringMath.toFixedTruncate(price, maxDecimals); // Apply sig figs limit: max 5 significant figures price = StringMath.toPrecisionTruncate(price, 5); if (price === "0") { throw new RangeError("Price is too small and was truncated to 0"); } return price; } /** * Format size according to Hyperliquid [rules](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size): * - Truncate decimal places to `szDecimals` * * @param size - The size to format (as string or number). * @param szDecimals - The size decimals of the asset. * * @throws {RangeError} If the formatted size is 0 * * @example * ```ts * import { formatSize } from "@nktkas/hyperliquid/utils"; * * const size = formatSize("1.23456789", 5); // → "1.23456" * ``` */ function formatSize(size, szDecimals) { size = size.toString().trim(); assertNumberString(size); // Apply decimal limit: szDecimals size = StringMath.toFixedTruncate(size, szDecimals); if (size === "0") { throw new RangeError("Size is too small and was truncated to 0"); } return size; } /** String-based Math operations for arbitrary precision */ const StringMath = { /** Floor log10 (magnitude): position of most significant digit */ log10Floor(value) { const abs = value[0] === "-" ? value.slice(1) : value; // Check if zero or invalid const num = Number(abs); if (num === 0 || isNaN(num)) return -Infinity; const [int, dec] = abs.split("."); // Number >= 1: magnitude = length of integer part - 1 if (Number(int) !== 0) { const trimmed = int.replace(/^0+/, ""); return trimmed.length - 1; } // Number < 1: count leading zeros in decimal part const leadingZeros = dec.match(/^0*/)?.[0].length ?? 0; return -(leadingZeros + 1); }, /** Multiply by 10^exp: shift decimal point left (negative) or right (positive) */ multiplyByPow10(value, exp) { if (!Number.isInteger(exp)) throw new RangeError("Exponent must be an integer"); if (exp === 0) return formatDecimalString(value); const neg = value[0] === "-"; const abs = neg ? value.slice(1) : value; const [intRaw, dec = ""] = abs.split("."); // Normalize empty integer part to "0" (handles ".5" → "0.5") const int = intRaw || "0"; let result; if (exp > 0) { // Shift right: move digits from decimal to integer if (exp >= dec.length) { result = int + dec + "0".repeat(exp - dec.length); } else { result = int + dec.slice(0, exp) + "." + dec.slice(exp); } } else { // Shift left: move digits from integer to decimal const absExp = -exp; if (absExp >= int.length) { result = "0." + "0".repeat(absExp - int.length) + int + dec; } else { result = int.slice(0, -absExp) + "." + int.slice(-absExp) + dec; } } return formatDecimalString((neg ? "-" : "") + result); }, /** Returns the integer part of a number by removing any fractional digits */ trunc(value) { const dotIndex = value.indexOf("."); return dotIndex === -1 ? value : value.slice(0, dotIndex) || "0"; }, /** Truncate to a certain number of significant figures */ toPrecisionTruncate(value, precision) { if (!Number.isInteger(precision)) throw new RangeError("Precision must be an integer"); if (precision < 1) throw new RangeError("Precision must be positive"); if (/^-?0+(\.0*)?$/.test(value)) return "0"; // zero is special case (don't work with log10) const neg = value[0] === "-"; const abs = neg ? value.slice(1) : value; // Calculate how much to shift: align most significant digit to ones place + (maxSigFigs-1) const magnitude = StringMath.log10Floor(abs); const shiftAmount = precision - magnitude - 1; // Shift right, truncate integer part, shift back const shifted = StringMath.multiplyByPow10(abs, shiftAmount); const truncated = StringMath.trunc(shifted); const result = StringMath.multiplyByPow10(truncated, -shiftAmount); // build final result and trim zeros return formatDecimalString(neg ? "-" + result : result); }, /** Truncate to a certain number of decimal places */ toFixedTruncate(value, decimals) { if (!Number.isInteger(decimals)) throw new RangeError("Decimals must be an integer"); if (decimals < 0) throw new RangeError("Decimals must be non-negative"); // Match number with up to `decimals` decimal places const regex = new RegExp(`^-?(?:\\d+)?(?:\\.\\d{0,${decimals}})?`); const result = value.match(regex)?.[0]; if (!result) { throw new TypeError("Invalid number format"); } // Trim zeros after truncation return formatDecimalString(result); }, }; function formatDecimalString(value) { return value // remove leading/trailing whitespace .trim() // " 123.45 " → "123.45" // remove leading zeros .replace(/^(-?)0+(?=\d)/, "$1") // "00123" → "123", "-00.5" → "-0.5" // remove trailing zeros .replace(/\.0*$|(\.\d+?)0+$/, "$1") // "1.2000" → "1.2", "5.0" → "5" // add leading zero if starts with decimal point .replace(/^(-?)\./, "$10.") // ".5" → "0.5", "-.5" → "-0.5" // add "0" if string is empty after trimming .replace(/^-?$/, "0") // "" → "0", "-" → "0" // normalize negative zero .replace(/^-0$/, "0"); // "-0" → "0" } function assertNumberString(value) { if (!/^-?(\d+\.?\d*|\.\d*)$/.test(value)) { throw new TypeError("Invalid number format"); } } //# sourceMappingURL=format.js.map