Skip to content
markpaper

src/history/twap.test.ts

v0.3.0 · 3.9 KB

Download file
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]);
  });
});
All files