SPB Git

Repository Public

format.ts 5.0 KB 85 lines typescript

1// Hyperliquid price and size rules — https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size2//   "Prices can have up to 5 significant figures, but no more than MAX_DECIMALS - szDecimals decimal places where MAX_DECIMALS3//    is 6 for perps and 8 for spot. Integer prices are always allowed, regardless of the number of significant figures."4//   "Sizes are rounded to the szDecimals of that asset."   "if implementing signing, trailing zeroes should be removed."5// Everything is done on decimal strings / bigint; a request that is off-tick is REFUSED, never rounded.6import { D, type DecStr } from '../../decimal.ts';78export const HL_PERP_MAX_DECIMALS = 6;9export const HL_MAX_SIG_FIGS = 5;10/** documented order minimum: "Order must have minimum value of $10." (error responses page) */11export const HL_MIN_NOTIONAL = '10';1213const CANON = /^(0|[1-9]\d*)(\.\d+)?$/;1415export type FormatResult = { ok: true; wire: string } | { ok: false; reason: string };1617/** canonical decimal → the venue's wire form: no trailing zeros, no trailing dot ("100.50" → "100.5", "100.0" → "100") */18export function hlWire(x: DecStr): string {19  if (!CANON.test(x)) throw new Error('not a canonical non-negative decimal');20  return D.str(D.from(x));21}2223const decimalsOf = (wire: string): number => { const i = wire.indexOf('.'); return i < 0 ? 0 : wire.length - i - 1; };24/** significant figures of a wire decimal: digits from the first non-zero one, trailing zeros of an integer included */25export function hlSigFigs(wire: string): number {26  const digits = wire.replace('.', '').replace(/^0+/, '');27  return digits.length;28}2930export function hlPriceDecimals(szDecimals: number): number {31  if (!Number.isInteger(szDecimals) || szDecimals < 0 || szDecimals > HL_PERP_MAX_DECIMALS) throw new Error('szDecimals out of range');32  return HL_PERP_MAX_DECIMALS - szDecimals;33}3435/** Validates a perp price against the documented rules; returns the wire string or the reason it is refused. */36export function hlFormatPrice(price: DecStr, szDecimals: number): FormatResult {37  if (typeof price !== 'string' || !CANON.test(price)) return { ok: false, reason: 'price is not a canonical positive decimal' };38  const wire = hlWire(price);39  if (wire === '0') return { ok: false, reason: 'price must be positive' };40  const maxDecimals = hlPriceDecimals(szDecimals), decimals = decimalsOf(wire);41  if (decimals > maxDecimals) return { ok: false, reason: `price ${wire} has ${decimals} decimals; at most ${maxDecimals} (6 − szDecimals ${szDecimals})` };42  if (decimals > 0 && hlSigFigs(wire) > HL_MAX_SIG_FIGS) return { ok: false, reason: `price ${wire} has ${hlSigFigs(wire)} significant figures; at most ${HL_MAX_SIG_FIGS} unless it is an integer` };43  return { ok: true, wire };44}4546/** Validates a size against the asset's lot (10^-szDecimals); returns the wire string or the reason it is refused. */47export function hlFormatSize(size: DecStr, szDecimals: number): FormatResult {48  if (typeof size !== 'string' || !CANON.test(size)) return { ok: false, reason: 'size is not a canonical positive decimal' };49  hlPriceDecimals(szDecimals); // range check50  const wire = hlWire(size);51  if (wire === '0') return { ok: false, reason: 'size must be positive' };52  const decimals = decimalsOf(wire);53  if (decimals > szDecimals) return { ok: false, reason: `size ${wire} has ${decimals} decimals; the lot allows ${szDecimals}` };54  return { ok: true, wire };55}5657/** 10^-n as a canonical decimal string */58export function hlStep(n: number): DecStr {59  if (!Number.isInteger(n) || n < 0 || n > 18) throw new Error('step exponent out of range');60  return n === 0 ? '1' : `0.${'0'.repeat(n - 1)}1`;61}6263/**64 * The price increment that applies AT a given price. Hyperliquid has no fixed tick: the grid is "5 significant figures, at most65 * 6 − szDecimals decimals, integers always allowed", so the step depends on the magnitude of the price.66 */67export function hlTickAt(price: DecStr, szDecimals: number): DecStr {68  if (!CANON.test(price) || hlWire(price) === '0') throw new Error('price must be a positive canonical decimal');69  const wire = hlWire(price), maxDecimals = hlPriceDecimals(szDecimals);70  const [int = '0', frac = ''] = wire.split('.');71  // position of the most significant digit: exponent e such that 10^e <= price < 10^(e+1)72  const e = int !== '0' ? int.length - 1 : -(frac.length - frac.replace(/^0+/, '').length + 1);73  const bySigFigs = HL_MAX_SIG_FIGS - 1 - e; // decimals allowed by the 5-significant-figure rule74  const decimals = Math.max(0, Math.min(maxDecimals, bySigFigs)); // integers are always allowed → never coarser than 175  return hlStep(decimals);76}7778/** Rounds a price onto the venue's grid in an explicit direction (used to derive bounds, never to "fix" a request). */79export function hlQuantizePrice(price: DecStr, szDecimals: number, mode: 'down' | 'up'): DecStr {80  const step = D.from(hlTickAt(price, szDecimals));81  const q = D.str(D.quantize(D.from(price), step, mode));82  // rounding up can cross a power of ten, where the step gets coarser; the result is still on the grid (it is a multiple of both)83  return q;84}85
Files main
Menu

Theme