Skip to content
markpaper

src/history/group.test.ts

v0.3.0 · 2.8 KB

Download file
import { describe, expect, it } from 'vitest';
import { groupFillsByOrder, orderGroupKey } from './group.js';
import type { FillLike } from './types.js';

function f(p: Partial<FillLike> & { time: number }): FillLike {
  return { coin: 'HYPE', px: '10', sz: '1', side: 'B', dir: 'Open Long', closedPnl: '0', fee: '0', ...p };
}

describe('groupFillsByOrder', () => {
  it('merges a resting limit eaten at different times by oid', () => {
    const groups = groupFillsByOrder([
      f({ oid: 7, tid: 1, time: 1_000, sz: '0.15', px: '20' }),
      f({ oid: 7, tid: 2, time: 9_000, sz: '0.05', px: '20' }),
      f({ oid: 8, tid: 3, time: 5_000 }),
    ]);
    expect(groups).toHaveLength(2);
    const g = groups.find((x) => x.oid === 7)!;
    expect(g.fillCount).toBe(2);
    expect(g.sz).toBe('0.2');
    expect(g.firstTime).toBe(1_000);
    expect(g.lastTime).toBe(9_000);
    expect(g.tids).toEqual([1, 2]);
  });

  it('SUMS partial fills instead of overwriting them', () => {
    const [g] = groupFillsByOrder([
      f({ oid: 1, time: 1, sz: '0.1', px: '100', closedPnl: '1.1', fee: '0.01', dir: 'Close Long', side: 'A' }),
      f({ oid: 1, time: 1, sz: '0.2', px: '103', closedPnl: '2.2', fee: '0.02', dir: 'Close Long', side: 'A' }),
    ]);
    expect(g!.closedPnl).toBe('3.3');
    expect(g!.fee).toBe('0.03');
    expect(g!.sz).toBe('0.3');
    expect(g!.notional).toBe('30.6');
    expect(g!.px).toBe('102');
  });

  it('falls back to coin|time|dir when oid is missing', () => {
    expect(orderGroupKey(f({ time: 5, dir: 'Close Short' }))).toBe('HYPE|5|Close Short');
    expect(orderGroupKey(f({ time: 5, oid: 9 }))).toBe('HYPE|oid:9');
    const groups = groupFillsByOrder([f({ time: 5 }), f({ time: 5 }), f({ time: 6 })]);
    expect(groups.map((g) => g.fillCount)).toEqual([2, 1]);
  });

  it('keeps different coins with the same oid apart', () => {
    expect(groupFillsByOrder([f({ oid: 1, time: 1 }), f({ oid: 1, time: 1, coin: 'xyz:TSLA' })])).toHaveLength(2);
  });

  it('weighted price with a repeating fraction and taker count, dirs, builder fee', () => {
    const [g] = groupFillsByOrder([
      f({ oid: 1, time: 1, sz: '1', px: '1', crossed: true, builderFee: '0.001' }),
      f({ oid: 1, time: 2, sz: '2', px: '2', crossed: false, dir: 'Long > Short' }),
    ]);
    expect(g!.px).toBe('1.666666666667');
    expect(g!.takerFills).toBe(1);
    expect(g!.dirs).toEqual(['Open Long', 'Long > Short']);
    expect(g!.builderFee).toBe('0.001');
    const [h] = groupFillsByOrder([f({ oid: 2, time: 1 })]);
    expect(h!.builderFee).toBeNull();
  });

  it('throws on a malformed size rather than silently skipping', () => {
    expect(() => groupFillsByOrder([f({ time: 1, sz: 'abc' })])).toThrow(/sz/);
  });

  it('empty input', () => {
    expect(groupFillsByOrder([])).toEqual([]);
  });
});
All files