test_order_coordinator.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. /**
  2. * Unit tests for OrderCoordinator
  3. */
  4. import { OrderCoordinator } from '../../src/core/OrderCoordinator';
  5. import { HedgingOrder, OrderDetails } from '../../src/types/hedging';
  6. import { Logger } from '../../src/utils/Logger';
  7. // Mock dependencies
  8. jest.mock('../../src/utils/Logger');
  9. describe('OrderCoordinator', () => {
  10. let orderCoordinator: OrderCoordinator;
  11. let mockConfig: any;
  12. beforeEach(() => {
  13. mockConfig = {
  14. maxConcurrentOrders: 10,
  15. orderTimeout: 30000,
  16. retryAttempts: 3,
  17. retryDelay: 1000
  18. };
  19. orderCoordinator = new OrderCoordinator(mockConfig);
  20. });
  21. afterEach(() => {
  22. jest.clearAllMocks();
  23. });
  24. describe('Initialization', () => {
  25. it('should initialize with correct configuration', async () => {
  26. await orderCoordinator.initialize();
  27. const status = orderCoordinator.getStatus();
  28. expect(status.isRunning).toBe(false);
  29. expect(status.totalOrders).toBe(0);
  30. });
  31. it('should throw error if already initialized', async () => {
  32. await orderCoordinator.initialize();
  33. await expect(orderCoordinator.initialize()).rejects.toThrow('OrderCoordinator is already initialized');
  34. });
  35. });
  36. describe('Order Execution', () => {
  37. beforeEach(async () => {
  38. await orderCoordinator.initialize();
  39. });
  40. it('should execute hedging order pair', async () => {
  41. const hedgingOrder: HedgingOrder = {
  42. id: 'test-order',
  43. sessionId: 'test-session',
  44. strategyId: 'test-strategy',
  45. orderPair: {
  46. buyOrder: {
  47. accountId: 'account1',
  48. side: 'buy',
  49. type: 'limit',
  50. size: 100,
  51. price: 2500
  52. },
  53. sellOrder: {
  54. accountId: 'account2',
  55. side: 'sell',
  56. type: 'limit',
  57. size: 100,
  58. price: 2500
  59. }
  60. },
  61. status: 'pending',
  62. fees: 0,
  63. slippage: 0,
  64. createdAt: new Date(),
  65. updatedAt: new Date()
  66. };
  67. const result = await orderCoordinator.executeHedgingOrderPair(hedgingOrder);
  68. expect(result).toBeDefined();
  69. expect(result.success).toBeDefined();
  70. expect(typeof result.success).toBe('boolean');
  71. });
  72. it('should handle order execution failure', async () => {
  73. const invalidOrder: HedgingOrder = {
  74. id: 'invalid-order',
  75. sessionId: 'test-session',
  76. strategyId: 'test-strategy',
  77. orderPair: {
  78. buyOrder: {
  79. accountId: 'account1',
  80. side: 'buy',
  81. type: 'limit',
  82. size: -100, // Invalid negative size
  83. price: 2500
  84. },
  85. sellOrder: {
  86. accountId: 'account2',
  87. side: 'sell',
  88. type: 'limit',
  89. size: 100,
  90. price: 2500
  91. }
  92. },
  93. status: 'pending',
  94. fees: 0,
  95. slippage: 0,
  96. createdAt: new Date(),
  97. updatedAt: new Date()
  98. };
  99. const result = await orderCoordinator.executeHedgingOrderPair(invalidOrder);
  100. expect(result.success).toBe(false);
  101. expect(result.error).toBeDefined();
  102. });
  103. it('should execute orders with retry mechanism', async () => {
  104. const hedgingOrder: HedgingOrder = {
  105. id: 'retry-order',
  106. sessionId: 'test-session',
  107. strategyId: 'test-strategy',
  108. orderPair: {
  109. buyOrder: {
  110. accountId: 'account1',
  111. side: 'buy',
  112. type: 'limit',
  113. size: 100,
  114. price: 2500
  115. },
  116. sellOrder: {
  117. accountId: 'account2',
  118. side: 'sell',
  119. type: 'limit',
  120. size: 100,
  121. price: 2500
  122. }
  123. },
  124. status: 'pending',
  125. fees: 0,
  126. slippage: 0,
  127. createdAt: new Date(),
  128. updatedAt: new Date()
  129. };
  130. // Mock retry scenario
  131. jest.spyOn(orderCoordinator as any, 'executeOrderWithRetry')
  132. .mockResolvedValueOnce({ success: false, error: 'Network error' })
  133. .mockResolvedValueOnce({ success: true, orderId: 'order-123' });
  134. const result = await orderCoordinator.executeHedgingOrderPair(hedgingOrder);
  135. expect(result).toBeDefined();
  136. });
  137. });
  138. describe('Order Validation', () => {
  139. beforeEach(async () => {
  140. await orderCoordinator.initialize();
  141. });
  142. it('should validate order details', () => {
  143. const validOrder: OrderDetails = {
  144. accountId: 'account1',
  145. side: 'buy',
  146. type: 'limit',
  147. size: 100,
  148. price: 2500
  149. };
  150. const isValid = orderCoordinator.validateOrderDetails(validOrder);
  151. expect(isValid).toBe(true);
  152. });
  153. it('should reject invalid order details', () => {
  154. const invalidOrder: OrderDetails = {
  155. accountId: 'account1',
  156. side: 'buy',
  157. type: 'limit',
  158. size: -100, // Invalid negative size
  159. price: 2500
  160. };
  161. const isValid = orderCoordinator.validateOrderDetails(invalidOrder);
  162. expect(isValid).toBe(false);
  163. });
  164. it('should validate order pair', () => {
  165. const validOrderPair = {
  166. buyOrder: {
  167. accountId: 'account1',
  168. side: 'buy',
  169. type: 'limit',
  170. size: 100,
  171. price: 2500
  172. },
  173. sellOrder: {
  174. accountId: 'account2',
  175. side: 'sell',
  176. type: 'limit',
  177. size: 100,
  178. price: 2500
  179. }
  180. };
  181. const isValid = orderCoordinator.validateOrderPair(validOrderPair);
  182. expect(isValid).toBe(true);
  183. });
  184. it('should reject invalid order pair', () => {
  185. const invalidOrderPair = {
  186. buyOrder: {
  187. accountId: 'account1',
  188. side: 'buy',
  189. type: 'limit',
  190. size: 100,
  191. price: 2500
  192. },
  193. sellOrder: {
  194. accountId: 'account2',
  195. side: 'sell',
  196. type: 'limit',
  197. size: 50, // Different size
  198. price: 2500
  199. }
  200. };
  201. const isValid = orderCoordinator.validateOrderPair(invalidOrderPair);
  202. expect(isValid).toBe(false);
  203. });
  204. });
  205. describe('Order Tracking', () => {
  206. beforeEach(async () => {
  207. await orderCoordinator.initialize();
  208. });
  209. it('should track order execution', async () => {
  210. const hedgingOrder: HedgingOrder = {
  211. id: 'track-order',
  212. sessionId: 'test-session',
  213. strategyId: 'test-strategy',
  214. orderPair: {
  215. buyOrder: {
  216. accountId: 'account1',
  217. side: 'buy',
  218. type: 'limit',
  219. size: 100,
  220. price: 2500
  221. },
  222. sellOrder: {
  223. accountId: 'account2',
  224. side: 'sell',
  225. type: 'limit',
  226. size: 100,
  227. price: 2500
  228. }
  229. },
  230. status: 'pending',
  231. fees: 0,
  232. slippage: 0,
  233. createdAt: new Date(),
  234. updatedAt: new Date()
  235. };
  236. await orderCoordinator.executeHedgingOrderPair(hedgingOrder);
  237. const trackedOrder = orderCoordinator.getOrder('track-order');
  238. expect(trackedOrder).toBeDefined();
  239. });
  240. it('should return null for non-existent order', () => {
  241. const order = orderCoordinator.getOrder('non-existent');
  242. expect(order).toBeNull();
  243. });
  244. it('should get all orders', async () => {
  245. const hedgingOrder1: HedgingOrder = {
  246. id: 'order-1',
  247. sessionId: 'test-session',
  248. strategyId: 'test-strategy',
  249. orderPair: {
  250. buyOrder: {
  251. accountId: 'account1',
  252. side: 'buy',
  253. type: 'limit',
  254. size: 100,
  255. price: 2500
  256. },
  257. sellOrder: {
  258. accountId: 'account2',
  259. side: 'sell',
  260. type: 'limit',
  261. size: 100,
  262. price: 2500
  263. }
  264. },
  265. status: 'pending',
  266. fees: 0,
  267. slippage: 0,
  268. createdAt: new Date(),
  269. updatedAt: new Date()
  270. };
  271. const hedgingOrder2: HedgingOrder = {
  272. id: 'order-2',
  273. sessionId: 'test-session',
  274. strategyId: 'test-strategy',
  275. orderPair: {
  276. buyOrder: {
  277. accountId: 'account1',
  278. side: 'buy',
  279. type: 'limit',
  280. size: 100,
  281. price: 2500
  282. },
  283. sellOrder: {
  284. accountId: 'account2',
  285. side: 'sell',
  286. type: 'limit',
  287. size: 100,
  288. price: 2500
  289. }
  290. },
  291. status: 'pending',
  292. fees: 0,
  293. slippage: 0,
  294. createdAt: new Date(),
  295. updatedAt: new Date()
  296. };
  297. await orderCoordinator.executeHedgingOrderPair(hedgingOrder1);
  298. await orderCoordinator.executeHedgingOrderPair(hedgingOrder2);
  299. const allOrders = orderCoordinator.getAllOrders();
  300. expect(allOrders).toHaveLength(2);
  301. });
  302. it('should get orders by session', async () => {
  303. const hedgingOrder: HedgingOrder = {
  304. id: 'session-order',
  305. sessionId: 'test-session',
  306. strategyId: 'test-strategy',
  307. orderPair: {
  308. buyOrder: {
  309. accountId: 'account1',
  310. side: 'buy',
  311. type: 'limit',
  312. size: 100,
  313. price: 2500
  314. },
  315. sellOrder: {
  316. accountId: 'account2',
  317. side: 'sell',
  318. type: 'limit',
  319. size: 100,
  320. price: 2500
  321. }
  322. },
  323. status: 'pending',
  324. fees: 0,
  325. slippage: 0,
  326. createdAt: new Date(),
  327. updatedAt: new Date()
  328. };
  329. await orderCoordinator.executeHedgingOrderPair(hedgingOrder);
  330. const sessionOrders = orderCoordinator.getSessionOrders('test-session');
  331. expect(sessionOrders).toHaveLength(1);
  332. expect(sessionOrders[0].id).toBe('session-order');
  333. });
  334. });
  335. describe('Order Cancellation', () => {
  336. beforeEach(async () => {
  337. await orderCoordinator.initialize();
  338. });
  339. it('should cancel hedging order pair', async () => {
  340. const hedgingOrder: HedgingOrder = {
  341. id: 'cancel-order',
  342. sessionId: 'test-session',
  343. strategyId: 'test-strategy',
  344. orderPair: {
  345. buyOrder: {
  346. accountId: 'account1',
  347. side: 'buy',
  348. type: 'limit',
  349. size: 100,
  350. price: 2500
  351. },
  352. sellOrder: {
  353. accountId: 'account2',
  354. side: 'sell',
  355. type: 'limit',
  356. size: 100,
  357. price: 2500
  358. }
  359. },
  360. status: 'pending',
  361. fees: 0,
  362. slippage: 0,
  363. createdAt: new Date(),
  364. updatedAt: new Date()
  365. };
  366. await orderCoordinator.executeHedgingOrderPair(hedgingOrder);
  367. const result = await orderCoordinator.cancelHedgingOrderPair(hedgingOrder);
  368. expect(result).toBeDefined();
  369. expect(result.success).toBeDefined();
  370. });
  371. it('should handle cancellation failure', async () => {
  372. const hedgingOrder: HedgingOrder = {
  373. id: 'cancel-fail-order',
  374. sessionId: 'test-session',
  375. strategyId: 'test-strategy',
  376. orderPair: {
  377. buyOrder: {
  378. accountId: 'account1',
  379. side: 'buy',
  380. type: 'limit',
  381. size: 100,
  382. price: 2500
  383. },
  384. sellOrder: {
  385. accountId: 'account2',
  386. side: 'sell',
  387. type: 'limit',
  388. size: 100,
  389. price: 2500
  390. }
  391. },
  392. status: 'pending',
  393. fees: 0,
  394. slippage: 0,
  395. createdAt: new Date(),
  396. updatedAt: new Date()
  397. };
  398. // Mock cancellation failure
  399. jest.spyOn(orderCoordinator as any, 'cancelOrder')
  400. .mockRejectedValue(new Error('Cancellation failed'));
  401. const result = await orderCoordinator.cancelHedgingOrderPair(hedgingOrder);
  402. expect(result.success).toBe(false);
  403. expect(result.error).toBeDefined();
  404. });
  405. });
  406. describe('Position Neutrality', () => {
  407. beforeEach(async () => {
  408. await orderCoordinator.initialize();
  409. });
  410. it('should ensure position neutrality', async () => {
  411. const hedgingOrder: HedgingOrder = {
  412. id: 'neutrality-order',
  413. sessionId: 'test-session',
  414. strategyId: 'test-strategy',
  415. orderPair: {
  416. buyOrder: {
  417. accountId: 'account1',
  418. side: 'buy',
  419. type: 'limit',
  420. size: 100,
  421. price: 2500
  422. },
  423. sellOrder: {
  424. accountId: 'account2',
  425. side: 'sell',
  426. type: 'limit',
  427. size: 100,
  428. price: 2500
  429. }
  430. },
  431. status: 'pending',
  432. fees: 0,
  433. slippage: 0,
  434. createdAt: new Date(),
  435. updatedAt: new Date()
  436. };
  437. const result = await orderCoordinator.executeHedgingOrderPair(hedgingOrder);
  438. if (result.success) {
  439. const isNeutral = orderCoordinator.checkPositionNeutrality('test-session');
  440. expect(isNeutral).toBe(true);
  441. }
  442. });
  443. it('should detect position imbalance', async () => {
  444. const hedgingOrder: HedgingOrder = {
  445. id: 'imbalance-order',
  446. sessionId: 'test-session',
  447. strategyId: 'test-strategy',
  448. orderPair: {
  449. buyOrder: {
  450. accountId: 'account1',
  451. side: 'buy',
  452. type: 'limit',
  453. size: 100,
  454. price: 2500
  455. },
  456. sellOrder: {
  457. accountId: 'account2',
  458. side: 'sell',
  459. type: 'limit',
  460. size: 50, // Different size causing imbalance
  461. price: 2500
  462. }
  463. },
  464. status: 'pending',
  465. fees: 0,
  466. slippage: 0,
  467. createdAt: new Date(),
  468. updatedAt: new Date()
  469. };
  470. await orderCoordinator.executeHedgingOrderPair(hedgingOrder);
  471. const isNeutral = orderCoordinator.checkPositionNeutrality('test-session');
  472. expect(isNeutral).toBe(false);
  473. });
  474. });
  475. describe('Statistics and Monitoring', () => {
  476. beforeEach(async () => {
  477. await orderCoordinator.initialize();
  478. });
  479. it('should return correct status', () => {
  480. const status = orderCoordinator.getStatus();
  481. expect(status).toBeDefined();
  482. expect(typeof status.isRunning).toBe('boolean');
  483. expect(typeof status.totalOrders).toBe('number');
  484. expect(typeof status.pendingOrders).toBe('number');
  485. expect(typeof status.successfulOrders).toBe('number');
  486. expect(typeof status.failedOrders).toBe('number');
  487. });
  488. it('should return correct statistics', () => {
  489. const stats = orderCoordinator.getStatistics();
  490. expect(stats).toBeDefined();
  491. expect(typeof stats.totalOrders).toBe('number');
  492. expect(typeof stats.successfulOrders).toBe('number');
  493. expect(typeof stats.failedOrders).toBe('number');
  494. expect(typeof stats.successRate).toBe('number');
  495. expect(typeof stats.averageExecutionTime).toBe('number');
  496. expect(typeof stats.totalVolume).toBe('number');
  497. });
  498. it('should calculate success rate correctly', async () => {
  499. const hedgingOrder1: HedgingOrder = {
  500. id: 'success-order',
  501. sessionId: 'test-session',
  502. strategyId: 'test-strategy',
  503. orderPair: {
  504. buyOrder: {
  505. accountId: 'account1',
  506. side: 'buy',
  507. type: 'limit',
  508. size: 100,
  509. price: 2500
  510. },
  511. sellOrder: {
  512. accountId: 'account2',
  513. side: 'sell',
  514. type: 'limit',
  515. size: 100,
  516. price: 2500
  517. }
  518. },
  519. status: 'pending',
  520. fees: 0,
  521. slippage: 0,
  522. createdAt: new Date(),
  523. updatedAt: new Date()
  524. };
  525. const hedgingOrder2: HedgingOrder = {
  526. id: 'fail-order',
  527. sessionId: 'test-session',
  528. strategyId: 'test-strategy',
  529. orderPair: {
  530. buyOrder: {
  531. accountId: 'account1',
  532. side: 'buy',
  533. type: 'limit',
  534. size: -100, // Invalid size
  535. price: 2500
  536. },
  537. sellOrder: {
  538. accountId: 'account2',
  539. side: 'sell',
  540. type: 'limit',
  541. size: 100,
  542. price: 2500
  543. }
  544. },
  545. status: 'pending',
  546. fees: 0,
  547. slippage: 0,
  548. createdAt: new Date(),
  549. updatedAt: new Date()
  550. };
  551. await orderCoordinator.executeHedgingOrderPair(hedgingOrder1);
  552. await orderCoordinator.executeHedgingOrderPair(hedgingOrder2);
  553. const stats = orderCoordinator.getStatistics();
  554. expect(stats.successRate).toBe(0.5); // 1 out of 2 successful
  555. });
  556. });
  557. describe('Event Emission', () => {
  558. beforeEach(async () => {
  559. await orderCoordinator.initialize();
  560. });
  561. it('should emit order executed event', async () => {
  562. const hedgingOrder: HedgingOrder = {
  563. id: 'event-order',
  564. sessionId: 'test-session',
  565. strategyId: 'test-strategy',
  566. orderPair: {
  567. buyOrder: {
  568. accountId: 'account1',
  569. side: 'buy',
  570. type: 'limit',
  571. size: 100,
  572. price: 2500
  573. },
  574. sellOrder: {
  575. accountId: 'account2',
  576. side: 'sell',
  577. type: 'limit',
  578. size: 100,
  579. price: 2500
  580. }
  581. },
  582. status: 'pending',
  583. fees: 0,
  584. slippage: 0,
  585. createdAt: new Date(),
  586. updatedAt: new Date()
  587. };
  588. const eventSpy = jest.fn();
  589. orderCoordinator.on('orderExecuted', eventSpy);
  590. await orderCoordinator.executeHedgingOrderPair(hedgingOrder);
  591. expect(eventSpy).toHaveBeenCalled();
  592. });
  593. it('should emit order failed event', async () => {
  594. const hedgingOrder: HedgingOrder = {
  595. id: 'fail-event-order',
  596. sessionId: 'test-session',
  597. strategyId: 'test-strategy',
  598. orderPair: {
  599. buyOrder: {
  600. accountId: 'account1',
  601. side: 'buy',
  602. type: 'limit',
  603. size: -100, // Invalid size
  604. price: 2500
  605. },
  606. sellOrder: {
  607. accountId: 'account2',
  608. side: 'sell',
  609. type: 'limit',
  610. size: 100,
  611. price: 2500
  612. }
  613. },
  614. status: 'pending',
  615. fees: 0,
  616. slippage: 0,
  617. createdAt: new Date(),
  618. updatedAt: new Date()
  619. };
  620. const eventSpy = jest.fn();
  621. orderCoordinator.on('orderFailed', eventSpy);
  622. await orderCoordinator.executeHedgingOrderPair(hedgingOrder);
  623. expect(eventSpy).toHaveBeenCalled();
  624. });
  625. });
  626. describe('Error Handling', () => {
  627. it('should handle initialization errors gracefully', async () => {
  628. const invalidConfig = {
  629. maxConcurrentOrders: -1, // Invalid negative value
  630. orderTimeout: 30000,
  631. retryAttempts: 3,
  632. retryDelay: 1000
  633. };
  634. const invalidCoordinator = new OrderCoordinator(invalidConfig);
  635. await expect(invalidCoordinator.initialize()).rejects.toThrow();
  636. });
  637. it('should handle order execution errors gracefully', async () => {
  638. await orderCoordinator.initialize();
  639. const invalidOrder: HedgingOrder = {
  640. id: 'error-order',
  641. sessionId: 'test-session',
  642. strategyId: 'test-strategy',
  643. orderPair: {
  644. buyOrder: {
  645. accountId: 'account1',
  646. side: 'buy',
  647. type: 'limit',
  648. size: 100,
  649. price: 2500
  650. },
  651. sellOrder: {
  652. accountId: 'account2',
  653. side: 'sell',
  654. type: 'limit',
  655. size: 100,
  656. price: 2500
  657. }
  658. },
  659. status: 'pending',
  660. fees: 0,
  661. slippage: 0,
  662. createdAt: new Date(),
  663. updatedAt: new Date()
  664. };
  665. // Mock execution error
  666. jest.spyOn(orderCoordinator as any, 'executeOrderWithRetry')
  667. .mockRejectedValue(new Error('Execution failed'));
  668. const result = await orderCoordinator.executeHedgingOrderPair(invalidOrder);
  669. expect(result.success).toBe(false);
  670. expect(result.error).toBeDefined();
  671. });
  672. });
  673. describe('Lifecycle Management', () => {
  674. it('should start and stop the coordinator', async () => {
  675. await orderCoordinator.initialize();
  676. await orderCoordinator.start();
  677. const status = orderCoordinator.getStatus();
  678. expect(status.isRunning).toBe(true);
  679. await orderCoordinator.stop();
  680. const stoppedStatus = orderCoordinator.getStatus();
  681. expect(stoppedStatus.isRunning).toBe(false);
  682. });
  683. it('should throw error when starting already running coordinator', async () => {
  684. await orderCoordinator.initialize();
  685. await orderCoordinator.start();
  686. await expect(orderCoordinator.start()).rejects.toThrow();
  687. });
  688. it('should throw error when stopping non-running coordinator', async () => {
  689. await orderCoordinator.initialize();
  690. await expect(orderCoordinator.stop()).rejects.toThrow();
  691. });
  692. });
  693. });