src/orders/size.test.ts
v0.2.0 · 4.7 KB
import { describe, expect, it } from 'vitest';
import { meetsMinSize } from '../numbers/quant.js';
import { decimalToX18, X18 } from '../numbers/x18.js';
import { planOrderSize } from './size.js';
const BTC_LOT = 50000000000000n; // 0.00005
const PX = 66120n * X18;
const MIN = 100n * X18;
const base = { priceX18: PX, lotX18: BTC_LOT, minRestingNotionalX18: MIN };
describe('planOrderSize', () => {
it('floors opens to the lot and sends when the notional clears the resting minimum', () => {
const plan = planOrderSize({ ...base, size: 0.00212, intent: 'resting', reduceOnly: false });
expect(plan).toMatchObject({
action: 'send',
sizeX18: decimalToX18('0.0021'),
rounding: 'floor',
bumped: false,
minNotionalX18: MIN,
});
expect(plan.notionalX18).toBe(decimalToX18('138.852'));
});
it('skips opens and partial reductions below the minimum instead of bumping them', () => {
const open = planOrderSize({ ...base, size: 0.001, intent: 'resting', reduceOnly: false });
expect(open).toMatchObject({
action: 'skip',
reason: 'below-minimum',
sizeX18: decimalToX18('0.001'),
notionalX18: decimalToX18('66.12'),
});
const partial = planOrderSize({ ...base, size: 0.001, intent: 'ioc', reduceOnly: true, fullClose: false });
expect(partial).toMatchObject({ action: 'skip', reason: 'below-minimum' });
});
it('IOC minimum defaults to the resting one and can be lowered after measuring the floor', () => {
const def = planOrderSize({ ...base, size: 0.001, intent: 'ioc', reduceOnly: false });
expect(def.action).toBe('skip');
const lowered = planOrderSize({
...base,
size: 0.001,
intent: 'ioc',
reduceOnly: false,
minTakerNotionalX18: 25n * X18,
});
expect(lowered).toMatchObject({ action: 'send', sizeX18: decimalToX18('0.001'), minNotionalX18: 25n * X18 });
const fok = planOrderSize({
...base,
size: 0.001,
intent: 'fok',
reduceOnly: false,
minTakerNotionalX18: 25n * X18,
});
expect(fok.action).toBe('send');
// the taker minimum never applies to resting orders
const resting = planOrderSize({
...base,
size: 0.001,
intent: 'resting',
reduceOnly: false,
minTakerNotionalX18: 25n * X18,
});
expect(resting.action).toBe('skip');
});
it('full reduce-only close: ceil to the lot, bump above the minimum, never skipped by the minimum', () => {
const plan = planOrderSize({ ...base, size: 0.00012, intent: 'ioc', reduceOnly: true, fullClose: true });
expect(plan.action).toBe('send');
if (plan.action !== 'send') return;
expect(plan.rounding).toBe('ceil');
expect(plan.bumped).toBe(true);
expect(meetsMinSize(PX, plan.sizeX18, MIN)).toBe(true);
expect(plan.sizeX18).toBe(decimalToX18('0.00155'));
// a full close already above the minimum is ceiled and not bumped
const big = planOrderSize({ ...base, size: 0.00212, intent: 'ioc', reduceOnly: true, fullClose: true });
expect(big).toMatchObject({ action: 'send', sizeX18: decimalToX18('0.00215'), bumped: false, rounding: 'ceil' });
// fullClose without reduceOnly is an ordinary order (floor + gate)
const notRo = planOrderSize({ ...base, size: 0.00012, intent: 'ioc', reduceOnly: false, fullClose: true });
expect(notRo).toMatchObject({ action: 'skip', reason: 'below-minimum', sizeX18: decimalToX18('0.0001') });
});
it('a size that quantizes to zero is a skip, even for a full close', () => {
expect(planOrderSize({ ...base, size: 0.00001, intent: 'resting', reduceOnly: false })).toMatchObject({
action: 'skip',
reason: 'zero-size',
});
expect(planOrderSize({ ...base, size: 0, intent: 'ioc', reduceOnly: true, fullClose: true })).toMatchObject({
action: 'skip',
reason: 'zero-size',
});
expect(planOrderSize({ ...base, size: '0', intent: 'ioc', reduceOnly: true, fullClose: true })).toMatchObject({
action: 'skip',
reason: 'zero-size',
});
});
it('accepts exact strings and bigints; validates price and minimum', () => {
expect(planOrderSize({ ...base, size: '0.0021', intent: 'resting', reduceOnly: false })).toMatchObject({
action: 'send',
sizeX18: decimalToX18('0.0021'),
});
expect(
planOrderSize({ ...base, size: decimalToX18('0.0021'), intent: 'resting', reduceOnly: false }),
).toMatchObject({ action: 'send' });
expect(() => planOrderSize({ ...base, priceX18: 0n, size: 1, intent: 'resting', reduceOnly: false })).toThrow(
RangeError,
);
expect(() =>
planOrderSize({ ...base, minRestingNotionalX18: -1n, size: 1, intent: 'resting', reduceOnly: false }),
).toThrow(RangeError);
});
});