/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { assert } from 'chai'; import { List } from 'immutable'; import { ActionResultEvent, AskPriceStrategy, DirectBuy, IMatchableOrder, IPriceStrategy, MatchingEngine, OrderCreationTimePickStrategy, Trade } from '.'; import { TestLogger } from './TestLogger'; interface ITestCase { orders: (IMatchableOrder | DirectBuy | string)[]; expectedTrades: Trade[]; asksAfter?: IMatchableOrder[]; bidsAfter?: IMatchableOrder[]; expectedStatusChanges?: ActionResultEvent[]; priceStrategies?: IPriceStrategy[]; } export class Testing { assertOrders( expected: List>, current: List>, type: string ) { assert.equal(current.size, expected.size, `Expected amount of ${type} orders`); const zipped = expected.zip(current); zipped.forEach(([a1, a2]) => { assert.equal(a1.id, a2.id, 'Wrong order id'); assert.equal(a1.volume.toString(10), a2.volume.toString(10), 'Wrong volume'); assert.equal(a1.price, a2.price, 'Wrong price'); }); } assertTrades(expected: List, current: List) { assert.equal( current.size, expected.size, `Expected amount of trades: current=${JSON.stringify( current )} expected=${JSON.stringify(expected)}` ); const zipped = expected.zip(current); zipped.forEach(([t1, t2]) => { assert.equal(t1.ask.id, t2.ask.id, 'Wrong askId'); assert.equal(t1.bid.id, t2.bid.id, 'Wrong bidId'); assert.equal(t1.volume.toString(10), t2.volume.toString(10), 'Wrong volume'); assert.equal(t1.price, t2.price, 'Wrong price'); }); } assertStatusChanges(expected: List, current: List) { assert.equal(current.size, expected.size, 'Expected amount of status changes'); const zipped = expected.zip(current); zipped.forEach(([t1, t2]) => { assert.deepEqual(t1, t2); }); } executeTestCase(testCase: ITestCase, done: any) { const strategies = testCase?.priceStrategies ?? [ new AskPriceStrategy(), new OrderCreationTimePickStrategy() ]; let doneTimer: NodeJS.Timeout; const signalReady = () => { clearInterval(doneTimer); doneTimer = global.setTimeout(() => done(), 50); }; const expectedTrades = List(testCase.expectedTrades); strategies.forEach((priceStrategy: IPriceStrategy) => { const matchingEngine = new MatchingEngine( priceStrategy, new TestLogger() ); testCase.orders.forEach((a) => { if (typeof a === 'string') { matchingEngine.cancelOrder(a); } else if (a instanceof DirectBuy) { matchingEngine.submitDirectBuy(a); } else { matchingEngine.submitOrder(a); } }); matchingEngine.trades.subscribe((res) => { this.assertTrades( expectedTrades, res.map((r) => r.trade) ); const expectedBidsAfter = List(testCase.bidsAfter); const expectedAsksAfter = List(testCase.asksAfter); const { asks, bids } = matchingEngine.orderBook(); this.assertOrders(expectedBidsAfter, bids, 'bids'); this.assertOrders(expectedAsksAfter, asks, 'asks'); signalReady(); }); matchingEngine.actionResults.subscribe((res) => { const expectedStatusChanges = List(testCase.expectedStatusChanges); this.assertStatusChanges(expectedStatusChanges, res); signalReady(); }); matchingEngine.tick(); }); if (testCase.expectedTrades.length === 0 && !testCase.expectedStatusChanges) { setTimeout(() => done(), 50); } } executeOrderBookQuery( asks: IMatchableOrder[], bids: IMatchableOrder[], productFilter: TProductFilter, expectedAsks: IMatchableOrder[], expectedBids: IMatchableOrder[], priceStrategies?: IPriceStrategy[] ) { const strategies = priceStrategies ?? [ new AskPriceStrategy(), new OrderCreationTimePickStrategy() ]; strategies.forEach((priceStrategy) => { const matchingEngine = new MatchingEngine( priceStrategy, new TestLogger() ); asks.forEach((b) => matchingEngine.submitOrder(b)); bids.forEach((a) => matchingEngine.submitOrder(a)); matchingEngine.tick(); const orderBook = matchingEngine.orderBookByProduct(productFilter); this.assertOrders( List>(expectedAsks), orderBook.asks, 'asks' ); this.assertOrders( List>(expectedBids), orderBook.bids, 'bids' ); }); } }