src/format/validate.test.ts
v0.3.0 · 4.8 KB
import { describe, expect, it } from 'vitest';
import { HlFormatError } from './decimal.js';
import { triggerPxMatches, validateOrder } from './validate.js';
describe('validateOrder', () => {
it('accepts a valid open and returns canonical wire strings', () => {
expect(validateOrder({ px: '83.2050', sz: '0.290', szDecimals: 2 })).toEqual({
ok: true,
issues: [],
p: '83.205',
s: '0.29',
notional: '24.12945',
});
});
it('flags an off-grid price and size', () => {
expect(validateOrder({ px: '83.2051', sz: '1', szDecimals: 2 }).issues).toEqual(['invalid_price']);
expect(validateOrder({ px: '0.26523', sz: '100', szDecimals: 2 }).issues).toEqual(['invalid_price']);
expect(validateOrder({ px: '83.205', sz: '0.291', szDecimals: 2 }).issues).toEqual(['invalid_size']);
expect(validateOrder({ px: 'x', sz: -1, szDecimals: 2 }).issues).toEqual(['invalid_price', 'invalid_size']);
});
it('flags opens and partial reduces below $10', () => {
expect(validateOrder({ px: '35', sz: '0.28', szDecimals: 2 })).toMatchObject({
ok: false,
issues: ['below_min_notional'],
notional: '9.8',
});
expect(validateOrder({ px: '5000', sz: '0.001', szDecimals: 3, reduceOnly: true }).issues).toEqual([
'below_min_notional',
]);
});
it('exempts a full reduceOnly close from the minimum', () => {
expect(validateOrder({ px: '3000', sz: '0.001', szDecimals: 3, reduceOnly: true, fullClose: true }).ok).toBe(true);
});
it('requires reduceOnly on a full close', () => {
expect(validateOrder({ px: '3000', sz: '0.001', szDecimals: 3, fullClose: true }).issues).toEqual([
'full_close_requires_reduce_only',
'below_min_notional',
]);
});
it('flags zero size on a normal order', () => {
expect(validateOrder({ px: '100', sz: '0', szDecimals: 2 }).issues).toEqual(['zero_size']);
});
it('checks positionTpsl legs: s = 0 and r = true', () => {
expect(validateOrder({ px: '90', sz: '0', szDecimals: 2, reduceOnly: true, positionTpsl: true }).ok).toBe(true);
expect(validateOrder({ px: '90', sz: '1', szDecimals: 2, reduceOnly: true, positionTpsl: true }).issues).toEqual([
'position_tpsl_size_must_be_zero',
]);
expect(validateOrder({ px: '90', sz: '0', szDecimals: 2, positionTpsl: true }).issues).toEqual([
'position_tpsl_requires_reduce_only',
]);
});
it('honours a custom minimum and rejects invalid ones', () => {
expect(validateOrder({ px: '35', sz: '0.28', szDecimals: 2, minNotionalUsd: 5 }).ok).toBe(true);
expect(() => validateOrder({ px: '35', sz: '0.28', szDecimals: 2, minNotionalUsd: -5 })).toThrow(HlFormatError);
expect(() => validateOrder({ px: '35', sz: '0.28', szDecimals: 10 })).toThrow(HlFormatError);
});
});
describe('triggerPxMatches', () => {
it('matches within max(target * 1e-5, 1e-9)', () => {
expect(triggerPxMatches('95.0', 95)).toBe(true);
expect(triggerPxMatches(95.0009, 95)).toBe(true);
expect(triggerPxMatches(95.001, 95)).toBe(false);
// a plain limit reads back triggerPx "0.0": never a match, not even against a tiny target
expect(triggerPxMatches('0', 0)).toBe(false);
expect(triggerPxMatches('0.0', '0.0000000005')).toBe(false);
expect(triggerPxMatches('', 0)).toBe(false);
expect(triggerPxMatches(' ', '0')).toBe(false);
expect(triggerPxMatches('-95', -95)).toBe(false);
// tiny but positive targets use the 1e-9 floor
expect(triggerPxMatches('0.0000000015', '0.000000001')).toBe(true);
expect(triggerPxMatches('0.0000000025', '0.000000001')).toBe(false);
expect(triggerPxMatches('abc', 1)).toBe(false);
});
});
describe('validateOrder: review additions', () => {
it('checks triggerPx on the same grid as p', () => {
const base = { px: '90', sz: '0', szDecimals: 2, reduceOnly: true, positionTpsl: true };
expect(validateOrder({ ...base, triggerPx: '95.5' }).ok).toBe(true);
expect(validateOrder({ ...base, triggerPx: '95.00001' }).issues).toEqual(['invalid_trigger_price']);
expect(validateOrder({ ...base, triggerPx: '0.0' }).issues).toEqual(['invalid_trigger_price']);
});
it('accepts real read-back strings with trailing zeros (recentTrades "79463.0")', () => {
expect(validateOrder({ px: '79463.0', sz: '0.00020', szDecimals: 5 })).toMatchObject({
ok: true,
p: '79463',
s: '0.0002',
notional: '15.8926',
});
});
it('validates the minimum even when price or size is already invalid', () => {
expect(() => validateOrder({ px: 'bad', sz: 'bad', szDecimals: 2, minNotionalUsd: Number.NaN })).toThrow(HlFormatError);
});
it('flags a wire size copied from a noisy book string', () => {
expect(validateOrder({ px: '0.000009', sz: '2817666.6600000001', szDecimals: 2, market: 'spot' }).issues).toEqual([
'invalid_size',
]);
});
});