src/orders/ioc.test.ts
v0.2.0 · 4.8 KB
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);
});
});