src/history/twap.test.ts
v0.3.0 · 3.9 KB
import { describe, expect, it } from 'vitest';
import type { InfoRequest, InfoRequester } from '../transport/types.js';
import {
fetchTwapSliceFills,
fetchTwapSliceFillsByTime,
mergeFillsWithTwap,
parseTwapSliceFills,
} from './twap.js';
import type { HlFill } from './types.js';
const USER = '0x0000000000000000000000000000000000000000'; // privacy-allow: placeholder address
function mkFill(p: Partial<HlFill> & { time: number; tid: number }): HlFill {
return {
coin: 'BTC',
px: '10',
sz: '1',
side: 'A',
startPosition: '-5',
dir: 'Close Short',
closedPnl: '-1',
hash: '0x00',
oid: p.tid,
crossed: true,
fee: '0.01',
feeToken: 'USDC',
...p,
};
}
function stub(response: unknown) {
const calls: InfoRequest[] = [];
const info = (async (b: InfoRequest) => {
calls.push(b);
return response;
}) as InfoRequester;
return { info, calls };
}
describe('parseTwapSliceFills', () => {
it('accepts both the {fill, twapId} wrapper and a bare fill', () => {
const res = parseTwapSliceFills([
{ fill: mkFill({ tid: 1, time: 1 }), twapId: 77 },
mkFill({ tid: 2, time: 2, twapId: 88 }),
mkFill({ tid: 3, time: 3 }),
null,
{ junk: true },
]);
expect(res.map((f) => f.twapId)).toEqual([77, 88, null]);
});
it('throws on a non-array response', () => {
expect(() => parseTwapSliceFills({})).toThrow(/non-array/);
});
});
describe('fetchTwapSliceFills', () => {
it('reads the separate userTwapSliceFills feed, sorted, with lastTime and cap flag', async () => {
const rows = Array.from({ length: 2000 }, (_, i) => ({ fill: mkFill({ tid: i + 1, time: 5000 - i }), twapId: 1 }));
const { info, calls } = stub(rows);
const res = await fetchTwapSliceFills(info, USER);
expect(calls).toEqual([{ type: 'userTwapSliceFills', user: USER }]);
expect(res.capped).toBe(true);
expect(res.firstTime).toBe(3001);
expect(res.lastTime).toBe(5000);
expect(res.fills[0]!.time).toBe(3001);
});
it('sends startTime and also filters on the client (server honouring it is unverified)', async () => {
const { info, calls } = stub([mkFill({ tid: 1, time: 10 }), mkFill({ tid: 2, time: 100 })]);
const res = await fetchTwapSliceFills(info, USER, { startTime: 50 });
expect(calls[0]!.startTime).toBe(50);
expect(res.fills.map((f) => f.tid)).toEqual([2]);
expect(res.capped).toBe(false);
});
it('empty feed has null times', async () => {
const { info } = stub([]);
const res = await fetchTwapSliceFills(info, USER);
expect(res.lastTime).toBeNull();
expect(res.firstTime).toBeNull();
});
it('experimental ByTime variant filters the window on the client', async () => {
const { info, calls } = stub([mkFill({ tid: 1, time: 10 }), mkFill({ tid: 2, time: 20 }), mkFill({ tid: 3, time: 30 })]);
const res = await fetchTwapSliceFillsByTime(info, USER, { startTime: 15, endTime: 25 });
expect(calls[0]).toEqual({ type: 'userTwapSliceFillsByTime', user: USER, startTime: 15, endTime: 25 });
expect(res.fills.map((f) => f.tid)).toEqual([2]);
});
});
describe('mergeFillsWithTwap', () => {
it('unions both feeds, dedups by tid, sorts and tags the source', () => {
const fills = [mkFill({ tid: 1, time: 30, dir: 'Open Long', side: 'B' }), mkFill({ tid: 2, time: 10 })];
const slices = parseTwapSliceFills([{ fill: mkFill({ tid: 3, time: 20 }), twapId: 9 }, mkFill({ tid: 2, time: 10 })]);
const merged = mergeFillsWithTwap(fills, slices);
expect(merged.map((f) => [f.tid, f.source])).toEqual([
[2, 'fills'],
[3, 'twap'],
[1, 'fills'],
]);
expect(merged[1]!.twapId).toBe(9);
});
it('cuts both feeds to the same window', () => {
const merged = mergeFillsWithTwap(
[mkFill({ tid: 1, time: 5 }), mkFill({ tid: 2, time: 50 })],
[mkFill({ tid: 3, time: 500 }), mkFill({ tid: 4, time: 40 })],
{ from: 10, to: 100 },
);
expect(merged.map((f) => f.tid)).toEqual([4, 2]);
});
});