Skip to content
markpaper

src/orders/iocFill.test.ts

v0.2.1 · 4.3 KB

Download file
import { describe, expect, it } from 'vitest';
import type { SignerWriteResult } from '../signer/types.js';
import { measureIocFill } from './iocFill.js';

const ok: SignerWriteResult = { status: 'ok', txHash: '0xabc', raw: {} };
const noSleep = async () => {};

/** Position reader that returns a scripted sequence (last value repeats). */
function positions(seq: Array<number | null>) {
  let i = 0;
  const calls: number[] = [];
  return {
    read: async () => {
      calls.push(i);
      const v = seq[Math.min(i, seq.length - 1)] as number | null;
      i++;
      return v;
    },
    calls,
  };
}

describe('measureIocFill', () => {
  it('reports FILLED with the position delta and the poll count', async () => {
    const pos = positions([0, 0, 0.5]); // before 0, first poll still 0, second poll 0.5
    let sent = 0;
    const r = await measureIocFill({
      readPosition: pos.read,
      send: async () => {
        sent++;
        return ok;
      },
      isBuy: true,
      limitPrice: 1650,
      sleep: noSleep,
    });
    expect(r).toEqual({
      status: 'FILLED',
      fillSize: 0.5,
      before: 0,
      after: 0.5,
      limitPrice: 1650,
      txHash: '0xabc',
      polls: 2,
    });
    expect(sent).toBe(1);
  });

  it('measures a sell as a decrease, including through zero', async () => {
    const pos = positions([0.4, -0.1]);
    const r = await measureIocFill({ readPosition: pos.read, send: async () => ok, isBuy: false, sleep: noSleep });
    expect(r).toMatchObject({ status: 'FILLED', fillSize: 0.5 });
  });

  it('does not send when the position before cannot be read', async () => {
    let sent = 0;
    const r = await measureIocFill({
      readPosition: async () => null,
      send: async () => {
        sent++;
        return ok;
      },
      isBuy: true,
      sleep: noSleep,
    });
    expect(r).toMatchObject({ status: 'REJECTED', unknownOutcome: false, before: null });
    expect(sent).toBe(0);
  });

  it('maps rateLimited to SKIPPED, unknown to REJECTED with unknownOutcome, rejected to REJECTED with the code', async () => {
    const pos = positions([1]);
    const skipped = await measureIocFill({
      readPosition: pos.read,
      send: async () => ({ status: 'rateLimited', error: 'write budget 22/34 exhausted' }),
      isBuy: true,
      sleep: noSleep,
    });
    expect(skipped).toEqual({ status: 'SKIPPED', reason: 'write budget 22/34 exhausted' });

    const unknown = await measureIocFill({
      readPosition: pos.read,
      send: async () => ({ status: 'unknown', error: 'timeout' }),
      isBuy: true,
      sleep: noSleep,
    });
    expect(unknown).toMatchObject({ status: 'REJECTED', unknownOutcome: true, before: 1, after: null });

    const rej = await measureIocFill({
      readPosition: pos.read,
      send: async () => ({
        status: 'rejected',
        error: '21734 too far',
        kind: 'far_from_mark',
        code: 21734,
        httpStatus: 502,
        raw: {},
      }),
      isBuy: true,
      sleep: noSleep,
    });
    expect(rej).toMatchObject({ status: 'REJECTED', kind: 'far_from_mark', code: 21734, unknownOutcome: false });
  });

  it('REJECTED when the position did not move (never FILLED on a guess)', async () => {
    const pos = positions([2, 2]);
    const sleeps: number[] = [];
    const r = await measureIocFill({
      readPosition: pos.read,
      send: async () => ok,
      isBuy: true,
      polls: 3,
      intervalMs: 50,
      sleep: async (ms) => {
        sleeps.push(ms);
      },
    });
    expect(r).toMatchObject({ status: 'REJECTED', before: 2, after: 2, txHash: '0xabc', unknownOutcome: false });
    expect(sleeps).toEqual([50, 50, 50]);
    expect(pos.calls).toHaveLength(4); // 1 before + 3 polls
  });

  it('REJECTED when the position moved the wrong way or the read after is unreadable', async () => {
    const wrongWay = await measureIocFill({
      readPosition: positions([1, 0.5]).read,
      send: async () => ok,
      isBuy: true,
      sleep: noSleep,
    });
    expect(wrongWay).toMatchObject({ status: 'REJECTED', before: 1, after: 0.5 });
    const unreadable = await measureIocFill({
      readPosition: positions([1, null]).read,
      send: async () => ok,
      isBuy: true,
      polls: 2,
      sleep: noSleep,
    });
    expect(unreadable).toMatchObject({ status: 'REJECTED', before: 1, after: null, txHash: '0xabc' });
  });
});
All files