Skip to content
markpaper

src/orders/size.test.ts

v0.2.0 · 4.7 KB

Download file
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);
  });
});
All files