From b5f379b88613be93dfc7d5d510e898b904aa8f53 Mon Sep 17 00:00:00 2001 From: Thibault You Date: Wed, 29 Sep 2021 15:07:59 +0200 Subject: [PATCH 1/2] :white_check_mark: Adding trades executor tests --- .../__tests__/trading.executor.test.ts | 157 +++++++++++++++--- .../trading/__tests__/trading.service.test.ts | 33 +++- src/services/trading/trading.executor.ts | 10 +- src/tests/fixtures/common.fixtures.ts | 27 +++ 4 files changed, 191 insertions(+), 36 deletions(-) diff --git a/src/services/trading/__tests__/trading.executor.test.ts b/src/services/trading/__tests__/trading.executor.test.ts index 0a09c38..4848759 100644 --- a/src/services/trading/__tests__/trading.executor.test.ts +++ b/src/services/trading/__tests__/trading.executor.test.ts @@ -1,49 +1,152 @@ +import { + sampleExchangeId, + sampleAccount, + sampleTrade, + sampleLongOrder, + sampleShortOrder, + sampleBuyOrder, + sampleSellOrder, + sampleCloseOrder +} from '../../../tests/fixtures/common.fixtures'; +import { FTXExchangeService } from '../../exchanges/ftx.exchange.service'; +import { TradingExecutor } from '../trading.executor'; +import { DELAY_BETWEEN_TRADES } from '../../../constants/exchanges.constants'; + +let executor: TradingExecutor; +jest.useFakeTimers(); + describe('Trading executor', () => { + beforeEach(() => { + if (executor) { + executor.stop() + } + executor = new TradingExecutor(sampleExchangeId); + jest.clearAllMocks() + jest.clearAllTimers() + }); + describe('constructor', () => { - it.todo('should init exchange service'); + it('should init exchange service', () => { + expect(executor.getExchangeService().exchangeId).toStrictEqual( + sampleExchangeId + ); + expect(executor.getExchangeService()).toBeInstanceOf(FTXExchangeService); + }); }); describe('getExchangeService', () => { - it.todo('should return exchange service'); + it('should return exchange service', () => { + expect(executor.getExchangeService()).toBeDefined(); + }); }); describe('getStatus', () => { - it.todo('should return running status'); + it('should return running status', () => { + expect(executor.getStatus()).toBeFalsy(); + }); }); describe('start', () => { - it.todo('should start executor'); - - it.todo('should not start executor if started'); - - it.todo('should update running status'); - - it.todo('should process trades'); - - it.todo('should add a delay between trades'); + it('should start executor', () => { + executor.stop(); + expect(executor.getStatus()).toBeFalsy(); + executor.start(); + expect(executor.getStatus()).toBeTruthy(); + }); + + it('should not start executor if started', () => { + expect(executor.start()).toBeTruthy(); + expect(executor.start()).toBeFalsy(); + }); + + it('should process trades', () => { + const spy = jest + .spyOn(executor, 'processTrade') + .mockImplementation(() => null); + executor.start(); + executor.addTrade(sampleAccount, sampleTrade); + executor.addTrade(sampleAccount, sampleTrade); + executor.addTrade(sampleAccount, sampleTrade); + jest.advanceTimersByTime(2000); + expect(spy).toBeCalledTimes(3); + }); + + it('should add a delay between trades', () => { + const spy = jest + .spyOn(executor, 'processTrade') + .mockImplementation(() => null); + executor.start(); + executor.addTrade(sampleAccount, sampleTrade); + expect(spy).toBeCalledTimes(0); + jest.advanceTimersByTime(DELAY_BETWEEN_TRADES[executor.getExchangeService().exchangeId] + 50); + expect(spy).toBeCalledTimes(1); + executor.addTrade(sampleAccount, sampleTrade); + jest.advanceTimersByTime(DELAY_BETWEEN_TRADES[executor.getExchangeService().exchangeId] + 50); + expect(spy).toBeCalledTimes(2); + }); }); describe('stop', () => { - it.todo('should stop executor'); - - it.todo('should update running status'); - - it.todo('should not stop executor if stopped'); + it('should stop executor', () => { + executor.start(); + expect(executor.getStatus()).toBeTruthy(); + executor.stop(); + expect(executor.getStatus()).toBeFalsy(); + }); + + it('should not stop executor if stopped', () => { + expect(executor.getStatus()).toBeFalsy(); + executor.start(); + expect(executor.getStatus()).toBeTruthy(); + }); }); describe('addTrade', () => { - it.todo('should add trade to execution queue'); + // TODO not sure that we need this one since adding the trade infos to the queue should not throw + it.todo('should throw on error') - it.todo('should throw on error'); - - it.todo('should return success'); + it('should return success', () => { + expect(executor.addTrade(sampleAccount, sampleTrade)).toBeTruthy(); + }); }); describe('processTrade', () => { - it.todo('should process create order'); - - it.todo('should process close order'); - - it.todo('should return processed order'); + it('should process long / short / buy / sell orders', async () => { + const spy = jest + .spyOn(executor.getExchangeService(), 'createOrder') + .mockImplementation(() => null); + jest + .spyOn(executor.getExchangeService(), 'createCloseOrder') + .mockImplementation(() => null); + await executor.processTrade(sampleLongOrder) + await executor.processTrade(sampleShortOrder) + await executor.processTrade(sampleBuyOrder) + await executor.processTrade(sampleSellOrder) + await executor.processTrade(sampleCloseOrder) + expect(spy).toHaveBeenCalledTimes(4) + }); + + it('should process close order', async () => { + const spy = jest + .spyOn(executor.getExchangeService(), 'createCloseOrder') + .mockImplementation(() => null); + jest + .spyOn(executor.getExchangeService(), 'createOrder') + .mockImplementation(() => null); + await executor.processTrade(sampleCloseOrder) + expect(spy).toHaveBeenCalledTimes(1) + }); + + it('should return processed order', async () => { + const mock = {test: 'test'} as any + jest + .spyOn(executor.getExchangeService(), 'createCloseOrder') + .mockImplementation(() => mock); + jest + .spyOn(executor.getExchangeService(), 'createOrder') + .mockImplementation(() => null); + const res = await executor.processTrade(sampleCloseOrder) + expect(res).toEqual(mock); + }); }); -}); +}); \ No newline at end of file diff --git a/src/services/trading/__tests__/trading.service.test.ts b/src/services/trading/__tests__/trading.service.test.ts index e418fdd..803ccf2 100644 --- a/src/services/trading/__tests__/trading.service.test.ts +++ b/src/services/trading/__tests__/trading.service.test.ts @@ -1,13 +1,36 @@ +import { sampleExchangeId } from '../../../tests/fixtures/common.fixtures'; +import { TradingService } from '../trading.service'; + describe('Trading service', () => { describe('getTradeExecutor', () => { - it.todo('should return executor'); + beforeEach(() => { + TradingService.executors.clear(); + }); - it.todo('should init executor'); + it('should return executor', () => { + const executor = TradingService.getTradeExecutor(sampleExchangeId); + expect(executor).not.toBeNull(); + }); - it.todo('should add executor to cache'); + it('should add executor to cache', () => { + expect(TradingService.executors.size).toStrictEqual(0); + TradingService.getTradeExecutor(sampleExchangeId); + expect(TradingService.executors.size).toStrictEqual(1); + }); - it.todo('should start executor'); + it('should start executor', () => { + TradingService.getTradeExecutor(sampleExchangeId); + expect( + TradingService.getTradeExecutor(sampleExchangeId).getStatus() + ).toBeTruthy(); + }); - it.todo('should not init executor if already started'); + it('should not init executor if already started', () => { + const executor = TradingService.getTradeExecutor(sampleExchangeId); + const spy = jest.spyOn(executor, 'start').mockImplementation(() => null); + expect(spy).toBeCalledTimes(0); + TradingService.getTradeExecutor(sampleExchangeId); + expect(spy).toBeCalledTimes(0); + }); }); }); diff --git a/src/services/trading/trading.executor.ts b/src/services/trading/trading.executor.ts index 149e313..54b143e 100644 --- a/src/services/trading/trading.executor.ts +++ b/src/services/trading/trading.executor.ts @@ -43,7 +43,7 @@ export class TradingExecutor { getStatus = (): boolean => this.isStarted; - start = (): void => { + start = (): boolean => { if (!this.isStarted) { this.isStarted = true; debug(TRADE_SERVICE_START(this.id)); @@ -53,9 +53,10 @@ export class TradingExecutor { await this.processTrade(tradeInfo); } }, DELAY_BETWEEN_TRADES[this.id]); - } else { - debug(TRADE_SERVICE_ALREADY_STARTED(this.id)); + return true; } + debug(TRADE_SERVICE_ALREADY_STARTED(this.id)); + return false; }; stop = (): void => { @@ -68,9 +69,10 @@ export class TradingExecutor { } }; - addTrade = async (account: Account, trade: Trade): Promise => { + addTrade = (account: Account, trade: Trade): boolean => { const { stub, exchange } = account; const { symbol, direction } = trade; + // TODO remove try / catch ? try { debug(TRADE_SERVICE_ADD(exchange)); const info: ITradeInfo = { diff --git a/src/tests/fixtures/common.fixtures.ts b/src/tests/fixtures/common.fixtures.ts index a0d3a95..d54bba0 100644 --- a/src/tests/fixtures/common.fixtures.ts +++ b/src/tests/fixtures/common.fixtures.ts @@ -1,8 +1,14 @@ import { Exchange } from 'ccxt'; import { ExchangeId } from '../../constants/exchanges.constants'; +import { Side } from '../../constants/trading.constants'; import { Account } from '../../entities/account.entities'; import { Market } from '../../entities/market.entities'; +import { Trade } from '../../entities/trade.entities'; import { IBalance } from '../../interfaces/exchanges/common.exchange.interfaces'; +import { ITradeInfo } from '../../interfaces/trading.interfaces'; +import { v4 as uuidv4 } from 'uuid'; + +export const sampleExchangeId: ExchangeId = ExchangeId.FTX; export const sampleAccount: Account = { apiKey: 'apiKey', @@ -42,3 +48,24 @@ export const invalidMarket = { } as unknown as Market; export const invalidSymbol = 'invalidSymbol'; + +export const sampleTrade: Trade = { + direction: Side.Long, + size: '10', + stub: 'test', + symbol: 'BTC-PERP' +}; + +export const sampleBaseOrder : ITradeInfo = {account : sampleAccount, id: uuidv4(), trade: sampleTrade } + +export const getSampleOrder = (side: Side): ITradeInfo => ({ ...sampleBaseOrder, trade: { ...sampleBaseOrder.trade, direction: side}}) + +export const sampleLongOrder : ITradeInfo = getSampleOrder(Side.Long) + +export const sampleShortOrder : ITradeInfo = getSampleOrder(Side.Short) + +export const sampleBuyOrder : ITradeInfo = getSampleOrder(Side.Buy) + +export const sampleSellOrder : ITradeInfo = getSampleOrder(Side.Sell) + +export const sampleCloseOrder : ITradeInfo = getSampleOrder(Side.Close) \ No newline at end of file From 826fc07eede6d7a0dfa3d6cb949df493938da115 Mon Sep 17 00:00:00 2001 From: Thibault You Date: Wed, 29 Sep 2021 15:08:51 +0200 Subject: [PATCH 2/2] :rotating_light: Fix linter issues --- .../__tests__/trading.executor.test.ts | 70 ++++++++++--------- src/tests/fixtures/common.fixtures.ts | 21 ++++-- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/src/services/trading/__tests__/trading.executor.test.ts b/src/services/trading/__tests__/trading.executor.test.ts index 4848759..79c055d 100644 --- a/src/services/trading/__tests__/trading.executor.test.ts +++ b/src/services/trading/__tests__/trading.executor.test.ts @@ -18,11 +18,11 @@ jest.useFakeTimers(); describe('Trading executor', () => { beforeEach(() => { if (executor) { - executor.stop() + executor.stop(); } executor = new TradingExecutor(sampleExchangeId); - jest.clearAllMocks() - jest.clearAllTimers() + jest.clearAllMocks(); + jest.clearAllTimers(); }); describe('constructor', () => { @@ -61,8 +61,8 @@ describe('Trading executor', () => { it('should process trades', () => { const spy = jest - .spyOn(executor, 'processTrade') - .mockImplementation(() => null); + .spyOn(executor, 'processTrade') + .mockImplementation(() => null); executor.start(); executor.addTrade(sampleAccount, sampleTrade); executor.addTrade(sampleAccount, sampleTrade); @@ -73,15 +73,19 @@ describe('Trading executor', () => { it('should add a delay between trades', () => { const spy = jest - .spyOn(executor, 'processTrade') - .mockImplementation(() => null); + .spyOn(executor, 'processTrade') + .mockImplementation(() => null); executor.start(); executor.addTrade(sampleAccount, sampleTrade); expect(spy).toBeCalledTimes(0); - jest.advanceTimersByTime(DELAY_BETWEEN_TRADES[executor.getExchangeService().exchangeId] + 50); + jest.advanceTimersByTime( + DELAY_BETWEEN_TRADES[executor.getExchangeService().exchangeId] + 50 + ); expect(spy).toBeCalledTimes(1); executor.addTrade(sampleAccount, sampleTrade); - jest.advanceTimersByTime(DELAY_BETWEEN_TRADES[executor.getExchangeService().exchangeId] + 50); + jest.advanceTimersByTime( + DELAY_BETWEEN_TRADES[executor.getExchangeService().exchangeId] + 50 + ); expect(spy).toBeCalledTimes(2); }); }); @@ -103,7 +107,7 @@ describe('Trading executor', () => { describe('addTrade', () => { // TODO not sure that we need this one since adding the trade infos to the queue should not throw - it.todo('should throw on error') + it.todo('should throw on error'); it('should return success', () => { expect(executor.addTrade(sampleAccount, sampleTrade)).toBeTruthy(); @@ -113,40 +117,40 @@ describe('Trading executor', () => { describe('processTrade', () => { it('should process long / short / buy / sell orders', async () => { const spy = jest - .spyOn(executor.getExchangeService(), 'createOrder') - .mockImplementation(() => null); + .spyOn(executor.getExchangeService(), 'createOrder') + .mockImplementation(() => null); jest - .spyOn(executor.getExchangeService(), 'createCloseOrder') - .mockImplementation(() => null); - await executor.processTrade(sampleLongOrder) - await executor.processTrade(sampleShortOrder) - await executor.processTrade(sampleBuyOrder) - await executor.processTrade(sampleSellOrder) - await executor.processTrade(sampleCloseOrder) - expect(spy).toHaveBeenCalledTimes(4) + .spyOn(executor.getExchangeService(), 'createCloseOrder') + .mockImplementation(() => null); + await executor.processTrade(sampleLongOrder); + await executor.processTrade(sampleShortOrder); + await executor.processTrade(sampleBuyOrder); + await executor.processTrade(sampleSellOrder); + await executor.processTrade(sampleCloseOrder); + expect(spy).toHaveBeenCalledTimes(4); }); it('should process close order', async () => { const spy = jest - .spyOn(executor.getExchangeService(), 'createCloseOrder') - .mockImplementation(() => null); + .spyOn(executor.getExchangeService(), 'createCloseOrder') + .mockImplementation(() => null); jest - .spyOn(executor.getExchangeService(), 'createOrder') - .mockImplementation(() => null); - await executor.processTrade(sampleCloseOrder) - expect(spy).toHaveBeenCalledTimes(1) + .spyOn(executor.getExchangeService(), 'createOrder') + .mockImplementation(() => null); + await executor.processTrade(sampleCloseOrder); + expect(spy).toHaveBeenCalledTimes(1); }); it('should return processed order', async () => { - const mock = {test: 'test'} as any + const mock = { test: 'test' } as any; jest - .spyOn(executor.getExchangeService(), 'createCloseOrder') - .mockImplementation(() => mock); + .spyOn(executor.getExchangeService(), 'createCloseOrder') + .mockImplementation(() => mock); jest - .spyOn(executor.getExchangeService(), 'createOrder') - .mockImplementation(() => null); - const res = await executor.processTrade(sampleCloseOrder) + .spyOn(executor.getExchangeService(), 'createOrder') + .mockImplementation(() => null); + const res = await executor.processTrade(sampleCloseOrder); expect(res).toEqual(mock); }); }); -}); \ No newline at end of file +}); diff --git a/src/tests/fixtures/common.fixtures.ts b/src/tests/fixtures/common.fixtures.ts index d54bba0..1e4e7b6 100644 --- a/src/tests/fixtures/common.fixtures.ts +++ b/src/tests/fixtures/common.fixtures.ts @@ -56,16 +56,23 @@ export const sampleTrade: Trade = { symbol: 'BTC-PERP' }; -export const sampleBaseOrder : ITradeInfo = {account : sampleAccount, id: uuidv4(), trade: sampleTrade } +export const sampleBaseOrder: ITradeInfo = { + account: sampleAccount, + id: uuidv4(), + trade: sampleTrade +}; -export const getSampleOrder = (side: Side): ITradeInfo => ({ ...sampleBaseOrder, trade: { ...sampleBaseOrder.trade, direction: side}}) +export const getSampleOrder = (side: Side): ITradeInfo => ({ + ...sampleBaseOrder, + trade: { ...sampleBaseOrder.trade, direction: side } +}); -export const sampleLongOrder : ITradeInfo = getSampleOrder(Side.Long) +export const sampleLongOrder: ITradeInfo = getSampleOrder(Side.Long); -export const sampleShortOrder : ITradeInfo = getSampleOrder(Side.Short) +export const sampleShortOrder: ITradeInfo = getSampleOrder(Side.Short); -export const sampleBuyOrder : ITradeInfo = getSampleOrder(Side.Buy) +export const sampleBuyOrder: ITradeInfo = getSampleOrder(Side.Buy); -export const sampleSellOrder : ITradeInfo = getSampleOrder(Side.Sell) +export const sampleSellOrder: ITradeInfo = getSampleOrder(Side.Sell); -export const sampleCloseOrder : ITradeInfo = getSampleOrder(Side.Close) \ No newline at end of file +export const sampleCloseOrder: ITradeInfo = getSampleOrder(Side.Close);