Skip to content
markpaper

src/numbers/quant.ts

v0.1.0 · 4 KB

Download file
// Exact decimal arithmetic. Avoid floating-point SDK priceToTicks.
const POW10 = (n: number): bigint => 10n ** BigInt(n);
export function decimalOf(x: number): {
  neg: boolean;
  mant: bigint;
  exp: number;
} | null {
  if (!Number.isFinite(x)) return null;
  let s = String(x);
  let neg = false;
  if (s.startsWith('-')) {
    neg = true;
    s = s.slice(1);
  }
  let exp = 0;
  const e = s.indexOf('e');
  if (e >= 0) {
    exp = Number(s.slice(e + 1));
    s = s.slice(0, e);
  }
  const dot = s.indexOf('.');
  if (dot >= 0) {
    exp -= s.length - dot - 1;
    s = s.slice(0, dot) + s.slice(dot + 1);
  }
  if (!/^\d+$/.test(s) || !Number.isInteger(exp)) return null;
  return { neg, mant: BigInt(s), exp };
}
function fractionOf(
  d: {
    mant: bigint;
    exp: number;
  },
  shift: number,
): {
  num: bigint;
  den: bigint;
} {
  const e = d.exp + shift;
  return e >= 0 ? { num: d.mant * POW10(e), den: 1n } : { num: d.mant, den: POW10(-e) };
}
export type Rounding = 'nearest' | 'floor' | 'ceil';
function divRound(num: bigint, den: bigint, mode: Rounding): bigint {
  if (mode === 'floor') return num / den;
  if (mode === 'ceil') return (num + den - 1n) / den;
  return (2n * num + den) / (2n * den);
}
function scaledToStr(n: bigint, scale: number): string {
  if (scale <= 0) return (n * POW10(-scale)).toString();
  const neg = n < 0n;
  const digits = (neg ? -n : n).toString().padStart(scale + 1, '0');
  const int = digits.slice(0, -scale);
  const frac = digits.slice(-scale).replace(/0+$/, '');
  const body = frac ? `${int}.${frac}` : int;
  return neg && body !== '0' ? `-${body}` : body;
}
const QUOT_EPSILON_INV = 1000000000n;
export interface PhoenixQuant {
  readonly phoenix: true;
  readonly lot: number;
  readonly minSz: number;
  readonly decimals: number;
  readonly tickUsd: number;
  pxToStr(px: number): string;
  floorSz(sz: number): number;
  ceilSz(sz: number): number;
  readonly tickSize: bigint;
  readonly baseLotsDecimals: number;
  pxToTicks(px: number, mode: Rounding): bigint;
  ticksToStr(ticks: bigint): string;
  sizeToLots(sz: number, mode: Rounding): bigint;
  lotsToStr(lots: bigint): string;
  lotsToSize(lots: bigint): number;
}
export function createQuant({
  tickSize,
  baseLotsDecimals,
}: {
  tickSize: bigint;
  baseLotsDecimals: number;
}): PhoenixQuant {
  if (tickSize <= 0n) throw new Error(`phoenixQuant: tickSize ${tickSize} ≤ 0`);
  if (!Number.isInteger(baseLotsDecimals) || baseLotsDecimals < -12 || baseLotsDecimals > 18)
    throw new Error(`phoenixQuant: baseLotsDecimals ${baseLotsDecimals} `);
  const bld = baseLotsDecimals;
  const E = 6 - bld;
  const pxToTicks = (px: number, mode: Rounding): bigint => {
    if (!(px > 0)) return 0n;
    const d = decimalOf(px);
    if (!d || d.neg) return 0n;
    const { num, den } = fractionOf(d, E);
    return divRound(num, den * tickSize, mode);
  };
  const ticksToStr = (ticks: bigint): string => scaledToStr(ticks * tickSize, E);
  const lotsToStr = (lots: bigint): string => scaledToStr(lots, bld);
  const lotsToSize = (lots: bigint): number => Number(lotsToStr(lots));
  const sizeToLots = (sz: number, mode: Rounding): bigint => {
    if (!(sz > 0)) return 0n;
    const d = decimalOf(sz);
    if (!d || d.neg) return 0n;
    const { num, den } = fractionOf(d, bld);
    if (mode === 'nearest') return divRound(num, den, 'nearest');
    if (mode === 'floor') return (num * QUOT_EPSILON_INV + den) / (den * QUOT_EPSILON_INV);
    const a = num * QUOT_EPSILON_INV - den;
    return a <= 0n ? 0n : divRound(a, den * QUOT_EPSILON_INV, 'ceil');
  };
  const lot = lotsToSize(1n);
  return {
    phoenix: true,
    tickSize,
    baseLotsDecimals: bld,
    lot,
    tickUsd: Number(ticksToStr(1n)),
    decimals: Math.max(0, bld),
    minSz: lot,
    pxToStr(px: number): string {
      const t = pxToTicks(px, 'nearest');
      return t > 0n ? ticksToStr(t) : '0';
    },
    floorSz: (sz: number) => lotsToSize(sizeToLots(sz, 'floor')),
    ceilSz: (sz: number) => lotsToSize(sizeToLots(sz, 'ceil')),
    pxToTicks,
    ticksToStr,
    sizeToLots,
    lotsToStr,
    lotsToSize,
  };
}
All files