diff --git a/src/services/trading/__tests__/trading.executor.test.ts b/src/services/trading/__tests__/trading.executor.test.ts index 0a09c38..79c055d 100644 --- a/src/services/trading/__tests__/trading.executor.test.ts +++ b/src/services/trading/__tests__/trading.executor.test.ts @@ -1,49 +1,156 @@ +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 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); + }); }); }); 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..1e4e7b6 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,31 @@ 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);