src/history/group.test.ts
v0.3.0 · 2.8 KB
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([]);
});
});