Skip to content
This repository has been archived by the owner on Dec 1, 2023. It is now read-only.

✅ Adding tests for trading executor #32

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 132 additions & 25 deletions src/services/trading/__tests__/trading.executor.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
33 changes: 28 additions & 5 deletions src/services/trading/__tests__/trading.service.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
10 changes: 6 additions & 4 deletions src/services/trading/trading.executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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 => {
Expand All @@ -68,9 +69,10 @@ export class TradingExecutor {
}
};

addTrade = async (account: Account, trade: Trade): Promise<boolean> => {
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 = {
Expand Down
34 changes: 34 additions & 0 deletions src/tests/fixtures/common.fixtures.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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);