src/numbers/quantize.test.ts
v0.2.1 · 4.9 KB
import { describe, expect, it } from 'vitest';
import { LighterNumbersError } from './errors.js';
import {
decimalsOf,
formatSize,
fromWire,
isOnPriceGrid,
isOnSizeGrid,
priceFromWire,
priceTick,
priceToWire,
quantizePrice,
quantizeSize,
scaledIntegerFromString,
scaledIntegerToString,
sizeFromWire,
sizeLot,
sizeToWire,
toWire,
} from './quantize.js';
describe('sizeLot / priceTick', () => {
it('builds exact powers of ten (the 10 ** -4 trap)', () => {
expect(10 ** -4).not.toBe(0.0001); // the trap the knowledge base warns about
expect(sizeLot(4)).toBe(0.0001);
expect(sizeLot(0)).toBe(1);
expect(sizeLot(3)).toBe(0.001);
expect(priceTick(2)).toBe(0.01);
expect(priceTick(5)).toBe(0.00001);
});
it('rejects bad decimals', () => {
expect(() => sizeLot(-1)).toThrow(LighterNumbersError);
expect(() => sizeLot(1.5)).toThrow(LighterNumbersError);
expect(() => sizeLot(13)).toThrow(LighterNumbersError);
});
});
describe('quantizeSize', () => {
it('floors by default and ceils on request', () => {
expect(quantizeSize(0.00718, 4)).toBe(0.0071);
expect(quantizeSize(0.00718, 4, 'ceil')).toBe(0.0072);
expect(quantizeSize(2.9, 0)).toBe(2);
expect(quantizeSize(2.1, 0, 'ceil')).toBe(3);
});
it('does not move a size that already lies on the grid', () => {
expect(quantizeSize(0.3, 1)).toBe(0.3);
expect(quantizeSize(0.3, 1, 'ceil')).toBe(0.3);
expect(quantizeSize(0.1 + 0.2, 1)).toBe(0.3); // 0.30000000000000004
expect(quantizeSize(0.1 + 0.2, 1, 'ceil')).toBe(0.3);
expect(quantizeSize(5, 0)).toBe(5);
});
it('clamps negatives to zero and rejects non-finite', () => {
expect(quantizeSize(-1, 2)).toBe(0);
expect(() => quantizeSize(Number.NaN, 2)).toThrow(LighterNumbersError);
expect(() => quantizeSize(Number.POSITIVE_INFINITY, 2)).toThrow(LighterNumbersError);
});
it('formatSize returns the wire string with fixed decimals', () => {
expect(formatSize(0.00718, 4)).toBe('0.0071');
expect(formatSize(3, 2)).toBe('3.00');
});
});
describe('quantizePrice', () => {
it('rounds to the tick as an exact string', () => {
expect(quantizePrice(41.236, 2)).toBe('41.24');
expect(quantizePrice(1605.344, 2)).toBe('1605.34');
expect(quantizePrice(7, 3)).toBe('7.000');
});
it('floors and ceils without moving on-grid prices', () => {
expect(quantizePrice(41.236, 2, 'floor')).toBe('41.23');
expect(quantizePrice(41.231, 2, 'ceil')).toBe('41.24');
expect(quantizePrice(41.23, 2, 'floor')).toBe('41.23');
expect(quantizePrice(41.23, 2, 'ceil')).toBe('41.23');
});
it('isOnPriceGrid checks the string, not a float', () => {
expect(isOnPriceGrid('52.37', 2)).toBe(true);
expect(isOnPriceGrid('52.371', 2)).toBe(false);
expect(isOnPriceGrid('52', 2)).toBe(true);
expect(isOnPriceGrid('-1', 2)).toBe(false);
expect(isOnPriceGrid('1e3', 2)).toBe(false);
});
it('isOnSizeGrid tolerates float noise', () => {
expect(isOnSizeGrid(0.1 + 0.2, 1)).toBe(true);
expect(isOnSizeGrid(0.35, 1)).toBe(false);
});
});
describe('wire encoding', () => {
it('encodes base_amount and price as exact integers', () => {
expect(sizeToWire(0.0071, 4)).toBe(71);
expect(sizeToWire(1.005, 3)).toBe(1005);
expect(priceToWire('1605.34')).toBe(160534);
expect(priceToWire('1605.34', 2)).toBe(160534);
expect(priceToWire('1605.3', 2)).toBe(160530);
expect(priceToWire('0.48213')).toBe(48213);
expect(toWire(0.01, '1605.34', 4)).toEqual({ base_amount: 100, price: 160534 });
});
it('rejects a price string with more decimals than the grid', () => {
expect(() => priceToWire('1605.345', 2)).toThrow(LighterNumbersError);
expect(() => scaledIntegerFromString('abc', 2)).toThrow(LighterNumbersError);
expect(() => sizeToWire(-0.5, 2)).toThrow(LighterNumbersError);
});
it('decodes back to the same values', () => {
expect(sizeFromWire(71, 4)).toBe(0.0071);
expect(priceFromWire(160534, 2)).toBe('1605.34');
expect(priceFromWire(5, 3)).toBe('0.005');
expect(priceFromWire(7, 0)).toBe('7');
expect(fromWire({ base_amount: 100, price: 160534 }, 4, 2)).toEqual({ size: 0.01, priceStr: '1605.34' });
expect(scaledIntegerToString(0, 2)).toBe('0.00');
expect(() => scaledIntegerToString(-1, 2)).toThrow(LighterNumbersError);
expect(() => scaledIntegerToString(1.5, 2)).toThrow(LighterNumbersError);
});
it('decimalsOf counts fractional digits', () => {
expect(decimalsOf('1605.34')).toBe(2);
expect(decimalsOf('7')).toBe(0);
});
it('round-trips a full grid of sizes without drift', () => {
for (let lots = 0; lots < 2000; lots++) {
const size = Number((lots * 0.0001).toFixed(4));
expect(sizeToWire(size, 4)).toBe(lots);
expect(sizeFromWire(lots, 4)).toBe(size);
expect(quantizeSize(size, 4)).toBe(size);
expect(quantizeSize(size, 4, 'ceil')).toBe(size);
}
});
});