src/orders/iocFill.test.ts
v0.2.1 · 4.3 KB
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' });
});
});