@nktkas/hyperliquid
Version:
Hyperliquid API SDK for all major JS runtimes, written in TypeScript.
178 lines • 7.39 kB
JavaScript
;
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