/
ikratkiy
/
Hyperliquid
Обзор
Документация
Войти
/
ikratkiy
/
Hyperliquid
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/shared/hyperliquid/decimalMath.ts
186 строк
6 KB
ikratkiy
fix(spot): validate canonical IOC preconditions
14 июл 2026, 22:42
14 июл 2026, 22:42
689be06
Код
Авторство
О чём код?
export type ExactDecimal = Readonly<{ units: bigint; scale: number; }>; export type ExactRatio = Readonly<{ numerator: bigint; denominator: bigint; }>; export type RatioRounding = "truncate" | "ceil" | "half-up"; const PLAIN_DECIMAL_PATTERN = /^-?\d+(?:\.\d+)?$/; function assertScale(scale: number) { if (!Number.isSafeInteger(scale) || scale < 0) { throw new Error(`Invalid decimal scale: ${scale}`); } } export function powerOfTen(exponent: number) { assertScale(exponent); return 10n ** BigInt(exponent); } export function normalizeDecimal(decimal: ExactDecimal): ExactDecimal { assertScale(decimal.scale); if (decimal.units === 0n) { return { units: 0n, scale: 0 }; } let units = decimal.units; let scale = decimal.scale; while (scale > 0 && units % 10n === 0n) { units /= 10n; scale -= 1; } return { units, scale }; } /** * Parses a plain base-10 decimal. Scientific notation, whitespace, NaN, * Infinity and incomplete fractions are deliberately rejected. */ export function parsePlainDecimal(value: string): ExactDecimal { if (!PLAIN_DECIMAL_PATTERN.test(value)) { throw new Error(`Invalid plain decimal: ${value}`); } const negative = value.startsWith("-"); const unsigned = negative ? value.slice(1) : value; const [integerPart, fractionPart = ""] = unsigned.split("."); const digits = `${integerPart}${fractionPart}`; const units = BigInt(digits) * (negative ? -1n : 1n); return normalizeDecimal({ units, scale: fractionPart.length }); } export function formatPlainDecimal(value: ExactDecimal): string { const decimal = normalizeDecimal(value); const negative = decimal.units < 0n; const digits = (negative ? -decimal.units : decimal.units).toString(); if (decimal.scale === 0) { return `${negative ? "-" : ""}${digits}`; } const padded = digits.padStart(decimal.scale + 1, "0"); const integerPart = padded.slice(0, -decimal.scale); const fractionPart = padded.slice(-decimal.scale); return `${negative ? "-" : ""}${integerPart}.${fractionPart}`; } function alignDecimals(left: ExactDecimal, right: ExactDecimal) { const scale = Math.max(left.scale, right.scale); return { leftUnits: left.units * powerOfTen(scale - left.scale), rightUnits: right.units * powerOfTen(scale - right.scale), scale, }; } export function compareDecimals(left: ExactDecimal, right: ExactDecimal): -1 | 0 | 1 { const aligned = alignDecimals(left, right); if (aligned.leftUnits < aligned.rightUnits) return -1; if (aligned.leftUnits > aligned.rightUnits) return 1; return 0; } export function addDecimals(left: ExactDecimal, right: ExactDecimal): ExactDecimal { const aligned = alignDecimals(left, right); return normalizeDecimal({ units: aligned.leftUnits + aligned.rightUnits, scale: aligned.scale }); } export function subtractDecimals(left: ExactDecimal, right: ExactDecimal): ExactDecimal { const aligned = alignDecimals(left, right); return normalizeDecimal({ units: aligned.leftUnits - aligned.rightUnits, scale: aligned.scale }); } export function multiplyDecimals(left: ExactDecimal, right: ExactDecimal): ExactDecimal { return normalizeDecimal({ units: left.units * right.units, scale: left.scale + right.scale }); } function greatestCommonDivisor(left: bigint, right: bigint): bigint { let a = left < 0n ? -left : left; let b = right < 0n ? -right : right; while (b !== 0n) { const remainder = a % b; a = b; b = remainder; } return a; } export function normalizeRatio(ratio: ExactRatio): ExactRatio { if (ratio.denominator === 0n) { throw new Error("Exact ratio denominator must be non-zero."); } const sign = ratio.denominator < 0n ? -1n : 1n; const numerator = ratio.numerator * sign; const denominator = ratio.denominator * sign; if (numerator === 0n) { return { numerator: 0n, denominator: 1n }; } const divisor = greatestCommonDivisor(numerator, denominator); return { numerator: numerator / divisor, denominator: denominator / divisor }; } export function decimalToRatio(decimal: ExactDecimal): ExactRatio { return normalizeRatio({ numerator: decimal.units, denominator: powerOfTen(decimal.scale) }); } export function divideDecimals(numerator: ExactDecimal, denominator: ExactDecimal): ExactRatio { if (denominator.units === 0n) { throw new Error("Cannot divide by zero."); } return normalizeRatio({ numerator: numerator.units * powerOfTen(denominator.scale), denominator: denominator.units * powerOfTen(numerator.scale), }); } export function compareRatioToDecimal(ratio: ExactRatio, decimal: ExactDecimal): -1 | 0 | 1 { const normalized = normalizeRatio(ratio); const left = normalized.numerator * powerOfTen(decimal.scale); const right = decimal.units * normalized.denominator; if (left < right) return -1; if (left > right) return 1; return 0; } export function formatRatio(ratio: ExactRatio, maxDecimals = 18, rounding: RatioRounding = "truncate"): string { assertScale(maxDecimals); const normalized = normalizeRatio(ratio); const negative = normalized.numerator < 0n; const absoluteNumerator = negative ? -normalized.numerator : normalized.numerator; const multiplier = powerOfTen(maxDecimals); const scaledNumerator = absoluteNumerator * multiplier; let scaledUnits = scaledNumerator / normalized.denominator; const remainder = scaledNumerator % normalized.denominator; if (remainder !== 0n) { if (rounding === "ceil" && !negative) { scaledUnits += 1n; } else if (rounding === "ceil" && negative) { // ceil(-x) is the negative of truncation toward zero. } else if (rounding === "half-up" && remainder * 2n >= normalized.denominator) { scaledUnits += 1n; } } return formatPlainDecimal({ units: negative ? -scaledUnits : scaledUnits, scale: maxDecimals }); }