SPB Git

Repository Public

dec.ts 8.0 KB 157 lines typescript

1/**2 * Exact decimals for the Futures terminal. The arithmetic is the repository's own `D` (bigint mantissa + scale): the file has no3 * import of its own, so it bundles for the browser as is. Everything shown as an amount to act on (size, price, margin, fee)4 * goes through it; JS numbers are used for charts and for compact read-only figures (24 h volume, open interest) only.5 */6import { D, type Dec, type DecStr } from '../../../../packages/derivatives/src/decimal.ts';78export { D };9export type { Dec, DecStr };1011const NBSP = ' ';12const MINUS = '−';13const TEN_K = D.from(10000n);14const HUNDRED = D.from(100n);1516/** a Dec from an API value, or null when the value is absent or not a canonical decimal */17export function dec(x: unknown): Dec | null {18  if (typeof x === 'number' && Number.isFinite(x)) x = numToDecStr(x);19  return D.isValid(x) ? D.from(x) : null;20}21/** tolerant conversion for fields a backend may send as JSON numbers (bps, fees): no exponent, at most 12 decimals */22export function numToDecStr(n: number): DecStr {23  const s = n.toFixed(12).replace(/\.?0+$/, '');24  return s === '-0' || s === '' ? '0' : s;25}26/** number of decimals of a tick / lot ('0.001' → 3, '1' → 0) */27export function dpOf(step: DecStr | null | undefined): number {28  const d = dec(step);29  return d ? d.s : 2;30}31/** rounds to `dp` decimals (half even) and pads with zeros: always exactly `dp` decimals */32export function toFixed(x: Dec, dp: number): string {33  const r = D.div(x, D.one, dp, 'half_even');34  const s = D.str(D.abs(r));35  const [i = '0', f = ''] = s.split('.');36  const body = dp > 0 ? `${i}.${f.padEnd(dp, '0')}` : i;37  return D.isNeg(r) ? `-${body}` : body;38}3940export interface FmtOpts { dp?: number; locale?: string; sign?: boolean; trim?: boolean }41/**42 * Localised decimal: thousands grouped (non-breaking space in French, comma in English), decimal comma in French, a real minus43 * sign, optional explicit '+'. `dp` fixes the decimals; `trim` removes trailing zeros.44 */45export function fmt(x: DecStr | Dec | null | undefined, o: FmtOpts = {}): string {46  const d = typeof x === 'string' ? dec(x) : x ?? null;47  if (!d) return '—';48  const fr = o.locale !== 'en';49  let s = toFixed(d, o.dp ?? Math.min(d.s, 8));50  const neg = s.startsWith('-');51  if (neg) s = s.slice(1);52  let [i = '0', f = ''] = s.split('.');53  if (o.trim) f = f.replace(/0+$/, '');54  i = i.replace(/\B(?=(\d{3})+(?!\d))/g, fr ? NBSP : ',');55  const body = f ? `${i}${fr ? ',' : '.'}${f}` : i;56  const zero = !/[1-9]/.test(body);57  return `${neg && !zero ? MINUS : o.sign && !zero ? '+' : ''}${body}`;58}59/** a fraction as a percentage: 0.0123 → « 1,23 % » */60export function pct(fraction: DecStr | Dec | null | undefined, dp: number, locale: string, sign = false): string {61  const d = typeof fraction === 'string' ? dec(fraction) : fraction ?? null;62  if (!d) return '—';63  return `${fmt(D.mul(d, HUNDRED), { dp, locale, sign })}${NBSP}%`;64}65/** USD amount with two decimals, e.g. « 1 234,50 $ » / "$1,234.50" */66export function usd(x: DecStr | Dec | null | undefined, locale: string, sign = false): string {67  const d = typeof x === 'string' ? dec(x) : x ?? null;68  if (!d) return '—';69  const body = fmt(D.abs(d), { dp: 2, locale });70  const sg = D.isNeg(d) && /[1-9]/.test(body) ? MINUS : sign && /[1-9]/.test(body) ? '+' : '';71  return locale === 'en' ? `${sg}$${body}` : `${sg}${body}${NBSP}$`;72}73/** compact read-only USD figure (volume, open interest): numbers are fine here, nothing is acted on */74export function compactUsd(x: DecStr | Dec | null | undefined, locale: string): string {75  const d = typeof x === 'string' ? dec(x) : x ?? null;76  if (!d) return '—';77  const n = D.toNumber(d);78  const s = new Intl.NumberFormat(locale === 'en' ? 'en-CA' : 'fr-CA', { notation: 'compact', maximumFractionDigits: 2 }).format(n);79  return locale === 'en' ? `$${s}` : `${s}${NBSP}$`;80}81/** |a − b| / b in basis points, 2 decimals; null when b is not positive */82export function bpsBetween(a: Dec, b: Dec): Dec | null {83  if (!D.isPos(b)) return null;84  return D.div(D.mul(D.abs(D.sub(a, b)), TEN_K), b, 2, 'half_even');85}86/** funding rate per hour from a rate per `intervalSec` */87export function fundingPerHour(rate: DecStr | null, intervalSec: number): Dec | null {88  const r = dec(rate);89  if (!r || !(intervalSec > 0)) return null;90  return D.div(D.mul(r, D.from(3600n)), D.from(BigInt(Math.round(intervalSec))), 12, 'half_even');91}92/** user input → canonical decimal string: accepts « 1 234,5 », "1,234.5", "0.01"; null when it is not a non-negative decimal */93export function parseInput(raw: string): DecStr | null {94  let s = raw.trim().replace(/[\s  ']/g, '');95  if (!s) return null;96  if (s.includes(',') && s.includes('.')) s = s.lastIndexOf(',') > s.lastIndexOf('.') ? s.replace(/\./g, '').replace(',', '.') : s.replace(/,/g, '');97  else s = s.replace(',', '.');98  if (!/^\d*\.?\d*$/.test(s) || !/\d/.test(s)) return null;99  if (s.startsWith('.')) s = `0${s}`;100  if (s.endsWith('.')) s = s.slice(0, -1);101  s = s.replace(/^0+(?=\d)/, '');102  if (s.includes('.')) s = s.replace(/0+$/, '').replace(/\.$/, '');103  return D.isValid(s) ? s : null;104}105/** base size bought by `usdAmount` at `price`, rounded DOWN to the lot */106export function usdToSize(usdAmount: Dec, price: Dec, lot: Dec): Dec {107  if (!D.isPos(price)) return D.zero;108  return D.quantize(D.div(usdAmount, price, 18, 'down'), lot, 'down');109}110/**111 * Worst acceptable price of a market-like order: `ref` moved by `slippageBps` against the taker, kept on the tick and INSIDE the112 * bound (a buy rounds down, a sell rounds up), never below one tick.113 */114export function worstPrice(ref: Dec, side: 'buy' | 'sell', slippageBps: number, tick: Dec, venue = ''): Dec {115  const k = D.from(BigInt(Math.round(slippageBps)));116  const moved = side === 'buy' ? D.div(D.mul(ref, D.add(TEN_K, k)), TEN_K, 18, 'down') : D.div(D.mul(ref, D.sub(TEN_K, k)), TEN_K, 18, 'up');117  const q = quantizePrice(venue, tick, moved, side === 'buy' ? 'down' : 'up');118  return D.isPos(q) ? q : tick;119}120/** a price put on the venue's grid (nearest below) */121export const onTick = (p: Dec, tick: Dec, venue = ''): Dec => quantizePrice(venue, tick, p, 'down');122/** decimals to SHOW for a price: at most the tick's, and about six significant digits (GMX ticks have 22 decimals) */123export function priceDp(tick: DecStr | null | undefined, ref: DecStr | null | undefined): number {124  const r = dec(ref), max = dpOf(tick);125  if (!r) return Math.min(max, 2);126  const int = D.str(D.abs(D.div(r, D.one, 0, 'down'))).replace(/^0$/, '').length;127  return Math.max(0, Math.min(max, 6 - int));128}129/** smallest price step actually present between adjacent levels (never below the tick): the book's natural grouping */130export function naturalStep(levels: readonly (readonly [DecStr, DecStr])[][], tick: DecStr): DecStr {131  const tk = dec(tick);132  let best: Dec | null = null;133  for (const side of levels) for (let i = 1; i < Math.min(side.length, 12); i++) {134    const a = dec(side[i - 1]![0]), b = dec(side[i]![0]); if (!a || !b) continue;135    const d = D.abs(D.sub(a, b));136    if (D.isPos(d) && (!best || D.lt(d, best))) best = d;137  }138  if (!best) return tk && D.isPos(tk) ? D.str(tk) : '1';139  return D.str(tk && D.isPos(tk) && D.lt(best, tk) ? tk : best);140}141/**142 * The price increment that applies AT a price. Hyperliquid has no fixed tick: prices have at most 5 significant figures and at143 * most the tick's decimals, integers always allowed (the backend refuses anything else with `price_not_on_tick`). Other venues144 * use their tick.145 */146export function stepAt(venue: string, tick: Dec, price: Dec): Dec {147  if (venue !== 'hyperliquid' || !D.isPos(price)) return tick;148  const s = D.str(price), [int = '0', frac = ''] = s.split('.');149  const e = int !== '0' ? int.length - 1 : -(frac.length - frac.replace(/^0+/, '').length + 1);150  const decimals = Math.max(0, Math.min(tick.s, 4 - e));151  return decimals === 0 ? D.one : D.from(`0.${'0'.repeat(decimals - 1)}1`);152}153/** a price put on the venue's grid in an explicit direction */154export function quantizePrice(venue: string, tick: Dec, price: Dec, mode: 'down' | 'up'): Dec {155  return D.quantize(price, stepAt(venue, tick, price), mode);156}157
Files main
Menu

Theme