src/numbers/quant.ts
v0.1.0 · 4 KB
// 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,
};
}