src/format/size.test.ts
v0.3.0 · 6.7 KB
import { describe, expect, it } from 'vitest';
import { HlFormatError } from './decimal.js';
import {
assertSzDecimals,
formatSize,
hasHiddenRemainder,
isFlatPosition,
isFullyFilled,
isValidSize,
positionMatchesExpected,
sizeFromNotional,
szStep,
} from './size.js';
describe('szStep / assertSzDecimals', () => {
it('returns the lot 10^-szDecimals as a canonical string', () => {
expect(szStep(0)).toBe('1');
expect(szStep(2)).toBe('0.01');
expect(szStep(8)).toBe('0.00000001');
});
it('rejects szDecimals outside integer 0..8 (broken meta)', () => {
for (const bad of [-1, 9, 1.5, NaN, Infinity]) {
expect(() => szStep(bad)).toThrow(HlFormatError);
expect(() => assertSzDecimals(bad)).toThrow(HlFormatError);
}
});
});
describe('formatSize', () => {
it('floors 0.29 at szDecimals=2 to 0.29 (epsilon rule)', () => {
expect(formatSize(0.29, 2)).toBe('0.29');
expect(formatSize(0.29 * 100, 0)).toBe('29');
});
it('absorbs float artifacts from arithmetic', () => {
expect(Math.floor(0.57 * 100) / 100).toBe(0.56); // the naive bug: 56.99999999999999
expect(formatSize(0.57, 2)).toBe('0.57');
expect(formatSize(0.3 - 0.1, 2)).toBe('0.2'); // 0.19999999999999998
expect(formatSize(3.3 * 0.9, 2)).toBe('2.97'); // 2.9699999999999998
expect(formatSize(0.1 + 0.2, 2)).toBe('0.3');
expect(formatSize(0.1 + 0.2, 2, 'ceil')).toBe('0.3');
});
it('floor vs ceil on a partial reduce: 0.2 at szDecimals=0', () => {
expect(formatSize(0.2, 0)).toBe('0');
expect(formatSize(0.2, 0, 'ceil')).toBe('1');
});
it('ceil for a full close covers the position (0.017 at szDecimals=2 -> 0.02)', () => {
expect(formatSize(0.017, 2, 'ceil')).toBe('0.02');
expect(formatSize(0.2345, 4, 'ceil')).toBe('0.2345');
});
it('ceil keeps exact values and only moves on real excess', () => {
expect(formatSize(0.29, 2, 'ceil')).toBe('0.29');
expect(formatSize('0.2900001', 2, 'ceil')).toBe('0.3');
});
it('round is half-up on the decimal literal', () => {
expect(formatSize(1.005, 2, 'round')).toBe('1.01');
expect(formatSize(0.125, 2, 'round')).toBe('0.13');
expect(formatSize(0.124, 2, 'round')).toBe('0.12');
});
it('emits canonical strings without trailing zeros or exponent', () => {
expect(formatSize(5, 2)).toBe('5');
expect(formatSize('5.00', 2)).toBe('5');
expect(formatSize(1.5, 3)).toBe('1.5');
expect(formatSize(1e-7, 8)).toBe('0.0000001');
expect(formatSize('1e3', 0)).toBe('1000');
});
it("returns '0' below one lot", () => {
expect(formatSize(0.004, 2)).toBe('0');
expect(formatSize(0, 2)).toBe('0');
});
it('keeps lot precision on large sizes', () => {
expect(formatSize('12345678.999999995', 0)).toBe('12345679');
expect(formatSize(12345678.999999998, 0)).toBe('12345679');
expect(formatSize('987654321.123456', 3)).toBe('987654321.123');
});
it('throws on negative, non-finite or malformed sizes', () => {
for (const bad of [-0.5, NaN, Infinity, 'abc', '']) {
expect(() => formatSize(bad, 2)).toThrow(HlFormatError);
}
});
});
describe('sizeFromNotional', () => {
it('sizes by the order price and floors by default', () => {
expect(sizeFromNotional(10, 3000, 3)).toBe('0.003');
expect(sizeFromNotional(10, 3000, 3, 'ceil')).toBe('0.004');
expect(sizeFromNotional(10.15, 35, 2)).toBe('0.29');
expect(sizeFromNotional(0, 35, 2)).toBe('0');
});
it('rejects a non-positive price', () => {
expect(() => sizeFromNotional(10, 0, 2)).toThrow(HlFormatError);
expect(() => sizeFromNotional(-1, 10, 2)).toThrow(HlFormatError);
});
});
describe('isValidSize', () => {
it('checks decimals without rounding', () => {
expect(isValidSize('0.29', 2)).toBe(true);
expect(isValidSize('1.50', 1)).toBe(true);
expect(isValidSize('0', 2)).toBe(true);
expect(isValidSize('0.291', 2)).toBe(false);
expect(isValidSize(-1, 2)).toBe(false);
expect(isValidSize('x', 2)).toBe(false);
});
});
describe('isFullyFilled', () => {
it('tolerates one lot of rounding', () => {
expect(isFullyFilled(1, 0.99, 2)).toBe(true);
expect(isFullyFilled('1.2', '1.2', 1)).toBe(true);
expect(isFullyFilled(1, 0.98, 2)).toBe(false);
expect(isFullyFilled(5, 3, 0)).toBe(false);
});
it('treats non-positive requested size as filled', () => {
expect(isFullyFilled(0, 0, 2)).toBe(true);
expect(isFullyFilled(-1, 0, 2)).toBe(true);
});
});
describe('hasHiddenRemainder', () => {
it('flags a real position larger than filled + lot', () => {
expect(hasHiddenRemainder('-0.05', '0.03', 2)).toBe(true);
expect(hasHiddenRemainder('0.04', '0.03', 2)).toBe(false);
expect(hasHiddenRemainder('0.03', '0.03', 2)).toBe(false);
expect(hasHiddenRemainder(0, '0.03', 2)).toBe(false);
});
});
describe('API size strings with float noise (live spot l2Book: "2817666.6600000001" at szDecimals=2)', () => {
it('rounds noisy strings back onto the lot and does not treat them as wire-valid', () => {
expect(formatSize('2817666.6600000001', 2)).toBe('2817666.66');
expect(formatSize('4216117.2599999998', 2)).toBe('4216117.26');
expect(formatSize('4216117.2599999998', 2, 'ceil')).toBe('4216117.26');
expect(isValidSize('2817666.6600000001', 2)).toBe(false);
expect(isFullyFilled('4216117.26', '4216117.2599999998', 2)).toBe(true);
expect(hasHiddenRemainder('2817666.6600000001', '2817666.66', 2)).toBe(false);
});
it('keeps huge integer lot counts of low-price spot tokens exact', () => {
expect(formatSize('123456789.0', 0)).toBe('123456789');
expect(sizeFromNotional(100, '0.00000011', 0)).toBe('909090909');
});
});
describe('isFlatPosition', () => {
it('is flat below one lot, not at exactly one lot', () => {
expect(isFlatPosition('0', 3)).toBe(true);
expect(isFlatPosition('-0.0009', 3)).toBe(true);
expect(isFlatPosition('0.0000000001', 0)).toBe(true);
expect(isFlatPosition('0.001', 3)).toBe(false);
expect(isFlatPosition('-0.001', 3)).toBe(false);
expect(isFlatPosition(1, 0)).toBe(false);
expect(() => isFlatPosition('x', 3)).toThrow(HlFormatError);
expect(() => isFlatPosition(0, -1)).toThrow(HlFormatError);
});
});
describe('positionMatchesExpected', () => {
it('accepts |actual - expected| <= 0.5 lot + 1e-12, signed', () => {
expect(positionMatchesExpected('1.005', '1', 2)).toBe(true); // exactly half a lot
expect(positionMatchesExpected('1.005000000001', '1', 2)).toBe(true); // + 1e-12
expect(positionMatchesExpected('1.005000000002', '1', 2)).toBe(false);
expect(positionMatchesExpected('-1', '1', 2)).toBe(false); // sign matters
expect(positionMatchesExpected(0.1 + 0.2, '0.3', 1)).toBe(true);
// snapshot still shows the pre-IoC position
expect(positionMatchesExpected('0', '0.5', 2)).toBe(false);
});
});