Skip to content
markpaper

src/orders/ioc.test.ts

v0.2.0 · 4.8 KB

Download file
import { describe, expect, it } from 'vitest';
import { decimalToX18 } from '../numbers/x18.js';
import type { Hex } from '../signing/types.js';
import type { PlaceOutcome } from './build.js';
import { DEFAULT_IOC_READ_ATTEMPTS, DEFAULT_IOC_READ_DELAY_MS, measureIocFill } from './ioc.js';

const digest = `0x${'cd'.repeat(32)}` as Hex;
const sent = decimalToX18('0.001');
const noSleep = async () => {};

function reads(values: Array<bigint | Error>) {
  let i = 0;
  return async () => {
    const v = values[Math.min(i++, values.length - 1)];
    if (v instanceof Error) throw v;
    return v as bigint;
  };
}

const ok: () => Promise<PlaceOutcome> = async () => ({ digest });

describe('measureIocFill', () => {
  it('defaults: 3 reads, 150 ms apart', () => {
    expect(DEFAULT_IOC_READ_ATTEMPTS).toBe(3);
    expect(DEFAULT_IOC_READ_DELAY_MS).toBe(150);
  });

  it('reports the directional delta as the fill (buy)', async () => {
    const r = await measureIocFill({
      readPositionX18: reads([0n, sent]),
      send: ok,
      isBuy: true,
      sentX18: sent,
      sleep: noSleep,
    });
    expect(r).toEqual({ status: 'FILLED', digest, fillX18: sent, beforeX18: 0n, afterX18: sent, partial: false });
  });

  it('sell on a long: delta is negative, reported as a positive fill; partial fills are flagged', async () => {
    const before = decimalToX18('0.5');
    const r = await measureIocFill({
      readPositionX18: reads([before, before - decimalToX18('0.0004')]),
      send: ok,
      isBuy: false,
      sentX18: sent,
      sleep: noSleep,
    });
    expect(r).toMatchObject({ status: 'FILLED', fillX18: decimalToX18('0.0004'), partial: true });
  });

  it('caps the fill by the sent size (a competing trade must not be credited)', async () => {
    const r = await measureIocFill({
      readPositionX18: reads([0n, decimalToX18('0.003')]),
      send: ok,
      isBuy: true,
      sentX18: sent,
      sleep: noSleep,
    });
    expect(r).toMatchObject({ status: 'FILLED', fillX18: sent, partial: false });
  });

  it('a delta in the wrong direction is not a fill', async () => {
    const r = await measureIocFill({
      readPositionX18: reads([0n, -sent]),
      send: ok,
      isBuy: true,
      sentX18: sent,
      sleep: noSleep,
    });
    expect(r).toMatchObject({ status: 'REJECTED', reason: 'no-delta', digest, fillX18: 0n });
  });

  it('does not send when the pre-read fails', async () => {
    let sends = 0;
    const r = await measureIocFill({
      readPositionX18: reads([new Error('503')]),
      send: async () => {
        sends++;
        return { digest };
      },
      isBuy: true,
      sentX18: sent,
      sleep: noSleep,
    });
    expect(sends).toBe(0);
    expect(r).toMatchObject({ status: 'REJECTED', reason: 'pre-read-failed', fillX18: 0n });
    expect((r as { error: string }).error).toContain('503');
  });

  it('a venue rejection is REJECTED with its code', async () => {
    const r = await measureIocFill({
      readPositionX18: reads([0n]),
      send: async () => ({ rejection: { code: 2117, message: 'post-only' } }),
      isBuy: true,
      sentX18: sent,
      sleep: noSleep,
    });
    expect(r).toMatchObject({
      status: 'REJECTED',
      reason: 'rejected',
      rejection: { code: 2117 },
      error: '2117: post-only',
    });
  });

  it('retries the read up to `attempts` with the delay, tolerating read failures, then REJECTED', async () => {
    const slept: number[] = [];
    const sleep = async (ms: number) => {
      slept.push(ms);
    };
    const late = await measureIocFill({
      readPositionX18: reads([0n, new Error('x'), 0n, sent]),
      send: ok,
      isBuy: true,
      sentX18: sent,
      sleep,
    });
    expect(late.status).toBe('FILLED');
    expect(slept).toEqual([150, 150]);
    slept.length = 0;
    const never = await measureIocFill({
      readPositionX18: reads([0n, 0n]),
      send: ok,
      isBuy: true,
      sentX18: sent,
      sleep,
      attempts: 2,
      delayMs: 10,
    });
    expect(never).toMatchObject({ status: 'REJECTED', reason: 'no-delta', digest });
    expect(slept).toEqual([10]);
  });

  it('propagates transport errors from send (outcome unknown, caller reconciles)', async () => {
    await expect(
      measureIocFill({
        readPositionX18: reads([0n]),
        send: async () => {
          throw new Error('timeout');
        },
        isBuy: true,
        sentX18: sent,
        sleep: noSleep,
      }),
    ).rejects.toThrow('timeout');
  });

  it('validates inputs', async () => {
    await expect(measureIocFill({ readPositionX18: reads([0n]), send: ok, isBuy: true, sentX18: 0n })).rejects.toThrow(
      RangeError,
    );
    await expect(
      measureIocFill({ readPositionX18: reads([0n]), send: ok, isBuy: true, sentX18: sent, attempts: 0 }),
    ).rejects.toThrow(RangeError);
  });
});
All files