orderRouter.test.ts 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { describe, expect, it, vi } from 'vitest';
  2. import { OrderRouter } from '../packages/execution/src/orderRouter';
  3. import type { Order, OrderBook } from '../packages/domain/src/types';
  4. const sampleBook: OrderBook = {
  5. bids: [{ px: 100, sz: 1 }],
  6. asks: [{ px: 100.5, sz: 1 }],
  7. ts: Date.now()
  8. };
  9. describe('OrderRouter', () => {
  10. const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
  11. it('sends limit order within slip guard', async () => {
  12. const send = vi.fn(async (order: Order) => ({ id: order.clientId }));
  13. const router = new OrderRouter(send, () => sampleBook, { maxBps: 100 });
  14. const order: Order = {
  15. clientId: 'o-1',
  16. symbol: 'BTC',
  17. side: 'buy',
  18. px: 100.55,
  19. sz: 0.1,
  20. tif: 'GTC'
  21. };
  22. const id = await router.sendLimit(order);
  23. expect(id).toMatch(uuidRegex);
  24. expect(send).toHaveBeenCalledWith(expect.objectContaining({ clientId: id }));
  25. expect(send).toHaveBeenCalledTimes(1);
  26. });
  27. it('rejects orders exceeding slip guard', async () => {
  28. const send = vi.fn(async (order: Order) => ({ id: order.clientId }));
  29. const router = new OrderRouter(send, () => sampleBook, { maxBps: 10 });
  30. const order: Order = {
  31. clientId: 'o-2',
  32. symbol: 'BTC',
  33. side: 'buy',
  34. px: 102,
  35. sz: 0.1,
  36. tif: 'GTC'
  37. };
  38. await expect(router.sendLimit(order)).rejects.toThrow(/slippage/);
  39. expect(send).not.toHaveBeenCalled();
  40. });
  41. it('rejects post-only orders that would cross', async () => {
  42. const send = vi.fn(async (order: Order) => ({ id: order.clientId }));
  43. const router = new OrderRouter(send, () => sampleBook, { maxBps: 100 });
  44. const order: Order = {
  45. clientId: 'o-3',
  46. symbol: 'BTC',
  47. side: 'buy',
  48. px: 100.5,
  49. sz: 0.1,
  50. tif: 'GTC',
  51. postOnly: true
  52. };
  53. await expect(router.sendLimit(order)).rejects.toThrow(/post-only/);
  54. expect(send).not.toHaveBeenCalled();
  55. });
  56. it('throttles orders based on min interval and enforces unique client ids', async () => {
  57. const send = vi.fn(async (order: Order) => ({ id: order.clientId }));
  58. const router = new OrderRouter(send, () => sampleBook, { maxBps: 100, minIntervalMs: 10 });
  59. const orderA: Order = {
  60. clientId: 'o-4',
  61. symbol: 'BTC',
  62. side: 'buy',
  63. px: 100.4,
  64. sz: 0.1,
  65. tif: 'GTC'
  66. };
  67. const orderB: Order = { ...orderA, clientId: 'o-5', px: 100.45 };
  68. await router.sendLimit(orderA);
  69. expect(send).toHaveBeenCalledTimes(1);
  70. const start = Date.now();
  71. await router.sendLimit(orderB);
  72. const elapsed = Date.now() - start;
  73. expect(send).toHaveBeenCalledTimes(2);
  74. expect(elapsed).toBeGreaterThanOrEqual(8);
  75. await expect(router.sendLimit(orderB)).rejects.toThrow(/duplicate clientId/);
  76. });
  77. });